Skip to content

docs-audit: qualify data-property anchors by their declaring container against the authorable surface - #14607

Merged
baozhoutao merged 5 commits into
mainfrom
claude/issue-13713-docs-audit-container-qualified-anchors
Sep 2, 2026
Merged

docs-audit: qualify data-property anchors by their declaring container against the authorable surface#14607
baozhoutao merged 5 commits into
mainfrom
claude/issue-13713-docs-audit-container-qualified-anchors

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #13713

The docs-audit half of option D on #12824 (maintainer ruling 2026-08-31, comment 5475441954). Option C (#13738) is already on main; this builds on the provenance shape it left.

 scripts/docs-audit/README.md         |  76 ++++++-
 scripts/docs-audit/affected-docs.mjs | 383 +++++++++++++++++++++++++++++++++--
 2 files changed, 437 insertions(+), 22 deletions(-)

The rule, in one paragraph

When the most specific declaration enclosing a changed line is a data property (name: / name?: / name =), its declaring container is resolved through the generated packages/spec/declaration-map/*.json (TS declaration name to spec type name) and the resulting cat/Type:property key is tested against the generated authorable surface. Three outcomes, and the third is the whole safety property: (a) the container resolves and the key is authorable, so the property anchor is minted exactly as before and its provenance clause now names the spec key (a field of const object ObjectSchemaBase, an authorable key of data/Object); (b) the container resolves and the key is not authorable, so the property anchor is dropped and the existing container branch decides on exactly the terms it already decides on for every other line that reaches it — no new fallback is invented; (c) the container is unmapped or ambiguous, so today's behaviour is kept unchanged. Only members whose form reads as a data property are qualified; a method, a nested interface, a namespaced type are all outside the rule. Option B (blanket prefer-container) is not implemented, including as a fallback: (c) mints. Fail toward noise, never toward silence.

Reconciliation with the projection: the ruled rule realises 0 rows removed, not minus 17.4 percent

This is the honest headline and it is measured, not argued. The projection on #12824 hand-classified 20 containers that minted a dropped data-property anchor and reported 18 of them internal implementation types. Under the ruled rule an internal container that the generated map does not carry is case (c) and therefore keeps. Only containers the map resolves can drop. Every named container from that list, checked against the map on this branch:

container (from the #12824 projection) in declaration-map verdict under the ruled rule
MetaOverlayCacheKey no keep — unmapped
MetaOverlayCacheEntry no keep — unmapped
LocalizationCacheEntry no keep — unmapped
AuthzCachePostureInput no keep — unmapped
MintScimConnectionCredentialInput no keep — unmapped
SysScimConnectionBinding no keep — unmapped
AUTH_MODEL_TO_PROTOCOL no keep — unmapped
enObjects no keep — unmapped
DatasourceDef no keep — unmapped (this is the schemaMode pin; see below)
ObjectSchemaBase yes, data/Object keep — data/Object:userActions is authorable (case a)

So none of the projected 20 drops. The reduction the card quotes is not reachable by the ruled rule against the map #13712 delivered, because the map's coverage is packages/spec declarations while the noisy containers all live outside it. ⛔ The drop set was not widened to reach the projected number — that would be option B by another name. The number moves when the map's coverage grows, which is spec-lane work, not a local table here.

What the change actually does, measured end to end

Two replay populations, both arms pinned as copies and run against the same tree at the same commit in a detached replay worktree, since-ref = SHA^.

Population 1 — 100 consecutive main commits touching packages/ (the shape #12824 and #13738 both used):

base arm this branch
rows (docs listed, summed) 516 516
anchors 660 660
commits whose output moved 0
container-qualified drops 0

Byte-identical on every commit, with the two additive fields removed and anchors[].from normalised away. Zero rows lost, zero rows gained.

Population 2 — 120 consecutive main commits touching packages/spec/src (deliberately chosen as the population where the rule can bite, since only spec declarations are in the map):

base arm this branch
rows (docs listed, summed) 1226 1226
anchors 1503 1512
commits whose output moved 1 of 120
container-qualified drops 10
rows lost 0
rows gained 0

The 10 drops are all history and guidance keys of the strictObject helper's own config block on nine spec schemas (ObjectSchemaBase, ApiEndpointSchema, ExpressionBodySchema, ExternalDatasourceSettingsSchema, ObjectExternalBindingSchema, RowCrudActionOverrideSchema, ScriptBodySchema, ActionAiSchema, PageContainerProps, ReferenceRailEntrySchema) — genuine noise, and noise that already matched no page, which is why the row count does not move. Every one is named in the new containerQualifiedDrops field.

On the same population, 8 anchors were kept and annotated as authorable spec keys, including userActions twice — the ruled true positive, arriving through real commits rather than through a probe.

Measured cost, stated rather than hidden. Case (b) hands the line to the container branch, and the container anchor is coarser than the property it replaces, so this rule is not purely subtractive: on a constructed probe editing surface: 'this object' inside ObjectSchemaBase, the base arm derived no anchor at all (surface is not code-shaped, so the shape guard already dropped it and published it in weakAnchorsDropped) while this branch drops it earlier and falls to ObjectSchemaBase, adding one row (concepts/metadata-driven.mdx). Neither replay population contains such a case, but the class is real and it is the same class option B would have.

The two ruled true positives, end to end, and they survive by different routes

Each pin: a one-line widening committed at the real declaration site in a throwaway worktree, then both arms run over that commit.

  • userActions (packages/spec/src/data/object.zod.ts) — data-modeling/objects.mdx listed in both arms, 5 docs to 5 docs, no row moved. Kept through case (a): ObjectSchemaBase resolves to data/Object and data/Object:userActions is authorable. The row now reads userActions (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object).
  • schemaMode (packages/objectql/src/engine.ts) — data-modeling/drivers.mdx listed in both arms, 3 docs to 3 docs, no row moved. ⚠️ Kept through case (c), not through the authorable lookup: its container is DatasourceDef, an objectql-local interface the generated map does not carry. --self-test pins that absence as a fact rather than papering over it, and pins alongside it that data/Datasource:schemaMode is authorable — so the day the map carries DatasourceDef, the pin survives via case (a) instead. ⛔ No mapping was hard-coded to make it resolve.

A defect found while measuring: the anchor artifact is stale by design

packages/spec/authorable-surface.base.json — the artifact #12824 and this card both name — is not the live surface. Its own description says it is "a verbatim copy of the keys in authorable-surface/ as they stood at baseRev", a fixed commit for the deletion gate (53ef05744, 2026-08-08). Measured on this tree it lags the live per-category ratchet by 532 keys, and the lag is load-bearing: data/Object:editMode and every key of security/OrgScopingEntitlement and api/ProvenanceWaiver are absent from the anchor and present in the ratchet. Reading the anchor alone suppressed anchors on genuinely authorable keys — a false negative, growing with every key added after baseRev, which is the one direction the ruling forbids. Measured directly: the first build of this branch produced 20 drops on population 2, ten of which were that class (editMode, platformGlobalObjects, supportedPostures, suppressUnboundedOrgAdminGrant, package, reason, registeredUnder, body).

So the authorable surface is read as the union of packages/spec/authorable-surface/ (the live ratchet, read as one set, exactly as its own gate reads it) and the named anchor. A union can only ever keep an anchor one source vouches for, never drop one more. [RETIRED] is stripped for the same reason — the ratchet's description says a tombstoned key "still rejects with an upgrade prescription", so it is still surface a page documents, and an exact-match lookup would have silently dropped all 103 of them.

Verification

  • node scripts/docs-audit/affected-docs.mjs --self-test — its own verdict line: ✓ affected-docs self-test: 545 cases pass. (516 on origin/main; +29, and the count confirms all 29 executed).

  • Reverse verification, twice, on the committed tree. Mutation and restore both proven on disk; there is no build or dist leg for this file — it is a plain node script with no compile step, so none is claimed. Each leg ran under a trap with absolute paths, and each restore is git checkout HEAD -- ABSOLUTE_PATH proven by observed state (git diff HEAD empty and worktree blob equal to the HEAD blob cc226a4f3), never by an exit code.

    1. Option B by another name — case (c) made to drop. Injected-marker count 1, removed-text count 0. Reds 12 cases, including all three anti-B keep pins, both degradation pins, the schemaMode live pin, and three pre-existing [decision] docs-audit: a data-property anchor is both the noisiest and the most valuable anchor the tool mints — 70 of 402 rows, and no cheap discriminator survives measurement #12824 provenance pins. EXIT=1.
    2. Case (b) made to stop dropping — the qualification does nothing. Injected 1, removed 0. Reds exactly the 2 drop pins (the anchor it removes and the ledger entry that names it). EXIT=1.
      The gate union below was run at this branch's final commit 7ea5cf10a (the merge of origin/main, which moved twice during this task), on a clean tree.
  • Run reconciliation: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RECORD at 7ea5cf10a✓ dispatch-gates --ran: 18 derived famil(ies) accounted for — 17 run, 1 NOT-MEASURED.

  • Gate family derived in this worktree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands scripts/docs-audit/affected-docs.mjs (the script confirmed the answer is about objectstack-ai/objectstack at this checkout). All green, each read from the gate's own verdict line, exit codes captured before any pipe: check-affected-docs.mjs, check-drift-comment.mjs (56 cases pass across 5 fixture diff(s) — it drives the workflow's real comment script over real mapper output), check-self-test-wired.mjs, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, bare-root-worklist.mjs --self-test, check:docs-audit-scope, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, check:nul-bytes (scanned 7976 text file(s) ... no raw ASCII control bytes).

  • pnpm check:pm-dispatch-gates (the heavy family, under scripts/pm/os-verify-lock.sh): VERDICT command-exit 0 · held the lock 295s · waited 6s, self-test ✓ dispatch-gates self-test: 1240 cases pass. — run twice, once before the origin/main merge and again at 7ea5cf10a after it, green both times.

  • check-test-completeness.mjs: NOT MEASURED, not a red — EXIT=3 PREREQUISITE NOT MET, the gate grades a saved turbo test log and the family names it with no argument.

  • Gate-script rule: no *.test.ts / *.test.mjs in the repo names affected-docs or docs-audit (git grep returned nothing). This script's suite is --self-test plus the two check-*.mjs gates above; all three run.

Scope

  • packages/spec is untouched: git diff origin/main --stat -- packages/spec is empty. The declaration map and both authorable-surface artifacts are generated (gen:declaration-map, gen:schema, covered by check:generated) and are only ever read here.
  • ⛔ No local container-to-spec-type table. A container the map lacks keeps today's behaviour and is reported, never patched around.
  • NON_DECLARATION_HEADS, the generic-name list and every threshold are untouched.
  • scripts/** only, no package publishes, so skip-changeset.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV


Generated by Claude Code

@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions github-actions Bot added the size/m label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

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 skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs-audit: qualify data-property anchors by declaring container against authorable-surface (option D on #12824)

2 participants