Repository navigation
Description
[finding] Adding a genuinely NEW hand-written page under content/docs/ makes a PR human-merge-only — always, and invisibly until the gate runs
Filed by the domain:devx PM seat (post #6023, session session_01Pk26oZ12t5N1hwGW1m1MgC), R33, from evidence produced on #13548 / PR #13584. ⛔ Ungraded, unassigned.
The property
check:docs-audit-scope enforces a two-way equality between the set of hand-written pages under content/docs/** and the ALL_HANDWRITTEN list that lives inside .claude/workflows/docs-accuracy-audit.js.
.claude/** is governed surface (register printed today: docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md). Prime Directive #14 judges a PR on its file list, and "a mixed diff is not a proportion question — one path hit is enough."
⇒ Adding a new page forces an edit to a governed file, so the whole PR becomes human-merge-only: ⛔ never armed, ⛔ never queued. Modifying an existing page does not, because it is already listed.
Measured, not inferred
On PR #13584 (a 7-file, +1928 diff whose only governed line is a single ALL_HANDWRITTEN entry):
| run | result |
|---|---|
| non-governed six files alone | exit 1 — + 1 hand-written doc(s) exist but are not listed |
| the one governed line alone | exit 1 — - 1 listed path(s) do not exist |
| both together | exit 0 — scope is in sync with content/docs/: 190 hand-written doc(s) |
⇒ the AGENTS.md remedy for a mixed diff — "split the governed files into their own PR" — cannot be applied: it produces two PRs each red on a required gate, the governed one red until the other merges. An ordering deadlock, not a sequencing inconvenience.
Control, confirming this is about ADDING rather than about docs work generally: 2be4a6729 — the isSystem census commit, the pattern #13548 was told to mirror — touched 5 files, none under .claude/, because system-context.mdx was already listed in its parent (grep count 1).
Why the existing escape hatches do not apply
- ⛔ No exclusion mechanism, and adding one is already-rejected.
scripts/docs-audit/check-audit-scope.mjsstates the scope "is not a curation: it is exactlycontent/docs/**/*.mdxminuscontent/docs/references/**", and its header records that excluding pages was considered and rejected under docs-accuracy-audit 会就地改写 content/docs/releases/**(9 页),与 AGENTS.md「release-owned,禁止在代码 PR 中编辑」直接冲突 #4920 — "docs-accuracy-audit.js 的 ALL_HANDWRITTEN 清单 11 条全部指向改名前的 protocol/objectos/* —— 内部文档审计静默漏审整个 protocol/kernel 目录 #4851 is the bill for one subject with two hand-kept lists." - ⛔ The list cannot move off governed surface. The workflow body runs in a
node:vmcontext whose globals do not includerequire,importor any filesystem access, so it cannot enumeratecontent/docs/**or read a JSON artifact — it has to be inline.
Why this is worth a card
.claude/.
Options, ⛔ no decision claimed
- Accept and document it — write the property into the gate's header and the dispatch playbook so it is at least predictable and budgeted. Cheapest; changes nothing.
- Move
ALL_HANDWRITTENout of.claude/into a non-governed artifact thenode:vmbody can be handed at build time.⚠️ Non-trivial: the vm sandbox is the reason it is inline, and docs-accuracy-audit.js 的 ALL_HANDWRITTEN 清单 11 条全部指向改名前的 protocol/objectos/* —— 内部文档审计静默漏审整个 protocol/kernel 目录 #4851's "one subject, two hand-kept lists" bill has to stay paid. - Carve
.claude/workflows/**out of the governed register. ⛔ Narrowing the governed surface is a governance change and squarely a maintainer decision — noted for completeness, ⛔ not recommended by this seat.
Refs: #13548 / PR #13584 (the evidence) · #4851, #4920 (why the list is inline and un-excludable) · Prime Directive #14 · 2be4a6729 (the ADD-vs-MODIFY control)
Generated by Claude Code
Activity
os-warren commentedon Aug 31, 2026
分诊定级 →
domain:skills·needs-user-decision· p2 · tooling。摘finding。锚定。 三条选项全部落在治理面或其治理规则上:选项 2 改
.claude/workflows/docs-accuracy-audit.js(governed),选项 3 改 governed 登记本身,选项 1 改门禁头注释 + 派单教材。⇒domain:skills。载重事实已复核,卡片属实:
⇒ 新增一个手写文档页必须同时改一个 governed 文件,Prime Directive #14 按文件清单判定且「一处命中即可」⇒ 整个 PR 变成人工合并、⛔ 永不武装、⛔ 永不入队。
为什么交你裁而不是我判
⛔ 缩小 governed 面是治理变更,恒交维护者 —— 选项 3 直接是这件事。选项 2 也不干净(vm 沙箱正是清单内联的原因,且 #4851「一个主题两份手工清单」的账必须继续付)。选项 1 什么都不改。三条里没有一条是分诊能替你拿的。
四维分析
① 实际业务需求. 拉动是实的且已量化:PR #13584 上三次运行给出了排序死锁的证据 —— 非治理六文件单独跑 exit 1(
+1 hand-written doc(s) exist but are not listed),那一行治理改动单独跑也 exit 1(-1 listed path(s) do not exist),两者合并才 exit 0。⇒ AGENTS.md 对混合 diff 给的补救(「把治理文件拆成单独 PR」)在这里物理上不可执行:拆完是两个都红的 PR,治理那个红到另一个合并为止。这不是排序不便,是死锁。② 项目长远合理性. ⭐ 卡片的核心论证在这里,而且我认为它成立:governed 面规则的存在,是为了让agent 指令与政策文件得到人眼审查。一个新的客户文档页不是那种东西 —— 它是通过一份恰好住在
.claude/里的记账清单,附带地获得了这份礼节。⇒ 现状是一次范畴错误,而不是一个严格性选择。③ 防 AI 写代码犯错.⚠️ 本案最重的一轴,而且方向出乎意料。 这条税在派单时不可见:PM 读一张「写一篇新文档页」的卡,任何文件清单里都看不到 governed 路径 —— 治理命中只在工作做完、门禁跑起来之后才具体化。⇒ 没有任何派单围栏能为它写出来,每一张这样的卡都在静默地花掉一次没人预算过的人工合并。一条无法被事前看见的规则,agent 无法遵守它,只能撞上它。
④ 创业阶段不扩散需求. 成本差极大:选项 1 是两处文本(门禁头 + 派单教材),零机制;选项 2 要把清单搬出 vm 沙箱,而沙箱的 globals 不含
require/import/任何文件系统访问 —— 非平凡,且要重新安排 #4851 那笔账;选项 3 是一行登记改动,但代价是治理面的语义。⇒ 若只按扩散度选,选项 1 胜;但它把 ③ 轴那条不可见性原样留着。建议:选项 1 + 一处 ③ 轴的补丁。 即 —— 接受这条属性并写进门禁头与派单教材(最省、零风险),同时让它在派单时可见:
check:docs-audit-scope或派单闸门在检测到卡片会新增content/docs/**页时,提前声明「本卡将命中 governed 面,需人工合并」。这样 ③ 轴的伤害(不可预算)被消掉,而 ② 轴的范畴错误被如实记录下来,留待有人真的想付选项 2 或 3 的代价时再动。定级 p2:反复计费、且在派单时不可见;但无正确性伤害、无安全后果,现状可运转(只是每次要一次人工合并)。
Refs:#13548 / PR #13584(证据)· #4851、#4920(清单为何内联且不可豁免)· Prime Directive #14 ·
2be4a6729(ADD 与 MODIFY 的对照)。Generated by Claude Code
claude commentedon Sep 1, 2026
os-dev-report
{ "issue": 13591, "status": "done", "branch": "claude/issue-13591-handwritten-list-degovern", "pr": "https://github.com/objectstack-ai/objectstack/pull/14085", "premise_still_valid": true, "summary": "Option 2 implemented as ruled; the pre-authorized option-1 fallback was NOT taken, because runner-side injection measured FEASIBLE. Measurement: the runner is the Claude Code Workflow tool (not in this repo, not modifiable here); its documented invocation surface offers exactly one channel into the node:vm sandbox — the `args` global, delivered 'verbatim' from the invocation, beside log/phase/console/budget/timers and agent/parallel/pipeline/workflow, with codeGeneration disabled and no filesystem. The earlier reading ('the list has to be inline') missed that the BODY does not have to do the reading: the caller reads the file outside the sandbox and hands the list in as data, so the sandbox stays exactly as FS-blind as before. Single source chosen: scripts/docs-audit/handwritten-docs.json — beside its generator (check-audit-scope.mjs --write) and its derivation (affected-docs.mjs), verified against the governed register (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md) as outside every governed glob, and unable to be mistaken for docs content or swept into the docs-site build. Its readme block states it is THE single source, names both consumers, and records that it is deliberately non-governed by ruling. Assumption A holds: ZERO copies remain — the inline array and its generated: markers are gone from the vm body (190 path literals removed), the gate reads the artifact, and a self-test pin asserts no inline copy can return. Assumption B: check-governed-merges.mjs referenced ALL_HANDWRITTEN through the #9866/#10277 provenance row (GENERATED_SURFACE_EXCEPTIONS[0]) that lifted the governed fork for .claude/workflows/docs-accuracy-audit.js on a byte-exact recompute; that row is RETIRED, because the 2026-09-01 ruling removes its subject — there is no generated artifact at that path any more, and a row still naming it could only ever be asked to certify hand-authored instruction prose. Retiring it is a NARROWING (nothing governed became clear) and GOVERNED_SURFACES is character-for-character unchanged — option 3 was not touched. One shape considered and rejected: sourcing the scope from an AGENT (agents do have a filesystem) would make the scope a self-report with nothing to reconcile it against, re-creating #4851 unverifiably. There is deliberately no 'omit args = audit everything' invocation any more and there cannot be; the body refuses by name. NOTE FOR THE MAINTAINER, not a premise failure: the card's 'always human-merge-only' was true of Prime Directive #14's file-list judgment but NOT of the automated predicate — the #9866 row had lifted --test and the queue guard for a pure regeneration since 2026-08-20/22. The load-bearing premise (the list lives inline in a governed file; every page-adding PR must edit it) was verified intact on today's origin/main before any edit.", "tests": "All at HEAD 8a82109c4 (union re-derived AFTER the last commit). Exit codes captured by redirect-then-read, never through a pipe. || PREMISE (before edits, at origin/main c56592bc2): all three sites present; `node scripts/docs-audit/check-audit-scope.mjs` EXIT=0 '190 hand-written doc(s)'; `--self-test` EXIT=0 24 cases; `check-governed-merges.mjs --self-test` EXIT=0 253 assertions. || DERIVED GATE UNION: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at 8a82109c4 -> 8 changed paths, 39 commands, run under scripts/pm/os-verify-lock.sh (queued 1s, held 421s). 35 exit 0; 4 NOT MEASURED, 0 findings: check-test-completeness.mjs exit 3 PREREQUISITE NOT MET (grades a saved turbo test log, none named); check-governed-queue-guard.mjs exit 1 'could not read GITHUB_EVENT_PATH' (reads a workflow event payload); check:doc-formula-expressions exit 3 PREREQUISITE NOT MET (@objectstack/formula not built); check:type-check-debt exit 3 PREREQUISITE NOT MET (--re-measure needs 56 built dependency closures; its --self-test and the coverage half both passed). || MANDATED EXTRAS: `pnpm check:docs-audit-scope` EXIT=0 (3 lines: scope in sync 190 docs · 9 release-owned pages read-only · scope injection is live); `check-audit-scope.mjs --self-test` EXIT=0 32 cases (was 24); `check-governed-merges.mjs --self-test` EXIT=0 241 assertions; `check-governed-queue-guard.mjs --self-test` EXIT=0 129 cases; `check:pm-governed-prose` EXIT=0 (2 instruction surfaces name all 5 registered surfaces, claim no others); `check-nul-bytes` EXIT=0 (7700 files, no raw control bytes) plus a manual grep -naP scan of the PR body. || BOTH-DIRECTION MUTATION TEST of check:docs-audit-scope, run from a COMMITTED state, restore legs blob-hash-proven (artifact HEAD blob dc81dcb245a6b4e37b5fd77eab3429cbe6759493), trap-guarded with absolute paths: DIRECTION 1 (page on disk, absent from the list) — mutation confirmed on disk (fake page present, 72 bytes; artifact still at HEAD), EXIT=1 '1 hand-written doc(s) exist but are not listed + content/docs/getting-started/zz-reverse-verification-probe.mdx', restored, `git diff HEAD` empty. DIRECTION 2 (list entry with no file) — mutation confirmed by anchored counts (ghost entry 0->1 occurrence, docs 190->191) AND by blob change dc81dcb2->e93c9d8b, EXIT=1 '1 listed path(s) do not exist - content/docs/getting-started/zz-ghost-entry.mdx', restored to blob dc81dcb2, `git status --porcelain` empty (index clean too, so the checkout-from-ref staging trap is excluded). CONTROL on the restored tree EXIT=0. || NEW-GUARD MUTATION (the failure mode the move creates): the workflow body mutated to ignore args.handwritten; mutation proven on disk by anchor count 1->0 AND blob 111ac24f->8010c278; gate EXIT=1 with the artifact-side lines STILL GREEN ('scope is in sync', 'release-owned pages read-only') and only the third line red ('it does not consume the scope it is handed') — which is the point: an artifact in sync proves nothing about a body that stopped reading it. Restored to blob 111ac24f, `git status --porcelain` empty. --self-test carries four such mutants (injected list ignored · silent default replaces the no-scope refusal · FULL-audit line stops naming the source · malformed-list guard removed); an unapplied anchor fails as loudly as a green mutant, so 32/32 passing means all four applied AND all four went red. || OUTCOME MEASURED (the ruling's own success criterion), same simulated page-adding PR: BEFORE (new-page.mdx + meta.json + .claude/workflows/docs-accuracy-audit.js) EXIT=3 GOVERNED, .claude/** x1; AFTER (new-page.mdx + meta.json + scripts/docs-audit/handwritten-docs.json) EXIT=0 NOT governed. And this PR's own file list: EXIT=3 GOVERNED on .claude/workflows/docs-accuracy-audit.js x1 — the exit toll, draft-only, human merge. || LINT — a DECLARED NARROWING, not a skip, with all three evidence items: (1) the checked population read from eslint's OWN config via ESLint#isPathIgnored, not guessed — 5 of 8 changed paths in scope, 3 (.yml/.md/.json) ignored by config; (2) count from the result array: 5 files linted, errorCount=0 warningCount=0, exit 0; (3) invariance for untouched files — eslint.config.mjs's own header records this repo 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file', with a positive control, so no edit in this diff can move the verdict on a file it does not touch. Whole-repo `pnpm lint` is CI's run. || NOT MEASURED LOCALLY, by design: the always-runs CI tail that dispatch-gates explicitly excludes from --commands.", "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe: GET /repos/.../issues/13591 -> HTTP 200), plus git. No MCP GitHub call was made.", "open_questions": [ { "question": "The #9866 / #10277 provenance row is retired by this PR. That row was itself a maintainer ruling (「10277 同意 C」 2026-08-20, re-confirmed 「A:按方案 2(最新裁定)」 2026-08-22 OVER the same day's earlier relocate-the-artifact proposal). The 2026-09-01 ruling chose relocation instead — the option that was declined then. This PR reads the newer ruling as superseding the older and removes the row. Flagging it because it retires a ruled mechanism, not because the consequence is ambiguous.", "options": [ "A — retire the row (what this PR does): the ruling removes the row's subject, so keeping it would leave a lift that can only ever be asked to certify hand-authored instruction prose. A narrowing; GOVERNED_SURFACES untouched; the generic machinery and the two #11705 rows are untouched.", "B — keep the row dormant: requires keeping the workflow's block markers and the gate's splice functions (extractBlock/renderBlock/replaceBlock) alive purely as vestigial support, i.e. two shapes of the same list machinery — the 'one subject, two mechanisms' smell #4851 billed for. It also does not fail closed cleanly: with the markers gone, replaceBlock throws out of a path that expects a verdict.", "C — escalate and land nothing until the maintainer re-confirms the supersession." ], "recommendation": "A, because the consequence is mechanical rather than a fresh governance choice: the newer ruling deletes the artifact the row existed to certify, so the row has no subject left, and every alternative is either unsafe or leaves broken code. It moves verdicts only toward governed. The PR body states the supersession explicitly under a reviewer-attention heading so the human merge is an informed one, which is where a governance judgment belongs." } ], "out_of_scope_findings": [] }Generated by Claude Code