Skip to content

spec: gate the entry-nameability invariant — fail any public entry whose declarations structurally mention a type not nameable from that entry #11986

Description

@os-support-ai

Chartered in #11709's ruling (maintainer, 2026-08-25, live PM chat, verbatim: 「11709 11742 同意」 — accepting the presented A′ + C recommendation; this is the C half). Filed by PM session session_01KWRU3s15AJz7PGW7a7wdCh.

Blocked-by: #11709

(A′ — the two root-entry re-exports — must land first, or this gate is born red on the very names it exists to catch.)

Content: generalize the minimal-program compile probe that packages/spec/scripts/root-entry-type-nameability.pin.test.ts already performs into a per-public-entry gate holding the #11350 invariant closed: a type that appears structurally in an entry's public declarations must be nameable from that same entry — fail the build on any TS2883-class leak. Wire it as a check: script alongside the spec gate family.

Why it exists: #11350 repaired three names, #11709 caught two more the same way — each cost a maintainer decision. This gate ends the per-name whack-a-mole; a third card of this class must not reach the decision inbox.

Acceptance criterion: with A′ reverted locally the gate goes red naming exactly BaseValidationRuleShape and FilterCondition (proves it would have caught this instance); on current main + A′ it is green; a synthetic leak added to any public entry turns it red.

Notes: spec lane (gate lives beside the spec build; domain:* is triage's to confirm). Premise-first: re-verify the pin test's location and the entry list on the current tree before implementing.

Activity

  1. os-litant commented on Aug 25, 2026

    @os-litant
    Collaborator

    Unblocked — Blocked-by: #11709 exhausted: PR #12327 merged to main as 5ae4303d (A′ landed; the root entry now re-exports both measured names, and the post-fix minimal-program probe read zero remaining unnameable names at the root entry). This card's gate generalizes exactly that probe per public entry. State: pm:blocked → pm:queue; domain:spec added per the #11709 ruling's charter ("the spec seat's follow-up gate"). Round R6, session session_01NDGG54XF5gbTLdQzCtnaVV.


    Generated by Claude Code

  2. self-assigned this
    on Aug 26, 2026
  3. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round R7
    Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
    Branch: claude/issue-11986-entry-nameability-gate
    Worktree: objectstack-issue-11986
    Domain: domain:spec
    File surface: packages/spec/scripts/** (new check: gate generalizing root-entry-type-nameability.pin.test.ts, re-verified present on origin/main at fire), packages/spec/package.json (script wiring); CI workflow wiring only if the gate family requires it (report if so — .github/workflows/ci.yml moved on main today 00:40Z) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — dispatch-gates --tier run at fire: no path-derived mandate; gate-architecture judgment ⇒ default judgment tier; not a contract accept/reject change
    Clause-②: no
    Serial constraints cleared: batch siblings disjoint — #10726-family (src/kernel/ + src/api/), #11658 (src/ui/ + src/data/). READ coupling declared: this gate enumerates public entries' structural type mentions; the siblings remove a schema member / widen a prop, neither adds an unnameable structural mention, so no pin-flip is expected — if measurement says otherwise, stop and report rather than adjust the siblings. A′ (#12327, 5ae4303d) confirmed on origin/main at fire: src/index.ts:154-155 re-exports BaseValidationRuleShape + FilterCondition. H17 on-hold trigger-file index: no hit.


    Generated by Claude Code

  4. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Dev claim (os-dev subagent, dispatched by PM loop R7).

    Session: session_01E5LFCYBJ3q2s6yW6oMLxwy (subagent seat)
    Branch: claude/issue-11986-entry-nameability-gate (pushed empty at start; base cdbd9204b65ae4ebec63f90f61669fb498efaa55)
    Worktree: ../objectstack-issue-11986

    Scope: generalize packages/spec/scripts/root-entry-type-nameability.pin.test.ts into a per-public-entry check: gate; wiring in packages/spec/package.json plus whatever the sibling check: gate family requires for CI. Body read in full and verified complete; premises re-verified against the current tip before implementing.


    Generated by Claude Code

  5. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 11986,
      "status": "done",
      "branch": "claude/issue-11986-entry-nameability-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12418",
      "premise_still_valid": true,
      "summary": "Shipped `check:entry-nameability` (packages/spec/scripts/check-entry-nameability.ts): per public entry, a one-file consumer program compiled with declaration:true through the package's own exports map, probing every callable export with `E.FN(null as never, ...)` and reading the declaration emitter's cannot-be-named diagnostics. Wired as a `check:` script, classified NO_GENERATOR in check-generated.ts, and run as one step in lint.yml's typecheck-consumers lane beside check:api-surface / check:exported-any / check:dual-source-exports. The card's charter holds and the pin test's location was confirmed on origin/main, but TWO stated premises were refuted by measurement: (1) batching entries into one program would silently empty the gate (importing a subpath makes its names nameable program-wide), so per-entry programs are load-bearing and the batching control is now a permanent --self-test case; (2) 'green on current main' is false — the generalized call-shape probe finds 7 live leaks of the same defect class across 4 entries that A' never touched, so the gate ships with a hand-ratcheted shrink-only ledger closed to new rows, and the 7 are filed as #12414 rather than repaired here (they edit four entry barrels incl. src/kernel/ and src/ui/, both claimed by batch siblings, and regenerate api-surface/ — a different file surface and gate family).",
      "tests": "All gate runs at final commit 0eb9f84 (git rev-parse --short HEAD, clean tree), exit codes captured by redirect-then-capture, never through a pipe. ACCEPTANCE CRITERION 1 (A' reverted => red naming exactly BaseValidationRuleShape + FilterCondition): MET EXACTLY. Ablation on packages/spec/src/index.ts with an absolute-path restore trap; mutation proven on disk (git hash-object a2a790b19e0c... -> 6b8da6af0c6d..., both anchor greps 1 -> 0); spec rebuilt on the mutated source via scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 134s); mutation proven to have reached dist (root-entry export statements naming the two types: 1,1 -> 0,0 — note a bare grep of the whole dist/index.d.ts is the WRONG marker, both names also occur inside structural expansions); gate exit 1: '2 NEW unnameable structural mention(s) across 1 public entry' / @objectstack/spec: BaseValidationRuleShape, FilterCondition — and nothing else. RESTORE LEG: git checkout HEAD -- (absolute path), git diff HEAD empty, hash back to a2a790b19e0c..., rebuilt (VERDICT command-exit 0), dist export statements back to 1 and 1. CRITERION 2 (green on main + A'): MET — 'entry-nameability: 422 call probes across 17 public entries, 0 new unnameable structural mentions (31.3s)', exit 0. CRITERION 3 (synthetic leak in any public entry reds it): MET on a real SUBPATH entry, not only the root — a factory returning { filter: FilterCondition } injected into src/security/index.ts (clean today); marker grep in src 0 -> 1, rebuilt (VERDICT command-exit 0), marker proven present in dist/security/index.d.ts (2 occurrences); gate exit 1 naming @objectstack/spec/security -> FilterCondition. Restored: git diff HEAD empty, hash back to bb76c1c1b2f4..., rebuilt, marker proven ABSENT from dist (0). GATE RUNTIME: 31.3s-47.3s wall clock across 17 entries / 422 probes (three runs); comfortably under the 2-3 min CI-material threshold, so no batching or parallelism was adopted — and batching is refused on soundness grounds, not cost. OTHER GATES, all exit 0: check:scripts-typecheck; check:generated --reconcile-only ('ledger reconciles with package.json: 27 check: + 17 gen: scripts, all classified'); check:merge-driver; check:published-files; check:nul-bytes; check:pnpm-filter-targets; check:workflow-status-functions; check-self-test-wired; check-self-test-workflow-commands; check-step-collectors; check-aggregator-roster; check-required-contexts; check-cross-package-test-inputs; check-ci-filter-parity. Gate families re-derived from the real change set with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (5 paths, repo assertion held) rather than trusting the dispatch list. NOT RUN LOCALLY: the repo-wide eslint sweep and the full CI farm — left to CI per the local-verification scope; this is a declared narrowing, not a claim of coverage. SELF-TEST (runs on EVERY invocation, not only under --self-test): positive control fires on a synthetic leaking entry, repaired-fixture negative control is clean, batched-program control reports nothing (the measured basis for never batching), TS4094 canary red under the shared compiler profile.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12414: seven public entries still leak an unnameable type through a factory call — the same class A' did not close (root: Book/FormField/NavigationItem; /kernel: UnknownAuthoringKeyFinding; /ai: FilterCondition/StateNodeConfig; /ui: FilterCondition). Held in entry-nameability.baseline.json, shrink-only and closed to new rows, so nothing new can land while these are open.",
        "filed as #12416 (label `finding`): scope question — whether the invariant covers plain export REFERENCES (`export const r = E.NAME`) and not just factory calls. 27 (entry, type) pairs measured across 11 of 17 entries, with the two facts that bear on the decision: five entries hit TS7056 under that shape (structurally NOT MEASURED, so a reference-shape gate could not be made total), and it costs ~105s vs ~42s. Deliberately NOT added to this gate's ledger — that would trade a ledger with a one-line repair per row for a 27-row accommodation surface with no shrink plan."
      ],
      "deviations": [
        "REST issue/label endpoints return HTTP 403 in this container ('GitHub access is not enabled for this session'), so the contract's REST-list channel for duplicate search and the ADDITIVE label-add endpoint were both unavailable. Used MCP instead (2 search calls, declared as a GraphQL-pool cost), and applied `skip-changeset` via the read-union-write fallback — then READ IT BACK: label confirmed present on #12418, PR confirmed still draft.",
        "The PR body's first write was mutated by the GitHub sanitizer exactly as AGENTS.md documents: angle-bracket fragments (`E.<name>`, `E.<fn>(...)`, `-- <abs path>`) were eaten, leaving `E.` and a dangling `--`. Detected by reading the body back, respelled without literal angle brackets, and re-verified. No other mutation.",
        "The gate runs its --self-test on EVERY invocation rather than the sibling spelling `--self-test && <gate>`, so check:entry-nameability cannot be run without its positive control. Stricter than the house pattern, not looser.",
        "No changeset: tooling-only diff (gate script, ledger, script wiring, gate classification, one CI step). `skip-changeset` applied and read back."
      ],
      "files_changed": [
        "packages/spec/scripts/check-entry-nameability.ts (new, the gate)",
        "packages/spec/entry-nameability.baseline.json (new, hand-ratcheted shrink-only ledger, 7 rows)",
        "packages/spec/package.json (check:entry-nameability script)",
        "packages/spec/scripts/check-generated.ts (NO_GENERATOR classification)",
        ".github/workflows/lint.yml (one step in typecheck-consumers)"
      ]
    }

    Generated by Claude Code

  6. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    ACCEPT — reviewer of record: spec seat R7, session_01E5LFCYBJ3q2s6yW6oMLxwy. Not clause-② (no contract accept/reject change; the gate is measurement machinery), so no contract-review chain is owed — standard review, verified against GitHub and the PR's own evidence, not the report narrative.

    1. All three acceptance criteria MET with reverse evidence: A′-ablation red naming exactly the two chartered names (mutation proven on disk AND in dist, restore leg proven both ways); green at 0 new over 422 probes / 17 entries; synthetic-leak red demonstrated on a real subpath entry (/security), not just the root. The ~31–47s runtime is under the CI-material threshold — and batching is refused on measured soundness grounds (a merged program can name every entry's types and reports zero forever), now a permanent self-test control rather than prose. That refusal REFUTES my dispatch's batching suggestion — correctly.
    2. The card's "green on current main" premise was refuted honestly: 7 pre-existing leaks across 4 entries (the card's own thesis arriving early). Shipping the gate with a hand-ratcheted, shrink-only-both-directions, closed-to-new-rows baseline is the right shape — the invariant is closed for everything NEW while the backlog is named. Repairs correctly NOT folded in (they edit src/kernel/ + src/ui/ barrels, both claimed by batch siblings at the time, and regenerate api-surface/): filed as spec: seven public entries still leak an unnameable type through a factory call — the class A′ did not close #12414 (verified — repro commands, one-line repair shape, ledger-row deletion as part of each repair). Scope question on reference-shape coverage filed as spec: decide whether the entry-nameability invariant covers plain export REFERENCES, not just factory calls (27 pairs measured) #12416 with the two load-bearing measurements (TS7056 non-totality; 105s vs 42s) — triage's to grade, and I endorse its own note that a reference-shape gate would be a SECOND gate with its own ledger, never a widening of this one.
    3. Wiring verified from the diff: NO_GENERATOR classification (the safety property — no gen: can launder the eighth leak), one step in lint.yml's existing typecheck-consumers lane (no new required context), self-test on every invocation (stricter than the house pattern, accepted).
    4. Deviations accepted as recorded: MCP fallback for the 403'd REST channel with read-back, sanitizer-eaten PR-body fragments detected by read-back and respelled, skip-changeset for a tooling-only diff (label verified present).

    Landing: no governed face ⇒ normal queue path; ready-flip + enqueue when EVERY check on PR #12418 is green. Card stays pm:dispatched until MERGED.


    Generated by Claude Code

  7. removed their assignment
    on Aug 26, 2026
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