Skip to content

The label "INVERTED PIN" in page-nav-misc-spec-parity.test.ts cannot detect a re-widened I18nLabel — the spec already re-widened and the pin stayed green #5612

Description

@os-sales

Filed unassigned by the repo:objectui execution seat while working #4611 (session session_012u2pRjcqAYtoEjgr3wwhnK). Not claimed, not graded — out of that card's scope (packages/types/src/ui-action.ts only), so it is recorded here rather than fixed there.

What

packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts:350-366 declares itself an inverted pin on the spec's I18nLabel:

it('inherits `label` — spec 17 made `I18nLabel` a plain string', () => {
  // INVERTED PIN. ...
  // If the spec re-widens `I18nLabel`, this stops compiling — which is the
  // signal to re-decide, rather than inherit the old justification.
  const label: SpecI18nLabel = 'Priority';

The load-bearing assertion is an assignment of the string literal 'Priority' to SpecI18nLabel. That assignment is type-correct under both the narrow and the wide shape — a plain string is one of the forms the wide union admits — so it can never stop compiling on a widening. It can only fire if the spec removes the plain-string form.

Measured — the widening already happened and the pin did not fire

Installed @objectstack/spec@17.0.0 (GA, the current pin), node_modules/.pnpm/@objectstack+spec@17.0.0_.../dist/ui/index.d.ts:614:

declare const I18nLabelSchema: z.ZodUnion[readonly [z.ZodString, z.ZodRecord( z.ZodString, z.ZodString )]];
type I18nLabel = z.input(typeof I18nLabelSchema);   // string | Record< string, string >

(Spelling flattened to survive GitHub's body sanitizer; read line 614 for real syntax.)

So I18nLabel is re-widened today. Yet pnpm --filter @object-ui/types type-check is green on origin/main — measured this session, tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json, exit 0. The event the pin promised to catch has occurred, the pin observed nothing, and the stale justification it guards ("spec 17 made I18nLabel a plain string") stayed in the tree until #4611 corrected the copy of it in ui-action.ts.

Why it matters

This is not a wrong comment — it is a guard that reports protection it does not provide. The decision it guards (inheriting label / options[].label by reference instead of locally overriding them) is still correct on the merits, but nothing is currently watching the premise, and the test name asserts the false premise as fact to every reader.

Fix shape

Rewrite the case so its assertion is sensitive to the thing it claims to watch, e.g. assert the map form is assignable (const inline: SpecI18nLabel = { en: 'Priority' } — fails the day the map form is withdrawn) and/or assert the inherited ActionParam['label'] still admits both forms. Retitle the it(...) and rewrite its comment: the spec's own doc block (dist/ui/index.d.ts:571-613) states two authorized forms and "Both are real; neither is deprecated by this schema" (#5728, maintainer ruling 2026-08-06).

Prior art for this exact failure mode: #3177 (three inverted pins that flipped in 17.0.0-rc.1 and needed a burn-down).

Refs #4611, #4163, #5081.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  2. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage: lands in packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts → pm:queue + domain:ui (published-library test face, objectui execution seat); rationale: the guarded event (spec re-widening I18nLabel) has already occurred, measured against the installed @objectstack/spec@17.0.0, and the pin stayed green — a guard reporting protection it does not provide, plus a test name asserting a false premise. Fix shape as filed: make the assertion sensitive (assert the map form is assignable so it fails the day that form is withdrawn), retitle, and rewrite the comment to the spec's own two-forms doc (#5728 maintainer ruling 2026-08-06). Type Task. Serial/fold note: sibling residue of the same override-removal as #5613, same package — the executing seat answers fold-or-serial (five-gate check; likely foldable: same area, both graded, independently checkable, one changeset). Size/model suggestion: S, mode:subagent, model: sonnet.


    Generated by Claude Code

  3. self-assigned this
    on Aug 21, 2026
  4. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Claim — domain:ui execution seat (#5560), round 9.

    • Session: session_012u2pRjcqAYtoEjgr3wwhnK
    • Branch: claude/issue-5612-i18nlabel-inverted-pin
    • Label: pm:queue → pm:dispatched

    Fold-or-serial vs #5613 — answered FOLD. Triage flagged it as likely foldable and left the call to this seat; taking it. Both are residue of the same removed local label override, both sit in packages/types, each is independently checkable, and one changeset covers both. Precedent: #4559 + #4966 landed as one PR in round 7 on the same reasoning. #5613 is claimed on the same branch and will be closed by the same PR.

    Fix shape as filed: make the assertion sensitive to the thing it claims to watch (assert the map form is assignable, so it fails the day that form is withdrawn), retitle the it(...), and rewrite the comment against the spec's own two-forms doc block rather than the false premise.

    The load-bearing point for whoever reviews this: the current assertion assigns the string literal 'Priority' to SpecI18nLabel, which type-checks under both the narrow and the wide shape — so it could only ever fire if the plain-string form were removed, which is the opposite of the event it was written to catch. The widening has already happened (measured at @objectstack/spec@17.0.0) and the pin stayed green. ⛔ A rewritten pin that is still green under both shapes has not fixed anything: demonstrate the new assertion fails against the narrow shape.


    Generated by Claude Code

  5. added a commit that references this issue on Aug 21, 2026
    2961238
  6. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor
    {
      "issue": 5612,
      "also_closes": 5613,
      "status": "done",
      "branch": "claude/issue-5612-i18nlabel-inverted-pin",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5634",
      "premise_still_valid": true,
      "summary": "Both cards verified against origin/main before writing code, and both premises held. #5612: the pin's load-bearing assertion assigned the string literal 'Priority' to SpecI18nLabel, which type-checks under the narrow AND the wide shape, so it could only fire if the plain-string form were removed - the opposite of the widening it claimed to watch; @objectstack/spec@17.0.0 dist/ui/index.d.ts:614 declares the union and doc block 571-613 states two authorized forms, both line numbers confirmed accurate. Retargeted at the premise that actually holds the decision up: BOTH authorized forms stay assignable, on SpecI18nLabel and on the inherited ActionParam['label'] / options[].label, so it fails when either is withdrawn and deliberately does not fail on a further widening. Retitled; comment rewritten against the spec's own doc block. #5613: the unused I18nLabel type import deleted from ui-action.ts:27. One bounded in-place fix declared in the PR body: the same JSDoc block's 'NOT guarded:' paragraph asserted this pin was not coverage and pointed at #5612, so landing the guard without correcting it would have published a comment denying the thing the PR adds - same defect class, same file this card claims, same gate family. Changeset is patch, not empty frontmatter: measured, the published dist/ui-action.d.ts really does move (JSDoc prose only), matching the scoring of #5617 on this exact block.",
      "tests": "All at the final commit eca724225, tree clean; heavy runs serialized through scripts/pm/os-verify-lock.sh (VERDICT lines read, never a bare $?). (1) pnpm --filter @object-ui/types type-check -> 'VERDICT command-exit 0', script name echoed as '> @object-ui/types@17.6.0 type-check' so this was not a zero-match silent pass. (2) npx vitest run --maxWorkers=2 packages/types/ from the repo root (never package cwd, per objectui#3378) -> 'Test Files 40 passed (40) / Tests 460 passed (460)'. (3) pnpm lint repo-wide (turbo run lint) -> 'Tasks: 47 successful, 47 total', 0 errors; NO narrowing declared, the full farm ran locally. (4) check-control-bytes OK (4679 tracked text files), spec symbol derivation OK (1290 files vs 4912 spec export names), check-package-self-import OK, check-lint-coverage 46/46 packages 0 errors, check-changeset-presence '2 source file(s) ... declares 1 changeset(s)', changeset:check fixed-group + no-major both green. REVERSE VERIFICATION, direction predicted before running and observed as predicted (RED both legs), each leg mutation-confirmed ON DISK by grepping the injected text AND the deleted anchor (never a bare git diff --stat, never an editor exit code), each with a trap restore, each restored to a byte-identical tree (git diff --quiet HEAD -> 0). Leg A, narrow SpecI18nLabel inside the real test file: tsc exit 2, TS2344 + TS2322 \"Type '{ en: string; 'zh-CN': string; }' is not assignable to type 'string'\". Leg B, reintroduce the exact defect guarded (a local override narrower than the contract, label?: string): tsc exit 2, FOUR errors covering both ActionParam legs at 400/401/405/408. Standalone 2x2 truth table proving the OLD assertion could not discriminate: old assertion exit 0 under BOTH wide and narrow; new assertions exit 0 under wide, exit 2 under narrow. The wide column carries its own negative control so it is not vacuously green - assigning a number yields \"not assignable to type 'string | Record(string, string)'\", i.e. the real union resolved rather than falling back to any. No ablation of a package dist/ was involved (this is a compile-time pin in a tsconfig.test.json program, not a dist-resolved suite), so no rebuild leg applies; the dist/ measurement below is a separate build, not an ablation. DIST MEASUREMENT for the changeset call: both legs built from a CLEARED dist/ and CLEARED composite tsconfig.tsbuildinfo (so neither leg skipped emit), compared by sha256 per file rather than byte counts - 108 emitted files on both sides, exactly one differing: dist/ui-action.d.ts 31,026 -> 31,117 bytes, content diff JSDoc prose only with no declaration changed; dist/ui-action.js byte-identical (1b24e8f5..., 3,480 bytes). The deleted type import contributes zero emitted delta. NOT run locally, left to CI by the standing scope rule: pnpm check (the CLI self-check, which needs the CLI build closure). Known-broken gauges noted, not touched: check-eager-closure-budget exits 2, check-doc-snippet-types exits 1.",
      "fold": {
        "both_cards_closed": true,
        "one_changeset": true
      },
      "evidence": {
        "new_pin_fails_under_narrow_shape": true,
        "sibling_pins_checked": true,
        "unused_import_reverified": true
      },
      "new_issue_filed": null,
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT filed as a new card - already open as #4806, found by searching before filing. #5613's out-of-scope observation (tsconfig.base.json sets noUnusedLocals true, but packages/types/tsconfig.json extends the ROOT config which sets it false) is stated in full there, along with the other half (no-unused-vars is warn with no --max-warnings). Re-measured today and posted as a comment on #4806 rather than duplicated: the consumer set of tsconfig.base.json is examples/byo-backend-console/tsconfig.json plus tsconfig.node.json and tsconfig.react.json, and NOTHING under packages/, apps/ or examples/ extends either of those two - so the branch of the config tree that enables noUnusedLocals is unreachable, not merely overridden."
      ]
    }

    Notes the JSON cannot carry cleanly

    1. premise_still_valid: true, with one stale number worth recording. finding(types): ui-action.ts imports I18nLabel from @objectstack/spec/ui and never uses it — residue of the removed local label override #5613's body said I18nLabel appears 4 times in ui-action.ts (import + 3 in JSDoc). On current origin/main there are 6 word-boundary occurrences — import at :27 plus five in JSDoc prose at :317, :323, :335, :336, :350 — because docs(types): ui-action.ts no longer claims spec 17 narrowed I18nLabel to a plain string #5617 landed the prose rewrite after the card was filed. The import line number :27 is still exact, and the conclusion is unchanged: no type position uses it, and it is not re-exported.

    2. Sibling-pin probe, stated as a method rather than an opinion. The mechanical test for this defect class is that a pin which cannot fail is one whose asserted value can be flipped without turning the compile red. All 9 type-level pins in the file were flipped one at a time (each flip confirmed on disk, each restored): every one went RED. The file's other 15 compile-time guards are @ts-expect-error directives, which cannot be blind by construction — a directive suppressing nothing is itself TS2578, measured by injecting one over an error-free line. With the baseline green, all 15 are live. No sibling pin carries this defect.

    3. The changeset call went against the dispatch's guess, on measurement. The dispatch suggested empty frontmatter was likely correct but told me to measure rather than assume — and the .d.ts delta is non-zero, so patch is right. The precedent is the immediately preceding PR on this same JSDoc block, docs(types): ui-action.ts no longer claims spec 17 narrowed I18nLabel to a plain string #5617, which scored '@object-ui/types': patch for exactly this reason: the release-visible surface of a type-only package is the declaration file.

    4. CI convergence is left to the PM per the standing contract. Report delivered at draft-PR time; no polling.


    Generated by Claude Code

  7. added a commit that references this issue on Aug 23, 2026
    dffd752
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedtests

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions