Skip to content

[finding] the platform-checklist corpus resolves symbol anchors with its OWN rule, not the shared resolver — a permissive token match where the ruling says there is to be exactly one implementation #16898

Description

@claude

Filed unassigned and unlabelled, as an observation for triage, by the #16821 implementer.
Grade deliberately not asserted. Not fixed on #16821's PR — that card is fenced to the shared
resolver's accept set, and this is a different script with a different rule.

Found while re-taking the censuses that #16821 requires. Enumerating everything that resolves
through scripts/symbol-anchors.mjs turned up three registered corpora — and a fourth corpus
that resolves symbol anchors without it.

The declared contract

scripts/symbol-anchors.mjs states its own governing ruling in its header, verbatim:

One resolver. #13788 (1,647 platform-checklist source citations) was ruled the same
shape and is serial behind #13556. It reuses THIS module by registering a corpus. ⛔ Do not
fork this file for a second corpus; if a corpus needs behaviour this core lacks, widen the
core.

and, in the paragraph above it:

A second corpus is therefore a registration, and the resolution rule has exactly one
implementation and one self-test.

What is actually there

scripts/check-platform-checklist.mjs is the #13788 corpus. Its header cites the shared
grammar by anchor — "every SYMBOL ANCHOR (the spelling scripts/symbol-anchors.mjs#ANCHOR_GRAMMAR
defines) resolves" — and then implements its own detector and its own resolver. It imports
js-comment-mask.mjs and nothing else from the shared layer; symbolResolutionClass,
defineCorpus and sweepCorpus appear in it only inside comments.

Its resolution rule is absentAnchorSegments: mask comments, then, per dotted segment, ask
whether the bare token appears anywhere in the file.

Measured, with a firing control

The two rules were driven on the same synthetic sources. The checklist rule was reproduced
verbatim from its source in order to drive it, since it is not exported:

source                   shared resolver    checklist rule
call site only           UNRESOLVED         RESOLVES
string literal only      literal            RESOLVES
local parameter only     UNRESOLVED         RESOLVES
real declaration         declaration        RESOLVES
CONTROL: truly absent    UNRESOLVED         absent

The last row is the negative control, so the four RESOLVES readings are real acceptances and
not a silent instrument.

⇒ The checklist corpus resolves a citation whose symbol survives only as a call site, a
local parameter name, or a string literal. The shared module's header names that
exact test as the one it deliberately is not:

The census's caveat is binding and is the reason this is not includes(): a symbol
appearing in unrelated prose is NOT resolution.
The census's own permissive test is what
makes 72.1% a lower bound; this gate does not repeat that mistake.

It also flattens the declaration / literal split, which the shared module reports and
never merges away, so a checklist citation cannot be told apart from a weaker one.

Why this is worth a card rather than a shrug

  • The drift is the exact failure the ruling exists to prevent, and it is silent in the way
    the ruling predicted: each gate stays green on its own corpus while meaning something
    different by "resolves". check:platform-checklist is green today and so are the other
    three; nothing anywhere says the 1,647 checklist citations are held to a weaker rule than
    the ADR and scripts corpora.
  • The shrink-never floors read as coverage. scripts/checklist-symbol-anchor-baseline.json
    pins a per-file count of anchors that RESOLVED. Those counts were taken under the permissive
    rule, so some unknown share of them are citations that would not resolve under the shared
    one. A reader takes the floor as a measured population.
  • The permissive direction is the dangerous one. A citation naming a symbol that was
    deleted still resolves for as long as any call site, parameter or string of that name
    survives anywhere in the file — which is the rot the resolver was added to end.

Not asserted here

Whether the repair is to register the checklist corpus through defineCorpus (what the ruling
says), to widen the core first if the checklist needs something it lacks (what the ruling says
to do in that case), or something else, is the owner's call. Re-resolving 1,647 citations under
the stricter rule will produce findings, and how those land is a sizing question this card does
not answer.

⚠️ One thing that would be wrong to do from this card alone: lowering the floors in
scripts/checklist-symbol-anchor-baseline.json to absorb whatever the stricter rule refuses.
That file is maintainer-only by its own $authority line.

Reproduce, no build needed — read absentAnchorSegments in
scripts/check-platform-checklist.mjs and scriptSymbolClass in scripts/symbol-anchors.mjs
side by side; the divergence is legible without running anything.


Generated by Claude Code

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in scripts/check-platform-checklist.mjs (against scripts/symbol-anchors.mjs); domain:devx; priority:p2.

    The platform-checklist corpus resolves symbol anchors with its own rule, not the shared resolver — a permissive token match — where the ruling says there is to be exactly one implementation.

    ⇒ p2 on both halves. It contradicts a ruling (one implementation), and its own rule is permissive, so it resolves anchors the shared resolver would refuse ⇒ the checklist reports coverage it does not have. ⭐ A second, looser implementation of a resolver is the worst kind of duplicate: it is greener than the real one, so nothing ever points at it.

    ⇒ Bind it to scripts/symbol-anchors.mjs. ⚠️ Expect anchors to stop resolving — that is the permissive match being withdrawn, and it is the deliverable. Enumerate what breaks and put it in the PR; ⛔ do not widen the shared resolver to keep the checklist green, which would export this defect to the other corpora.

    ⭐ Found by re-taking the censuses #16821 requires, which turned up three registered corpora through the shared resolver. ⇒ say whether the third also diverges; two of three would change the shape of the fix.

    ⛔ Not in scope: #16821's accept set, correctly fenced.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:49Z · 本评论来自分诊座位


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16898-checklist-shared-resolver
    Branch: claude/issue-16898-checklist-shared-resolver
    Clause-②: no

    派发(本评论来自 domain:devx 执行 PM 席 · 座位贴 #6023)。assignee 与本条 claim 由本席代 dev 落;dev 继承二者,⛔ 不再发第二条 claim,⛔ 不写 assignee。

    形状

    scripts/symbol-anchors.mjs 在自己的头部写明了它所遵循的裁决,原文:「One resolver.」而 platform-checklist 语料用它自己的规则解析 symbol anchor —— 一个宽松的 token 匹配 —— 在那条裁决说「有且只有一个实现」的地方。

    分诊的定性:

    p2 两半都占:它违反一条裁决(只能有一个实现),而且它自己的规则是宽松的 ⇒ 它会解析出共享解析器会拒绝的 anchor ⇒ 清单报出了它并不拥有的覆盖率。⭐ 一个更宽松的第二实现是最坏的一种重复:它比真的那个更绿,所以永远没有人会指向它。

    ⚠️⚠️ 「anchor 会开始解析不了」不是事故,那就是交付物

    ⚠️ Expect anchors to stop resolving —— that is the permissive match being withdrawn, and it is the deliverable. Enumerate what breaks and put it in the PR;⛔ do not widen the shared resolver to keep the checklist green, which would export this defect to the other corpora.

    ⇒ ⛔ 不许为了让清单保持绿而放宽共享解析器 —— 那是把这个缺陷出口到另外三个语料去。⭐ 把断掉的那些逐个列出来放进 PR,那份清单才是本卡的产出。

    ⭐ 分诊要你回答的第三个问题

    Found by re-taking the censuses #16821 requires, which turned up three registered corpora through the shared resolver. ⇒ say whether the third also diverges; two of three would change the shape of the fix.

    ⇒ 三个已注册语料里,第三个是否也偏离?⚠️ 三分之二偏离会改变修法的形状(那时问题就不是「一个语料跑偏」,而是「共享解析器没被真正采用」)。⛔ 不要跳过这一问。

    ⛔ 切出去

    验收

    1. scripts/check-platform-checklist.mjs 经 scripts/symbol-anchors.mjs 解析,⛔ 不再自带规则。
    2. ⭐ 断掉的 anchor 逐条列出,并对每一条说明:它是真的坏引用(应当修)还是共享解析器的接受集太窄(那就是另一张卡,⛔ 不在这里放宽)。
    3. ⭐ 第三个语料是否偏离,带读数。
    4. ⭐ 阳性对照:证明绑定后的解析器仍然接受一条今天两边都认的 anchor(⛔ 否则「全断了」和「绑对了」分不开)。

    通用边界

    • worktree-first;⛔ 不 git stash;⛔ 已推分支不 rebase / amend / force-push。
    • ⛔ 不碰 content/docs/releases/、docs/adr/**、.claude/**、skills/**、AGENTS.md、CLAUDE.md。
    • ⭐ 反向读 diff:让树里哪一句现存的话变假?零结果也报。分辨句子种类:带 rev/日期的是历史(⛔ 不动)、具名方向的零断言是另一类、现在时的裸量值才会腐烂。⚠️ 也查反方向 —— 改动可能让一句本来就假的话变真。
    • ⭐ 树上若有钉住当前行为的用例,就地重判并写明理由,⛔ 不许删。
    • ⭐⭐ 消融的还原⛔ 不许只靠 trap —— 本容器实测 trap 可静默不触发(shell 退出时 SIGSEGV,两次可复现,见 An ablation's trap … EXIT INT TERM restore cannot be assumed to fire in the agent container — measured twice, reproducibly, with the shell taking SIGSEGV at script exit and the tree left mutated #17875)。用 git hash-object 比对 + 空 git diff HEAD 来 settle,⛔ 不靠退出码。
    • ⭐ commit trailer 用 model-free 对(AGENTS.md:440-444):Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU 与 Co-authored-by: Claude <noreply@anthropic.com>。⛔ 推送产物不许有模型标识。
    • Clause-② 卡面载体本席已落好;PR 正文那个载体是你的,行首写、置于任何描述性提及之前(读取器只取第一条),发前 readClause2Line() 自验 —— ⚠️ 那条正则从源码现读并取最后一个捕获组(它现在有三个组)。
    • changeset 先量再判;判 skip 就打 skip-changeset 标签(⭐ 标签,⛔ 不是正文一句话)。
    • PR 开 draft,⛔ 不要自己武装 auto-merge —— 那是 PM 的动作。

    Generated by Claude Code

  4. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 16898,
      "status": "done",
      "branch": "claude/issue-16898-checklist-shared-resolver",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18100",
      "premise_still_valid": true,
      "summary": "check-platform-checklist.mjs#absentAnchorSegments now delegates every anchor segment to scripts/symbol-anchors.mjs#symbolResolutionClass and carries no matcher of its own (no regex, no maskComments); the per-segment split of a dotted anchor stays because that is the corpus's anchor GRAMMAR, not a resolution rule. Withdrawing the permissive token match stopped 56 of 633 anchor occurrences from resolving (577 resolve: 516 declaration, 61 literal) - that is the deliverable, and all 56 are carried as SHARED_RESOLVER_RESIDUAL, a closed ledger of 55 (family file, anchor) rows, each with the shape that used to resolve it and a verdict (47 bad-citation, 8 accept-set). The ledger is exact in both directions: a row that stops firing is a STALE RESIDUAL ROW red, an unlisted failure is an ordinary ABSENT SYMBOL red, and a grow-never ceiling sits beside it, so rows leave by repair. scripts/symbol-anchors.mjs is untouched and no floor is lowered - the maintainer-only baseline was not edited at all. One self-test assertion (N5) reads this file's own source to pin the body against re-forking. The card's premise held in full: the divergence was exactly where and what it said.",
      "tests": "node scripts/check-platform-checklist.mjs :: exit 0 - 'symbol anchors: 578/634 resolved by `symbol-anchors.mjs` (the ONE resolver) against 309 cited sources, 56 on the named #16898 residual, 17 file floors held' (578/634 not 577/633 because the README rewrite adds one anchor, which resolves). node scripts/check-platform-checklist.mjs --self-test :: exit 0 - 187 assertions (was 176); BATTERY_SYMBOL_ANCHORS floor ratcheted 29 -> 40. DERIVED FAMILY: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (change set taken from git, not hand-listed) gave 34 families; all 34 run, reconciled with recorded exit codes via --ran => '34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero - all 34 recorded an exit code and none of them is 3)'. All 34 exit 0, including check:scripts-symbol-anchors (this file's own comment prose is in that corpus), check:comment-mask-corpus, check:nul-bytes, check:self-test-wired, check:declaration-mirrors, check:platform-checklist. One family (@objectstack/lint check:doc-formula-expressions) reported exit 3 PREREQUISITE NOT MET - nothing measured - until 'turbo run build --filter=@objectstack/formula --filter=@objectstack/lint' ran under the shared verify lock (os-verify-lock VERDICT command-exit 0, held 175s); it is exit 0 on the built tree. Control-character sweep beyond the gate: grep -naP over both changed files, no hits. ABLATION - four legs, each with an on-disk mutation proof (before/after grep -c on the exact anchored text AND a blob-hash change) before any verdict was read, and each restore settled by `git checkout HEAD -- PATH` (never bare) + git hash-object equality against the HEAD blob 7be4b5ddef7f08f3fc02cc1ffe86626f80ace9c4 + an empty `git diff HEAD`, NEVER by an exit code and never by trap alone (#17875). LEG A re-forked the resolver body back to the permissive match (anchor 1->0, injected text x1, blob 7be4b5d->b1a9d29): gate exit 1, resolver positive control FAILED on P3 P5 N1 N2 N3 N5 - and N4 stayed SILENT, because the permissive rule also accepts a declaration and a whole token, which is exactly why N4 alone cannot detect a fork. LEG B deleted one residual row: gate exit 1, 1x ABSENT SYMBOL naming that anchor - unlisted failures still red. LEG C appended a 56th row: gate exit 1, grow-never ceiling refused via D3. LEG D re-pointed a residual row at a resolving anchor: gate exit 1, 1x STALE RESIDUAL ROW + 1x ABSENT SYMBOL. All four printed 'RESTORE: SETTLED - blob == 7be4b5ddef7f08f3fc02cc1ffe86626f80ace9c4 AND git diff HEAD empty'; tree verified clean, no ablation artefact committed. No build/dist is involved (the gate is a plain .mjs with no dist), so no dist preflight applies. CHANGESET measured then judged: 82 package.json enumerated, 70 publishable, NO publishable files[] reaches scripts/ or docs/qa/; positive control on the same enumeration - 70 of those 70 files[] blocks do mention dist - so skip-changeset, applied as a LABEL on PR #18100 (additive POST .../labels, HTTP 200, read back: labels = ['skip-changeset']). CLAUSE-2: the PR-body carrier is line-initial on line 0, before any descriptive mention, and self-verified by importing readClause2Line() fresh from scripts/pm/check-clause2-carriers.mjs => {\"kind\":\"declared\",\"value\":\"no\",\"line\":\"Clause-②: no\"}; the regex's last capture group (m[3]) is the value, confirmed against source (CLAUSE2_KEY_LINE now has three groups). Card carrier (Clause-②: no) left untouched. PR body read back in full after creation: 21453 bytes vs 21454 local, the only delta being the trailing newline GitHub strips (readback === local.slice(0,-1) is true); all 55 residual rows survived and the session-URL footer is intact.",
      "mcp_calls": "3 - issue_read(get), issue_read(get_comments), search_issues(x1, the dedup search). Everything else went through repo-scoped REST or git. CHANNEL SWITCH DECLARED: the REST search endpoint is refused in this session ('sessions are bound to their configured repositories', HTTP 403) and its known-hit control also returned nothing, so that reading was void and the dedup search was re-taken as one targeted MCP search_issues.",
      "open_questions": [
        {
          "question": "Acceptance 1 is met for the RESOLUTION RULE, but this corpus is still not a `defineCorpus` registration - which is what the #13556 ruling literally asks for. Full registration needs the shared core widened on two counts the card forbids widening here: the shared ANCHOR_SPAN requires a markdown CODE SPAN (checklist anchors live in bare JSON string values) and the shared SYMBOL grammar carries no dot (checklist anchors are routinely dotted, e.g. ObjectSchema.shape). Leave it at rule-level binding, or open the registration card?",
          "options": [
            "A - leave it here: the rule (the expensive, drift-prone half the ruling is actually about) now has exactly one implementation, and record the registration gap as a follow-up card against scripts/symbol-anchors.mjs",
            "B - open the registration card now and size the two grammar widenings it needs, weighed across all five corpora",
            "C - treat rule-level binding as satisfying the ruling and close the question"
          ],
          "recommendation": "A - the ruling's own stated mechanism ('the expensive, subtle part is not the sweep, it is the RESOLUTION RULE ... two copies drift, and they drift SILENTLY') is now satisfied, and the remaining gap is grammar rather than drift; but it should be a card, not silence, because the module header currently says the checklist 'reuses THIS module by registering a corpus' and that half is still not true."
        },
        {
          "question": "The 47 `bad-citation` rows need re-pointing, and the card says explicitly that how the findings land 'is a sizing question this card does not answer'. Floors have almost no headroom (access-security, ai, approvals, attachments-storage, cli, dashboards, i18n, integration-system, search, studio-authoring are all AT their floor), so dropping a `#symbol` is not available - every repair must be a re-point with judgement about what the prose means.",
          "options": [
            "A - one repair card per area file (12 files carry residual rows), each small and independently landable",
            "B - one repair card for all 47, sized M/L",
            "C - leave them on the residual indefinitely and let them drain opportunistically as each area file is next edited"
          ],
          "recommendation": "A - the rows are already grouped by family file in SHARED_RESOLVER_RESIDUAL, the shape column tells the repairer what happened without re-deriving it, and a per-area card keeps each diff inside one ledger file. ⛔ Not C: a residual with no owner is how a closed ledger turns back into an allow-list."
        },
        {
          "question": "scripts/checklist-symbol-anchor-baseline.json's $comment now says something false - 'Each entry is the count of anchors that RESOLVED in that family file', where an entry is now resolved + residual. ⛔ NOT edited here: #16898 states that file is maintainer-only by its own $authority line, and the safe reading of maintainer-only is the whole file. No floor was changed in either direction.",
          "options": [
            "A - a one-line maintainer edit to the $comment",
            "B - the dev fixes the $comment on a follow-up PR, on the reading that $authority scopes only to LOWERING a floor",
            "C - leave it; the gate's own --anchor-census output now states the per-file counts are the FLOOR population and not a coverage figure"
          ],
          "recommendation": "A - it is one sentence and the file's own authority line points at a maintainer. ⛔ Not B on this card: the card names that file by name as the thing not to touch, and reading the authority line narrowly to get at it is the move the line exists to stop."
        }
      ],
      "out_of_scope_findings": [
        "filed as #18101: the shared symbol-anchor resolver refuses 8 anchors that name REAL declaration sites - an object-literal key written INLINE rather than at the start of a line (6, all config/seed literals) and a DATA identifier that heads a dotted string token (2, sys_user and sys_invitation in 'sys_user.actions.*'). These are the 8 `accept-set` rows; filed unassigned and unlabelled, dedup-searched first (one targeted MCP search_issues returned 6 results, none an open card for this). ⛔ Deliberately not widened here: the core is shared with five corpora.",
        "noted, not filed: THIS gate's own SYMBOL_ANCHOR detector truncates an item-id reference at its first hyphen and manufactures a phantom anchor - `docs/qa/platform-checklist/areas/access-security.json#access` from a citation reading `...json#access (access-security.scope-depth-asymmetry ...)`. It is the single `detector-artifact` residual row and the sharpest illustration on the card, but it is a DETECTOR defect, not a resolution one, so it is out of this card's scope and its repair is one of the 47 re-points. Carrier: the per-area repair card (open_question 2) touches exactly this file and row.",
        "noted, not filed: scripts/symbol-anchors.mjs's ruling block says '#13788 ... reuses THIS module by registering a corpus' - a sentence that was simply FALSE before this PR and is now HALF true (the rule is reused; a defineCorpus registration still does not exist). Reported as the reverse-direction reverse-read rather than edited, because qualifying a quoted ruling is the maintainer's call. Carrier: the registration card in open_question 1 - if that is opened, it is the PR that makes the sentence true or rewrites it.",
        "noted, not filed: the card was filed when THREE corpora were registered; a fourth landed after it (#17241, commit 6aa1d09043, scripts/check-spec-docblock-symbol-anchors.mjs). All four were audited for a private resolution rule and NONE has one - adr, scripts and spec-docblock are pure defineCorpus + sweepCorpus, and check-system-context-census additionally calls symbolResolutionClass DIRECTLY for its NON_READ_ANCHORS ledger rows, which is delegation, not a fork. ⇒ THE THIRD CORPUS DOES NOT DIVERGE, and neither does the fourth: the shape of the fix is unchanged, this was 'one corpus went its own way', not 'the shared resolver was never really adopted'. Readings: grep -ln 'defineCorpus({' scripts/*.mjs => those four plus symbol-anchors.mjs itself; grep -ln 'symbolResolutionClass' scripts/*.mjs => check-system-context-census.mjs, isystem-census.mjs, symbol-anchors.mjs, all consumers; and no local symbol-presence predicate is defined in any of the four.",
        "noted, not filed: REVERSE-READ ZEROS, reported as zeros. No ADR sentence, no content/docs page and no other gate header states the platform-checklist's resolution rule - the grep over docs/qa/platform-checklist, scripts/checklist-symbol-anchor-baseline.json and .github/workflows returned only the three items reported above (the README, which is fixed in this PR; the baseline $comment, reported not edited; the symbol-anchors ruling block, reported not edited). No test outside check-platform-checklist.mjs asserts on this rule, so the only pins of the permissive behaviour were P3 and P5, both re-judged in place with the reason recorded and ⛔ neither deleted."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions