Run-record integrity: typed-comment validation and suite provenance stamp #57
Labels
No labels
component:adapters
component:lifecycle
component:qa
component:setup
component:shared
component:worktrees
phase/accepted
phase/backlog
phase/deployed
phase/developing
phase/integrating
phase/planning
phase/qa
phase/requirements
phase/uat
priority:critical
priority:critical
priority:high
priority:high
priority:low
priority:low
priority:medium
priority:medium
type:bug
type:chore
type:docs
type:feature
type:infra
type:tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jbr870/devwork-skills#57
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PREQ: Run-record integrity — typed-comment validation + suite provenance stamp
Created: 2026-08-24 · Panel-amended: 2026-08-24 (Tier 3, all five lenses)
Problem
The Phase Outcome ledger is the pipeline's only durable account of what happened on a feature,
and it is untrustworthy on both of its axes — what it counts and what it can attribute.
What it counts (#54, reproduced live on #50): any comment whose first line merely contains
a schema token (e.g.
phase-outcome:v1) is absorbed into the ledger with no shape validation.Three consequences, in severity order:
count of scanned comments. One stray note made #50's next Phase Outcome due to mint as
PO-50-7whilePO-50-6did not exist — a gap that trips the density check on everysubsequent read, forever, naming nothing.
jq: null (null) only strings can be parsed— no comment id, no field, no diagnosis. Everyconsumer that funnels through it (including
/dev:resolvedecision discovery) dies the same way.immutability seemed to protect it — the very defect made its own fix look illegal.
The exposure is a family, not a one-off: every typed comment schema
(
decision-resolution:v1,work-unit-outcome:v1,qa-report:v1, …) is matched the samesubstring way. And the trigger is ordinary: the offending header was written by an agent
reaching for the nearest-looking schema name for a note that had no schema.
What it can attribute (#34, record half): nothing records which suite text produced a Phase
Outcome. On the dev machine the skill text is a live checkout that received 20+ commits during
one measured run; on a consumer machine it is an installed release — and in the one real
consumer run, the manual "record the version" runbook step was not done (UAT-F33). A "resolved
at HEAD" verification therefore says nothing about what text a given run actually followed, and
cross-machine or cross-day comparisons are confounded.
Users:
reads that name their failure instead of dying opaquely.
/meta-amendverification) —they need any Phase Outcome to be attributable to the exact suite text that produced it.
Current state: substring header matching, count-based ordinals, bare jq stack errors, no
provenance field; attribution rests on an operator remembering a README step (measured: not done).
Proposed Solution
Make the typed-comment record defensively written, defensively read, and self-attributing:
first line matches the schema's declared header shape (
<!-- {schema} id=… skill=… -->-class,per schema; the exact pattern per schema is stated in that schema's doc — a plan deliverable).
A near-miss (schema token present in the first line, anchored shape absent) is never collected —
and is surfaced as a warning naming the comment, so a typo'd real record cannot be silently
dropped. A schema token on a non-first line is a plain comment, not a near-miss. Schema
header shapes are mutually exclusive by construction (the schema name is the first token).
max(ordinal parsed from id) + 1over records that actually parse — never from a count ofscanned comments. Over an empty ledger this mints ordinal 1. Over a ledger with a
pre-existing gap it continues from the max (it does not fill gaps; repairing
already-corrupted ledgers is out of scope).
its body cannot be parsed, the writer refuses to mint and the reader fails with a diagnosis —
never a bare jq error. Both name the comment id and the reason (the missing attribute or the
unparseable element, to the precision the parser has), and the refusal names the sanctioned
repair route (fix or strip the malformed comment — which is not §8-protected, because a
comment that does not parse as a record is not a record). A record tool never counts, and
never writes past, something it cannot read.
The asymmetry with (1) is deliberate: a near-miss header makes no well-formed claim to be a
record, so it is warned about and excluded; an anchored header is such a claim, and a claim
the tooling cannot read must stop the writer rather than be guessed around.
phase-outcome:v1records theidentity of the suite text that produced it, resolved from the suite root (the resolved
plugin/skills root the run executes from) by one probe with no mode flag and no operator
declaration: root inside a git work tree → commit SHA + dirty/clean marker; otherwise →
the release version read from the plugin's own manifest at that root
(
.claude-plugin/plugin.json), which every install carries. If the probe cannot decide(e.g.
gitunavailable mid-probe, manifest missing), the stamp records an explicitunknown— provenance never blocks a post. Existing records without the field remainreadable.
Scope: Standard — matching/diagnosis fixes apply to all typed schemas at the code path
they share (fix the family once); the new provenance field lands on the Phase Outcome schema
only (whether as an optional
:v1field or a:v2is a plan-time decision).User Stories
the Phase Outcome ordinals, so that the ledger's density invariant survives contact with
imperfect input.
so that I can repair the actual cause instead of debugging a jq stack trace.
produced it, so that run behaviour is attributable after the fact on both machines.
actually executed, so that retrospective findings rest on evidence rather than assumption.
Acceptance Criteria
"Warns/warning" below always means: a message on the helper's diagnostic (non-data) output
stream that includes the forge comment id of the comment it concerns. Warnings recur on every
affected post/read until the stray comment is cleaned up — persistence is intended.
comment whose first line contains
phase-outcome:v1but does not match the anchored headershape, when a new Phase Outcome is posted, then the new record is minted at ordinal N+1
(no skip), and the post warns, naming the near-miss comment's id.
read succeeds (exit 0), returns exactly the N parsed records, excludes the near-miss, and
warns naming its comment id.
as the schema, when a new Phase Outcome is posted, then the post exits non-zero, no new
comment exists on the issue afterwards, and its error names the offending comment id, the
missing/unparseable element, and the repair route.
non-zero with a diagnosis naming the comment id and the missing/unparseable element — never
an unhandled jq error.
decision-resolution:v1) and a near-misscomment for it, when that schema's records are scanned through the shared matching path,
then the near-miss is excluded with a warning; and the ledger consumers enumerated under
Dependencies, exercised against such a ledger, complete without error.
Phase Outcome is posted, then it is minted at ordinal 1; and a read of an empty ledger
reports empty without error.
posted, then the record carries the checkout's commit SHA and a dirty/clean marker —
demonstrated in both a clean and a dirty state.
the same post invocation runs unchanged, then the record carries the release version that
the manifest at the suite root declares.
Outcome is posted, then the post still succeeds and the record carries an explicit
unknown-provenance marker.
are well-formed), when it is read with the new tooling, then the read exits 0, returns the
same records as the pre-feature tooling did, and emits no warnings.
Out of Scope
are already immutable per #50; only the dev box's live checkout can move).
pipeline_statuslookinglike a broken helper. Same "diagnose, don't stack-trace" principle, different surface.
filled or healed; the density check's existing behaviour on legacy gaps is unchanged.
readability is required; backfill is not.
The existing collision check keeps its role; a single-orchestrator-per-issue assumption stands.
~/.claude/plugins/installed_plugins.jsonas an authority — its path and shapeare Claude Code's, not ours; the version authority in the installed case is the plugin's own
manifest at the suite root.
explicit that a comment which does not parse as a record is not one, so repairing it is
sanctioned. The rule itself is untouched.
Dependencies
shared by the four adapters (
local-fs,tea-cli,glab-cli,gh-cli); the contract(
forge-contract.md) and each schema doc must state the exact header pattern being enforced.The plan must confirm the match genuinely lives at one shared path — if it turns out to be
per-adapter, family-wide coverage is a real cost to re-estimate, not a free win.
field or becomes
:v2is deferred to/dev:technical-plan— the retry-guard normalisation andboth readers parse these records, and every already-posted PO lacks the field.
/dev:resolvedecision discovery, theintegrate/promote resume detections, and the QA retry guard — all read through the affected
helpers; their behaviour on the new diagnostics must not regress.
and ids are read back through adapter primitives; no other external system is touched.
Timeline
Notes
refusals must identify the failing comment per the "a gate's failure output identifies the
failure" rule (#49's principle, applied here). Provenance detail for the plan: "dirty" =
non-empty
git status --porcelainscoped to the suite root's repo; symlinked roots resolvebefore probing.
:v2; exact layerfor the anchored match (adapter primitive vs shared skill-facing path) — the AC constrains the
behaviour, not the placement.
an unreadable collected record set to "refuse loudly" over "warn and continue" — this is the
audit trail, and a stall is the loud failure. Both decided by the orchestrator under the
2026-08-24 decision-escalation protocol.
amendment in this revision. Notable: the two ordinal phrasings were reconciled (parsed-max
rule is authoritative; AC1's "N+1" holds only for contiguous ledgers, now stated); "warning"
and AC10's baseline were made observable; the installed-version authority was named (root
manifest); probe edge cases got AC9 (explicit
unknown, never blocks). Simpler-alternativechallenges considered and declined with reasons: parsed-ordinals alone fixes minting but not
the read crash, the silent drop, or the repair trap (matching is the diagnosis layer);
provenance is kept in this feature because it shares the writer and the schema decision;
the dirty flag stays because a dirty-checkout SHA without it misattributes exactly the runs
the meta lane cares about.
(tech-debt); #34's pinning half split to #56. #54's three-fix hypothesis and #34's
two-machine ground-truth table (measured 2026-08-22) are the seed material.
Linked: this issue is parent #54 (recorded by the devwork pipeline).
Linked: this issue is parent #34 (recorded by the devwork pipeline).
Linked: this issue is sibling #56 (recorded by the devwork pipeline).
Test Plan: run-record-integrity-typed-comment-validation-and-suite-provenance-stamp
Prerequisites
This feature has no browser or service surface — every scenario is executed against the suite's
record tooling and observed through exit codes, emitted diagnostics, and the comments present on
a forge issue afterwards. The state the scenarios need:
freely write (never a real feature's ledger).
malformed records).
in both a clean and a locally-modified state; (b) an installed-plugin-shaped directory that
is not inside any git work tree and carries the plugin's own manifest declaring a version;
(c) a root where neither identity is determinable.
tooling before this feature (read-only — it is never written to).
Required Test Data
ordinals 1..N, written by the record tooling itself.
does not match the anchored header shape (the real-world shape: a prose note borrowing the
schema name).
does not parse as the schema (e.g. the JSON fence absent or truncated).
prose first line).
Test Scenarios
Scenario 1: A look-alike comment cannot shift the numbering
Acceptance criterion: AC1 — near-miss present, new record minted at N+1 (no skip), post warns naming the near-miss comment's id.
Expected outcome: the ledger numbering is exactly as if the look-alike did not exist, and the operator was told which comment to clean up.
Scenario 2: Reading past a look-alike succeeds and says so
Acceptance criterion: AC2 — reads succeed (exit 0), return exactly the N parsed records, exclude the near-miss, warn naming its comment id.
Expected outcome: consumers get a clean, complete ledger plus a pointer to the stray comment — never a crash.
Scenario 3: A record that claims the format but can't be read blocks the writer, loudly
Acceptance criterion: AC3 — post exits non-zero, no new comment created, error names comment id + unparseable element + repair route.
Expected outcome: the writer refuses to extend a ledger it cannot fully read, and its refusal is a repair instruction, not a stack trace.
Scenario 4: The same bad record makes reads diagnose, not crash
Acceptance criterion: AC4 — reader exits non-zero with a diagnosis naming comment id + element; never an unhandled jq error.
Expected outcome: the first thing a failed read tells you is which comment broke it and why.
Scenario 5: The fix covers the whole family of record types
Acceptance criterion: AC5 — near-miss for another typed schema is excluded with a warning via the shared path; the enumerated ledger consumers complete without error.
Expected outcome: no typed record family remains where a look-alike comment can crash or pollute a consumer.
Scenario 6: The very first record of a feature
Acceptance criterion: AC6 — empty ledger: first post mints ordinal 1; a read of an empty ledger reports empty without error.
Expected outcome: a brand-new feature's first record behaves exactly as every feature's first record always has.
Scenario 7: A dev-checkout run stamps the commit it ran, and whether the tree was clean
Acceptance criterion: AC7 — suite root inside a git work tree: record carries the checkout's commit SHA and a dirty/clean marker, demonstrated in both states.
Expected outcome: a record from the dev machine says exactly which commit produced it — and admits when the tree had uncommitted changes on top.
Scenario 8: An installed-plugin run stamps its release version, with the identical invocation
Acceptance criterion: AC8 — suite root not inside a git work tree: the same post invocation, unchanged, records the version the root's own manifest declares.
Expected outcome: consumer-machine records are attributable to a release with nobody remembering anything.
Scenario 9: When the tooling can't tell, it says "unknown" and never blocks the record
Acceptance criterion: AC9 — probe cannot determine either identity: post still succeeds; record carries an explicit unknown-provenance marker.
Expected outcome: provenance trouble can never cost you the record itself.
Scenario 10: Yesterday's ledgers still read clean
Acceptance criterion: AC10 — a pre-feature ledger of tooling-written records: read exits 0, returns the same records as the pre-feature tooling did, emits no warnings.
Expected outcome: every ledger written before this feature is untouched by it — no migration, no noise.
Traceability
Forward: AC1→S1, AC2→S2, AC3→S3, AC4→S4, AC5→S5, AC6→S6, AC7→S7, AC8→S8, AC9→S9, AC10→S10.
Backward: every scenario names its criterion above. No orphans; no implementation-necessity
exceptions were flagged (the project's observability policy is
none).Software Requirements: run-record-integrity
Context
The Phase Outcome ledger — typed
<!-- schema:v1 … -->comments on forge issues — is the pipeline'sonly durable account of what happened on a feature, and it is untrustworthy on both axes: any comment
whose first line merely contains a schema token is absorbed as a record (corrupting ordinal minting
and killing reads with bare jq errors — #54, reproduced live on #50), and nothing records which suite
text produced a Phase Outcome (#34 record half). Users are the orchestrator model mid-run and the
operator/meta-lane doing retrospective attribution.
Approaches Considered
Approach A: Anchored matching inside each adapter primitive
Summary: Change all four
scan_comments.shprimitives to enforce the anchored header shape.Pros: Filtering happens at the source; consumers untouched.
Cons: Four implementations of schema knowledge in the dumb-transport tier; the primitive contract
changes for every adapter (and any future adapter must re-implement it); near-miss warnings need a
diagnostic channel from primitives that don't have one; violates "fix the family once".
Effort: High
Approach B: Shared validation tier in
_lib.sh(selected)Summary: Adapters stay coarse substring prefilters (contract unchanged); one new shared function
partitions anchored records from near-misses, warns, and every skill-facing consumer routes through it.
Pros: Genuinely one shared path (every consumer already sources
_lib.sh); zero adapter changes;uniform warning channel (stderr, the helper tier's existing
_logconvention); mechanicallyenforceable (lint: no scan call outside the shared function).
Cons: Near-misses travel from the adapter to the shared tier before being excluded (negligible —
they must be seen anyway to be warned about).
Effort: Medium
Approach C: Standalone
typed-scan.shwrapper helperSummary: Same partition logic, but as a new bin script consumers exec instead of a sourced function.
Pros: Isolation; testable as a unit.
Cons: One extra process per scan; consumers already source
_lib.sh, so a script adds surfacewithout adding capability; the post helper needs the parsed records too, which a sourced function
shares more naturally.
Effort: Medium
Decision
Selected: Approach B — shared validation tier in
_lib.sh.Rationale: The PREQ's family-wide requirement hinges on one shared path existing;
_lib.shisthat path (every scan consumer sources it). The adapter primitive's substring match stops being a
correctness surface and becomes an over-inclusive prefilter — which is exactly what lets the shared
tier see near-misses in order to warn about them.
Architecture
Component Overview
_typed_scancontract (explicit): inputKIND ISSUE;KINDvalidated against[a-z0-9-]+before any use. Calls
_prim scan_comments --header "KIND:v1". Returns on stdout a JSON array in thesame envelope as the primitive —
[{comment_id, created_ms, edited_ms, raw_body}, …]— filteredto anchored records; near-miss warnings go to stderr. A primitive failure propagates as a hard
failure (die), never as an empty result — a failed scan treated as empty would mint a duplicate
ordinal 1. Empty successful scan returns
[], exit 0.Data Flow
phase-outcome-post.sh→_typed_scan phase-outcome N→ pre-mint parse pass over everyanchored record (refuse loudly on any unparseable one, naming comment id + element + repair route) →
ordinal =
((map(.ordinal) | max) // 0) + 1(empty → 1; gaps continued past) → collision check +retry guard against parsed records (retry guard compares the highest-ordinal record) → body JSON
gains optional
"suite": <_suite_provenance>→ post.phase-outcome-read-all.sh→_typed_scan→ §8 immutability check over anchored records(unchanged in strength for records: an edited anchored record still hard-stops) → per-record parse
with diagnosis (comment id + missing/unparseable element; never a bare jq error) →
suitepassedthrough (
nullon pre-feature records). Density check behaviour on legacy gaps unchanged — and itis also the read-side tamper-evidence: a record edited away from its anchored shape, or a duplicate
parsed ordinal, leaves a gap/dup the density check refuses loudly.
_typed_scan KINDand keeps its ownnarrowing (exact-ref token filter, attr filters) on the anchored set. The one skill-text fence that
invokes the primitive directly (
requirements/procedures/requirements-from-deferred.mdStep D3) isre-pointed at
comments-scan.sh.Anchored header rule (family-wide)
First line (after stripping a trailing
\r— forge APIs may return CRLF) must match:^<!-- {kind}:v1( +{key}={value})* -->$— the schema token anchored as the first token after<!--,followed only by space-delimited
key=valueattributes. Three disjoint outcomes:exits non-zero naming id + element). Repair is an operator action, never automated: delete the
malformed comment, or edit its first line so it no longer claims the schema — a comment that does
not parse as a record is not a record, so §8 does not protect it. Do not edit it into a valid
record: an edited record still violates §8.
recurs every run until cleaned). A near-miss with
edited_ms > created_msgets a distinct,louder diagnostic ("edited near-miss — if this was once a record, its §8 history is broken;
verify before cleaning"), because an edit is how a record could be made to vanish; for Phase
Outcomes the density check additionally hard-stops on the resulting gap.
Each schema doc states its concrete pattern; the generic rule is the one implementation. jq hygiene
(constraint, lint-visible): comment content and KIND reach jq only via
--arg/--rawfile— neverstring-spliced into a jq program or shell command.
Suite provenance probe
Root =
_SKILLS_ROOT(already derived fromBASH_SOURCEin_lib.sh— never the caller's cwd,which is the project repo), resolved physically (
pwd -P) so the deployed symlink farm probes thereal checkout. Then, in order:
git -C "$root" rev-parse --is-inside-work-treeoutputstrue→{"source":"git", "sha": <rev-parse HEAD>, "dirty": <status --porcelain non-empty, scoped to that repo>}..versionfrom"$root/../.claude-plugin/plugin.json"(the plugin manifest every installcarries;
marketplace.json#plugins[0].versionis the repo-release-side field and is NOT probed)→
{"source":"release", "version": …}.{"source":"unknown"}.The function is guarded so it always exits 0 with a JSON object (checks command output, not just
rc; safe under
set -einside$(…)), because provenance must never block a post (AC-9).External Data Contracts
comment_idstring;created_ms/edited_msepoch-ms ints (equal when unedited);raw_bodyreturns the posted header byte-exact on line 1recorded→external-contracts/tea-scan.provenance.jsonphase-outcome:v1records — AC10 baseline)<!-- phase-outcome:v1 id=PO-N-k skill=s -->, JSON-fence body, nosuitefield; #50 ledger re-read live: 7 contiguous recordsrecorded→external-contracts/tea-scan-po-issue57.sample.jsonKey Decisions
_lib.sh), not adapter primitives_typed_scanever saw them (panel: architect)suitebody field, stays:v1; the schema doc states the general policy (additive optional body fields = no bump) and the retry-guard's excluded-field setsuite: null;:v2forces migration for zero consumer benefit_SKILLS_ROOT→ git worktree → parent-dirplugin.json→unknownsuite_log; distinct diagnostic for edited near-missesTechnical Risks
fold-/promotion-candidatesuse2>/dev/null) suppress near-miss warnings_typed_scanunderset -e: consumers that tolerated scan failure now diescripts/lint-conventions.shgates; new code is jq-centric; jq precedence trap ((max // 0) + 1mis-parse) called out in Data FlowExpert Review
Tier 2 panel on
fable: Backend Developer, Solution Architect, Security Specialist (one responseeach, 300-token cap, no cross-talk).
Reviewers
BASH_SOURCE, not cwd (helpers run withcwd = the project repo); edited-record-into-near-miss escapes §8; jq precedence/CRLF/KIND-injection
and
set -edetails; manifest field ambiguity; fence-aware lint._typed_scanreturnenvelope unspecified; primitive-failure vs empty semantics; versioning-policy generalization;
normalization excluded-field set; exempt-consumer audit.
forgery / authorship trust boundary; who is sanctioned to strip; jq injection hygiene; dirty-SHA
surfacing at release time; read-side duplicate-ordinal semantics.
Changes Made
_SKILLS_ROOT(BASH_SOURCE-derived), never cwd (Backend).read-side tamper-evidence for edit-away and duplicate ordinals (Backend + Security, raised
independently — promoted).
scan_commentsrow (Architect)._typed_scanenvelope + primitive-failure-propagates semantics specified (Architect).(Security).
--arg/--rawfileonly, KIND validated[a-z0-9-]+), CRLF stripping, jq max//0precedence,
set -e-safe probe absorbed as stated constraints (Backend + Security).plugin.json#versionfor installs;marketplace.jsonis release-sideonly (Backend).
lint-conventions.shfence tracker (Backend; CLAUDE.md#50 lesson).
requirements-from-deferred.mdStep D3 re-pointed atcomments-scan.sh(Architect's exempt-consumer audit; found by grep).
(Architect).
Noted (not actioned)
rejected on scope and portability grounds: the PREQ threat model is imperfect input from the
orchestrator itself, not a malicious forge-writer (who can already delete/edit anything the ACLs
allow — the forge ACL is the trust boundary); and
local-fshas no author identity at all, so acontract-level authorship check is not implementable portably.
outside this PREQ's ACs; the natural home is #56's detection follow-on ("did the suite text move
mid-run"), to be decided after this feature ships.
_suite_provenancein a separate sourced file (Architect) — rejected: a clearly-marked sectionin
_lib.shgives the same separation without adding a second sourcing surface.Acceptance Criteria
_shared/procedures/test/typed-scan.sh(S1)_shared/procedures/test/provenance.sh(S7)Route-through rule (mechanical, AC-5's second half): no
_prim scan_commentsor direct{adapter}/bin/scan_comments.shinvocation may exist in_shared/procedures/bin/*.shoutside_typed_scanitself, and no skill-text shell fence may invoke the primitive directly — enforced by anew fence-aware check in
scripts/lint-conventions.sh(reusing its existing fence tracker).Implementation Scope
Areas
plugin/skills/_shared/procedures/bin/_lib.sh_typed_scan,_suite_provenance(own marked section)bin/phase-outcome-post.shsuitestampbin/phase-outcome-read-all.sh(read-latest inherits)suitepass-throughbin/decision-resolution-read-state.sh,deliverable-get.sh,comments-scan.sh,issue-fold-finding.sh,issue-unfold-finding.sh,fold-candidates.sh,promotion-candidates.sh_typed_scanplugin/skills/_shared/procedures/test/typed-scan.sh,test/provenance.shscripts/lint-conventions.shforge-contract.md(primitives-tablescan_commentsrow incl. superset rule, §8 non-record note),schemas/*.md(anchored pattern per typed schema;phase-outcome.v1.mdalso:suitefield, parsed-ordinal wording, versioning policy, normalization excluded set),procedures/phase-outcome.md,requirements/procedures/requirements-from-deferred.md(Step D3 call site)File Boundaries
_lib.sh+ post + read-all are one dependency chain (sequential). The seven satellite consumers areindependent of each other once
_typed_scanexists (parallelizable). Docs and lint rule areindependent of everything except final naming. Test suites are written red-first against the intended
behaviour.
Dependencies & Sequencing
_lib.shfunctions (+ red-first suites) → 2. post + read-all → 3. satellite consumers (parallel)→ 4. docs + lint rule. AC-10 regression check runs read-only against real ledgers at each step.
Constraints & Non-Goals
Constraints:
mapfile, nozsh-reserved names in emitted glue, JSON via jq only.
--arg/--rawfileonly; KIND validated[a-z0-9-]+.superset wording. No adapter
bin/edits.Non-goals (do NOT build):
continued past, never filled); provenance backfill onto existing records; concurrent-writer races;
treating
installed_plugins.jsonas a version authority; any §8 rule change; comment-authoridentity verification (see Noted).
Test Plan: run-record-integrity-typed-comment-validation-and-suite-provenance-stamp
Prerequisites
This feature has no browser or service surface — every scenario is executed against the suite's
record tooling and observed through exit codes, emitted diagnostics, and the comments present on
a forge issue afterwards. The state the scenarios need:
freely write (never a real feature's ledger).
malformed records).
in both a clean and a locally-modified state; (b) an installed-plugin-shaped directory that
is not inside any git work tree and carries the plugin's own manifest declaring a version;
(c) a root where neither identity is determinable.
tooling before this feature (read-only — it is never written to).
Required Test Data
ordinals 1..N, written by the record tooling itself.
does not match the anchored header shape (the real-world shape: a prose note borrowing the
schema name).
does not parse as the schema (e.g. the JSON fence absent or truncated).
prose first line).
Test Scenarios
Scenario 1: A look-alike comment cannot shift the numbering
Acceptance criterion: AC1 — near-miss present, new record minted at N+1 (no skip), post warns naming the near-miss comment's id.
Expected outcome: the ledger numbering is exactly as if the look-alike did not exist, and the operator was told which comment to clean up.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project).Scenario 2: Reading past a look-alike succeeds and says so
Acceptance criterion: AC2 — reads succeed (exit 0), return exactly the N parsed records, exclude the near-miss, warn naming its comment id.
Expected outcome: consumers get a clean, complete ledger plus a pointer to the stray comment — never a crash.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project).Scenario 3: A record that claims the format but can't be read blocks the writer, loudly
Acceptance criterion: AC3 — post exits non-zero, no new comment created, error names comment id + unparseable element + repair route.
Expected outcome: the writer refuses to extend a ledger it cannot fully read, and its refusal is a repair instruction, not a stack trace.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project).Scenario 4: The same bad record makes reads diagnose, not crash
Acceptance criterion: AC4 — reader exits non-zero with a diagnosis naming comment id + element; never an unhandled jq error.
Expected outcome: the first thing a failed read tells you is which comment broke it and why.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project).Scenario 5: The fix covers the whole family of record types
Acceptance criterion: AC5 — near-miss for another typed schema is excluded with a warning via the shared path; the enumerated ledger consumers complete without error.
Expected outcome: no typed record family remains where a look-alike comment can crash or pollute a consumer.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project).Scenario 6: The very first record of a feature
Acceptance criterion: AC6 — empty ledger: first post mints ordinal 1; a read of an empty ledger reports empty without error.
Expected outcome: a brand-new feature's first record behaves exactly as every feature's first record always has.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project).Scenario 7: A dev-checkout run stamps the commit it ran, and whether the tree was clean
Acceptance criterion: AC7 — suite root inside a git work tree: record carries the checkout's commit SHA and a dirty/clean marker, demonstrated in both states.
Expected outcome: a record from the dev machine says exactly which commit produced it — and admits when the tree had uncommitted changes on top.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/provenance.sh(new suite delivered red-first by this slice; exercises git-clean, git-dirty, manifest-only and undecidable suite-root shapes via temp copies of the helper tree).Scenario 8: An installed-plugin run stamps its release version, with the identical invocation
Acceptance criterion: AC8 — suite root not inside a git work tree: the same post invocation, unchanged, records the version the root's own manifest declares.
Expected outcome: consumer-machine records are attributable to a release with nobody remembering anything.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/provenance.sh(new suite delivered red-first by this slice; exercises git-clean, git-dirty, manifest-only and undecidable suite-root shapes via temp copies of the helper tree).Scenario 9: When the tooling can't tell, it says "unknown" and never blocks the record
Acceptance criterion: AC9 — probe cannot determine either identity: post still succeeds; record carries an explicit unknown-provenance marker.
Expected outcome: provenance trouble can never cost you the record itself.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/provenance.sh(new suite delivered red-first by this slice; exercises git-clean, git-dirty, manifest-only and undecidable suite-root shapes via temp copies of the helper tree).Scenario 10: Yesterday's ledgers still read clean
Acceptance criterion: AC10 — a pre-feature ledger of tooling-written records: read exits 0, returns the same records as the pre-feature tooling did, emits no warnings.
Expected outcome: every ledger written before this feature is untouched by it — no migration, no noise.
Lane:
integration-covered—plugin/skills/_shared/procedures/test/typed-scan.sh(new suite delivered red-first by this slice; drives the record tooling against the local-fs adapter in a temp project). Additionally executed read-only against the real #50 and #57 ledgers during QA validation.Traceability
Forward: AC1→S1, AC2→S2, AC3→S3, AC4→S4, AC5→S5, AC6→S6, AC7→S7, AC8→S8, AC9→S9, AC10→S10.
Backward: every scenario names its criterion above. No orphans; no implementation-necessity
exceptions were flagged (the project's observability policy is
none).Decomposition follows the SREQ's own dependency chain (§Dependencies & Sequencing): the shared tier
plus the PO post/read pair are one chain and land first with the two red-first executed suites
(WU-1); the seven satellite consumers are a mechanical route-through once
_typed_scanexists(WU-2); docs and the fence-aware lint rule come last since the lint rule can only go green after
every consumer is routed (WU-3). One deviation from the SREQ file list: S5's consumer-facing
scenarios live in a new
test/typed-scan-family.showned by WU-2 rather than being appended toWU-1's
typed-scan.sh— work units must not share files. The workspace is a single worktree, sowaves execute sequentially (Test Writer → Implementer per WU, one WU at a time). WU-3 is
artifact-shape (docs + a lint rule that is itself the mechanical verification), so its Test Writer
is skipped per /dev:develop 3.2; WU-1 and WU-2 have behavioral ACs and run full 2-phase TDD.
Additional per-schema anchored-pattern statements (SREQ: "each schema doc states its concrete
pattern") are covered in WU-3 via
schemas/README.mdstating the generic family-wide rule, withphase-outcome.v1.mdcarrying its concrete pattern plus thesuitefield, versioning policy, andretry-guard excluded-field set; other schema docs are touched only if the implementer finds they
already restate a header shape that contradicts the rule.
Development Journal — WU-57-3-1 (shared validation tier + PO write/read chain)
2026-08-24T00:00 DISCOVERY: the anchored partition and the PO parse are two different jobs
Context:
_typed_scanper the SREQ answers one question — "is this comment a record ofkind K?" — and it is kind-agnostic. But both post and read-all then need a second,
phase-outcome-SPECIFIC pass ("does this record actually parse?") whose refusal text is
identical in both (comment id + element + the operator repair route).
Pattern: kept
_typed_scangeneric, and added a small sibling_po_parse CONTEXT RECORDS_JSONin_lib.shthat owns the parse-and-diagnose pass. It returnsordinal-ascending records with
{id, skill, ordinal, body, missing[]}and dies with thefull diagnosis if any
missing[]is non-empty. Post uses.bodyfor the retry guard and.ordinalfor minting; read-all projects the record fields. Only the caller's verb differs,so
CONTEXTis the only parameter.Why it matters: the alternative was the same ~20-line jq program and the same multi-line
refusal message duplicated in two files, which is exactly the drift surface the SREQ's
"fix the family once" argument was written against.
_lib.shalready carriesphase-outcome-specific logic (
_validate_producer, the ordinal/collision exit codes), sothis is in-layer, not a new concern. Noted as a small deviation from the WU brief's literal
"add
_typed_scanand_suite_provenance" — it adds a third private function to a filethe WU already owns.
2026-08-24T00:20 STRUGGLE: jq
//collapses a correct booleanfalseProblem:
provenance.shS7 asserts the clean-checkout case withjq -r '.suite.dirty // "absent"'and expects"false".Expected:
{"dirty": false}renders asfalse.Actual: jq's
//treats booleanfalseas falsy, so the alternative fires and the checkreads
absent. The ONLY value that satisfies that expression is the JSON string"false".The sibling dirty-state check is blind to this — boolean
trueand string"true"bothrender
trueunder-r— which is why only the clean case shows it.Solution: not taken unilaterally. The suite is off-limits for this WU, so
_suite_provenanceemits the boolean (the right schema type — a string"false"is truthyin most consumer languages) and the conflict was escalated to the lead with two options:
amend that one assertion to an absence-safe idiom (
if (.suite|has("dirty")) then (.suite.dirty|tostring) else "absent" end), or pindirtyto a string in the schema doc.Key insight:
// "default"is only safe as an absence probe for fields that can neverOutcome: the Test Writer adopted the absence-safe idiom (
suite_dirty_of, landed as9d70fe6) rather than pinning a string type.dirtystays a boolean; the suite is green.legitimately be
falseornull. For a boolean field the absence probe ishas(...).2026-08-24T00:35 DISCOVERY: AC-10 is a true no-op on the live ledgers
Context: read-only AC-10 check with the modified helpers against the real Gitea ledgers.
Pattern: issue #50 reads 7 records, ordinals
[1..7],suite: nullon every one, exit 0,empty stderr; issue #57 reads 2 records, ordinals
[1,2], same. Running the coarseadapter prefilter directly returns 7 and 2 as well — so the anchored partition and the
prefilter currently agree on both issues and no near-miss is live on either.
Why it matters: the #54 near-miss that motivated the feature is no longer present on #50
(cleaned since it was reproduced), so these two ledgers cannot serve as a live near-miss
regression witness — the synthetic S1/S2 fixtures are the only executable evidence for the
exclusion path. Worth QA knowing: "AC-10 passes on the real ledgers" here means "the change
is invisible on clean ledgers", which is the property AC-10 actually asks for.
2026-08-24T00:45 COMPLETE
Hardest part: deciding not to bend
_suite_provenance's schema type to fit an assertionwhose idiom, not whose intent, was wrong.
If I did this again: run the two new suites against a throwaway implementation of just
_suite_provenancebefore touching the post chain — the type conflict would have surfaced inthe first minute instead of after the whole chain was rewired.
2026-08-24T01:00 DISCOVERY:
git add -Ais the wrong reflex on a shared branchContext: the Test Writer was mid-edit on
test/provenance.sh(a file this WU isforbidden to touch) in the same working tree when I committed.
git add -Aswept theirhalf-finished amendment — the new
suite_dirty_ofhelper without its two call sites —into my implementation commit, which left HEAD red on the very check the amendment fixes.
Pattern: on a branch two agents share a working tree on, stage by explicit path
(
git add <the files this WU owns>), never-A. The escalation resolved on its own —the Test Writer landed the call-site half as
9d70fe6and HEAD is green — but the windowwhere HEAD was red was self-inflicted.
Why it matters: the WU brief's file-ownership boundary only holds if staging respects
it too; a shared tree makes
-Aa way to commit work you were told not to touch.Dev Journal — WU-57-3-2 (route satellite consumers through _typed_scan)
2026-08-24T00:00 STRUGGLE: every routed consumer suddenly exited 126
Problem: Applying the route-through edits with an
awk ... > f.tmp && mv f.tmp fsplice.Expected: Only the scan call and its comment change.
Actual: All 36 family assertions failed with exit 126 — the helpers were no longer executable.
Solution: The awk-and-move rewrite drops the source file's mode;
chmod +xrestored it, andgit diff --summaryconfirmed no mode change was staged.Key insight: A whole-suite failure at a uniform, non-helper exit code (126/127) is an
environment/file-mode symptom, not a logic one — check the mode before re-reading the diff.
2026-08-24T00:00 DISCOVERY: routing decision-resolution widens the primitive's header filter
Context: This consumer was the only one passing a composite header to the primitive
(
decision-resolution:v1 ref=$did), while_typed_scantakes a bare KIND.Pattern: Route on the kind alone and let the existing exact-
refpost-filter do the narrowing.Why it matters: No behaviour is lost — the primitive's
ref=match was already a substring(
ref=D-PO-2-4-1pulled inref=D-PO-2-4-12, observed live in verity), which is exactly why thepost-filter exists. The scan is now broader by one kind's worth of comments and correct by anchoring.
2026-08-24T00:00 COMPLETE
Hardest part: Nothing in the routing itself — the one real cost was the file-mode regression
introduced by my own editing technique, which masqueraded as a total functional failure.
If I did this again: Preserve the mode in the splice helper (
chmod --reference) before thefirst run, so a mechanical edit can never look like a behavioural break.
Dev Journal — WU-57-3-3 (docs + route-through lint)
2026-08-24T00:00 DISCOVERY: the D3 offender was inline prose, not a fence — a fence-only rule would have missed the very defect it guards
The SREQ specifies the route-through rule as "no skill-text shell fence may invoke the primitive
directly", and the exempt-consumer audit named
requirements-from-deferred.mdStep D3 as the oneoffender. D3 is not in a fence — it is inline code inside a numbered list item
(
`{adapter}/bin/scan_comments.sh --issue {S} --header "folded-finding:v1"`). A fence-only checkwould therefore have gone green over the exact call site that motivated it, and nothing would stop the
next one being written the same way.
So the rule splits on the fence boundary rather than restricting itself to one side of it, reusing the
existing tier-1 fence tracker for the partition (CLAUDE.md #50: never line-regex skill markdown):
_prim scan_comments, or a path ending/scan_comments.sh), because everything in a fence is a recipe;/scan_comments.sh --…) violates. A bare mentionof the primitive's name is legitimate prose, and there is a lot of it: forge-contract's primitives
table,
decision-resolution.md's abstractscan_comments(issue, header_pattern=…)signatures, andthe four adapter SKILL.md quick-references (those are additionally path-exempt).
Fenced hits are subtracted from the prose pass by
(path, lineno)so an offender is reported once.Verified at HEAD: the prose half flagged D3 and nothing else — zero false positives across 689
fence lines and the whole markdown population.
2026-08-24T00:00 STRUGGLE: awk
-veats backslashes, so the shared pattern carries noneProblem: one regex serves all three passes — two
grep -Eand oneawkdynamic match.Actual: awk processes escape sequences in a
-vassignment, so\.arrives as a bare.(andgawk warns). The pattern would silently widen.
Solution: write the literal dot as the bracket expression
[.], which means the same thing toboth grep -E and awk and survives the
-vround trip. The pattern now contains no backslash at all.Linked: this issue is sibling #59 (recorded by the devwork pipeline).