Sitelet https://github.com/objectstack-ai/objectstack/pull/12274
Skip to content

Carve generator-owned skills/** outputs out of the governed-merge fork - #12274

Draft
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-11705-generated-artifact-exemption
Draft

Carve generator-owned skills/** outputs out of the governed-merge fork#12274
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-11705-generated-artifact-exemption

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes #11705

Implements the maintainer's ruling by extending the provenance-aware register PR #11078 built in scripts/pm/check-governed-merges.mjs — no second mechanism, no hand-copied path list, no blind path exemption.

The ruling (issue #11705, comment 5406727512, maintainer 2026-08-25)

The ruling: files that are generator-owned outputs inside skills/** are carved out of the governed-merge fork. The generator plus its verifying gate (gen:skill-refs + check:skill-refs) are the review for these files — no agent-authored instruction content can enter through them, which is what the governed fence exists to stop. The strict reading is not merely heavy but unworkable: the measured incident (7125cdc6e, job 97435569091) shows splitting the regenerated index out of its source PR turns CI red on the same gate, because the artifact must ride the source change.

Scope and shape, per the option's own terms:

  • The exemption is enumerated from the generator, never a hand-copied path list — the same provenance-aware pattern PR feat(pm): provenance-aware exception for the docs-audit --write artifact in the governed-fence predicate #11078 already built into check-governed-merges.mjs for .claude/workflows/docs-accuracy-audit.js (the checker verifies the file matches its generator's output rather than exempting a path blindly). Extend that registry; ⛔ do not author a second mechanism.
  • Covers the gen:skill-refs outputs (skills/*/references/_index.md) and the build-react-blocks-contract.ts outputs carried the same way (react-blocks.md, contracts/react-blocks.contract.json).
  • Hand-authored skills/** content is untouched — it stays governed, human-merge-only. A file qualifies only by being reproducible from its generator, and the checker must prove that per-file rather than trust the path.
  • Option C (move generated output out of skills/**) is not taken; option B (strict reading) is retired by this ruling.

Historical note preserved: the six merged spec PRs that carried these files through the queue are not retroactively sanctioned — #9319 records one such landing as a PD #14 bypass. This ruling makes the class legitimate going forward; it does not rewrite the record.

What a row is now

A register row is a generator, not a path. A hit lifts only when both halves are the generator's own answer, recomputed on the tree under test:

  1. The generator owns the path. The spec generators write through the shared sink (packages/spec/scripts/lib/generated-output.ts), which now declares its output set on demand: --generated-manifest=<file> writes the repo-relative paths of everything a real run emits, from the same emitted map the write disposition reads. The register consults that list and never restates it.
  2. The bytes match. The generator's own --check — "the real run minus the writes" by the sink's construction — must report no drift. One hand-edited byte reddens it.

A row's candidate regexp is a narrowing gate only: matching it earns a path the question, never the answer. Too narrow ⇒ a real regeneration stays governed (heavy, safe); too wide ⇒ the manifest refuses it (safe). No spelling of it lifts anything on its own — the same property the #11084 co-edit fence has, pinned in both directions.

Measured, on this tree

The #11685 shape now lifts (--test, exit 0):

$ node scripts/pm/check-governed-merges.mjs --test packages/spec/src/ui/view.zod.ts skills/objectstack-ui/references/_index.md
  ✅  NOT governed — ordinary queue landing applies to a PR with exactly this file list.
  ℹ️  generated-surface exception (#11705): skills/objectstack-ui/references/_index.md is a PURE REGENERATION —
      byte-equal to `pnpm --filter @objectstack/spec gen:skill-refs` recomputed on THIS tree (never a stored baseline)
EXIT=0

Ablation — one hand-edited byte in the real generated file flips it back to governed. Run from a committed state, with a trap … EXIT INT TERM restore; the mutation was confirmed on disk before the reading was taken (anchor ## Core schemas count 1 → 0, injected marker count 0 → 1, git diff --numstat = 1 1), and the restore confirmed after (1 → back, injected 0, clean git status):

=== leg 1: same path, one hand-edited byte ===
  ⛔  generated-surface exception (#11705) did NOT lift skills/objectstack-ui/references/_index.md:
      `check:skill-refs` does not certify this tree (the generator's own --check exited 1:
      ✗ skills/*/references/_index.md is out of date … ~ skills/objectstack-ui/references/_index.md (out of date));
      byte-exact agreement with the generator is the only pass … — fail closed: the path stays governed
EXIT=3
=== leg 2: restored tree lifts again ===  EXIT=0

The ruled limit holds — hand-authored skills/** content is untouched:

--test input exit why
skills/objectstack-ui/SKILL.md 3 GOVERNED not on the register at all; no generator is consulted, and no exception line is even rendered
skills/objectstack-upgrade/references/_index.md 3 GOVERNED matches the candidate, but the generator declared 9 outputs and this is not one of them (no SKILL_MAP entry) — "hand-authored content sitting beside generated output"
react-blocks.md + react-blocks.contract.json 0 both lifted by their own generator
_index.md + packages/spec/scripts/build-skill-references.ts 3 GOVERNED #11084 co-edit fence: the diff edits the instrument that would certify it

Does the exemption reach the #12102 merge-group guard?

check-governed-queue-guard.mjs already shared the predicate (testVerdict, governedPathsIn, applyGeneratedExceptions) but held its own copy of the recompute, and derived its own membership set from e.path — which reads undefined for a row that matches by candidate instead. That silent miss is now impossible: membership is the register's generatedExceptionFor and the recompute is its recomputeProvenanceFor, both imported. A future row reaches both tools with nothing to change in the guard. Its lift path had zero self-test coverage; it now has six cases.

⚠️ One measured limit, stated rather than implied. The #11705 rows recompute by running the generator's own --check, and governed-surface-guard.yml deliberately installs no dependencies ("the script is dependency-free"). There the rows fail closed: a spec PR carrying its regenerated _index.md is still governed at merge-group time and needs the approving review, while the seat-side --test — the predicate the measured incident actually turned on — lifts it in a dev container. Closing that gap means giving the guard job a dependency install, a cost on every queue build that no ruling covers, so it is filed, not taken (see the report on #11705). The no-toolchain environment is itself pinned as a self-test case in both scripts, so the fail-closed answer there is a tested behaviour, not an accident.

Ratchet: state-machine.md 43 → 44, and why it was not paid in place

The dispatch priced the one-line note as pay-in-place. It could not be: the file holds 34 content lines over 3,467 bytes, and every bullet is already packed to its own minimum against the 120-byte cap (measured per bullet — the only bullets spanning >1 line each exceed 120 bytes on their own). The only in-place payment available was deleting an existing ruled clause. The raise is instead backed by this card's own ruling, which orders the note ("the skills seat opens the charter card to land the exemption in check-governed-merges.mjs plus the one-line state-machine doc note") and is quoted in the ratchet comment beside the number, per that file's convention. The note carries no issue number, per that document's own rule.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (27 matched families), re-derived after the final commit — unchanged. Run at 100accdaa, exit codes captured before any pipe:

  • pnpm check:pm-governed-merges✓ check-governed-merges --self-test: 159 assertions (was 129 — 30 new)
  • node scripts/pm/check-governed-queue-guard.mjs --self-test✓ check-governed-queue-guard self-test: 86 cases pass (was 80 — 6 new)
  • pnpm check:pm-skill-ratchet✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/state-machine.md is 44 lines (ceiling 44; headroom 0).
  • pnpm --filter @objectstack/spec run check:skill-refs✅ 9 generated files in sync with packages/spec
  • pnpm --filter @objectstack/spec run check:react-blocks✅ 2 generated files in sync with packages/spec
  • pnpm check:nul-bytes✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)
  • Also green: check:agent-test-spelling, check:doc-authoring, check:skill-frame-sync, check:pm-skill-id-lint, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:cross-package-test-inputs, check-ci-filter-parity, check:merge-driver, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:scripts-typecheck (spec), check:doc-formula-expressions (after building its @objectstack/formula + @objectstack/lint closure), check-plugin-teardown-shape, check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs, docs-audit/check-affected-docs, docs-audit/check-drift-comment.

Declared narrowing — one family of the 27 not run: node scripts/check-dev-prereqs.mjs exits 1 in a fresh worktree because 63 of 67 workspace packages have no dist/ on disk. Its own output classifies that as "1 unmet precondition, not a list of problems" with Fix: pnpm build — a whole-workspace build, which CI owns. It measured nothing about this diff either way.

An ablation proves the new self-test cases can fail rather than passing vacuously: neutering the manifest-membership branch (if (!run.outputs.includes(path))if (false), injection confirmed on disk) turned the suite red on 11705-case-C-a-path-the-generator-never-declared-stays-governed; restoring returned it to green.

Not a changeset

No user-visible change: scripts/pm/** is seat tooling, packages/spec/scripts/** is generator-side and outside the package's published files allowlist, and .claude/** is the instruction tree. skip-changeset label applied and read back.

Draft, human merge — .claude/** and the governed register itself are governed surface. Review requested from os-zhuang.

Generated by Claude Code


Generated by Claude Code

Implements the maintainer's 2026-08-25 ruling on #11705 (option A) by
EXTENDING the provenance-aware register PR #11078 built in
`scripts/pm/check-governed-merges.mjs` — no second mechanism.

A row is a GENERATOR, not a path exemption. A hit lifts only when both
halves are the generator's own answer on the tree under test: it declared
the path among its outputs (`--generated-manifest`, written from the same
map the write disposition uses), and its own `--check` reported no drift.
Hand-authored `skills/**` content is not on the register at all.

- `packages/spec/scripts/lib/generated-output.ts`: the shared sink declares
  its output set on demand, so the register never restates a path list.
- `scripts/pm/check-governed-merges.mjs`: two ruled rows (gen:skill-refs,
  gen:react-blocks), the shared recompute driver, per-row co-edit fences.
- `scripts/pm/check-governed-queue-guard.mjs`: consumes the shared
  membership test and driver instead of its own copy.
- `.claude/skills/pm-dispatch/references/state-machine.md`: one-line note.

Closes #11705
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f86839d4cd5543a0f8c7a3f4806b0d0b35a33524packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

2 participants