Skip to content

feat(spec)!: book audience gates by permission set — retire the { profile } arm (ADR-0090) - #2732

Merged
os-zhuang merged 1 commit into
mainfrom
claude/tender-johnson-nq25xp
Jul 10, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/tender-johnson-nq25xp

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

BookAudienceSchema (packages/spec/src/system/book.zod.ts) still modelled its gated arm as { profile: string } — a reference to the Profile concept ADR-0090 D2 deleted — with pre-D3 vocabulary ("role-gated", "guest profile") in the comments. This PR retires it with a launch-window one-step rename, no aliases.

Decision: permission set, not position

The gated arm becomes { permissionSet: string }, a capability reference, for three reasons grounded in ADR-0090:

  1. Package ownership (D9). Books ship in packages; packages own permission sets but never positions/BUs/teams — those are environment-owned. A book gating to a position name would reference an entity the package cannot know, which is exactly the environment-ownership argument (D2 Add metamodel interfaces for ObjectQL/ObjectUI contract #1) that removed profiles.
  2. D9's admin stance is this shape. App-level admin is "an ordinary package set (crm_admin) the customer binds to a position of their own choosing" — so a CRM package's Admin Guide gates to { permissionSet: 'crm_admin' }, keeping ADR-0086 provenance and uninstall semantics intact.
  3. Vocabulary axis (D3). "Who may read this book" is a capability question; capability = permission_set. Positions are distribution. The two literals already map to the D9 anchors: 'org' inherits the package grant (authenticated members), 'public' ≡ the built-in guest position.

ADR-0046 §6.7 itself glossed the arm as "gates a book to a profile / permission set", so this lands on the reading that survived ADR-0090.

Changes

The REST read layer only evaluates isPublicAudience (anonymous gate), so no runtime evaluation code changes; enforcement of the gated arm remains the ADR-0046 Phase D item it already was.

Companion PR

ObjectUI mirrors this shape in three places (metadata-admin JSON schema oneOf, BookPreview audience chip, book list-column renderer) — companion PR in objectstack-ai/objectui updates them to { permissionSet }.

Why the D3 forbidden-word lint didn't catch this

The D3 lint (security-role-word, packages/lint/src/validate-security-posture.ts, shipped in P3 #2711) validates published stack metadata — names/labels of objects, fields, actions, permission sets, positions, and apps. It could not have caught this because:

  1. The violation lives in spec source code (a TS comment and a zod key), which no lint scans for the word ban — the ADR's "identifiers, UI copy, and docs" scope is enforced for metadata only; source hygiene relied on the P1 rename sweep (feat(security)!: ADR-0090 P1 breaking wave — position rename, profile removal, secure OWD default, principal/external spec shapes #2697), which was identifier-driven (sys_role, isProfile, …) and missed this file because the arm was spelled profile, not role.
  2. Even as metadata, books are not among the kinds the lint scans — and profile (unlike role) is not a banned word, so a book authored with { profile: ... } drew no finding. After this PR the schema itself rejects the shape, which is the stronger, contract-first gate.

Possible follow-ups (not in this PR): a source/docs-level \brole\b sweep check (e.g. content/docs/ui/role-based-interfaces.mdx still violates the D3 docs ban), and adding book name/label to the lint's role-word scan now that book audience is a permission-model reference. Two more D2 leftovers found in objectui's default-schemas.ts (role and profile metadata-type entries mirroring the framework's metadata-form-registry.ts profile: permissionForm row) are flagged in the companion PR rather than fixed, since removing them needs a coordinated registry change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom


Generated by Claude Code

…file } arm (ADR-0090)

ADR-0090 D2 removed the Profile concept, but BookAudienceSchema still
modelled its gated arm as { profile: string } (with pre-D3 vocabulary in
the comments). Books ship in packages, and packages own permission sets
but never positions (D9 — the same environment-ownership argument that
removed profiles), so the gate is a capability reference:

- `{ profile: string }` → `{ permissionSet: string }` — one-step rename,
  no alias; the zod union now rejects `{ profile }` at parse time.
- `'public'` comment now points at the built-in `guest` position (D9)
  instead of the removed "guest profile".
- Regenerated content/docs/references (book.mdx), which also catches up
  security/* with the AdminScope (D12) and explain (D6) shapes that
  landed without a docs regen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom
@vercel

vercel Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 10, 2026 3:44am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling size/m labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

93 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 10, 2026 04:43
@os-zhuang
os-zhuang merged commit 80f12ca into main Jul 10, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/tender-johnson-nq25xp branch July 10, 2026 04:43
os-zhuang added a commit to objectstack-ai/objectui that referenced this pull request Jul 10, 2026
…pec's { permissionSet } shape (ADR-0090) (#2371)

The framework retired BookAudience's { profile } arm (ADR-0090 D2 removed
the Profile concept; D9 makes the gate a capability reference). Update the
three UI mirrors: the metadata-admin default JSON schema oneOf, the
BookPreview audience chip, and the book list-column renderer. The "Role-
gated" form title also violated the D3 word ban and is now
"Permission-set gated".

Companion to objectstack-ai/objectstack#2732.


Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 10, 2026
… the ADR-0090 D2/D3 cleanup the P1 wave missed (#2774)

* feat(security)!: enforce book audience at the REST read layer; finish the ADR-0090 D2/D3 cleanup the P1 wave missed

Follow-through on the { permissionSet } book-audience rename (#2732), in
ADR-0049 discipline — the gated arm existed but nothing enforced it:

- rest: /meta/book, /meta/doc, and /meta/book/:name/tree now enforce the
  ADR-0046 §6.7 audience model. Anonymous callers see only public
  books/docs; { permissionSet }-gated books require holding the named set;
  a doc's effective audience is the union over the books that CLAIM it
  (unclaimed → org; orphan rendering never inherits public). Fails CLOSED
  when holdings cannot be resolved. doc/book item reads bypass the shared
  meta cache (per-caller gate vs shared ETag). Nine new route tests.
- spec: pure helpers powering the gate (audienceAllows,
  resolveDocAudiences, docAudienceAllows, resolveBookClaimedDocs) with
  unit tests; the REST layer and any future portal share ONE semantics.
- plugin-security: security service exposes resolvePermissionSetNames —
  the same resolution as data-plane enforcement.
- D2/D3 leftovers: METADATA_FORM_REGISTRY role→position (the position
  type had LOST its form layout in the P1 rename) and profile removed;
  artifact ingestion maps positions→'position' (stale roles→'role'
  matched nothing and silently dropped compiled positions);
  EnvironmentArtifactMetadataSchema declares positions; metadata-form
  translations regain position and drop role/profile in all four locales
  (+ vocabulary regression test); position.test/audit.zod/security-plugin
  identifiers and comments de-role'd; eslint + doc-authoring domain lists
  Role→Position; content/docs/ui/role-based-interfaces.mdx renamed to
  audience-based-interfaces.mdx with stale permission-vocabulary copy
  fixed across five docs.
- lint: books join the D3 role-word scan; new advisory rule
  security-book-audience-unknown-set flags a gated audience naming a set
  the stack does not declare (runtime fails closed — surface the typo at
  author time).
- scripts/check-role-word.mjs: ADR-0090 D3 vocabulary RATCHET over
  content/docs + skills (baseline freezes the 45 current files; new
  occurrences fail CI; improvements ratchet the baseline down). Wired
  into the lint workflow.

Verified: spec/objectql(808)/cli(464)/rest(227+9)/metadata(260)/
platform-objects(76)/plugin-security/lint(32) suites green; eslint,
check:role-word, check:doc-authoring, check:api-surface (regenerated),
check:liveness, check:skill-docs, check-changeset-fixed all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom

* test(lint): de-flake lazy-deps in-process gate test — explicit 30s timeout

The in-process variant cold-loads sucrase + typescript and took 5.6s on a
loaded CI runner, tripping vitest's default 5s per-test timeout (Test Core
failure on this PR's first run). The test asserts a loading CONTRACT, not
latency — give it an explicit generous timeout like the sibling dist-based
variants effectively have via their spawn overhead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ned and URL-spelled citations, pinned in one spelling table (objectstack-ai#20989)

Fixes objectstack-ai#20636
Clause-②: no

The family closeout for the citation extractor in
`scripts/check-issue-citations.mjs`: every spelling a seat measured as
invisible to the diff gate and the census is now read, every exclusion
keeps only the shapes it was measured protecting, and the self-test
carries the one enumeration table the triage asked for (48 spellings,
each "extracted as" or "not a citation, because"). Landing site:
`scripts/check-issue-citations.mjs` only.
`scripts/check-doc-authoring.mjs` is untouched; H2 below says why.

## What changed

- **Hyphen after the number.** The lookahead no longer refuses a `-`, so
`objectstack-ai#13398-class`, `objectstack-ai#5347-A` and `ui#6206-B` read as citations of their
number. It still refuses a word character, so a hex colour stays out.
- **Slash before the `#`.** A `/` is now valid context before a bare
`#`. It stays refused only before a qualifier candidate, so a URL path
fragment such as `https://example.com/docs/page#12` never reads `page`
as prose. A `/` right after a digit is the exception, so
`objectstack-ai#3076/objectui#2614` still reads its own qualifier.
- **Slash-joined continuation.** In `#A/#B`, the second number takes the
chain head's reading: bare after a bare or prose head, the head's
repository after a qualified head, and an ordinal after an ordinal. Only
a joined `/` continues a chain. `objectui#1 / objectstack-ai#2` and `objectui#1 + objectstack-ai#2`
stay two separate readings.
- **URL spelling.** `https://github.com/OWNER/REPO/issues/N` and
`.../pull/N` are now citations, qualified by their own `OWNER/REPO`.
That puts `objectstack-ai/framework` in this repository and makes every
other repository's URL cross-repo, never a finding. Inside a markdown
link `[#N](URL)`, the citation counts once.
- **Head rows.** I retired `re-charter`, `clause` and `option` (named in
the thread), plus `acceptance` and the section mark (found by the same
measurement). Each protects 0 sites repo-wide at two or more digits and
hides board citations. The grammar's two-digit floor already keeps
one-digit ordinals out. I added a `](` row, the narrower guard the
hyphen exclusion leaves behind for markdown in-page heading anchors.
- **Self-test.**
  - A new `spellings` battery (56 cases) reads the table row by row.
- Every head row must excuse at least one table row, and every required
spelling (the card's list plus the thread's) must be present.
- The `live-corpus` battery gains three floors, one per new arm, each
counting only a number spelled once on its line.
  - The roster floor rises from 6 to 9 batteries.
  - Total: 114 cases in 8 batteries became 173 in 9.
- I edited the header in place and kept its line count, because
`scripts/pm/dispatch-gates.mjs`'s self-test pins
`scripts/check-issue-citations.mjs:204 local-env`. The marker is still
on line 204, and that pin passes (see Gates).

## H1: what each exclusion protected and hid

Measured on `3693a1b50` over the declared surfaces. Every arm was judged
against one enumerated board: 188 pages, frontier 20959,
2026-09-30T22:55Z. The instrument mirrors the gate's extractor and
matched it file for file on all 2,640 files (0 mismatches).

| exclusion | hid (sites, dead) | protected in the declared surfaces |
disposition |
|---|---|---|---|
| hyphen after the number | 58, 1 dead (8 cross-repo) | 0: no numeric
range, slug or hex-like token. Repo-wide: in-page heading anchors, 16
lines in `docs/design/**` and `skills/**`, plus one range,
`docs/audits/...md`, whose first number is a citation | dropped; the
`](` row keeps the anchor out |
| `/` before the `#` | 528, 10 dead: 523 `#A/#B` second numbers and 5
`TOKEN/#N` such as `ADR-0049/objectstack-ai#1888` | 0 paths and 0 URL fragments |
narrowed to the candidate arm; continuations read as their chain |
| head `re-charter` | 0 left on this tree (the 26 dead `re-charter
objectstack-ai#13135` were rewritten by PR objectstack-ai#20750) | 0; only the gate's own fixtures
used it | retired |
| head `clause` | 0 in the surfaces; 4 in deferred test files, all board
citations | 0 | retired |
| head `option` | 1 (`option objectstack-ai#14088`, live); 1 more under `scripts/**` |
0 | retired |
| head `acceptance` | 1 (`the silent acceptance objectstack-ai#6132 closed`, live) | 0
at two or more digits | retired (in-place, below) |
| head section mark | 0 in the surfaces; 4 in test files (`§6 objectstack-ai#11176's
decisions`) | 0 at two or more digits | retired (in-place, below) |
| heads kept | none measured hiding a citation | directive 203 (max 13),
`PD` 85 (max 13), batch 276 (69 distinct, 11 to 227), `OQ` 10, `PKCS` 1
| kept |

The 8 slash chains headed by another repository are not a case where the
two populations cannot be told apart. The 5 on objectui's public board
each name objectui's record, the issue and then the pull request that
fixed it, read one by one against both boards:

- `objectui#2715/objectstack-ai#2717`
- `objectstack-ai#2711/objectstack-ai#2722`
- `objectstack-ai#2725/objectstack-ai#2732`
- `objectstack-ai#2967/objectstack-ai#2904`
- `objectstack-ai#4648/objectstack-ai#4901`

This repository's records with the same numbers are unrelated. The other
3 (`cloud`, `hotcrm-heimao`) are boards one credential cannot read, so
they stay unjudged, as they were before.

## H2: where the URL spelling belongs

The extractor. At `3693a1b50`, 57 URL sites sit in the gate's
projection: 56 in package comments and 1 link on a release page. 4 of
them are dead. Only 2 URL sites in package sources are inside string
literals, both internal `note:` strings in
`packages/runtime/src/route-ledger.ts`.

`check:doc-authoring` asks a different question: may a runtime string
carry a tracker reference at all? It reads string literals, skills and
spec refusal messages. The two projections are disjoint, so adding the
URL to the extractor double-counts nothing there. Inside the extractor,
the one double-spelled site (`[objectstack-ai#15325](...objectstack-ai/issues/15325)` on
`v17/17-3.mdx`) counts once. `check-doc-authoring.mjs` is not touched.

## H3: open PRs' added lines

All 13 open PRs at 2026-09-30T23:2xZ: their heads were fetched into a
private ref namespace (deleted afterwards). For each, the BASE extractor
and this one were run over the lines it adds, against its merge base.
Result: 85 added-line citations under both extractors, 0 newly
extracted, 0 lost. No PR's verdict changes. Lines a PR does not add are
never judged, which is unchanged and pinned in the `diff-scope` battery.

## Census, before and after

The gate's own `--census --json`, once with the `3693a1b50` script and
once with this one, over the same tree:

| | judged | resolves | resolves as PR | cross-repo |
allocated-but-absent |
|---|---|---|---|---|---|
| before (frontier 20965) | 37,152 | 33,403 | 1,985 | 1,024 | 740 |
| after (frontier 20966) | 37,796 | 33,917 | 2,083 | 1,041 | 755 |

That is 644 more judged sites and 15 more dead ones, with 0 findings
lost. By arm: hyphen 1 dead, slash 10 dead, URL 4 dead.

Newly visible dead sites per lane. I rewrote none of them; they belong
to the lane cards:

- **objectstack-ai#20594 (`domain:cli`): 1.** `packages/rest/src/rest-server.ts:7456`,
objectstack-ai#11006.
- **objectstack-ai#20595 (`domain:engine`): 6.**
- `driver-sql`: `sql-driver.ts:3933` (URL, objectstack-ai#17590), `:12110` (objectstack-ai#10629),
`:16106` (objectstack-ai#17343).
  - `metadata`: `loaders/ambiguous-metadata-stem.ts:40` (objectstack-ai#14423).
- `metadata-protocol`: `migrations/partial-index-probe.ts:395` (objectstack-ai#16657).
  - `objectql`: `plugin.ts:1496` (objectstack-ai#10629).
- **objectstack-ai#20596 (`domain:services`): 0.**
- **objectstack-ai#20597 (`domain:spec`, `packages/lint`): 0.**
- **objectstack-ai#20234 (`packages/spec/src`): 7.**
  - `data/datasource.zod.ts:701` (objectstack-ai#9040).
  - `data/filter.zod.ts:1040` (URL, objectstack-ai#17590) and `:1042` (URL, objectstack-ai#17286).
  - `data/value-roundtrip-conformance.ts:100` (URL, objectstack-ai#12380).
  - `ui/component.zod.ts:593` and `:669` (objectstack-ai#6276), and `:3771` (objectstack-ai#9972).
- **Release pages (`domain:devx`, no lane card): 1.**
`content/docs/releases/v17/index.mdx:168` (objectstack-ai#6075).

This census was run on the PR head's tree, not after landing. A re-run
after landing reads the same corpus plus whatever `main` has gained by
then.

## In-place fixes beyond the three named rows

I retired the `acceptance` and section-mark rows here rather than filing
them. All four conditions hold:

1. They are the same defect class as `option` and `clause`: a head row
hiding a board citation.
2. The fix is mechanical, and the shape is pinned by the table.
3. The file is this claim's own surface (`NON_CITATION_HEADS`).
4. The same gate's self-test covers them, so no new verification surface
is added.

Evidence: `acceptance objectstack-ai#6132` (live) in
`packages/formula/src/cel-pushdown-limits.ts:82`, and the section-mark
sites in deferred test files. Neither row has any two-or-more-digit
ordinal anywhere in the repository.

## Gates (final head `25d96fcc0`)

I re-derived the list with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`, which gave 32 commands.
The same derivation on a throwaway tree at `origin/main` (`05be35259`)
with this diff applied gave an identical list. I ran all 32, plus `pnpm
check:doc-authoring` and the self-test, and every one exited 0.
Reconciliation verdict line:

`✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0
NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of
them is 3).`

Verdict lines worth quoting:

- `node scripts/check-issue-citations.mjs --self-test`: `✅ ... every
spelling enumerated ... (173 cases, 9 batteries)`. It also passes on
`origin/main` `05be35259` with this diff applied.
- `node scripts/check-issue-citations.mjs` (diff mode): `✅
check-issue-citations: no issue citations added against 3693a1b (0
file(s) read).` The script lives in the deferred `scripts/**` surface.
- `pnpm check:pm-dispatch-gates`: `✓ dispatch-gates self-test: 1976
cases pass.` (1237.9s). Its pin `scripts/check-issue-citations.mjs:204
local-env` holds.
- `pnpm check:doc-authoring`: `✓ doc authoring guard: ... hold the
baseline — 549 pinned site(s)`.
- `pnpm check:nul-bytes`: `check-nul-bytes: OK (scanned 9582 text
file(s) ...)`.
- `node scripts/check-scripts-symbol-anchors.mjs`: `✅ ... 3706 anchors
across 282 scripts resolve`.

## Ablations (one-shot, from committed `25d96fcc0`, via
`scripts/ablation-replace.mjs`)

Every leg was expected to go red, and every leg did. Each one restored
to blob `c732ce2e21c7` (equal to HEAD) with an empty `git diff HEAD`. No
permanent ablation file is left.

| mutation | first red |
|---|---|
| hyphen refused again after the number | `the live corpus must yield a
#N-word citation` |
| `/` refused again before the `#` | `the live corpus must yield a
slash-joined #A/#B second number` |
| URL arm disabled | `the live corpus must yield a URL-spelled citation`
|
| `option` head row restored | `spelling option #N in "the option objectstack-ai#14088
gave" must read objectstack-ai#14088; got nothing` |
| continuation disabled | `spelling repo#A/#B ... must read
objectstack-ai/objectui#2711, objectstack-ai/objectui#2722` |
| `](` row disabled | `a markdown link's in-page heading anchor is not a
citation` |
| link de-duplication disabled | `spelling [#N](URL) ... must read
objectstack-ai#15325` |

The first slash ablation, run before the floors were tightened, went red
in the table but left the live-corpus continuation floor green. A line
citing the same number twice let a plain citation stand in for the
slash-joined one. Commit `25d96fcc0` makes each new floor count only a
number spelled once on its line. The re-run above is red at that floor.

## Acceptance notes

- **URL spelling in runtime strings.** `check:doc-authoring` does not
read it. At `3693a1b50` the population is 2 internal route-ledger
`note:` strings (`packages/runtime/src/route-ledger.ts:459`, `:465`),
and no author-facing door shows them. Noted, not filed; carrier: none.
- **Range second numbers.** In a range such as `objectstack-ai#712-714`, the second
number carries no `#` and is not read. There is 1 site repo-wide, in
`docs/audits/**`, outside the declared surfaces. Pinned in the table
with its reason.
- **Dormant test-file citations.** The 8 test-file citations the retired
`clause` and section-mark rows hid are in the deferred test surface.
They become visible only when that surface is swept.
- **Line-number pin.** `dispatch-gates.mjs` pins this file's `local-env`
marker by line number (`:204`), so any future header growth above it has
to move that pin in the same PR.

No changeset: a root `scripts/` file publishes nothing (the root
`package.json` is private, and no package's `files` ships `scripts/`),
so this PR takes `skip-changeset`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — b723b7b5 Deployed Jul 10, 2026 by vercel[bot]
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants