You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] The provenance exception's recompute trusts the PR tree's own scripts/docs-audit/** — a PR editing the generator and the workflow artifact together can self-certify; cheap hardening: fail closed on that co-edit #11084
Found by the skills seat during the contract-grade review of the #9866 landing (PR #11078), session session_01MsbKEG4LtERSLaDrbehM3e, 2026-08-22. Filed under the skills-lane self-triage exception; not a blocker to that PR — recorded residual, graded next round.
The residual
recomputeDocsAuditProvenance derives the doc set by executing scripts/docs-audit/affected-docs.mjsfrom the tree under test (necessarily — the derivation must reflect the PR's own docs). Consequence: a PR that edits scripts/docs-audit/affected-docs.mjs (or the generator) and hand-edits .claude/workflows/docs-accuracy-audit.js in the same diff can, in principle, construct a derivation whose recomputed splice byte-equals its hand-edited artifact — self-certifying the "pure regeneration" verdict.
Why it is residual, not a hole
The attacker-controlled surface is limited to the generated block's list contents (everything outside the block must still equal the merge-base bytes).
The co-edit is fully visible in the PR's file list, and scripts/docs-audit/** edits are unusual on a docs PR — a reviewing seat reading the file list sees it.
The pre-existing trust posture is unchanged in kind: the predicate has always been runnable from the PR's own checkout.
Cheap hardening (grading input)
In --test, before consulting provenance: if the submitted path list also contains any scripts/docs-audit/** path, skip the recompute and fail closed with a stated reason ("the tree under test modifies the generator this exception trusts — the path stays governed"). One conditional + one self-test case in each direction (co-edit stays governed; a pure regen with untouched generator still lifts).
Refs: #9866 (the ruled exception) · PR #11078 (the landing under review when found).
Grading (skills seat, session session_01RMTpSRF5CjMmQBFfPtPCwJ, 2026-08-23 concentrated round): promoted finding → pm:queue. Cheap fail-closed hardening exactly as the card scopes it: in --test, a submitted path list containing any scripts/docs-audit/** path skips the recompute and stays governed, with the stated reason; one self-test case in each direction. Narrows the ruled #9866 exception, never widens it — no ruling needed.
Claim: PM loop round 1, wave 3
Session: session_01RMTpSRF5CjMmQBFfPtPCwJ
Branch: claude/issue-11084-provenance-coedit-failclosed
Worktree: objectstack-issue-11084
Domain: domain:skills
File surface: scripts/pm/check-governed-merges.mjs (the --test provenance exception + its self-test) (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus — no path-derived mandate; governance-gate logic
Clause-②: no
Serial constraints cleared: none in flight on this file — wave-3 siblings are check-half-states.mjs+workflow (patrol family) and AGENTS.md (#11079), disjoint; no open PR touches it.
{
"issue": 11084,
"status": "done",
"branch": "claude/issue-11084-provenance-coedit-failclosed",
"pr": "https://github.com/objectstack-ai/objectstack/pull/11283",
"premise_still_valid": true,
"summary": "Premise verified on main before any edit: GENERATED_SURFACE_EXCEPTIONS, docsAuditRegenVerdict, recomputeDocsAuditProvenance and the --test wiring all exist as the card describes, and the recompute does execute the tree under test's own scripts/docs-audit/affected-docs.mjs plus its check-audit-scope.mjs replaceBlock. Added exactly the card's cheap hardening: a pure exported generatorCoEditProvenance(paths) consulted in --test BEFORE provenance — any scripts/docs-audit/** path in the submitted list skips the recompute entirely (no git, no generator exec) and keeps the path governed with the stated reason. Strictly a narrowing: null (untouched generator) is the only branch that goes on to recompute, no branch lifts anything, so verdicts can only move toward governed. File surface held to exactly scripts/pm/check-governed-merges.mjs (dispatch-gates re-derivation: 1 path, committed 1 / working tree 0 / untracked 0); no check-half-states.mjs, no half-state-patrol.yml, no ensure-pm-labels.sh, no .claude/** file touched. Left as draft, no auto-merge.",
"tests": "All readings at final commit 04371a39 (= HEAD of the PR; gate union re-run on that tree). SELF-TEST COUNT: 119 before -> 129 after; verdict line printed by the gate itself: '✓ check-governed-merges --self-test: 129 assertions (… the #11084 generator co-edit fence in both directions …)'. BOTH DIRECTIONS END-TO-END against the real tree, not fixtures: (A) co-edit — `node scripts/pm/check-governed-merges.mjs --test .claude/workflows/docs-accuracy-audit.js scripts/docs-audit/affected-docs.mjs` exit 3 (EXIT_TEST_GOVERNED), render line: '⛔ generated-surface exception (#9866) did NOT lift .claude/workflows/docs-accuracy-audit.js: the tree under test modifies the generator this exception trusts — the path stays governed (co-edited here: scripts/docs-audit/affected-docs.mjs)'. (B) untouched generator — `--test .claude/workflows/docs-accuracy-audit.js content/docs/some-new-page.mdx` exit 0 (EXIT_TEST_NOT_GOVERNED), render line: 'ℹ️ generated-surface exception (#9866): .claude/workflows/docs-accuracy-audit.js is a PURE REGENERATION — byte-equal to `node scripts/docs-audit/check-audit-scope.mjs --write` recomputed on THIS tree' — the ruled lift is unchanged. REVERSE-VERIFICATION (predicted direction: RED, observed: RED): ran from the COMMITTED state, fence neutered to return null unconditionally; mutation confirmed on disk by anchor/inject grep counts, not by editor exit code — 'before: anchor=1 inject=0' -> 'after: anchor=0 inject=1' (the python replace also asserts count==1 and refuses otherwise); result 'ABLATED_SELFTEST_EXIT=1 / ✗ check-governed-merges --self-test — 4 failure(s)' naming a-generator-co-edit-answers-fail-closed-before-any-recompute, the-co-edit-reason-states-the-ruled-words-and-names-the-co-edited-file, the-co-edit-render-stays-GOVERNED-and-tells-the-seat-why-it-did-not-lift, and the ./-spelling case. Restore leg: EXIT INT TERM trap in the ablation script, 'RESTORED: 0 dirty entries', git status --porcelain empty, self-test re-run green at 129. No build/dist leg applies — the script is plain .mjs executed directly, nothing resolves through a package dist. GATES: derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` on the actual change set (stderr confirmed the answer came from objectstack-ai/objectstack at 04371a39); 7 families named, all run under scripts/pm/os-verify-lock.sh, each exit captured before any pipe (redirect-then-capture), all GATE_EXIT=0: check:cross-package-test-inputs ('OK: 14 package(s) read outside themselves, all declared'), check:entry-guard ('✓ check:entry-guard: 138 scripts/ file(s) — every entry guard goes through invoked-as.mjs'), check:parse-guard, check:pm-governed-merges ('✓ … 129 assertions'), check:pnpm-filter-targets ('✓ check:pnpm-filter-targets: 120/148 `--filter` occurrence(s) … resolve'), check-ci-filter-parity.mjs ('OK: all 88 declared cross-package glob(s) (76 unique) are covered'), check-cross-package-test-inputs.mjs ('OK: 14 package(s) …'). Re-derivation after the final commit named the same 7 families — no additions. Control-byte self-scan on the edited file: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' returned exit 1 (no matches). This PR's own file list is not governed: `--test scripts/pm/check-governed-merges.mjs` -> 0 of 1 hit, exit 0.",
"deviations": "One, declared: check-ci-filter-parity.mjs first exited 1 with ERR_MODULE_NOT_FOUND for 'yaml' because the fresh worktree had no node_modules yet — my setup gap, not a code failure. Ran pnpm install in the worktree and re-ran it to a real green (GATE_EXIT=0). No narrowing of any gate's scope was taken; the full derived family list was run. No repo-wide scan (pnpm lint) was run and none is claimed — it is CI's run, not a declared narrowing.",
"files_changed": ["scripts/pm/check-governed-merges.mjs"],
"labels": "skip-changeset added via the additive POST endpoint; read back after the bots settled — ['size/m', 'skip-changeset'], so it survived the size-labeler's group write. No changeset file: PM tooling only, nothing user-visible ships.",
"open_questions": [],
"out_of_scope_findings": []
}
Checklist conclusions, verified against GitHub: draft ✓ · base main ✓ · Fixes #11084 first line ✓ · changed files = exactly scripts/pm/check-governed-merges.mjs ✓ · skip-changeset read back surviving the size-labeler ✓. Diff read in full: the fence is a pure exported function consulted BEFORE any recompute (no git, no generator exec on the co-edit branch); prefix-anchored with the ./ spelling folded in and a near-miss sibling directory pinned as NOT fenced; the register-coupling assertion (the registered generator command must live under the fenced prefix) means a relocated generator turns this red first — good defensive coupling. Self-test 119→129 with both directions pinned end-to-end against the real tree, and the reverse-verification (fence neutered → 4 named reds, mutation proven on disk, trap-restored) confirms the pins bite. Strictly a narrowing of the ruled #9866 exception: no branch lifts anything.
Landing class: --test on its own file list says NOT governed (0 of 1 hit) and no card-recorded constraint binds this one — ordinary queue landing applies. Per the queue-entry eligibility discipline (all checks on the PR green, not the required subset — the #11135 grading), the seat flips ready and queues once the PR's full check set reports green; the PR subscription will deliver that reading.
Activity
os-zhuang commentedon Aug 23, 2026
Grading (skills seat, session
session_01RMTpSRF5CjMmQBFfPtPCwJ, 2026-08-23 concentrated round): promotedfinding→pm:queue. Cheap fail-closed hardening exactly as the card scopes it: in--test, a submitted path list containing anyscripts/docs-audit/**path skips the recompute and stays governed, with the stated reason; one self-test case in each direction. Narrows the ruled #9866 exception, never widens it — no ruling needed.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim: PM loop round 1, wave 3
Session:
session_01RMTpSRF5CjMmQBFfPtPCwJBranch:
claude/issue-11084-provenance-coedit-failclosedWorktree:
objectstack-issue-11084Domain:
domain:skillsFile surface:
scripts/pm/check-governed-merges.mjs(the--testprovenance exception + its self-test) (stop on breach; explain in the report)Container & model: S,
mode:subagent,model: opus— no path-derived mandate; governance-gate logicClause-②: no
Serial constraints cleared: none in flight on this file — wave-3 siblings are
check-half-states.mjs+workflow (patrol family) andAGENTS.md(#11079), disjoint; no open PR touches it.Generated by Claude Code
fix(pm): fail closed when the tree under test co-edits the generator …
claude commentedon Aug 23, 2026
{ "issue": 11084, "status": "done", "branch": "claude/issue-11084-provenance-coedit-failclosed", "pr": "https://github.com/objectstack-ai/objectstack/pull/11283", "premise_still_valid": true, "summary": "Premise verified on main before any edit: GENERATED_SURFACE_EXCEPTIONS, docsAuditRegenVerdict, recomputeDocsAuditProvenance and the --test wiring all exist as the card describes, and the recompute does execute the tree under test's own scripts/docs-audit/affected-docs.mjs plus its check-audit-scope.mjs replaceBlock. Added exactly the card's cheap hardening: a pure exported generatorCoEditProvenance(paths) consulted in --test BEFORE provenance — any scripts/docs-audit/** path in the submitted list skips the recompute entirely (no git, no generator exec) and keeps the path governed with the stated reason. Strictly a narrowing: null (untouched generator) is the only branch that goes on to recompute, no branch lifts anything, so verdicts can only move toward governed. File surface held to exactly scripts/pm/check-governed-merges.mjs (dispatch-gates re-derivation: 1 path, committed 1 / working tree 0 / untracked 0); no check-half-states.mjs, no half-state-patrol.yml, no ensure-pm-labels.sh, no .claude/** file touched. Left as draft, no auto-merge.", "tests": "All readings at final commit 04371a39 (= HEAD of the PR; gate union re-run on that tree). SELF-TEST COUNT: 119 before -> 129 after; verdict line printed by the gate itself: '✓ check-governed-merges --self-test: 129 assertions (… the #11084 generator co-edit fence in both directions …)'. BOTH DIRECTIONS END-TO-END against the real tree, not fixtures: (A) co-edit — `node scripts/pm/check-governed-merges.mjs --test .claude/workflows/docs-accuracy-audit.js scripts/docs-audit/affected-docs.mjs` exit 3 (EXIT_TEST_GOVERNED), render line: '⛔ generated-surface exception (#9866) did NOT lift .claude/workflows/docs-accuracy-audit.js: the tree under test modifies the generator this exception trusts — the path stays governed (co-edited here: scripts/docs-audit/affected-docs.mjs)'. (B) untouched generator — `--test .claude/workflows/docs-accuracy-audit.js content/docs/some-new-page.mdx` exit 0 (EXIT_TEST_NOT_GOVERNED), render line: 'ℹ️ generated-surface exception (#9866): .claude/workflows/docs-accuracy-audit.js is a PURE REGENERATION — byte-equal to `node scripts/docs-audit/check-audit-scope.mjs --write` recomputed on THIS tree' — the ruled lift is unchanged. REVERSE-VERIFICATION (predicted direction: RED, observed: RED): ran from the COMMITTED state, fence neutered to return null unconditionally; mutation confirmed on disk by anchor/inject grep counts, not by editor exit code — 'before: anchor=1 inject=0' -> 'after: anchor=0 inject=1' (the python replace also asserts count==1 and refuses otherwise); result 'ABLATED_SELFTEST_EXIT=1 / ✗ check-governed-merges --self-test — 4 failure(s)' naming a-generator-co-edit-answers-fail-closed-before-any-recompute, the-co-edit-reason-states-the-ruled-words-and-names-the-co-edited-file, the-co-edit-render-stays-GOVERNED-and-tells-the-seat-why-it-did-not-lift, and the ./-spelling case. Restore leg: EXIT INT TERM trap in the ablation script, 'RESTORED: 0 dirty entries', git status --porcelain empty, self-test re-run green at 129. No build/dist leg applies — the script is plain .mjs executed directly, nothing resolves through a package dist. GATES: derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` on the actual change set (stderr confirmed the answer came from objectstack-ai/objectstack at 04371a39); 7 families named, all run under scripts/pm/os-verify-lock.sh, each exit captured before any pipe (redirect-then-capture), all GATE_EXIT=0: check:cross-package-test-inputs ('OK: 14 package(s) read outside themselves, all declared'), check:entry-guard ('✓ check:entry-guard: 138 scripts/ file(s) — every entry guard goes through invoked-as.mjs'), check:parse-guard, check:pm-governed-merges ('✓ … 129 assertions'), check:pnpm-filter-targets ('✓ check:pnpm-filter-targets: 120/148 `--filter` occurrence(s) … resolve'), check-ci-filter-parity.mjs ('OK: all 88 declared cross-package glob(s) (76 unique) are covered'), check-cross-package-test-inputs.mjs ('OK: 14 package(s) …'). Re-derivation after the final commit named the same 7 families — no additions. Control-byte self-scan on the edited file: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' returned exit 1 (no matches). This PR's own file list is not governed: `--test scripts/pm/check-governed-merges.mjs` -> 0 of 1 hit, exit 0.", "deviations": "One, declared: check-ci-filter-parity.mjs first exited 1 with ERR_MODULE_NOT_FOUND for 'yaml' because the fresh worktree had no node_modules yet — my setup gap, not a code failure. Ran pnpm install in the worktree and re-ran it to a real green (GATE_EXIT=0). No narrowing of any gate's scope was taken; the full derived family list was run. No repo-wide scan (pnpm lint) was run and none is claimed — it is CI's run, not a declared narrowing.", "files_changed": ["scripts/pm/check-governed-merges.mjs"], "labels": "skip-changeset added via the additive POST endpoint; read back after the bots settled — ['size/m', 'skip-changeset'], so it survived the size-labeler's group write. No changeset file: PM tooling only, nothing user-visible ships.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
ACCEPT (skills seat, session
session_01RMTpSRF5CjMmQBFfPtPCwJ, reviewer of record) — PR #11283.Checklist conclusions, verified against GitHub: draft ✓ · base
main✓ ·Fixes #11084first line ✓ · changed files = exactlyscripts/pm/check-governed-merges.mjs✓ ·skip-changesetread back surviving the size-labeler ✓. Diff read in full: the fence is a pure exported function consulted BEFORE any recompute (no git, no generator exec on the co-edit branch); prefix-anchored with the./spelling folded in and a near-miss sibling directory pinned as NOT fenced; the register-coupling assertion (the registered generator command must live under the fenced prefix) means a relocated generator turns this red first — good defensive coupling. Self-test 119→129 with both directions pinned end-to-end against the real tree, and the reverse-verification (fence neutered → 4 named reds, mutation proven on disk, trap-restored) confirms the pins bite. Strictly a narrowing of the ruled #9866 exception: no branch lifts anything.Landing class:
--teston its own file list says NOT governed (0 of 1 hit) and no card-recorded constraint binds this one — ordinary queue landing applies. Per the queue-entry eligibility discipline (all checks on the PR green, not the required subset — the #11135 grading), the seat flips ready and queues once the PR's full check set reports green; the PR subscription will deliver that reading.Generated by Claude Code
fix(pm): fail closed when the tree under test co-edits the generator …
fix(pm): the queue guard — a CLEAR reached through a generated-artifa…
fix(pm): the mixed-diff diversion on PR objectstack-ai#15284 fired an…
spec: the genuine duration rows declare their unit — DurationMs/Durat…
spec(ui): declare the columns x hiddenFields x fieldOrder composition…