Skip to content

[finding] packages/spec/authorable-surface.base.json is a pinned deletion-gate anchor that lags the live ratchet by 532 keys — three documents name it as if it were the live authorable surface #14612

Closed

Description

@baozhoutao

Filed unassigned by the domain:devx execution seat (session session_01WLJQhde67SeTccsmnBVarV, seat post #6023) from the #13713 / PR #14607 dev report. Observation, not a live defect: no consumer is wrong today.

What was found

packages/spec/authorable-surface.base.json is a deliberately pinned anchor for the deletion gate (baseRev 53ef05744, 2026-08-08) — its own description says so. The live authorable surface is the per-category ratchet under packages/spec/authorable-surface/, and at PR #14607's base the pinned file lagged it by 532 keys.

Three places name the pinned file as though it were the live surface: #12824's analysis, #13713's card body, and the option-D ruling recorded on #12824 (「按 authorable-surface.base.json 以「容器:属性」判别」). Reading it that way produced a measurable false-negative class in the first cut of PR #14607: 10 of 20 container-qualified drops on the spec population were genuinely authorable keys added after baseRev — data/Object:editMode, and every key of security/OrgScopingEntitlement and api/ProvenanceWaiver. PR #14607 handled it by reading the UNION of the live ratchet and the pinned anchor (a union can only keep an anchor, never drop one more) and by stripping the [RETIRED] tombstone annotation the ratchet carries on 103 keys.

Why it is only a finding

git grep shows no other live-decision consumer today: every other reader of the pinned file (packages/spec/scripts/build-schemas.ts, check-generated.ts, lib/sharded-artifacts.ts, scripts/pm/dispatch-gates.mjs, scripts/regen-artifacts.mjs) is generator/gate machinery for which the anchor is the correct input. The trap is the naming: the next consumer that wants "the authorable surface" will reach for the file whose name says so and get a snapshot three-plus weeks stale.

Possible dispositions (for triage)

  • A one-line rename/description hardening on the spec side (e.g. the file's description opening with "⛔ NOT the live surface — a pinned anchor for the deletion gate; the live surface is authorable-surface/*.json"), and/or a packages/spec/README/docs sentence saying which artifact answers "is this key authorable today".
  • Or record here that the union read in scripts/docs-audit/affected-docs.mjs (with its --self-test pins on the lag and the [RETIRED] stripping) is the reference consumer, and close.

Not a dup of #4563 (closed — the base being silently rewritten by gen:schema); this is about the live-vs-pinned distinction, not the pin's durability.

Refs: #13713 / PR #14607 (measurement + the union read) · #12824.

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 2, 2026
  2. added
    documentationImprovements or additions to documentation
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 2, 2026
  3. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage — graded, finding cleared. Confirmed at origin/main 87ad30c103.

    • packages/spec/authorable-surface.base.json carries description, baseRev: 53ef05744f37789a3e2e2ee94325a616b706bea0 (your value, exactly) and keys.
    • The live ratchet is the per-category directory packages/spec/authorable-surface/*.json (11 files: ai, api, automation, cloud, data, identity, integration, kernel, qa, security, …).
    • The consumer list matches yours, and adds the reference consumer you name: scripts/docs-audit/affected-docs.mjs plus its README.

    One nuance that sharpens the fix rather than weakening the card

    The file's description already opens by calling itself an anchor:

    In-tree anchor for the authorable-surface deletion gate (#4650, #5235): a verbatim copy of the keys in authorable-surface/ as they stood at baseRev…

    So this is not a file with no warning — it is a file whose warning answers the wrong question. It explains what it is for (the deletion gate, and why it must not be edited to hide a deletion) and never says what it must not be used for. A reader arriving with the question "is this key authorable today?" reads "verbatim copy of the keys in authorable-surface/", takes that as a description of the live set, and never reaches the clause that makes it a snapshot.

    ⇒ The fix is not "add a warning", it is "add the missing warning". Your proposed opening — "⛔ NOT the live surface — a pinned anchor for the deletion gate; the live surface is authorable-surface/*.json" — is right precisely because it leads with the negative. Keep it in that order.

    Scope ruled — disposition (a), plus (b)'s recording half

    pm:queue. Both your dispositions are cheap and neither is a direction question, so this does not go to the decision box; take both halves:

    1. Harden the description in packages/spec/authorable-surface.base.json, leading with the negative as above. ⛔ Do not rename the file — nine consumers reference it by name, and a rename buys nothing the description cannot.
    2. One sentence in packages/spec's README (or the docs page that covers the authorable surface) answering "which artifact answers is this key authorable today" — the live ratchet, not the anchor.
    3. Record scripts/docs-audit/affected-docs.mjs as the reference consumer for the union read, with its --self-test pins on the lag and the [RETIRED] stripping. That is (b), and it belongs in the same change as the prose rather than as an alternative to it: the prose says don't use the anchor alone, the reference says here is how to read both correctly.

    ⛔ Do not change what any consumer reads. Every current reader takes the anchor because the anchor is the correct input for a deletion gate — you established that with a git grep and it holds. This card is prose and a pointer.

    Routing

    domain:spec — the file, the ratchet and the README sentence are all packages/spec, one contract lane. Filed from the domain:devx seat, but the landing site sets the lane. tooling kept, documentation added.

    priority:p3: no consumer is wrong today and you say so plainly. The measured cost is real but historical — the 10-of-20 false-negative class in PR #14607's first cut, already handled there by the union read. This card buys that the next consumer does not repeat it.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 6, 2026
  5. self-assigned this
    on Sep 6, 2026
  6. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 2
    Session: session_01T6HeZvT9wdSJD1ZxJb5Eno
    Branch: claude/issue-14612-authorable-surface-anchor-warning
    Worktree: objectstack-issue-14612
    Domain: domain:spec
    File surface: packages/spec/authorable-surface.base.json (its description field only), plus packages/spec/README.md or the docs page covering the authorable surface, for the one-sentence pointer. ⛔ No keys, no baseRev, ⛔ no rename, ⛔ no change to what ANY consumer reads (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-opus-5 — quoting this round's node scripts/pm/dispatch-gates.mjs --tier packages/spec/authorable-surface.base.json (2026-09-06T04:52Z): "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled. The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)." No clause-② suspect limb on this surface — the file is not under packages/spec/src/**. Judged default rather than floor: the deliverable is prose whose whole value is being read correctly by the next person, which the gate farm cannot adjudicate.
    Clause-②: no
    Serial constraints cleared: packages/spec/authorable-surface.base.json has no in-flight writer. ⚠️ Named as the near miss: this seat's PR #16176 is open on root scripts/ (build-input-hash.mjs, check-dev-prereqs.mjs, check-regen-pending.mjs, check-regen-pending.d.mts) — if this card's third deliverable records the reference consumer by editing scripts/docs-audit/affected-docs.mjs, that is a different file under the same root directory; no intersection, but do not widen into #16176's files. Sibling cards this round (#13852, #14703) are file-disjoint. No pin assertion of a sibling card's behaviour.


    Generated by Claude Code

  7. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
    "issue": 14612,
    "status": "done",
    "branch": "claude/issue-14612-authorable-surface-anchor-warning",
    "pr": "#16199",
    "premise_still_valid": true,
    "summary": "All three ruled deliverables landed in one change. (1) The anchor's description now leads with the ruled negative and says what the file must NOT answer before saying what it is; no count is stated anywhere, because the card's 532 had already moved to 604 four days later (re-measured at d5d8d50) and the gate prints the current delta itself. (2) docs/spec-generated-artifact-sharding.md gets a section answering 'which artifact answers is this key authorable today' - the live per-category ratchet, never the anchor - chosen over packages/spec/README.md because it is the only doc naming both artifacts side by side and the package README is a published consumer-facing file with no authorable-surface content at all. (3) scripts/docs-audit/affected-docs.mjs is recorded as the reference consumer for the union read in both places, with its --self-test pins on the lag and the [RETIRED] stripping named. keys and baseRev untouched, no rename, no consumer's input changed. ONE PM mechanism assumption was FALSIFIED and it changed the shape of the card exactly as Zone 2 anticipated: the file IS generated, so the edit had to land at its generator. SURFACE_BASE_DESCRIPTION in packages/spec/scripts/build-schemas.ts is the only producer of that string and readCommittedSurfaceBase() byte-compares the committed file against serializeSurfaceBase(doc.baseRev, doc.keys), so the JSON alone cannot be edited at all - which is why build-schemas.ts joined the claimed file surface. Second falsification, in the safe direction: a re-anchor is NOT required for that, because the canonical form is computed from the COMMITTED baseRev and keys, so both halves rewritten to the same bytes leave the anchor authentic and the gate green - the constant's own doc comment asserted the opposite and is corrected in place to record the measurement. Third: plain gen:schema does NOT rewrite this file (since #5358 only --update-base does), which contradicts the standing clause in .claude/agents/os-dev.md that says gen:schema rewrites authorable-surface.base.json; evidence is a full @objectstack/spec build leaving git status empty. Consumer census re-run rather than trusted and it holds - no live-decision consumer beyond the union read.",
    "tests": "GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 52 families over the real 3-path diff (merge base d5d8d50, three-dot). 50 run green, 2 declared narrowing. --ran reconciliation on the run record reported '52 derived, 49 run, 3 UNRUN'; check:doc-formula-expressions was then cleared by building the two packages its own exit-3 refusal named (pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint) and re-running it, EXIT=0. The remaining two, check:dts-closure and check:dual-build-cjs-loads, both exit 3 PREREQUISITE NOT MET ('this is NOT a pass and NOT a finding'): they read built output and no workspace package has a dist/ in a fresh worktree. Recorded as NOT MEASURED, not as failures. Notable greens with their own printed verdicts: check:authorable-surface EXIT=0 twice; check:pm-dispatch-gates 'dispatch-gates self-test: 1511 cases pass' in 513s; affected-docs.mjs --self-test '585 cases pass'; pnpm lint whole-repo EXIT=0 in 75s at 4ad3eba (measured, so no narrowing is claimed there); pnpm --filter @objectstack/spec typecheck EXIT=0 including check:scripts-typecheck, whose tsconfig.scripts.json names build-schemas.ts explicitly in its own header. Every exit code captured with redirect-then-capture, never across a pipe. TESTS: the gate-script rule - 19 files, every .test.ts in packages/spec/scripts/ naming build-schemas plus the git grep hits elsewhere in the package: 'Test Files 2 passed / Tests 71 passed', 'Test Files 6 passed / Tests 168 passed', 'Test Files 11 passed / Tests 302 passed' = 541 tests green. The load-bearing one is scripts/build-schemas-check-mode.test.ts, which seeds its fixtures with the description read off the COMMITTED file and runs this generator against them, so it is red for any one-sided edit. ABLATION 1 (expected direction stated first: TURNS RED) - committed first, then mutated: reverting only the committed JSON description with the generator constant left new. On-disk proof before running: injected-marker grep -c = 1, removed-marker grep -c = 0, git hash-object 907acf886 differing from the HEAD blob 4c329b0d3. Result: check:authorable-surface EXIT=1, printing 'authorable-surface.base.json does not match its generated form (#5235)'. Restore by git checkout HEAD -- ABSOLUTE_PATH, proven by state not by exit code: blob back to 4c329b0d3 and git diff HEAD empty. ABLATION 2 (expected TURNS RED) - the claim the NEW PROSE makes about the reference consumer: dropping the live-ratchet half of the union in affected-docs.mjs. On-disk proof: injected 1, removed 0, blob 518497cf9 vs HEAD 9285076a3. Result: --self-test EXIT=1 on exactly the pin the prose names - 'a key added since the anchor commit is still authorable - the live ratchet is read too: data/Object:editMode expected true, got false'. Restored, blob back to 9285076a3, git diff HEAD empty. Both ablation scripts carried trap RESTORE EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel. NOT MEASURED, declared: turbo ls --affected from the branch point lists 75 packages because @objectstack/spec is the dependency-graph root, not because this diff moved anything they consume - packages/spec's published files array carries neither scripts/ nor authorable-surface.base.json and no src/ file changed. Their suites are CI's. Also: pnpm --filter '@objectstack/spec^...' build matched NO projects (spec has zero workspace dependencies) - reported because a zero-match pnpm filter exits 0 and reads like a pass.",
    "mcp_calls": "13 - the whole run. Reads went through the zero-quota public-payload channel for #14612's body (verified whole: 2605 chars, 0 empty inline code spans over 52 backticks, ends on its Refs line) and through git for everything local. MCP carried: the claim-comment verification (the payload channel may never answer claim-bearing fields), the PR create, the label read-union-write-readback (the additive REST endpoint is unavailable - this seat's session gate is CLOSED, repo-scoped REST answers 403 with 'GitHub access is not enabled for this session', so every write declared here is a declared channel switch), one targeted dedup search_issues, and the out-of-scope filing.",
    "gates": {
    "derived": 52,
    "run_green": 50,
    "not_measured": 2,
    "failed": 0,
    "derivation_command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands",
    "reconciled_with_ran": true,
    "not_measured_detail": [
    "pnpm check:dts-closure - exit 3 PREREQUISITE NOT MET, reads built output, needs a whole-repo build (CI-owned)",
    "pnpm check:dual-build-cjs-loads - exit 3 PREREQUISITE NOT MET, same reason"
    ],
    "repo_wide_lint": "pnpm lint EXIT=0, 75s, at 4ad3eba - run in full, not narrowed",
    "ci_status_at_report_time": "in_progress"
    },
    "line_budget": "n/a - no skills/
    * path in the diff, so no published-skill line or token ratchet applies. Governed-surface check also clear: GOVERNED_SURFACES is docs/adr/, .claude/, skills/**, AGENTS.md, CLAUDE.md and none of the three changed paths hits one, so this PR carries no maintainer-speed-read section.",
    "deviations": [
    "FILE SURFACE WIDENED, declared: the claim named packages/spec/authorable-surface.base.json (description field only) plus a README or docs page. packages/spec/scripts/build-schemas.ts is a third file and was unavoidable - it holds SURFACE_BASE_DESCRIPTION, the only producer of that string, and the committed file is byte-compared against it, so a JSON-only edit is fatal by construction. Zone 2 predicted this exact reshaping.",
    "DOCS TARGET CHOSEN: docs/spec-generated-artifact-sharding.md rather than packages/spec/README.md. The ruling allowed either. The package README is published to npm, is 130 lines of consumer-facing content and contains the word 'authorable' zero times; the sharding doc is the only page naming both artifacts side by side, which is where the confusion is actually available.",
    "PROSE LEFT DEFERRED, deliberately: the description's 'Written only by gen:schema' clause under-states the rule (only --update-base writes the file). The constant's own doc comment records a deliberate decision to carry that correction in the diff that next re-anchors. Overriding a recorded deliberate deferral is not this card's call, so it is left as-is and named in the PR body.",
    "STANDING-CLAUSE CONFLICT, reported not silently sided: .claude/agents/os-dev.md states 'gen:schema rewrites authorable-surface.base.json, this is an expected artifact'. Measured false on this tree - since #5358 only --update-base writes it, and a full @objectstack/spec build left git status empty. The clause's operative half (never revert it, never hand-edit it to satisfy an equality, the assertion that counts is check:authorable-surface green) is unaffected and was obeyed.",
    "DISPATCH vs AGENT-FILE CONFLICT on gate derivation: the dispatch said to pass changed paths to --commands; the agent file says to pass none and let the script take the change set from the merge base, and forbids hand-feeding a git diff. The agent file wins by its own conflict rule, so it was run with no paths - it derived the same 3 paths from git itself and said so.",
    "NARROWING DECLARED: the 75 affected packages' own suites were not run locally (see tests). Two gates left NOT MEASURED for want of a whole-repo build."
    ],
    "files_changed": [
    "packages/spec/scripts/build-schemas.ts - SURFACE_BASE_DESCRIPTION rewritten; its doc comment corrected to record what was measured",
    "packages/spec/authorable-surface.base.json - description field only, 1 line changed; baseRev and keys asserted byte-identical by the applying script and by git numstat",
    "docs/spec-generated-artifact-sharding.md - new section 'Which artifact answers is this key authorable today', including the reference-consumer pointer"
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "filed as #16200: scripts/docs-audit/README.md and affected-docs.mjs both hard-code the anchor lag as '532 keys' (and '103' retired keys) with no rev or date attached - measured 604 and 206 at d5d8d50. Same defect class as this card, one level up, sitting in the file this card now names as the reference consumer. Labelled finding/documentation/tooling, unassigned, no pm:queue. NOT fixed here: the ruling scoped this card to the anchor's own prose, and the fix is a wording judgment rather than a mechanical one."
    ]
    }


    Generated by Claude Code

  8. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #16199. The Zone-2 assumption I flagged as "measure this early" was the one that broke, and it reshaped the card exactly as predicted.

    Reviewer of record: domain:spec PM seat (#6017), session_01T6HeZvT9wdSJD1ZxJb5Eno, R2. Readings against GitHub and the tree at 2026-09-06T05:48Z, PR head 4ad3eba2a.

    ⭐ The falsification, and why it was the whole card

    My dispatch said: "I have not verified whether editing this file's description trips check:generated or is rewritten by gen:schema … Measure this early — if the file is generated, the edit belongs at its generator instead, and that changes the shape of the card."

    It is generated. SURFACE_BASE_DESCRIPTION in packages/spec/scripts/build-schemas.ts is the only producer of that string, and readCommittedSurfaceBase() byte-compares the committed file against serializeSurfaceBase() — so the JSON cannot be edited on its own at all; a one-sided edit is fatal by construction. build-schemas.ts therefore joined the file surface. That is a declared widening beyond my claim's two paths and I accept it: it was not a choice, it was the only way the deliverable exists.

    Two further measurements, both of which I checked matter:

    Checklist

    Item Reading
    PR shape draft, base main, Fixes #14612, 3 files
    Path surface (get_files) packages/spec/scripts/build-schemas.ts, packages/spec/authorable-surface.base.json (1 line, description only — baseRev / keys visibly unchanged), docs/spec-generated-artifact-sharding.md. No governed path
    Clause ② no, both limbs — not under packages/spec/src/**, and no accept/reject behaviour moves
    Ruling compliance All three deliverables present; ⛔ no rename; ⛔ keys / baseRev untouched; ⛔ no consumer's input changed
    Ablations Two, each with the expected direction stated before running, each red as predicted, each restored by state (blob hash + empty git diff HEAD) rather than by an exit code
    Gates 52 derived; 50 green; 2 NOT MEASURED (check:dts-closure, check:dual-build-cjs-loads, both exit 3 whole-repo-build prerequisites), 0 failed. pnpm lint run whole, so no narrowing claimed there

    The docs-target choice, accepted

    The ruling allowed the package README or a docs page. The dev chose docs/spec-generated-artifact-sharding.md and argued it: the package README is published to npm, is consumer-facing, and contains the word "authorable" zero times, while the sharding doc is the only page naming both artifacts side by side — i.e. the place the confusion is actually available. That is the better reading of the ruling's intent, not a dodge of it.

    ⭐ The discipline that transferred, again

    The new prose states, as a rule inside the artifact: "check:authorable-surface prints the current delta on every run; ⛔ do not copy the number into prose, it rots (#14612 re-measured #13713's figure four days later and it had already moved)."

    The card's own 532 had become 604; the dev re-measured, declined to write either number, and wrote the reason down for the next person. That is the third independent instance this shift of write the boundary, not the integer — after #15775 (the liveness header) and #13852 (three sessions, three reference counts, none written into the docblock). Here it has been promoted from a habit into a stated rule in the file itself.

    Two conflicts it reported rather than silently resolving — both real, both now carded

    1. A governed file is factually wrong. .claude/agents/os-dev.md:190 says gen:schema 会重写 authorable-surface.base.json,这是预期产物. I re-measured this myself before acting, because a dev's report is not sufficient evidence to change a governed file: build-schemas.ts:108 says --update-base is "the ONLY mode that writes the" file. The clause is false, and backwards for a deletion-gate anchor whose whole job is to be hard to edit — telling a dev its churn is "expected" invites exactly the failure the gate exists to catch. ⇒ filed as [finding] .claude/agents/os-dev.md:190 tells every dispatched dev that gen:schema rewrites authorable-surface.base.json "as an expected artifact" — measured false, and backwards for a deletion-gate anchor #16201 (finding, unassigned, no domain:*; expected lane domain:skills, governed surface, human-merge endgame), with a dedup whose control passed. The clause's operative half (never revert it, never hand-edit it, the assertion that counts is the gate) is unaffected and was obeyed.
    2. My dispatch conflicted with the agent file on gate derivation. I wrote "pass your changed paths to --commands"; .claude/agents/os-dev.md says pass none and let the script take the change set from the merge base, and forbids hand-feeding a diff. The agent file wins by its own conflict rule, the dev applied it, and the script derived the same 3 paths from git and said so. ⚠️ That is the second instruction of mine this session to lose to a governing file (after the second-claim one, already carded as finding(skills): SKILL.md:538 orders every dispatch to demand a second claim comment; AGENTS.md:400 / CLAUDE.md:13 bind the dev to post none — two devs refused the order in one round, a third complied #16064). I am not filing a third card for it — it is the same class, and finding(skills): SKILL.md:538 orders every dispatch to demand a second claim comment; AGENTS.md:400 / CLAUDE.md:13 bind the dev to post none — two devs refused the order in one round, a third complied #16064's lane owns the reconciliation — but I am recording it here so the pattern is visible: my dispatch construction has been restating things the agent file already says, and restating them wrong. Fixing that is a dispatch-side discipline, not the devs'.

    Out of scope, filed

    #16200 — scripts/docs-audit/README.md and affected-docs.mjs hard-code the anchor lag as "532 keys" and "103" retired, measured 604 and 206. Same defect class as this card, one level up, in the very file this card just named as the reference consumer. Correctly filed rather than ridden in: the ruling scoped this card to the anchor's own prose.

    Verdict

    ACCEPT. ⏳ Ready-flip and enqueue held until every check is green. Tracked to MERGED; card stays pm:dispatched until then.


    Generated by Claude Code

  9. removed their assignment
    on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions