You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
Split out of #12730 by the R19 dev seat after measuring it. #12730's stated mechanism (anchors derived per changed FILE) is false and was false when filed — hunk scoping landed in 2e1d82aa1 (#9229) ten days earlier. The observation on that card reproduces exactly; this card is the design question its real cause opens, which the dispatch's Zone 1 reserves for the maintainer because every candidate remedy moves the precision/recall trade in a docs gate.
The finding, in one line
scripts/docs-audit/affected-docs.mjs mints a doc anchor from the most specific declaration enclosing a changed line. When that is a data property (name: / name?: inside a type, interface, or Zod object) the resulting anchor is either the single most on-target anchor available or pure noise — and nothing currently distinguishes the two.
Measurements
Instrument identical throughout: affected-docs.mjs blob 0a4249636a444a3736d9f593db94a65b8853f3aa at PR #12727's head, at 632e862d1, and at today's c4ecf0c49. Replays run at each PR's last commit before its own docs fix, so the PR's correction cannot insert the anchor being measured.
On PR #12727 (the run #12730 was filed from) — reproduced exactly at 25 anchors / 25 hand-written rows:
13 of 25 anchors produced ZERO rows — and those 13 are every name specific to the change (MetaOverlayCacheKey, readMetaOverlayCache, cacheKeyOf, readWriteEpoch, …). Precision on the change's own vocabulary is perfect.
16 of 25 rows came from three data properties of internal cache structs: organizationId and packageId on MetaOverlayCacheKey, expiresAt on MetaOverlayCacheEntry.
Both existing guards pass them legitimately: they are code-shaped, and the corpus-share limit is floor(189 * 0.15) = 28 pages while organizationId matches 10. They are not hub terms — they are ordinary field names.
Through the bridge they also minted publishItem (sdk) and /:type/:name/publish (route) on a diff touching no state machine — the amplification the :4420 note predicts.
Population — 91 consecutive main commits touching packages/. A variant that prefers the container whenever the winning declaration is a data property:
rows
current
402
variant
330
removed
72 (-17.9%), zero added
Only 10 of 91 runs change at all — surgical, not blunt. Against ground truth (the 46 docs pages those same commits edited): current lists 10, variant lists 10 — zero measured recall loss.
Why that number does not license the change
The ground truth is too weak to license it — it can only detect losses among pages the tool already finds, and it finds 10 of 46. Reading the 72 dropped rows by hand finds true positives the aggregate cannot see:
userActions — dropped data-modeling/objects.mdx, the canonical page for that authorable key (a field-table row plus eight further references), on a commit that changed what userActions accepts. Declared in packages/spec/src/data/object.zod.ts — a definition site.
schemaMode — dropped data-modeling/drivers.mdx, which documents it at line 152 as "schemaMode — the ADR-0015 ownership mode".
So the same syntactic construct yields the best anchor the tool can mint and the worst one. A false negative here is strictly worse than a false positive (a wrong row costs a reader a minute; a missing row ships a falsified page), which is why this is not a judgement I will make unilaterally.
The discriminators that do NOT work — each disproven, not assumed
Syntactic form (data property vs method). Fails: userActions and expiresAt are both name: in an object/interface.
Declaring package (trust packages/spec only). Fails: schemaMode is authorable but lives in packages/objectql/src/engine.ts.
Property NAME against the authorable-surface registry. Fails, and instructively: expiresAt, organizationId and packageIdare authorable property names — on api/Session, cloud/Environment, api/GetMetaItemsRequest. A name-only test keeps all three noisy anchors and gains nothing.
The discriminator that DOES appear to work — and its one blocker
packages/spec/authorable-surface.base.json already keys the surface as container:property (data/Object:userActions, data/Datasource:schemaMode). Qualifying the anchor by its declaring container separates every measured case correctly:
Of the 20 distinct containers that minted a dropped data-property anchor, 18 are internal implementation types — MetaOverlayCacheKey, LocalizationCacheEntry, AuthzCachePostureInput, MintScimConnectionCredentialInput, SysScimConnectionBinding, AUTH_MODEL_TO_PROTOCOL, enObjects, … — and 2 are authorable spec types (ObjectSchemaBase, DatasourceDef).
Projected: removes 70 of 402 rows (-17.4%) while preserving both demonstrated true positives.
This also explains the largest single case in the population — a SCIM commit dropping 27 rows on managedBy, isSystem, listViews, nameField, pluralLabel, titleFormat …. Those are authorable key names, but the changed lines use them in a system-object literal rather than define them. Use sites move no contract; definition sites do. The container test encodes that distinction; the name test cannot.
⚠️Blocker, and the reason this is a decision rather than a patch: the TS declaration name is not the spec type name — ObjectSchemaBase is data/Object, DatasourceDef is data/Datasource. No lookup exists today. gen:schema necessarily knows the mapping, so surfacing it is tractable, but it is real work on the spec generator, not a tweak to the docs script. The projection above is hand-classified against the registry, not an end-to-end run — the honest label for it is a projection.
B. Prefer the container for every data property. -17.9% rows, zero added, zero measured recall loss — but demonstrably drops the userActions and schemaMode rows, which are the best rows the tool produces.
C. Publish anchor provenance only. Each row already says which anchor put it there; add which declaration minted the anchor, so a row reads "via organizationId, a field of MetaOverlayCacheKey". Zero recall change, no threshold moved. Does not reduce the row count, so the display-cut problem survives.
D. Container-qualified against authorable-surface.base.json. Projected -17.4% with both true positives preserved. Requires the TS-name to spec-name mapping.
Four-axis analysis
Real business need. Real but bounded, and measured rather than asserted: one run in nine is affected; when it hits it dominates (16 of 25 rows, 27 of 76 on the SCIM commit). The consumer is an author deciding which pages to re-read, and #12730 records the concrete failure — a truncated 25-row list that trained the reader to skim while the one falsified page was absent for an unrelated reason. Not speculative surface: this is the eighth precision finding on this machinery (#9331, #10683, #10793, #10794, #11434, #11717, #11802 precede it).
Long-term soundness. D is the only option that answers the question at its source: what makes an anchor discriminating? — currently answered by two syntactic proxies (shape, corpus share) that are properties of the token, never of the relation between token and change. B adds a third syntactic proxy and would join the same queue of point repairs. C is honest but is transparency about a defect rather than its repair. Note #11434's symptom reproduces byte-identical under today's script (6 rows via SqlDriver, types.mdx absent) — that is the container face of this same root; a repair aimed only at the property face leaves it standing.
Making AI-written metadata apps hard to get wrong. This is where D separates sharply. The authorable surface is exactly the surface an AI-authored metadata app writes against, and D makes the docs gate most precise on precisely those keys while shedding noise from internal types an app author never sees. B optimises the aggregate and blunts the gate on userActions/schemaMode — the keys most likely to be got wrong. Consumer-side tolerance is not in play here; nothing is being widened.
Startup scope discipline. Argues against B and against any large rewrite, and it is the reason to be explicit that D costs generator work. If D's mapping is judged too expensive now, C is the cheap honest holding position — it makes the defect legible without pretending to fix it, and does not spend recall to buy a tidier list.
Recommendation
D, with C as the immediate step if D is not funded now. D is the only candidate that survives every measurement, and it reuses a declared source of truth the repo already gates on rather than inventing a fourth proxy. ⛔ Not B — it buys a 17.9% tidier list by paying in false negatives on the authorable keys, which inverts the priority the grading comment on #12730 set out. Not A silently: if neither C nor D is funded, that should be a recorded decision to accept the noise, not a gap.
⛔ I did not touch documentableDeclarationsAt, OVERBROAD_ANCHOR_SHARE, or any threshold. No PR.
Split out of #12730 by the R19 dev seat after measuring it. #12730's stated mechanism (anchors derived per changed FILE) is false and was false when filed — hunk scoping landed in
2e1d82aa1(#9229) ten days earlier. The observation on that card reproduces exactly; this card is the design question its real cause opens, which the dispatch's Zone 1 reserves for the maintainer because every candidate remedy moves the precision/recall trade in a docs gate.The finding, in one line
scripts/docs-audit/affected-docs.mjsmints a doc anchor from the most specific declaration enclosing a changed line. When that is a data property (name:/name?:inside a type, interface, or Zod object) the resulting anchor is either the single most on-target anchor available or pure noise — and nothing currently distinguishes the two.Measurements
Instrument identical throughout:
affected-docs.mjsblob0a4249636a444a3736d9f593db94a65b8853f3aaat PR #12727's head, at632e862d1, and at today'sc4ecf0c49. Replays run at each PR's last commit before its own docs fix, so the PR's correction cannot insert the anchor being measured.On PR #12727 (the run #12730 was filed from) — reproduced exactly at 25 anchors / 25 hand-written rows:
MetaOverlayCacheKey,readMetaOverlayCache,cacheKeyOf,readWriteEpoch, …). Precision on the change's own vocabulary is perfect.organizationIdandpackageIdonMetaOverlayCacheKey,expiresAtonMetaOverlayCacheEntry.floor(189 * 0.15) = 28pages whileorganizationIdmatches 10. They are not hub terms — they are ordinary field names.publishItem (sdk)and/:type/:name/publish (route)on a diff touching no state machine — the amplification the:4420note predicts.Population — 91 consecutive
maincommits touchingpackages/. A variant that prefers the container whenever the winning declaration is a data property:Only 10 of 91 runs change at all — surgical, not blunt. Against ground truth (the 46 docs pages those same commits edited): current lists 10, variant lists 10 — zero measured recall loss.
Why that number does not license the change
The ground truth is too weak to license it — it can only detect losses among pages the tool already finds, and it finds 10 of 46. Reading the 72 dropped rows by hand finds true positives the aggregate cannot see:
userActions— droppeddata-modeling/objects.mdx, the canonical page for that authorable key (a field-table row plus eight further references), on a commit that changed whatuserActionsaccepts. Declared inpackages/spec/src/data/object.zod.ts— a definition site.schemaMode— droppeddata-modeling/drivers.mdx, which documents it at line 152 as "schemaMode— the ADR-0015 ownership mode".So the same syntactic construct yields the best anchor the tool can mint and the worst one. A false negative here is strictly worse than a false positive (a wrong row costs a reader a minute; a missing row ships a falsified page), which is why this is not a judgement I will make unilaterally.
The discriminators that do NOT work — each disproven, not assumed
userActionsandexpiresAtare bothname:in an object/interface.packages/speconly). Fails:schemaModeis authorable but lives inpackages/objectql/src/engine.ts.expiresAt,organizationIdandpackageIdare authorable property names — onapi/Session,cloud/Environment,api/GetMetaItemsRequest. A name-only test keeps all three noisy anchors and gains nothing.The discriminator that DOES appear to work — and its one blocker
packages/spec/authorable-surface.base.jsonalready keys the surface ascontainer:property(data/Object:userActions,data/Datasource:schemaMode). Qualifying the anchor by its declaring container separates every measured case correctly:MetaOverlayCacheKey,LocalizationCacheEntry,AuthzCachePostureInput,MintScimConnectionCredentialInput,SysScimConnectionBinding,AUTH_MODEL_TO_PROTOCOL,enObjects, … — and 2 are authorable spec types (ObjectSchemaBase,DatasourceDef).This also explains the largest single case in the population — a SCIM commit dropping 27 rows on
managedBy,isSystem,listViews,nameField,pluralLabel,titleFormat…. Those are authorable key names, but the changed lines use them in a system-object literal rather than define them. Use sites move no contract; definition sites do. The container test encodes that distinction; the name test cannot.ObjectSchemaBaseisdata/Object,DatasourceDefisdata/Datasource. No lookup exists today.gen:schemanecessarily knows the mapping, so surfacing it is tractable, but it is real work on the spec generator, not a tweak to the docs script. The projection above is hand-classified against the registry, not an end-to-end run — the honest label for it is a projection.Options
userActionsandschemaModerows, which are the best rows the tool produces.organizationId, a field ofMetaOverlayCacheKey". Zero recall change, no threshold moved. Does not reduce the row count, so the display-cut problem survives.authorable-surface.base.json. Projected -17.4% with both true positives preserved. Requires the TS-name to spec-name mapping.Four-axis analysis
Real business need. Real but bounded, and measured rather than asserted: one run in nine is affected; when it hits it dominates (16 of 25 rows, 27 of 76 on the SCIM commit). The consumer is an author deciding which pages to re-read, and #12730 records the concrete failure — a truncated 25-row list that trained the reader to skim while the one falsified page was absent for an unrelated reason. Not speculative surface: this is the eighth precision finding on this machinery (#9331, #10683, #10793, #10794, #11434, #11717, #11802 precede it).
Long-term soundness. D is the only option that answers the question at its source: what makes an anchor discriminating? — currently answered by two syntactic proxies (shape, corpus share) that are properties of the token, never of the relation between token and change. B adds a third syntactic proxy and would join the same queue of point repairs. C is honest but is transparency about a defect rather than its repair. Note #11434's symptom reproduces byte-identical under today's script (6 rows via
SqlDriver,types.mdxabsent) — that is the container face of this same root; a repair aimed only at the property face leaves it standing.Making AI-written metadata apps hard to get wrong. This is where D separates sharply. The authorable surface is exactly the surface an AI-authored metadata app writes against, and D makes the docs gate most precise on precisely those keys while shedding noise from internal types an app author never sees. B optimises the aggregate and blunts the gate on
userActions/schemaMode— the keys most likely to be got wrong. Consumer-side tolerance is not in play here; nothing is being widened.Startup scope discipline. Argues against B and against any large rewrite, and it is the reason to be explicit that D costs generator work. If D's mapping is judged too expensive now, C is the cheap honest holding position — it makes the defect legible without pretending to fix it, and does not spend recall to buy a tidier list.
Recommendation
D, with C as the immediate step if D is not funded now. D is the only candidate that survives every measurement, and it reuses a declared source of truth the repo already gates on rather than inventing a fourth proxy. ⛔ Not B — it buys a 17.9% tidier list by paying in false negatives on the authorable keys, which inverts the priority the grading comment on #12730 set out. Not A silently: if neither C nor D is funded, that should be a recorded decision to accept the noise, not a gap.
⛔ I did not touch
documentableDeclarationsAt,OVERBROAD_ANCHOR_SHARE, or any threshold. No PR.Reproduction
Generated by Claude Code