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
fix(docs-audit): derive the docs-drift advisory from what pages document, not package edges (#9229)
The advisory resolved pages by PACKAGE DEPENDENCY -- "which hand-written docs
reference @objectstack/x". That is a dependency-graph proxy answering a semantic
question, and #9192 measured it wrong in BOTH directions on PR #9191 (the three
metadata read verbs): 3 pages listed of which 1 was relevant, while the 2 pages
that actually document the changed surface were absent, because they document it
through the SDK -- which does not depend on the implementing package at all.
A page is now listed when it NAMES something the change touched. Three anchor
kinds: the documentable declaration enclosing each changed line (top-level, or a
member of a top-level container -- a local inside a function body is not surface,
which is what drops the measured `singular` false positive); the wire routes a
changed symbol's registrar handler serves; and the client method the route
ledgers bind to those routes, which is the hop that carries the derivation across
the surface boundary the package graph cannot cross.
Two guards keep it precision-first, both running BEFORE the route bridge so a
weak name cannot mint route anchors from every handler that mentions it: a shape
guard (a single lowercase word like `label` or `object` matched 82 and 113 of 178
pages) and a corpus-share guard (`ObjectQL` is code-shaped and genuinely changed,
and names 59 of 178 pages). Everything either guard removes is published, as is
every changed file that yielded no anchor -- silence from this tool must never be
readable as absence, which is the whole subject of the card.
Measured before/after across ten real PRs; the PR comment now also states what
the run could not see, at the point of use.
Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
Co-authored-by: Claude <noreply@anthropic.com>
if (anchorless.length) limits.push(`**${anchorless.length}** changed file(s) yielded no anchor (\`${anchorless.slice(0, 3).join('`, `')}\`${anchorless.length > 3 ? ', …' : ''}) — pages documenting those are invisible to this run`);
100
+
if (crossCutting.length) limits.push(`**${crossCutting.length}** cross-cutting symbol(s) contributed no route anchor: \`${crossCutting.join('`, `')}\``);
101
+
if (overbroad.length) limits.push(`**${overbroad.length}** anchor(s) matched too much of the corpus to be a work list: \`${overbroad.join('`, `')}\``);
102
+
if (weak.length) limits.push(`**${weak.length}** name(s) were too generic to anchor anything (single lowercase words)`);
103
+
// Rendered whenever there is anything to say, INCLUDING when the only thing to
104
+
// say is "the wide net exists and holds N pages". A short list is the right
105
+
// answer here, but a reader must be able to tell a short list from a blind one
? ['', '<details><summary>What this run could not see</summary>', '', ...limits.map(l => `- ${l}`), ...(limits.length ? [''] : []), `Coarse fallback — **${coarse.length}** page(s) merely *mention* a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): \`${rederive}\` → \`packageMentionDocs\`.`, '</details>']
109
+
: [];
78
110
// The release-owned rows are PARTITIONED OUT of the editable list, never
79
111
// dropped (#6893, following the #4920 ruling). They keep getting audited —
80
112
// `docs` above is still the full set the audit workflow is scoped to — but
body = `${marker}\n### 📓 Docs Drift Check\nNo hand-written docs reference the ${pkgs.length} changed package(s). ✅`;
131
+
// "Nothing found" and "I derived nothing to look for" are DIFFERENT results
132
+
// and must never render alike — that conflation is #9192's own subject.
133
+
const headline = anchorList.length === 0
134
+
? `Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from **${pkgs.length}** changed package(s)), so **this run has no opinion** about the docs.`
135
+
: `**${anchorList.length}** anchor(s) derived from **${pkgs.length}** changed package(s); no hand-written page names any of them. ✅`;
136
+
body = [marker, '### 📓 Docs Drift Check', headline, ...limitsBlock].join('\n');
Heuristic: a doc is *affected* by a changed package `P` if it mentions `P`'s npm
30
-
name (`@objectstack/<x>`) or repo path (`P`'s directory, e.g.
31
-
`packages/services/service-automation`). Over-inclusion is preferred over misses; the
32
-
periodic **full** audit (part 4) is the backstop for docs that describe a package
33
-
without naming it.
29
+
**Derivation (#9192): a doc is *affected* when it NAMES something the change touched.**
30
+
Not when it mentions the changed package — that predicate is a dependency-graph proxy
31
+
answering a semantic question, and it was measured wrong in *both* directions on PR #9191
32
+
(three read verbs in `@objectstack/metadata-protocol`): 3 pages listed of which 1 was
33
+
relevant, while the 2 pages that actually document the changed surface —
34
+
`api/client-sdk.mdx` and `kernel/contracts/metadata-service.mdx` — were absent, because
35
+
they document it through the **SDK** surface, which does not depend on the implementing
36
+
package at all.
37
+
38
+
Over-inclusion is not free, and that is the correction. A wrong-both-ways advisory trains
39
+
its reader to skip it, and then it fails on the PR where it is right — the same bill
40
+
exclusion 1 below already paid. The derivation is therefore **precision-first**: a shorter
41
+
right list beats a longer noisy one.
42
+
43
+
Three anchor kinds, each exact:
44
+
45
+
| anchor | what it is | how it is derived |
46
+
|:--|:--|:--|
47
+
|`symbol`| a documentable declaration the diff touched | the top-level declaration, or a member of a top-level **container** (class / interface / type / enum / schema object), enclosing each changed line — on **both** sides of the diff, so a removed export still anchors the pages naming it |
48
+
|`route`| a wire path the change touched | a path literal on a changed line, plus every route whose **registrar handler** references a changed symbol |
49
+
|`sdk`| the client method bound to an anchor route | the declared `route` ⟷ `client` rows in the repo's route ledgers |
50
+
51
+
The `route` and `sdk` hops are what carry the derivation across the surface boundary the
0 commit comments