What
Add a deterministic tracker-integrity check to blueprint-plugin — a script that validates feature-tracker.json against itself and against doc frontmatter, and exits non-zero on divergence. Plus a manifest config surface so repo-specific conventions can extend it without forking.
This is the verification counterpart to #1354. That issue fixed sync writing wrong statuses; nothing detects the disagreement afterwards, so a tracker that drifts by any other route stays wrong indefinitely.
Why
Dogfooding blueprint on a C/SDL2 port (skullcaps-native, format 3.3 → 3.4) surfaced five tracker defects that had accumulated for ~3 months. All five are mechanical and none was caught by any existing check:
| Defect |
Observed |
statistics cache diverged from features[] |
total_features: 25 vs 27 actual; in_progress: 5 vs 8; completion_percentage: 68.0 vs recomputed 66.7 |
| Status enum drift |
one feature "completed", seventeen "complete" — the complete count silently undercounted |
| Dead statistics buckets |
partial: 0, pending: 2 naming statuses no feature record uses |
| Two timestamps for one fact |
updated_at: 2026-05-20 vs last_updated: 2026-05-19, already disagreeing |
| FR cited by docs, absent from tracker |
FR-025 referenced by five documents — including inside another feature's own evidence string — but never minted, so invisible to every status query |
| Doc frontmatter vs tracker |
13 PRDs/PRPs at status: Draft whose FRs were all complete |
The statistics one is the sharpest: it's a cache of the features array, so every downstream "68% complete" figure quoted from that file was wrong, and nothing recomputed it.
These are schema invariants, not project opinions — which is what makes them plugin-ownable.
How
A scripts/blueprint-tracker-check.sh (or Python, matching tools/ conventions) emitting the existing === SECTION === / KEY=VALUE / STATUS= convention used by check-adr-numbers.sh and get-automation-config.sh, exit 1 on defects. Checks:
- Recompute
statistics from features[]; report per-field divergence.
- Status enum: flag any value outside the canonical set.
features[].status vs tasks.{pending,completed}[] membership.
- FR ids cited in
docs/** but absent from features[].
- Doc frontmatter
status: vs tracker, for docs whose FRs have all landed.
- Duplicate/dead statistics buckets and duplicate timestamp fields.
One implementation note worth encoding centrally. My first version walked the whole JSON tree for id-bearing objects and reported 7 false "duplicate records" — .tasks.pending[] / .tasks.completed[] legitimately repeat an FR id already in .features[], which is the documented drain design. Status must be read from .features[] only, with the task lists checked separately for agreement. Easy to get wrong once per repo; better encoded once.
Config surface
Checks 2 and 5 need repo-specific vocabulary — which statuses count as "unfinished", which as "done". Following the precedent #1043 set with structure.generated_rules_path, a manifest block with working defaults:
"validation": {
"status_vocabulary": {
"done": ["complete"],
"unfinished": ["draft", "proposed", "ready", "in_progress"]
},
"exclude_basenames": ["README.md"],
"doc_globs": ["docs/prds/*.md", "docs/prps/*.md", "docs/adrs/*.md"]
}
Absent block behaves as the defaults, so it's purely additive.
What should NOT be upstreamed
To draw the boundary explicitly — the same dogfooding produced checks that are genuinely project-specific and belong in the repo, not the plugin:
- "any feature title containing 50 levels contradicts ADR-0024" — a project fact.
- An exemption for
SAVE_LEVEL_COUNT 50, a deliberate over-allocation that a naive sweep would have "fixed" into a bug.
Those want the config surface to point at a repo-local extension, or simply to stay as a local script. Happy to be argued out of the config block entirely if the preference is "plugin owns invariants, repos own conventions, no bridge."
Status
Working, tested implementation exists locally (~200 lines, ruff-clean, fault-injection verified: regressing one enum and one doc status turns it red, restoring returns clean). Wired to just, pre-commit, and a CI job. Happy to open a PR against blueprint-plugin/scripts/ with a companion tests/test-blueprint-tracker-check.sh matching the existing four — say the word on the config-surface shape first, since that's the part with a real design choice in it.
What
Add a deterministic tracker-integrity check to
blueprint-plugin— a script that validatesfeature-tracker.jsonagainst itself and against doc frontmatter, and exits non-zero on divergence. Plus a manifest config surface so repo-specific conventions can extend it without forking.This is the verification counterpart to #1354. That issue fixed sync writing wrong statuses; nothing detects the disagreement afterwards, so a tracker that drifts by any other route stays wrong indefinitely.
Why
Dogfooding blueprint on a C/SDL2 port (
skullcaps-native, format 3.3 → 3.4) surfaced five tracker defects that had accumulated for ~3 months. All five are mechanical and none was caught by any existing check:statisticscache diverged fromfeatures[]total_features: 25vs 27 actual;in_progress: 5vs 8;completion_percentage: 68.0vs recomputed 66.7"completed", seventeen"complete"— thecompletecount silently undercountedpartial: 0,pending: 2naming statuses no feature record usesupdated_at: 2026-05-20vslast_updated: 2026-05-19, already disagreeingFR-025referenced by five documents — including inside another feature's ownevidencestring — but never minted, so invisible to every status querystatus: Draftwhose FRs were allcompleteThe
statisticsone is the sharpest: it's a cache of the features array, so every downstream "68% complete" figure quoted from that file was wrong, and nothing recomputed it.These are schema invariants, not project opinions — which is what makes them plugin-ownable.
How
A
scripts/blueprint-tracker-check.sh(or Python, matchingtools/conventions) emitting the existing=== SECTION ===/KEY=VALUE/STATUS=convention used bycheck-adr-numbers.shandget-automation-config.sh, exit 1 on defects. Checks:statisticsfromfeatures[]; report per-field divergence.features[].statusvstasks.{pending,completed}[]membership.docs/**but absent fromfeatures[].status:vs tracker, for docs whose FRs have all landed.One implementation note worth encoding centrally. My first version walked the whole JSON tree for
id-bearing objects and reported 7 false "duplicate records" —.tasks.pending[]/.tasks.completed[]legitimately repeat an FR id already in.features[], which is the documented drain design. Status must be read from.features[]only, with the task lists checked separately for agreement. Easy to get wrong once per repo; better encoded once.Config surface
Checks 2 and 5 need repo-specific vocabulary — which statuses count as "unfinished", which as "done". Following the precedent #1043 set with
structure.generated_rules_path, a manifest block with working defaults:Absent block behaves as the defaults, so it's purely additive.
What should NOT be upstreamed
To draw the boundary explicitly — the same dogfooding produced checks that are genuinely project-specific and belong in the repo, not the plugin:
SAVE_LEVEL_COUNT 50, a deliberate over-allocation that a naive sweep would have "fixed" into a bug.Those want the config surface to point at a repo-local extension, or simply to stay as a local script. Happy to be argued out of the config block entirely if the preference is "plugin owns invariants, repos own conventions, no bridge."
Status
Working, tested implementation exists locally (~200 lines, ruff-clean, fault-injection verified: regressing one enum and one doc status turns it red, restoring returns clean). Wired to
just, pre-commit, and a CI job. Happy to open a PR againstblueprint-plugin/scripts/with a companiontests/test-blueprint-tracker-check.shmatching the existing four — say the word on the config-surface shape first, since that's the part with a real design choice in it.