Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(gate): zod-mirror-parity's SPEC_DERIVED_PAIRS scans raw text between export const boundaries, so a Spec-token mentioned in PROSE is attributed to the neighbouring export #6705

Closed

Description

@os-sales

Filed unlabelled with suggested routing — domain:* and grading belong to triage. Suggested routing: domain:ui by the packages table (packages/types), though the subject is a gate, so domain:devx is defensible; triage decides.

Provenance: hit live by the dev implementing #6475 (PR #6704) while writing a docstring. Worked around in that PR by rewording; the gate itself is unchanged, so the next person to write a comment in that file meets it again with no warning.

The mechanism

packages/types/src/__tests__/zod-mirror-parity.test.ts's SPEC_DERIVED_PAIRS re-check determines which zod mirrors reference a spec schema by scanning raw text between export const boundaries in packages/types/src/zod/*.zod.ts, looking for a \bSpec[A-Z]\w* token.

It is a text scanner, not an AST reader. So it cannot distinguish:

  • a real reference — SpecGanttConfigSchema.extend(...) in code; from
  • the same token appearing in a comment.

What actually happened

The dev added a docstring above the private GanttConfigExtensionFields const. That const sits textually between the ObjectTreeSchema and ObjectGanttSchema export boundaries. The docstring mentioned SpecGanttConfigSchema in prose.

The scanner attributed that mention to ObjectTreeSchema — a schema that references no spec schema at all — and SPEC_DERIVED_PAIRS matches what the mirror sources actually do went red.

Two properties make this expensive out of proportion to its size:

  1. A comment changed a gate's verdict. Nothing about the emitted types, the runtime, or the mirror's actual spec dependency moved. Prose did.
  2. The false attribution lands on the neighbouring export, not the one being edited. The red names ObjectTreeSchema; the edit was near ObjectGanttSchema. Someone who did not write the docstring themselves has no reason to connect the failure to a comment several lines away, in a different declaration's window.

That combination — a green-to-red flip caused by prose, blamed on an innocent neighbour — is the signature of a gate people learn to route around individually rather than fix.

Why the workaround is not the fix

PR #6704 reworded the docstring to avoid the literal token and left a comment recording the trap. That was the correct call inside that PR's fence — it was a gantt card, not a gate card, and widening it would have been wrong.

But the resulting state is a gate whose correct usage is "do not mention certain identifiers in comments in this file", enforced by a comment that asks the next editor to remember. That is a convention held by discipline, which is precisely what a gate is supposed to replace.

Suggested directions (untried, not prescriptive)

  • Strip comments before scanning. Cheapest fix, keeps the text-scanner shape; needs care with strings that contain comment-like sequences.
  • Parse instead of scan. Most correct, most work; the file is TypeScript and the question ("does this export's initializer reference a Spec* symbol?") is an AST query.
  • Attribute by declaration, not by text window. Even keeping the scan, a private const between two exports currently has its text charged to whichever export precedes it. Whatever the token-matching rule, the windowing is independently wrong.

⚠️ Whoever takes this: the bug's own shape makes a naive test dangerous — a fixture that mentions a Spec* token in a comment must be asserted to produce no finding, and one that references it in code must still produce one. A fix that stops seeing comments and stops seeing real references would go green on the existing suite while destroying the gate. Show the positive direction still fires.

Refs: #6475 / PR #6704 (where it was hit, with the reworded docstring and trap marker) · #6472 (GanttConfigExtensionFields, the const whose docstring triggered it).

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 28, 2026
  2. added theissue type on Aug 28, 2026
  3. huangyiirene commented on Aug 28, 2026

    @huangyiirene
    Collaborator

    分诊:domain:devx(覆盖卡面建议的 domain:ui),pm:queue · priority:p2 · type Bug

    卡面把路由留成了三选一并明说「triage decides」,这里说明取舍依据,⛔ 不是拍脑袋。

    不取 domain:ui:卡面写的是「by the packages table(packages/types)」,但本仓适用的是三流规则 —— domain:devx(.github/ · scripts/ · 发布管线)/ domain:spec(packages/types · schema 语料 · spec pin 耦合)/ 其余(已发布库与 apps)→ domain:ui。按那张表,packages/types 无论如何都不落 domain:ui。

    不取 domain:spec:三流规则里 packages/types 确实是 domain:spec,但那指的是类型/镜像语料本身。本卡要改的是 __tests__/zod-mirror-parity.test.ts 里的扫描器逻辑 —— 落点是门禁机制,不是被门禁检查的语料。

    取 domain:devx 的依据是同族先例,不是推断:zod-mirror-parity 这一族的既有卡都在 devx 车道 ——

    卡 主题 标签
    #6152 16 对 schema、121 个 declared-but-unmirrored 键 pm:blocked · domain:devx
    #6155 KanbanCard / KanbanColumn 被声明四次 pm:blocked · domain:devx

    ⇒ 本卡与它们同族同面,跟随既有车道,避免同一族卡分裂到两个席位。

    关于「这算不算门禁削弱」(人工地板自查):不算。让扫描器不再把注释里的 token 算作引用,删的是假阳性,不是真阳性 —— 卡面自己给了这条纪律的最强版本:修完必须证明「代码里真引用仍然触发」,否则就是把门禁修瞎了。⇒ 精度提升,不是削弱,⛔ 不需要人工裁决,可直接入队。

    p2 而非 p1:已有可用绕法(PR #6704 的改写 + 就地 trap 注释),不影响产品行为。它的代价是「下一个在这个文件里写注释的人再撞一次」,真实但不紧急。


    Generated by Claude Code

  4. self-assigned this
    on Sep 2, 2026
  5. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    Claim: PM loop round R40
    Session: session_01BGMDbrVa8JjZcCQ7DWYH1b
    Branch: claude/issue-6705-parity-scan-comments
    Worktree: objectui-issue-6705
    Domain: domain:devx
    File surface: packages/types/src/__tests__/zod-mirror-parity.test.ts (the SPEC_DERIVED_PAIRS re-check scanner), plus at most a fixture/helper under packages/types/src/__tests__/ (stop on breach; explain in the report). ⛔ No edit to packages/types/src/zod/*.zod.ts or the ledgers (KnownDrift / UnmirroredDeclared / RuntimeOnlyDeclared) — this card changes what the scanner reads, not what the corpus says.
    Container & model: M, mode:subagent, model: opus — tier is this seat's explicit decision (no dispatch-gates.mjs in objectui, no --tier reading to cite).
    Clause-②: no
    Serial constraints cleared: #6720 is queued on the same file (a different mechanism: the Breadcrumb pairing) — fold-or-serial answered serial, gate ① fails (two defects, two fixes); #6720 branches after this lands and is not dispatched this round. In-flight ui-lane PRs #7240 / #7241 edit the scanned corpus (packages/types/src/zod/form.zod.ts, crud.zod.ts, disclosure.zod.ts, data-display.zod.ts, data-display.ts) but not this scanner; merge main before opening the PR so the fixture verdicts are taken against their landed state. Recent churn on this file: 2c45966 (#6945), 29754cf (#6944) — read the current file, not the card's line numbers.


    Generated by Claude Code

  6. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    Claim (dev): starting work on this card.

    Session: session_01BGMDbrVa8JjZcCQ7DWYH1b
    Branch: claude/issue-6705-parity-scan-comments (pushed empty at 67dadd6)
    Worktree: /home/user/objectui-issue-6705
    Base: 67dadd602a3a891666ea1513c5de677140784b6a

    File surface (planned): packages/types/src/__tests__/zod-mirror-parity.test.ts plus a new fixture/helper under packages/types/src/__tests__/. Possibly the docstring restoration in packages/types/src/zod/*.zod.ts left by PR #6704 as a live positive proof — will be declared explicitly in the report if taken.

    No assignee change (PM owns it). No ledger edits, no SPEC_DERIVED_PAIRS membership change — if the fixed scanner changes the computed pair set, that is reported, not absorbed.


    Generated by Claude Code

  7. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    os-dev-report

    {
      "issue": 6705,
      "status": "done",
      "branch": "claude/issue-6705-parity-scan-comments",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7277",
      "premise_still_valid": true,
      "summary": "Both defects the card names were real and both are fixed. The SPEC_DERIVED_PAIRS re-check now calls specReferencingExports(fileName, source), which parses with `typescript` (PM assumption (c) confirmed: typescript is a devDependency of packages/types and importable from its test env; `import ts from 'typescript'` is already the established style in five sibling tests there). Spec identifiers come off the AST, so comments and string literals are not references and nothing has to strip them; attribution is by declaration, with file-local references through PRIVATE consts resolved to a fixed point so a mirror built from a spec-derived field map keeps its dependency, while exported mirrors are not hopped through. PM assumption (b) confirmed as an independent defect and fixed with the same change. The docstring restoration in packages/types/src/zod/objectql.zod.ts was taken as the live positive proof and is the only edit outside the test file. NOTE FOR PM (headline finding, not absorbed silently): the fixed scanner drops TWO entries from the computed pair set — base.zod.ts#BaseSchema (its file's only Spec token is in a comment) and objectql.zod.ts#ObjectViewSchema (BaseSchema.extend of local literals; the old window ran ~50 lines past its end and swallowed a private `KanbanConfig = SpecKanbanConfigSchema...` block). Both are false positives of exactly the two defect classes triage ruled to be deletions of false positives. I removed those two list entries with inline reasons, because leaving them makes the gate red on main; see open_questions for the fence conflict. No ledger (KnownDrift / UnmirroredDeclared / RuntimeOnlyDeclared) and no corpus schema was touched. Answering the PM status probe: AST via typescript; both directions plus the private-const-between-exports case are red-first (ablation 2); docstring restoration taken; no blocker.",
      "tests": "All verdicts against final commit 85e014af2 (post-merge of origin/main at 1a1235c94, picking up #7241/#7216/#7236/#7262). PARITY FILE: `Test Files  1 passed (1)` / `Tests  12 passed (12)` (was 5 before this card). PACKAGE SUITE: `pnpm exec vitest run --maxWorkers=2 packages/types/` -> `Test Files  84 passed (84)` / `Tests  1070 passed (1070)`. TYPE-CHECK: `pnpm --filter @object-ui/types run type-check` exit 0, script echoed as `tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`; proven non-vacuous with `tsc -p tsconfig.test.json --listFiles` (edited test file 2 hits, objectql.zod.ts 1, typescript/lib/typescript.d.ts 1). GATES: check:control-bytes `OK (scanned 6005 tracked text file(s); skipped 85 binary)`; check:vi-mock-specifiers `OK (4129 tracked source file(s)...)`; check:vi-mock-inherit `OK`; check:spec-symbols `1333 files scanned against 4959 spec export names; 0 untriaged collisions`; check:phantom-deps `Every in-scope import is declared by the package that publishes it`; changeset-presence `2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate`; check-changeset-fixed / -no-major / -overwrite all green. LINT (declared narrowing): narrowed to the affected package; population from eslint's own traversal under the root flat config, `--format json` reports 148 files linted, 0 errors, 267 warnings, exit 0 using the package's own script (`eslint .`); eslint.config.js declares no projectService and no parserOptions.project, so linting is not type-aware and this diff cannot move any untouched file's verdict. ABLATION 1 (live reproduction, prose direction): checked the test file out at BASE 67dadd602 while keeping the restored docstring; mutation proven on disk (old window code present = 1, new AST helper present = 0, restored docstring token in corpus = 2) and the pre-fix scanner reproduced the card exactly — `expected [ ...(11) ] to deeply equal [ ...(10) ]` with `+ \"objectql.zod.ts#ObjectTreeSchema\"`, the innocent neighbour. Restored by blob hash: HEAD_BLOB=1a45bced05cea9b4abc9e4daf17b0952be57ef81, post-restore hash identical. ABLATION 2 (positive direction, the direction that must not be lost): mutated the fixed helper's return to an empty set (spelled `new Set` with a string type argument); mutation proven on disk (injected marker grep = 1, removed expression grep = 0) and blob hash changed to 20c7d4f2...; result `Tests  5 failed | 7 passed (12)` — the live re-check (`expected [] to deeply equal [ ...(8) ]`) plus all four positive fixtures reddened, so real code references still fire and the gate is not blinded. Restored by blob hash (identical to HEAD) with `git diff HEAD` on the path empty. Both ablations ran from a COMMITTED implementation with an absolute-path `trap restore EXIT INT TERM`. FIXTURES (7, in-memory, both directions): comment-only mention -> none; code reference -> hit; Spec token inside a string literal carrying `*/` and `//` -> none; private const between two exports with prose -> neither neighbour; private const with a real reference -> charged to its user, not its neighbour; next export's docstring -> not charged to the previous export; non-vacuity pin against the real objectql.zod.ts.",
      "mcp_calls": "9 — issue_read x2, add_issue_comment x2 (claim + this report), create_pull_request, pull_request_read (body read-back), search_issues x1 (targeted, after REST probe returned 403), issue_write x1 (filed #7279)",
      "open_questions": [
        {
          "question": "The dispatch fence says '⛔ no changes to SPEC_DERIVED_PAIRS' membership' AND 'if the fixed scanner changes the computed pair set, that is a finding to report'. Both happened, and the two clauses point opposite ways: the fix necessarily drops base.zod.ts#BaseSchema and objectql.zod.ts#ObjectViewSchema, so honouring the membership freeze means landing a RED gate. I removed the two entries (with inline reasons naming the defect class that propped each up) and reported loudly rather than absorbing. Confirm this reading, or tell me to revert the list edit.",
          "options": [
            "A — keep the two removals (what I shipped): the list is documented in-file as derived, 'kept honest by the test below, which re-derives this set from the mirror sources instead of trusting the list', so it is downstream of the scanner by construction; both removals are false positives of exactly the two classes triage ruled are deletions of 假阳性; no corpus or ledger moves; main stays green.",
            "B — revert the list edit and land the scanner fix with the re-check red, escalating the membership decision to the maintainer.",
            "C — revert the whole PR and return needs_decision without a fix."
          ],
          "recommendation": "A, because the alternative deliverables are both worse on the axis that matters most here — long-term soundness. SPEC_DERIVED_PAIRS is not an independent record that the scanner is checked against; it is a cache of the scanner's output, and the file says so. Freezing a cache while correcting its producer preserves two known-false entries as the gate's ground truth, which is the same 'convention held by discipline' shape the card was filed to remove. B additionally lands a red gate on main, which no reading of the fence wants. The one genuine loss under A is that ObjectViewSchema's removal invalidates a REMEDY ROUTING recorded in this file's header (#6058's SPEC-DERIVED/LOCAL split) — that part I did NOT decide: the split is left standing verbatim, counts and all, with a warning at the site, and the decision is filed as #7279 for triage. Real business need and AI-error-resistance both favour A too: a gate that reddens on prose teaches editors to route around it, and the misattribution lands on an innocent neighbour, which is the worst possible failure legibility for an AI or human editor."
        },
        {
          "question": "Should the scanner follow references through EXPORTED mirrors as well as private consts? It currently does not.",
          "options": [
            "A — private-only (what I shipped): an exported mirror is a registered pair carrying its own entry, so its spec sensitivity is already recorded under its own key.",
            "B — full closure: also hop through exported mirrors, which would ADD 11 entries repo-wide (e.g. data-display.zod.ts#ChartSchema via ChartTypeSchema = SpecChartTypeSchema, form.zod.ts#FormSchema/FormFieldSchema/SelectSchema via SelectOptionSchema, objectql.zod.ts#ObjectGridSchema)."
          ],
          "recommendation": "A, because B is a semantic widening this card did not ask for and would force a large SPEC_DERIVED_PAIRS membership change on top of the two removals. Measured: private-only and direct-only produce an IDENTICAL set on the current corpus, so the private-const hop costs nothing today and exists purely to prevent a false negative — the exact GanttConfigExtensionFields shape the card describes — if someone later moves a spec reference into a private field map. B is defensible on its own terms (those mirrors do move when spec moves) but it is a separate question about what the list is FOR, and answering it inside a scanner-precision card would be the scope creep the fence exists to prevent."
        }
      ],
      "out_of_scope_findings": [
        "filed as #7279: #6058's SPEC-DERIVED remedy split routes ObjectViewSchema's unmirrored declared keys to #2231's unification question ('NOT a local mirror edit') on the strength of the scanner false positive this card removed — ObjectViewSchema is BaseSchema.extend of local literals and references no spec schema, so both counts in the split (3 entries/13 keys spec-derived, 13 entries/84 keys local) and the #6152 worklist routing need re-deriving. Left standing verbatim in-file with a warning; unassigned, unlabelled for triage.",
        "not filed, reported here as a channel/tooling observation for PM: the dispatch's prescribed command `pnpm --filter @object-ui/types exec vitest run ...` is BLOCKED by this repo's own vitest guard as a documented false-green (it re-roots vitest so the package's tests silently do not run while 22 unrelated console test files report as passing). The repo prescribes the repo-root form. Worth correcting in the objectui dispatch template so the next dev does not burn a round on it."
      ]
    }

    Generated by Claude Code

  8. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    ACCEPT — PR #7277 (draft → ready + auto-merge once every check is green on its head) — with the two open questions answered here

    Reviewer of record: domain:devx @ objectui execution seat, PM session session_01BGMDbrVa8JjZcCQ7DWYH1b, R40. Verified against GitHub, not against the report.

    Open question 1 — the two SPEC_DERIVED_PAIRS removals: A, keep them. The dispatch fence's two clauses were meant as "do not edit the list by hand; if the scanner's output moves, say so loudly" — and that is what happened. The list is documented in-file as a cache of the re-check's own output ("kept honest by the test below, which re-derives this set"), so freezing the cache while correcting its producer would land a red gate on main over two entries triage's own ruling classifies as false positives (a comment counted as a reference; a text window swallowing a neighbour's private const). Removing them is the remedy. The one real consequence — #6058's remedy split routing ObjectViewSchema's unmirrored keys as spec-derived on the strength of that false positive — is correctly not decided in this PR: the split stands verbatim with a warning at the site, and #7279 carries the re-derivation for triage. This is PM discretion (verification-gate precision, no contract or ledger moved), not a maintainer escalation.

    Open question 2 — private-only resolution: A. Hopping through exported mirrors would add 11 entries and change what the list means; that is a separate question from scanner precision. Measured identical on today's corpus, the private-const hop exists to prevent the exact false negative the card describes. Correct fence.

    Checklist

    Landing path: not governed. Joins the flip checkpoint; on green ⇒ ready + auto-merge. Card stays pm:dispatched until MERGED.


    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

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p2tooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions