Be Civic — Lifecycle, Branch Policy, and Test Fixtures
Canonical system specifications for the Be Civic project.
Be Civic — Lifecycle, Branch Policy, and Test Fixtures
This sub-spec covers the artefact lifecycle: the state-machine promotion model (§9) with its promotion thresholds and rollback/quarantine mechanics, the branch and CI policy (§10) including retraction and rollback semantics, and the test fixture structure (§12) that exercises every validator and PII detector.
For the schemas of the artefacts that advance through this state machine, see schemas.md. For PII scrub mechanics invoked at commit time, see privacy.md. For the skills-drafting protocol that produces new artefacts, see skills.md.
9. State-machine promotion
Validation-by-consensus is the corpus-growth path. The state machine is automated from day 1 (S31): D1 aggregates drive promotion of catalogue rows (volatile values, references) by direct UPDATE on the row, and drive promotion of skill / path bodies by opening a PR that flips the status frontmatter field. Maintainer review is reserved for draft PRs (target_type=skill | path; S31, brand-new artefacts); amendments, catalogue updates, and concern flows are fully automatic.
Compaction as a primary corpus-growth mechanism is superseded in v1 by validation-by-consensus and the deterministic state machine. Any future "extract patterns from concern clusters into proposed amendments" job (compaction in the old sense) is deferred to v1.1 as a separate concern; it would produce amendment (target_type=skill) submissions that enter the same state machine as any other PR.
9.1 State diagram
draft submitted (target_type=skill | path; 24h staging) ── consumer DELETE ──► [cancelled]
│
▼
Worker opens PR (S10 / S18) ──── PR-CI green ──── maintainer review (S31) ──┐
│
▼
[alpha] on canonical.md or paths/index.json
│
│ validations accumulate (D1)
▼
[beta]
│
│ validations accumulate
▼
[stable]
│
│ rejects exceed confirms by ≥2
│ (D1 supersession or git revert)
▼
(rollback mechanism;
no `rolled_back` enum value)
amendment submitted (target_type=skill | path | path_source; 24h staging) ── PR-CI green ── auto-merge
► canonical.md or paths/index.json edit;
cohort resets if `version` bumps;
status returns to alpha
amendment submitted (target_type=volatile_value | reference) ── INSERT-with-supersede in D1 ─► row at alpha;
same threshold table promotes
(fast-path; no PR pipeline)
Validation submitted (any of six target_types) ── written to D1 immediately ─► aggregates feed state machine
Concern / feedback / rating submitted ── staged + scrubbed ─► concerns / feedback_channel / ratings table
(concerns: visible via <Observations> aggregator;
feedback: operator-private triage;
ratings: <CohortStats> on canonical)
(any artefact) → quarantine via D1 supersession or git revert; maintainer reviews
≥1 injection_flag from a non-submitter
Naming discipline. The draft feedback type is the submission that introduces a new artefact; the resulting on-disk artefact's status: starts at alpha, NOT draft. PR-CI Rule 17 (§10.1 below) rejects any skill/path canonical commit authored via a draft submission whose on-disk artefact carries status: draft. Agent-facing prose should prefer "proposal" / "new-artefact proposal" to avoid confusion with status: draft (which is reserved for skeletons and pre-alpha content).
9.2 Promotion thresholds (first-pass per G.5)
These numbers are tunable post-launch — revisit at end of v1's first 90 days based on observed validation volume. The same threshold table applies to skills and to catalogue rows (volatile values, references) per S12.
| Transition | Conditions |
|---|---|
draft → alpha |
For skills and paths: 24h staging window elapses on the draft submission; Worker opens PR; PR-CI green; the maintainer reviews and merges (S31; the draft flow is the one feedback type that requires maintainer review). For catalogue rows: row INSERTed in D1 directly at status: alpha. |
alpha → beta |
≥3 confirms, 0 rejects, ≥48h since cohort start, validations from ≥3 distinct IPs (per-artefact salted hash) |
beta → stable |
≥10 confirms, ≥14 days since cohort start, confirm rate >85%, ≥10 distinct IPs |
| Rollback (any → cohort restart) | Rejects exceed confirms by ≥2 — for skill / path bodies: state machine opens a PR reverting canonical.md or paths/index.json to the prior stable (via git revert) and resetting status to the prior stable's status. For catalogue rows: state machine performs a fresh INSERT in D1 with the prior stable's value (or null if no prior stable) and supersedes the rejected row. |
| Quarantine | ≥1 injection_flag: true from a non-submitter validator (per G.6). For skills / paths: git revert + maintainer-review issue opened; the reverted body is the served body. For catalogue rows: D1 supersession + maintainer-review issue. |
Cohort-start anchor (S22, refined 2026-05-15). The cohort_started_at timestamp is the moment the artefact's current content was last committed (skills: the last commit to canonical.md that bumped version; paths: the last commit to the entry that bumped version; catalogue rows: the row's committed_at). Threshold time anchors (≥48h, ≥14 days) are measured from cohort_started_at, not from the first validation. A maintenance edit (typo, citation refresh, whitespace) on stable content keeps the cohort because version is unchanged (S25); a substantive edit bumps version and resets the cohort to alpha.
cohort_anchor Worker-stamp (C1, added 2026-05-15). On every staged row whose target_type ∈ {skill, path}, the Worker stamps cohort_anchor: <target_id>@<version> between cross-ref (submit pipeline step 6) and timing (step 7). The Worker reads the current version: from the targeted canonical at staging time; agents never carry cohort_anchor (the schema rejects it as additionalProperty). The state-machine cron uses cohort_anchor as the canonical key for "which cohort does this validation belong to" — rows whose cohort_anchor doesn't match the artefact's current <target_id>@<version> are counted as historical (do not contribute to the current cohort).
Validation cohort = matched cohort_anchor. Validations whose cohort_anchor does not match the artefact's current <target_id>@<version> do not count toward the current cohort. (Per pre-2026-05-15 semantics this was expressed as "validations submitted before cohort_started_at do not count"; the new cohort_anchor field is the precise mechanism.)
Distinct-IP counting uses the per-artefact salt described in §8.3 (see privacy.md) (self-validation prevention subsection). Because the per-artefact salt is stable for the artefact's lifetime in alpha/beta, validations submitted on different days from the same IP hash to the same value and are correctly de-duplicated. The daily-rotating salt is used only for rate-limit counters; it is not used for state-machine distinct-IP counting. The per-artefact IP record is destroyed when the artefact reaches stable or is superseded.
Rollback mechanism (S29, S36).
- Catalogue rows (D1). A rollback is a fresh INSERT — the state machine inserts a new row carrying the prior stable's content (or a null/superseded marker if no prior stable existed) and sets the rejected row's
superseded_at = now(). History is preserved: queryingWHERE uid = X ORDER BY committed_atreturns the full chain. TheGET /api/volatile-values/<uid>/historyandGET /api/references/<uid>/historyendpoints (S36) expose this directly. - Skill bodies (Git). A rollback is
git revert <last-content-changing-commit>oncanonical.md. History is preserved in the Git log;GET /api/skills/<id>/history(S36) surfaces the commit chain via the GitHub API. The post-revert body returns to its priorstatus(typically the last stable). A future re-attempt at the rejected change opens a fresh PR; there is no "un-revert" shortcut in v1.
Quarantine. A non-submitter validator setting injection_flag: true triggers immediate quarantine: the offending content is rolled back via the same mechanism (D1 supersession or git revert), and a maintainer-review issue is opened. The maintainer reviews; if the flag was bogus, the flagger's IP-hash receives a stronger rate limit (or permanent ban on repeat), and the maintainer reinstates the content via a fresh INSERT (D1) or PR (skill body). (Per G.6.)
9.3 State-machine Action
A small Action (state-machine.yml, calling tools/scripts/state-machine-tick.ts) runs on a scheduled basis (every 5–15 minutes) and on D1 webhook (when one becomes available). It:
- Queries D1 for all artefacts at
status: alphaorbeta(skills via the skills index; paths via the paths index; catalogue rows directly) - For each artefact, computes aggregates from D1: confirms, rejects, distinct IP hashes, elapsed time since
cohort_started_at, injection flags. Cohort membership is determined bycohort_anchor = <target_id>@<version>matching the artefact's current version (per §9.2cohort_anchorWorker-stamp rule). - Applies the threshold table (§9.2) deterministically
- On
→ beta/→ stablefor a catalogue row: UPDATE the row'sstatusdirectly in D1 - On
→ beta/→ stablefor a skill or path: open a PR editing the canonical's frontmatterstatusfield (or the path entry'sstatusinpaths/index.json); PR-CI green ⇒ auto-merge. The state-machine bot is the author identity (Be Civic Bot <bot@becivic.be>); the bot bundles status flips with version bumps in a single PR per the auto-version-bumping amendment (see §9.7 below). - On rollback: D1 supersession (catalogue rows) or
git revertPR (skills / paths); auto-merge on PR-CI green - On quarantine: same mechanism as rollback PLUS open a maintainer-review issue (no PII echoed)
- The Action is excluded from triggering itself (D1 UPDATEs and PRs authored by the state-machine bot do not retrigger it).
[skip ci]discipline on bot-authored commits follows theuid-fill.ymlprecedent.
The logic is purely deterministic; no LLM. The validation-as-LLM-judgment work happens consumer-side at validation submission time. Maintainer review is required only on draft PRs (new skill / path creation, S31).
9.4 Optional learning-from-concerns job (deferred to v1.1)
A future job may extract patterns from concern clusters (volatile-value drift, citation-rot accumulation, repeated commune-specific caveats) into proposed amendment submissions or catalogue corrections, which then enter the state machine like any other change. This is deferred to v1.1; v1 corpus growth does not depend on it.
Per-skill thresholds and recency windows for any such job would live in tools/compaction/config.json with PR-reviewed prompt versions in tools/compaction/prompts/v<N>.md. Architecture is sketched but not built.
9.5 Path and path-source lifecycle
Paths follow the same draft → alpha → beta → stable state machine as skills (§9.1, §9.2). Promotion thresholds and cohort semantics are identical, with separate cohorts per path entry. Sources within a path do not carry their own status field; source health is consensus-driven and expressed at the path-entry level.
When the validation cohort for a path entry accumulates rejects exceeding confirms by two or more across all its sources, the state-machine Action transitions the path entry's status to deprecated. When all sources of a path have been rejected by the validation cohort, the path entry transitions to status: deprecated. When the path entry transitions to status: deprecated, the agent surfaces this to the customer at traversal time with a message in the following form:
"Be Civic doesn't currently have a working online way to obtain this document. Would you like to visit your commune instead?"
This message is surfaced only when no source of any kind (including offline sources) remains viable. If an offline source is still available, the agent offers it as the traversal option rather than surfacing the deprecation notice.
Post-2026-05-15 normalization, path submissions land via the unified amendment (target_type=path | path_source) and draft (target_type=path) shapes — there are no longer separate path_amendment / path_draft types. The Worker routes by target_type to the appropriate D1 table or repository path. The state-machine Action (§9.3) treats path artefacts identically to skill artefacts for cohort tracking and threshold-driven promotion; no separate tick logic is required.
9.6 PR composition — research-report sidecars on submissions carrying provenance
When a submission carries provenance.research_notes_markdown (see schemas.md §6.2.2 / §6.2.4), the Worker's PR composition extends to include a research-report.md sidecar. Sidecar placement and write mode depend on the submission's (type, target_type) cell:
(type, target_type) |
Sidecar path | Write mode |
|---|---|---|
(draft, skill) |
bc-docs/skills/<proposed_id>/research-report.md |
Create — written alongside canonical.md in the same PR; both files land on main together. |
(draft, path) |
bc-docs/paths/research-reports/<proposed_id>.md |
Create — written alongside the bc-docs/paths/index.json entry insert in the same PR; both land on main together. |
(amendment, skill) |
bc-docs/skills/<target_id>/research-report.md |
Append — Worker appends a dated section to the existing research-report.md in the same PR that applies the amendment to canonical.md. If no file exists yet (legacy skill predating this convention), the Worker creates it with just the new section. |
(amendment, path) / (amendment, path_source) |
bc-docs/paths/research-reports/<path_id>.md |
Append — Worker appends a dated section to the existing path research-report in the same PR that applies the field edit or source add to paths/index.json. Creates if absent. |
Atomicity. The canonical (or path entry) and the sidecar share a single PR and a single commit on main; they MUST land or fail together. PR-CI failure on either file fails the PR.
Sidecar content. On draft (target_type=skill | path), the sidecar is the scrubbed provenance.research_notes_markdown with a frontmatter header carrying kind, submitted_at, session_count, first_session_at, last_session_at, verified_corpus_refs, and research_sources (per schemas.md §6.2.4). On amendment (target_type=skill | path | path_source), the appended section uses an H2 heading like ## 2026-05-14 — amendment <amendment_id> followed by the same metadata in a fenced YAML block, then the scrubbed body.
Auto-merge interaction. amendment PRs (target_type=skill | path | path_source) auto-merge on green PR-CI per §10.1; the sidecar file diff is part of the auto-merge surface and PR-CI gates on both the canonical edit AND the appended section (Layer 2 scrub re-applied to the appended content, size cap re-checked after append). draft PRs (target_type=skill | path) require maintainer review per S31 and §10.1 regardless of sidecar presence; the sidecar does not change the review gate.
24h staging. Staging-window semantics (§6.2 in schemas.md) apply to the submission as a whole; provenance content stages with the submission and is not separately gated.
PR-CI checks on sidecars:
- File size ≤50KB after the write (
draft) or after the append (amendment). - New content passes Layer 2 regex scrub.
- Every URL in
provenance.research_sources[]resolves (well-formedness; reachability deferred to the monthly linkcheck Action per §11).
Legacy canonicals. When an amendment (target_type=skill | path | path_source) with provenance targets a canonical authored before this convention and no research-report.md exists yet, the Worker creates the file with just the new dated section. Subsequent amendments append. No retroactive backfill of pre-convention history is required; the maintainer-side bc-corpus-creator walks remain the path for richer backfill.
9.7 Auto-version-bumping workflow (added by 2026-05-15-auto-version-bumping.md)
A scheduled version-bump.yml workflow inspects every commit on main and bumps the artefact's version: frontmatter (skill canonicals) or path-entry version: (paths/index.json) according to a deterministic mapping driven by the artefact's status: field. The mapping:
| Status | Version range |
|---|---|
draft |
0.0.x (patch increments per commit) |
alpha |
0.1.x (patch increments per commit) |
beta |
0.2.x (patch increments per commit) |
stable |
1.0.x (patch increments per commit; sub-cohort changes per OPEN-1 stable-lock) |
Bot identity. Same as the state-machine bot — Be Civic Bot <bot@becivic.be>. Differentiated via commit-message prefix (version-bump: …). The state-machine bot bundles status flips with version bumps in a single PR (OPEN-3 squash-merge collapse); only the bump observable on main post-merge counts as the canonical version.
Concurrency. No cancel-in-progress (OPEN-2 locked). Per-push correctness over latency.
Quarantine demote. Relies on the existing previous_stable_sha field on the skill's frontmatter (OPEN-5 locked); no new audit artefact.
Audit trail. Git log on the version-bumping workflow's commits is the audit trail (OPEN-4 locked); no separate JSON artefact.
First-deploy migration. When the workflow lands on a corpus where skills already carry hand-authored version: values that don't match the auto-bump rule, the workflow emits a warning (not an error) and proceeds (OPEN-6 locked). The operator may sweep manually with the optional rebase script shipped as a companion (OPEN-7).
Full workflow spec lives in bc-docs/.github/workflows/version-bump.yml; the spec records the contract, not the implementation.
10. Branch policy and retraction
10.1 Branches and CI
Single-branch flow on main. PRs target main, merged after review, skills are immediately live (subject to state-machine gating for non-canonical content). No develop branch. Consuming agents fetch skills from main. State-machine commits, communes-refresh PRs, and protocol-amendment PRs all target main.
Branch protection: main requires PR review (self-review acceptable for solo maintainer). PRs into main run a CI suite checking:
- Schema validity (skill frontmatter, concern / amendment / draft / validation / feedback-channel / rating fixtures — renamed from observation / skill-amendment / skill-draft / path-amendment / path-draft per the 2026-05-15 taxonomy normalization)
- Citation URL well-formedness (not reachability — that's the monthly linkcheck Action)
- Skills-index regeneration consistency (the index file in the PR must match what regeneration would produce — no hand-edits)
- Cross-reference validation (
tools/scripts/validate-cross-refs.ts):- Every skill's
submission_contract_versionresolves to an existingdocs/submission-contract-v<N>.mdx - Every skill's frontmatter
idequals its parent folder name - Frontmatter
statusconsistency. Everycanonical.mdcarriesstatus ∈ {draft, alpha, beta, stable}. Skeletons are atdraft(body empty or marker-only). All other content is at one ofalpha,beta, orstable. There is noproposal_id,version_status,corpus_status, orproposals//archive/directory in round 6. - State-transition validation (on commits authored by the state-machine bot):
- Threshold conditions in §9.2 must hold for the asserted transition
- Distinct-IP counting honoured (queried from D1)
- Injection-flag quarantine check: an artefact with ≥1
injection_flag: truevalidation from a non-submitter must have been quarantined (D1 supersession orgit revert) and a maintainer-review issue opened - Every
requires:field resolves to an existing skill_id (an existingskills/<id>/canonical.mdfolder). Skills atstatus: deprecatedorstatus: quarantinedare not valid targets; skills at any of the promotion statuses (draft,alpha,beta,stable) are valid (the consumer loads them at their current status; alpha banner applies recursively per §6.1 (see schemas.md)) - Type-matched inputs/outputs across the DAG; DAG is acyclic
- Every
categoryvalue matches the^[a-z][a-z0-9-]+(-[a-z][a-z0-9-]+)*$regex; deterministic Levenshtein guards (auto-extension on first commit; monthly audit) - Every
superseded_byresolves to an existing skill - Rule 7 —
authority_idresolves. When a skill's frontmatter carriesauthority_id, the value resolves to a top-level entry id indata/authorities.json - Rule 8 —
<VV>and<Ref>tag uids resolve. Every<VV uid="...">value</VV>and<span class="dsl dsl-ref">label</span>tag in body MDX resolves to an existing row in D1 (queried via/api/volatile-values/<uid>or/api/references/<uid>). Tags whoseuidis empty (authoring stage, before PR-CI uid assignment) are accepted on the authoring branch but rewritten by PR-CI before merge (§6.11 (see schemas.md)) - Rule 9 —
requires.selects_onvalues resolve. Each value supplied for aselects_onkey (region,origin_country,sponsor_type,entry_type,card_outcome) resolves to the corresponding enum inschemas/types.json.origin_countryis open against ISO-3166-1 alpha-2 lowercase. The validator emits a warning (not an error) whenschemas/types.jsonis absent, supporting phased corpus migration - Rule 10 —
origininvariant. Every skill carriesorigin ∈ {be-civic, community}. Skeletons (status: draft) MAY haveorigin: be-civiconly; community drafts are not skeletons by definition. - Rule 11 —
(name, uid)consistency on tags. For every<VV name="X" uid="Y">value</VV>and<span class="dsl dsl-ref">label</span>tag in body MDX, the row in D1 withuid = YMUST carryname = X. PR-CI rewrites the tag'snameto match D1 when D1's row was renamed since the tag was authored; PR-CI fails the PR when the tag's name + uid combination is inconsistent in a way that can't be auto-resolved. (Rename safety is in §6.11.) - Rule 12 — agents never author uids. PR-CI rejects any commit where a
<VV>or<Ref>tag'suidwas filled by a non-bot author identity (the PR validator inspectsgit blameagainst the line; a uid filled in the same commit as the tag's introduction by a non-bot author is a Rule-12 failure). Authors leaveuidempty; PR-CI fills it via the/api/_internal/catalogue-entriesendpoint (§6.11 (see schemas.md)). - Rule 13 — wrapper-tag form on first citation. Every first appearance of a volatile value or reference in a skill body MUST use the wrapper-tag form
<VV name="..." uid="..."><value or label></VV>or<span class="dsl dsl-ref"><label></span>per §6.10 (see schemas.md). Self-closing forms (<span class="dsl dsl-vv">VV: </span>,<span class="dsl dsl-ref">Ref: Ref</span>) are rejected on first appearance. Subsequent re-citations of the same reference within the same body MAY use the bibliography-style`[ref-id]`bracket form per S81. - Rule 14 — tag-only edits do not bump
version. When a PR changes ONLY tag-form (e.g. converts`[ref-id]`brackets to<Ref>wrapper tags, or populates empty children with the current catalogue value), the skill'sversionfield MUST NOT bump per §6.1 cohort-reset rules (the semantic content is unchanged). The validator detects tag-only diffs by comparing the post-edit body with the pre-edit body after stripping tag wrappers; if the stripped bodies are equal, the edit is tag-only. A PR that bumpsversionon a tag-only diff fails Rule 14. See S83. - Rule 15 —
target_type/target_idconsistency on every submission (added 2026-05-15). PR-CI rejects submission rows in D1 wheretarget_typeis not in the permitted set for that submission'stype(per the §6.2 target_type table inschemas.md), or wheretarget_iddoes not resolve to a live artefact in the right table/path. Carve-out:target_type=skill_graph(concern only) MAY have an unresolved or emptytarget_id— the corpus-graph-itself-has-a-gap signal. Cross-ref step 6 enforces the resolution at submission time with theskill_graphshort-circuit; Rule 15 lifts the same check into the formal PR-CI rule list so that any committed row that fails the invariant (e.g. due to a manual D1 edit or a Worker bug) is rejected at PR-CI. - Rule 16 — DROPPED (cohort_stats are render-time-derived, not frontmatter-materialised). Pass 1 of the 2026-05-15 proposal proposed a PR-CI rule rejecting any skill commit that authored
cohort_stats:in frontmatter. Per locked G4, cohort_stats are NOT materialised in canonical frontmatter — the<CohortStats>element is composed at render time from D1. There is no frontmatter field to author; Rule 16 is unnecessary and dropped from the rule list. (Rule numbering preserved; the slot stays at 16 to keep downstream rule references stable.) - Rule 17 —
draftsubmission type vsstatus: draftdistinction (added 2026-05-15). PR-CI rejects skill / path canonical commits where the resulting on-disk artefact hasstatus: draftAND was authored via adraftsubmission. Adraftsubmission produces analpha-status artefact on commit (per §6.2.4 inschemas.md);status: draftis reserved for skeletons and pre-alpha content authored directly by maintainers. The validator inspects the PR's commit author identity + the new file'sstatus:frontmatter; on conflict it fails the PR with aninvalid_status_for_draft_submissionerror. - Rule 18 — auto-bump consistency (added 2026-05-15 by
2026-05-15-auto-version-bumping.md; numbered Rule 18 to keep Rule 15 anchored on target_type consistency per the prior taxonomy amendment). PR-CI checks every modified canonical / path-entry'sversionagainst itsstatusand the prior commit'sversion. The post-commitversion's major.minor MUST equal the expected major.minor forstatusper §6.1 (see schemas.md). The patch increment MUST be either(prior_patch + 1)(within-major.minor edit),0(status flip in the same commit), or unchanged (tag-only edit per Rule 14, orversion_pin: trueper §6.1 override). Any otherversionvalue fails the PR with aninvalid_version_for_statuserror. The version-bump workflow's own commits ARE subject to Rule 18 (defence against bumper bugs); the rule is verifiable purely from the diff so the workflow's commits pass cleanly when the bumper is correct. Author identity is recognised viagit author.email == 'bot@becivic.be'; the validator silently accepts the bumper's correctly-formed[skip ci]commits and applies Rule 18 to the diff regardless of author. - Every fixture in
tests/fixtures/<type>/invalid/violates exactly one rule and has a corresponding rule in the relevant schema orregex-rules.json
- Every skill's
Cloudflare Workers preview deployments auto-run for PRs that change bc-infra/site/ (renderer + router) or bc-infra/api/ (staging Worker). Renderer preview auto-runs for PRs that change content (skills, docs, mdx files in bc-docs).
Path catalogue PR policy. Path catalogue entries follow the same auto-merge PR policy as skill amendments. Path drafts (new path entries, submitted via draft with target_type=path) require maintainer review before merging, matching the target_type=skill draft policy (S31). Source amendments (updates to URLs, eligibility predicates, validation_path shapes, or priority values within an existing path entry, submitted via amendment with target_type=path | path_source) auto-merge on green PR-CI, matching the skill-amendment auto-merge path. The rationale is the same: new entries carry authorial intent risk that warrants a maintainer check; amendments to existing entries are narrow changes that validators and automated checks can gate adequately.
10.2 Skill retraction and rollback
Status field — skill status encodes both the promotion lifecycle and the terminal maintainer states in a single 6-value enum (draft | alpha | beta | stable | quarantined | deprecated):
draft | alpha | beta | stable— the promotion lifecycle; consuming agents render skills at all four states (alpha and beta carry in-page banners per §6.1 (see schemas.md))deprecated— superseded or no longer recommended; consuming agents render with a prominent warning; state machine skips deprecated skills;superseded_bypoints at the replacementquarantined— content is harmful or fundamentally wrong; skill body is pulled for cause (history preserved in Git); consuming agents refuse to render; state machine skips; audit-only
Skill rollback (a content rejection or quarantine, distinct from retraction) is git revert against canonical.md, executed automatically by the state-machine bot when validation thresholds trip (§9.2) or when a non-submitter validator sets injection_flag: true. The post-revert body returns to its prior status; the cohort restarts.
Catalogue-row rollback (volatile values, references) is D1 supersession: a fresh INSERT carrying the prior stable's content marks the rejected row's superseded_at. History is preserved (S29).
Retraction does not delete files; rollback does not delete history. Git history is the audit trail for skill bodies; D1's superseded_at chain is the audit trail for catalogue rows. A quarantined skill may carry any prior status value in the audit trail (the quarantine notice replaces the served body); amendments against a quarantined skill are blocked at the Worker.
10.3 Submission retraction
If a committed submission is later found to contain PII (slipped through both consumer-side and receiving-side scrub AND was released by the maintainer from NER hold as a false positive), the only remediation that removes it from the public record is destructive history rewrite via git filter-repo. This requires:
- Maintainer acknowledgement and a Tier C-flagged decision
- Force-push to
main(the one canonical exception to "no force pushes") - Notification to consumers that they must refresh
- An incident note in
docs/incidents/<date>-<id>.md(PII details obviously not included)
Pre-emption is the protection; rewrite is the last resort. Documented in docs/retraction-protocol.md.
11. Citation rot
11.1 Source rot
Path sources (URLs, deeplinks, form addresses) are inherently rot-prone. URL changes from upstream government portals are the dominant rot vector for the Path Directory. The following mitigations apply:
Validation submissions. Every successful or failed traversal generates a validation submission with target_type=path_source and the traversal_metadata block (per §6.2.3 in schemas.md — single shape; pre-2026-05-15 the same wire was named path_validation). Aggregate validations drive promotion (toward stable) or demotion (toward deprecated) using the same threshold table as skill artefacts (§9.2). When the validation cohort accumulates rejects exceeding confirms by two or more, the state-machine Action transitions the parent path entry to status: deprecated.
CI probes. For sources without audited_document_delivery: true, CI runs periodic non-mutating probes (HTTP HEAD request, page-load with success-signal check) and submits machine-authored validation records on the same schema as customer-submitted validations. Machine-authored validations carry a submitter_type: ci_probe marker; the state machine counts them toward the cohort but applies a lower per-record weight than customer-confirmed validations (weight configuration in tools/compaction/config.json).
30-day staleness flag. A source whose last_validated timestamp is more than 30 days old is flagged by the renderer with a stale badge but remains in the catalogue and continues to be offered to customers. After 90 days without a confirming validation of any kind (customer-submitted or CI probe) across all sources of a path entry, the state machine transitions the path entry to status: deprecated. The path entry's last_validated reflects the most recent confirming validation across any of its sources.
Audited deliveries. Sources flagged audited_document_delivery: true MUST NOT be CI-probed: each call generates a real, audited document delivery on a live government system. These sources rely entirely on customer-driven validation submissions. The renderer surfaces "last user confirmation" prominently for these sources, in place of the CI-probe-derived last-validated timestamp, so customers can assess freshness before consenting to an audited delivery.
Source-class template rot. When a source_class itself changes (for example, a government portal restructures its deeplink scheme), all sources of that class are flagged for re-validation regardless of their individual last_validated timestamps. This is a maintainer-triggered operation recorded as an amendment (target_type=path_source) submission to the spec.
12. Test fixtures
tests/fixtures/ contains synthetic data exercising every validator and detector. Updated alongside any change to detection logic.
concerns/valid/*.json— realistic, fully scrubbed concerns that pass all validators. Cover everytarget_type(skill / volatile_value / reference / path / path_source / skill_graph), everyregion, single and multi-cell cases. Renamed fromobservations/valid/per the 2026-05-15 taxonomy normalization.concerns/invalid/*.json— each file violates exactly one rule, named for the rule (schema-missing-target-id.json,pii-nrn-in-body.json,skill_graph_with_resolvable_target_id.json, etc.).amendments/valid/<target_type>/*.json— clean amendments grouped bytarget_type:skill/covers body (unified-diff) and frontmatter subtypes;path/andpath_source/cover field_edit and source_add;volatile_value/andreference/cover the fast-path scalar shapes.amendments/invalid/<target_type>/*.json— each violates exactly one rule (capability tier mismatch, oversized rationale, identity-shaped field,skill_commitdrift, source_class template non-conformance, etc.).drafts/valid/<target_type>/*.json—skill/covers full main-skill and sub-skill drafts;path/covers full path-entry drafts.drafts/invalid/<target_type>/*.json— each violates one rule (missing required frontmatter, oversized commit_message, missingrequiresresolution, proposed_id already exists, etc.).validations/valid/*.json— confirm + reject + injection_flag examples for everytarget_type, against active submission IDs in fixtures.feedback_channel/valid/*.jsonandfeedback_channel/invalid/*.json— bug, suggestion, praise, confusion, accessibility shapes (new in 2026-05-15 amendment).ratings/valid/*.jsonandratings/invalid/*.json— three-axis star fixtures per §6.2.7 (Lock A, sprint 2026-W23).validations/invalid/*.json— self-validation attempt, oversizedrationaleorinjection_reason, missingrationaleon reject, missinginjection_reasonwheninjection_flag: true, etc.pii-samples/*.txt— text snippets with known PII, accompanied by<name>.expected.jsonlisting detector hits.nrn-checksum/*.json— known valid and invalid Belgian NRN values with expected outcomes.
A dedicated test-fixtures Action runs these on every PR touching tools/scrub/, schemas/, or any submission schema (separate from the per-commit NER Action).
Path entry fixtures. Path entries are tested via two complementary fixture types:
- Per-source
validation_pathfixtures (intests/fixtures/path-sources/) — HTTP response samples and page-text samples paired with each source'ssuccess_signalsandfailure_signals. The validator checks that the signal classifiers correctly identify each sample assuccess,failure, orambiguous. Everysource_classtemplate MUST have at least one valid and one invalid sample in this directory. - Per-path eligibility fixtures (in
tests/fixtures/path-eligibility/) — sampleprofile.jsonobjects with varyingregion,civic_status,residency_status, andcommune_nis5values, paired with expected source-inclusion outcomes. The validator confirms that each path's eligibility predicates include and exclude sources correctly for each sample profile. Edge cases MUST include a profile that matches zero sources (expected outcome:no-eligible-sources) and a profile that matches multiple sources in priority order.
Path fixture validation runs as part of the existing test-fixtures Action. No separate Action is required.
Running tests locally. All test runners are TypeScript and invoked via npx tsx:
| Suite | Command |
|---|---|
| Schema validation against fixtures | npx tsx tools/scripts/validate-fixtures.ts |
| Cross-reference validation | npx tsx tools/scripts/validate-cross-refs.ts |
| State-machine logic against synthetic validations | npx tsx tools/scripts/state-machine-tick.ts --dry-run |
| PII-samples regression | npx tsx tools/scripts/validate-pii-samples.ts |
| NRN checksum implementation | npx tsx tools/scripts/validate-nrn-checksum.ts |
| Category audit | npx tsx tools/scripts/audit-categories.ts |
Cross-references
Cross-doc references are inlined throughout this document in the form §X.Y (see
- §3 (Non-negotiable principles) — see
architecture.md§3 - §6.1 (Skill schema / status enum / alpha banner) — see
schemas.md§6.1 - §6.2 (Submission schemas / staging windows) — see
schemas.md§6.2 - §6.2.4 (Validation submission / immediate D1 write) — see
schemas.md§6.2.4 - §6.3 (Volatile values / INSERT-with-supersede) — see
schemas.md§6.3 - §6.11 (Catalogue UID convention / PR-CI uid assignment) — see
schemas.md§6.11 - §6.12 (Path Directory schema — new section) — see
schemas.md§6.12 - §7 (Trust model / maintainer review queue) — see
protocol.md§7 - §8.3 (Per-artefact salted IP hash for self-validation prevention) — see
privacy.md§8.3 - §8.7.2.2 (path_history files) — see
privacy.md§8.7.2.2 - §8.10.4 (Paths anonymous-by-construction) — see
privacy.md§8.10.4 - §15.1 (Skill-drafting protocol / submission flow) — see
skills.md§15.1