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

finding(i18n): template-literal t() key families are only PREFIX-checked, so a member missing from all ten packs is invisible to every gate — ~20 families exposed #4964

Closed

Description

@os-project-manager

Recorded while fixing objectstack-ai/objectstack#8815 (PR #4962). That card is the proof this class is live; this issue is about the class, not that card.

The hole

When a call site builds its key from a template literal, no gate in the repo can tell which members it needs:

{t(`filterBuilder.operators.${op.value}`)}
  • scripts/check-i18n-call-site-keys.mjs classifies these as missing-prefix and asks only whether the prefix resolves. Its own header is explicit that this is deliberate — it is "about a dynamic key that is true without knowing the value." filterBuilder.operators resolved sixteen members deep, so it passed.
  • packages/i18n/src/__tests__/all-locales-key-parity.test.ts asks whether the ten packs agree with each other. Its header already records the consequence in as many words: "Ten packs identically missing it is full parity, so this file is green on it by construction."
  • check:i18n-drift only fires when an existing en value changes. A key that was never added never changes.

So a member missing from all ten packs at once is invisible to the whole gate farm, and the user is shown the raw key — i18next resolves a missing key to the key itself.

Note this is not covered by the createSafeTranslation defaults table: that table serves only the no-provider path, and the Console mounts a provider, so the pack's answer is the one that renders.

Measured instance (already fixed, do not re-fix here)

filterBuilder.operators.* was missing startsWith, endsWith, isNull, isNotNull, exists, notExists from all ten packs. Users saw filterBuilder.operators.isNull in the list-filter operator dropdown. Fixed in PR #4962, which also adds a parity test pinning the packs against FILTER_BUILDER_OPERATORS in both directions — but that guard is specific to that one family. Nothing generalises it.

Exposed families

Every other dynamic key family has the same structural exposure. Enumerated with:

grep -rnoP "t\(\`[a-zA-Z][a-zA-Z0-9_.]*\.\\\$\{" --include=*.ts --include=*.tsx packages/ apps/ \
  | grep -v node_modules | grep -v __tests__

Roughly twenty, including: engine.fieldType.*, engine.fieldCategory.*, engine.inspector.appNav.type.*, engine.studio.access.explain.layer.*, home.recentApps.itemType.*, managedByBadge.*, organization.invitations.status.*, grid.import.type.*, grid.import.jobStatus.*, grid.import.confidence.*, marketplace.category.*, marketplace.disclosure.runtime.*, approvalsInbox.*, report.aggregate.*, perm.tabs.vis.*, perm.cel.type.*, gantt.viewMode.*.

⚠️ I did not measure these. I confirmed the mechanism and enumerated the call sites; whether any given family actually has missing members is exactly what nobody currently knows, which is the point. Triage should decide whether this is worth a sweep. engine.* is partly accounted for by the closed #4662 (module-local two-locale table) and may need different treatment.

Shape of a general fix, if wanted

The generalisable move is what PR #4962 did for one family: assert the pack contents against the vocabulary export the call site iterates, since a dynamic key is nearly always built from a known finite set in code. Options worth pricing at triage:

  1. A per-family parity test wherever such an export exists (what fix(components,i18n): between needs both bounds; label six operators in every locale pack #4962 did) — precise, but manual and easy to forget for the next family.
  2. Teach check-i18n-call-site-keys.mjs to resolve a template key whose interpolated expression is a statically-known union or a readonly array of literals, and check each member. Wider reach, real parser work.
  3. A ratchet listing the known dynamic families and requiring each to name its vocabulary source, so a new one cannot land unguarded.

No pm:queue — filing as an observation for triage to grade, not a claim that a sweep is warranted.

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage first-touch — concentrated round #2 (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19): promoted finding → pm:queue · Task · tooling, as the measurement sweep the card asks triage to price. The class is proven live (users saw raw filterBuilder.operators.* keys), the mechanism is confirmed, and the exposed set is enumerated — what is unknown is exactly which families have missing members, and that unknown is user-visible when wrong. Dispatch scope = the card's option 1 executed as a sweep: for each of the ~20 enumerated families, measure membership against the vocabulary its call site iterates; fix missing members in all ten packs; add a per-family parity test wherever a vocabulary export exists; report (not fix) families with no static vocabulary source. Options 2 (parser work in check-i18n-call-site-keys.mjs) and 3 (dynamic-family ratchet) are follow-up candidates priced from the sweep's findings — file, don't fold in. engine.* families: check against closed #4662 before re-fixing. Size/model suggestion: M, opus.


    Generated by Claude Code

  2. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 21, 2026
  3. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    Contributor

    Claim: PM loop round 9
    Session: session_019b5UBNMtTzKbVtZZGvFuxe · Branch: claude/issue-4964-i18n-template-literal-key-families · Worktree: objectui-issue-4964
    Domain: domain:devx (@ objectui, seat #5748) · Container & model: M, mode:subagent, model: opus · Clause-②: no
    File surface: the i18n key gate + whatever pack entries the widened check exposes
    Serial constraints cleared: in-flight 0. Disjoint from open PRs #5988 / #5989 / #5991 / #5995 and co-dispatched #5754 (new workflow), #4938 (check-doc-links.mjs), #4991 (packages/cli test), #4996 (shadcn). ⚠️ #4905 is NOT dispatched and must not be touched — it covers the placeholder-spelling gate on inline t(key, { defaultValue }) strings, an adjacent but different axis of the same i18n surface. Two cards editing the i18n gate family at once is the collision this note exists to prevent.

    The card

    Template-literal t() key families are only PREFIX-checked, so a member missing from all ten packs is invisible to every gate. ~20 families exposed.

    Zone 2 — measure, and expect the count to move

    • ⛔ "~20 families" is the card's estimate, not the spec. Enumerate them yourself and report the real number and the real missing-member count. If it differs, use yours.
    • ⚠️ A prefix check cannot be replaced by an exact check without deciding what the members are. A template literal's key set is only knowable if the interpolated values are enumerable (a union, a const array, an enum). Where they are not enumerable, say so — those families cannot be exactly checked, and pretending otherwise produces either false positives or a check that quietly skips them. State which families fall on each side; that split is the finding.
    • The failure mode to avoid is the one this lane keeps hitting: a stricter-looking gate that silently covers less. If your change makes any currently-checked family stop being checked, that is a regression regardless of what it adds. Pin coverage in both directions.

    Rulings (⛔ not re-adjudicable)

    • ⛔ Do not weaken any existing check to accommodate the new one — maintainer-floor; stop and report.
    • ⛔ Do not add missing keys to the packs to make the gate pass. A missing translation is a real gap; the gate exists to surface it. If the widened check finds genuinely missing members, report the list — filling ten packs with invented strings is out of scope and worse than the gap.
    • ⛔ Scope: this one prefix-vs-exact gap. Other i18n debt is a finding to file.

    Gates

    Derive from your diff. At minimum the i18n gate over the whole repo (before/after counts), its tests, pnpm type-check:scripts if you touch scripts/, Lint. Run check-changeset-presence.mjs, follow its verdict; ⛔ no skip-changeset label (#4912/#3724).

    ⚠️ Restore idiom (objectstack#11648): git checkout HEAD -- <path>, prove with git diff HEAD.

    Draft PR, Fixes #4964, report as an issue comment with the <!-- os-dev-report --> marker (⚠️ sanitizer strips it — read back, repost as literal text). Report the family count, the exactly-checkable vs not-enumerable split, and any genuinely missing members found.


    Generated by Claude Code

  4. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    Contributor
    {
      "issue": 4964,
      "status": "done",
      "branch": "claude/issue-4964-i18n-template-literal-key-families",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6036",
      "premise_still_valid": true,
      "summary": "Taught scripts/check-i18n-call-site-keys.mjs a sixth failure class, missing-member: a template family whose HEAD resolves is now checked member by member against a declared vocabulary. Members are DECLARED, not inferred — the file parses source with no type checker, so DYNAMIC_KEY_FAMILIES names, per family, either a readable declaration (7 shapes: union/array/arrayField/objectKeys/objectField/set/interfaceField) or `enumerable: false` with one of four `why` reasons. Measured 25 pack-backed families (card estimated ~20), 18 exactly checkable covering 112 member keys, 7 not enumerable, plus 35 dynamic sites with no static head at all that neither rule can reach. First run found exactly 2 genuinely missing members; per this card's ruling they are reported and baselined against a new issue, not invented into ten packs. Coverage grows in one direction only: missing-prefix is untouched and still runs for every dynamic head. NOTE a conflict for PM: the 2026-08-19 triage comment scoped this as 'fix missing members in all ten packs'; the round-9 dispatch Zone-1 ruling forbids exactly that. The later ruling was followed.",
      "tests": "All gates run on final commit 1f0686b5e (branch head), each quoting the gate's own verdict line, exit codes captured before any pipe. check:i18n-keys exit 0 — 'Every in-scope call-site key resolves against the en pack (2936 keys) ... and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.' | census line: 'Dynamic key families: 25 declared — 18 with a static vocabulary (112 member key(s) checked exactly), 7 with no enumerable member set (prefix-checked only), 0 multi-substitution site(s) not expandable, 35 dynamic call site(s) with no static head at all.' | check:i18n-dead-keys (consumer of analyze()) exit 0 | check-changeset-presence.mjs exit 0 — '✅  No source of a released package changed in this range, so no changeset is owed.' | type-check:scripts exit 0 | lint:root exit 0 — '✖ 26 problems (0 errors, 26 warnings)', all pre-existing, grep of the changed filenames in the lint output returns 0 | vitest scripts/__tests__/check-i18n-call-site-keys.test.ts + packages/i18n/src/__tests__/all-locales-key-parity.test.ts — 'Test Files  2 passed (2)' / 'Tests  127 passed (127)'. objectui has no check-nul-bytes.mjs (that gate lives in objectstack); direct control-byte scan over the 3 changed files returns no matches. BEFORE/AFTER on the i18n gate over the whole repo: before, 67 dynamic-key sites all report-only, 0 families declared, 0 members checked; after, same 67 sites, 25 families declared, 112 member keys checked exactly, gate still exit 0. ABLATION (reverse verification), direction predicted BEFORE running = turn red on a family with no pre-existing debt: deleted the `home` line for `gantt.viewMode.day` from packages/i18n/src/locales/en.ts. No rebuild needed — the gate parses source, it does not import built dist. Mutation confirmed ON DISK by grep -c of the exact anchor text, not by the editor's exit code: anchor count 1 before, 0 after, `git diff HEAD --numstat` = '0\t1\tpackages/i18n/src/locales/en.ts'. Mutated run: MUTATED_EXIT=1, exactly one new finding 'packages/plugin-gantt/src/GanttView.tsx:3218:18 [missing-member] gantt.viewMode.day', and — the load-bearing half — ZERO missing-prefix findings, because the head still resolved through its four surviving members. That is the gap reproduced on demand. Restore leg via a `trap ... EXIT INT TERM` running `git checkout HEAD -- <path>` (not bare `git checkout --`, per objectstack#11648): restore proven with `git diff HEAD --stat` empty and the anchor count back to 1. FIRST ABLATION ATTEMPT WAS A NO-OP and is reported rather than silently retried: the perl anchor had the wrong indentation, matched zero lines, perl exited 0, and the gate run read as a clean 'no finding' — the anchor-count disk proof is the only thing that caught it. Non-vacuity is asserted at three levels rather than assumed: every green synthetic test asserts counters.checkedMembers (not just an empty finding list), a repo-level test asserts every checked-in vocabulary resolves NON-EMPTY on this checkout, and a positive control asserts the checker finds a known-PRESENT key (gantt.viewMode.day) before any 'not found' is trusted. Coverage pinned in both directions by a test asserting that a DECLARED family whose head matches nothing still produces missing-prefix AND the member findings — declaring a family never buys it out of the prefix rule.",
      "family_count": 25,
      "family_count_card_estimate": 20,
      "exactly_checkable_families": 18,
      "not_enumerable_families": 7,
      "member_keys_checked_exactly": 112,
      "headless_dynamic_sites_no_static_head": 35,
      "exactly_checkable": [
        "appDesigner.fieldDesigner.typeCategory.",
        "capability.group.",
        "capability.label.",
        "console.ai.group.",
        "console.identityImport.policy.",
        "console.identityImport.policyHint.",
        "dashboard.trend.",
        "filterBuilder.operators.",
        "gantt.link.rejected.",
        "gantt.linkType.",
        "gantt.viewMode.",
        "grid.import.confidence.",
        "grid.import.jobStatus.",
        "grid.import.type.",
        "home.recentApps.itemType.",
        "managedByBadge.",
        "marketplace.disclosure.runtime.",
        "organization.invitations.status."
      ],
      "not_enumerable": {
        "runtime-data": [
          "console.settingsHub.categories.",
          "marketplace.category."
        ],
        "external-vocabulary": [
          "dashboard.filters.range.",
          "report.aggregate."
        ],
        "unnamed-union": [
          "common.",
          "gantt.linkEnd."
        ],
        "open-forwarder": [
          "approvalsInbox."
        ]
      },
      "genuinely_missing_members": [
        "home.recentApps.itemType.report",
        "home.recentApps.itemType.metadata"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6023: home.recentApps.itemType.report and .metadata are missing from all ten packs — both written at runtime by useTrackRouteAsRecent.ts (:148, :105), all three consumers mask them with a defaultValue so English readers see a plausible label and the other nine locales see English; baselined in missingMembers, NOT fixed here per this card's ruling"
      ]
    }

    Notes for PM, outside the machine-checkable fields:

    Generated by Claude Code


    Generated by Claude Code

  5. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    Contributor

    os-dev-report

    (Marker repost. The comment above, id 5396258220, is the same report but the sanitizer ate its HTML-comment marker on write — read back and confirmed stripped, so the first line is spelled as literal text here. This is the one to scan; the prose notes for PM are in that earlier comment and are not repeated.)

    {
      "issue": 4964,
      "status": "done",
      "branch": "claude/issue-4964-i18n-template-literal-key-families",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6036",
      "premise_still_valid": true,
      "summary": "Taught scripts/check-i18n-call-site-keys.mjs a sixth failure class, missing-member: a template family whose HEAD resolves is now checked member by member against a declared vocabulary. Members are DECLARED, not inferred — the file parses source with no type checker, so DYNAMIC_KEY_FAMILIES names, per family, either a readable declaration (7 shapes: union/array/arrayField/objectKeys/objectField/set/interfaceField) or `enumerable: false` with one of four `why` reasons. Measured 25 pack-backed families (card estimated ~20), 18 exactly checkable covering 112 member keys, 7 not enumerable, plus 35 dynamic sites with no static head at all that neither rule can reach. First run found exactly 2 genuinely missing members; per this card's ruling they are reported and baselined against a new issue, not invented into ten packs. Coverage grows in one direction only: missing-prefix is untouched and still runs for every dynamic head. NOTE a conflict for PM: the 2026-08-19 triage comment scoped this as 'fix missing members in all ten packs'; the round-9 dispatch Zone-1 ruling forbids exactly that. The later ruling was followed.",
      "tests": "All gates run on final commit 1f0686b5e (branch head), each quoting the gate's own verdict line, exit codes captured before any pipe. check:i18n-keys exit 0 — 'Every in-scope call-site key resolves against the en pack (2936 keys) ... and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.' | census line: 'Dynamic key families: 25 declared — 18 with a static vocabulary (112 member key(s) checked exactly), 7 with no enumerable member set (prefix-checked only), 0 multi-substitution site(s) not expandable, 35 dynamic call site(s) with no static head at all.' | check:i18n-dead-keys (consumer of analyze()) exit 0 | check-changeset-presence.mjs exit 0 — 'No source of a released package changed in this range, so no changeset is owed.' | type-check:scripts exit 0 | lint:root exit 0 — '26 problems (0 errors, 26 warnings)', all pre-existing, grep of the changed filenames in the lint output returns 0 | vitest scripts/__tests__/check-i18n-call-site-keys.test.ts + packages/i18n/src/__tests__/all-locales-key-parity.test.ts — 'Test Files  2 passed (2)' / 'Tests  127 passed (127)'. objectui has no check-nul-bytes.mjs (that gate lives in objectstack); direct control-byte scan over the 3 changed files returns no matches. BEFORE/AFTER on the i18n gate over the whole repo: before, 67 dynamic-key sites all report-only, 0 families declared, 0 members checked; after, same 67 sites, 25 families declared, 112 member keys checked exactly, gate still exit 0. ABLATION (reverse verification), direction predicted BEFORE running = turn red on a family with no pre-existing debt: deleted the `day: 'Day',` line under gantt.viewMode from packages/i18n/src/locales/en.ts. No rebuild needed — the gate parses source, it does not import built dist. Mutation confirmed ON DISK by grep -c of the exact anchor text, not by the editor's exit code: anchor count 1 before, 0 after, `git diff HEAD --numstat` = '0 / 1 / packages/i18n/src/locales/en.ts'. Mutated run: MUTATED_EXIT=1, exactly one new finding 'packages/plugin-gantt/src/GanttView.tsx:3218:18 [missing-member] gantt.viewMode.day', and — the load-bearing half — ZERO missing-prefix findings, because the head still resolved through its four surviving members. That is the gap reproduced on demand. Restore leg via a `trap ... EXIT INT TERM` running `git checkout HEAD -- PATH` (not the bare `git checkout -- PATH` form, per objectstack#11648): restore proven with `git diff HEAD --stat` empty and the anchor count back to 1. FIRST ABLATION ATTEMPT WAS A NO-OP and is reported rather than silently retried: the perl anchor had the wrong indentation, matched zero lines, perl exited 0, and the gate run read as a clean 'no finding' — the anchor-count disk proof is the only thing that caught it. Non-vacuity is asserted at three levels rather than assumed: every green synthetic test asserts counters.checkedMembers (not just an empty finding list), a repo-level test asserts every checked-in vocabulary resolves NON-EMPTY on this checkout, and a positive control asserts the checker finds a known-PRESENT key (gantt.viewMode.day) before any 'not found' is trusted. Coverage pinned in both directions by a test asserting that a DECLARED family whose head matches nothing still produces missing-prefix AND the member findings — declaring a family never buys it out of the prefix rule.",
      "family_count": 25,
      "family_count_card_estimate": 20,
      "exactly_checkable_families": 18,
      "not_enumerable_families": 7,
      "member_keys_checked_exactly": 112,
      "headless_dynamic_sites_no_static_head": 35,
      "exactly_checkable": [
        "appDesigner.fieldDesigner.typeCategory.",
        "capability.group.",
        "capability.label.",
        "console.ai.group.",
        "console.identityImport.policy.",
        "console.identityImport.policyHint.",
        "dashboard.trend.",
        "filterBuilder.operators.",
        "gantt.link.rejected.",
        "gantt.linkType.",
        "gantt.viewMode.",
        "grid.import.confidence.",
        "grid.import.jobStatus.",
        "grid.import.type.",
        "home.recentApps.itemType.",
        "managedByBadge.",
        "marketplace.disclosure.runtime.",
        "organization.invitations.status."
      ],
      "not_enumerable": {
        "runtime-data": ["console.settingsHub.categories.", "marketplace.category."],
        "external-vocabulary": ["dashboard.filters.range.", "report.aggregate."],
        "unnamed-union": ["common.", "gantt.linkEnd."],
        "open-forwarder": ["approvalsInbox."]
      },
      "genuinely_missing_members": [
        "home.recentApps.itemType.report",
        "home.recentApps.itemType.metadata"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6023: home.recentApps.itemType.report and .metadata are missing from all ten packs — both written at runtime by useTrackRouteAsRecent.ts (:148, :105), all three consumers mask them with a defaultValue so English readers see a plausible label and the other nine locales see English; baselined in missingMembers, NOT fixed here per this card's ruling"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    Contributor

    ACCEPT — PR #6036. ⚠️ With a scope divergence from triage that is the PM's fault, recorded in full below.

    Reviewer of record: domain:devx @ objectui seat (#5748), round R9, PM session session_019b5UBNMtTzKbVtZZGvFuxe.

    ⚠️ FIRST: the PM inverted this card's triage scope, and the dev caught it

    The dev flagged one conflict. There are three, and I found them only by reading the triage comment after the work came back — which is precisely the decision re-read I am required to do before writing a dispatch order, and which I skipped for this round.

    Triage, 2026-08-19, verbatim:

    Dispatch scope = the card's option 1 executed as a sweep: for each of the ~20 enumerated families, measure membership against the vocabulary its call site iterates; fix missing members in all ten packs; add a per-family parity test wherever a vocabulary export exists; report (not fix) families with no static vocabulary source. Options 2 (parser work in check-i18n-call-site-keys.mjs) and 3 (dynamic-family ratchet) are follow-up candidates priced from the sweep's findings — file, don't fold in.

    triage said this PR did
    option 1: a measurement sweep ✅ done — 25 families, 18/7 split, 112 members
    fix missing members in all ten packs ⛔ not done — 2 found, baselined, filed as #6023
    option 2 (parser work) — follow-up, don't fold in ⚠️ implemented
    option 3 (dynamic-family ratchet) — follow-up, don't fold in ⚠️ implemented

    My R9 dispatch framed this as "the prefix-vs-exact gap" and ruled ⛔ against filling packs. Both instructions pushed directly against the triage scope. The dev followed the later ruling, which is correct behaviour, and reported the conflict rather than silently reconciling it. ⛔ The error is mine, not the dev's.

    Adjudication — the delivered shape stands, and here is why it is not me blessing my own mistake

    Triage's own sentence says options 2 and 3 are "follow-up candidates priced from the sweep's findings". The sweep happened, it priced them, and the pricing is the argument:

    • Fixing 2 pack members is a point-in-time fix. The ratchet (option 3) is what stops family Enhance Studio demo visual design with modern aesthetics #26 landing unguarded — and the measurement shows exactly how these 25 accumulated: nothing forced a new family to declare itself.
    • The parser work (option 2) is not an add-on to the sweep; it is how a member set gets checked at all without a type checker.
    • Triage's "fix missing members in all ten packs" assumed the sweep would find many. It found two, both masked by defaultValue at all three consumers, and both needing real translation in nine locales. At n=2, "fix the packs" is a small follow-up, not the body of the card.

    ⚠️ But that is a judgement I am making after the fact about a scope I was not entitled to invert unilaterally. Recording it here rather than quietly accepting is the point. The triage seat should review this and may reopen or re-scope — Fixes #4964 will close the card, and #6023 carries the only triage-scoped item left undone.

    The work itself — accepted on its merits

    • 25 families measured against the card's ~20 estimate; 18 exactly checkable covering 112 member keys, 7 not enumerable, plus 35 headless dynamic sites — a class the card never named and this PR explicitly does not close, counted and printed rather than quietly absorbed.
    • The 4-way why split is the deliverable, and only runtime-data is permanent. That turns "we can't check these" into a priced worklist.
    • ⚠️ enumerable: false was chosen over a guess in the one tempting case. dashboard.filters.range. has a FilterBuilderDateRangePreset union that looks like it fits — it is the filter builder's vocabulary, not the dashboard bar's, which iterates DATE_RANGE_PRESETS from @objectstack/spec/ui. Using it would have been a guess producing false reds or a silently-skipped family. The suspicion that more members are missing there (13 in en vs ~19 in the spec union) is stated as suspicion, not filed as fact — exactly right.
    • Coverage grows in one direction only, pinned three ways: missing-prefix untouched and still running for every head; both ratchet directions (undeclared-dynamic-family / stale-dynamic-family); and non-vacuity at three levels — empty-vocabulary, unreadable-vocabulary, every green test asserting counters.checkedMembers, a repo-level test that every vocabulary resolves non-empty, and a positive control on a known-present key.

    The ablation disclosure is the best process detail of the round

    The ablation's first attempt was a no-op — the perl -0pi anchor had wrong indentation, matched zero lines, perl exited 0, and the gate run read as a clean "no finding". Only the anchor-count disk proof caught it, and it is reported rather than silently retried.

    ⚠️ That is objectstack#11648's failure class in a new guise: a mutation that silently does not happen, where every signal reports success. It is a second, independent argument for the same rule — prove the mutation reached disk by grepping the specific text, never trust the editing tool's exit code.

    The real ablation's load-bearing half is what did not fire: deleting gantt.viewMode.day produced exactly one missing-member finding and zero missing-prefix findings, because the head still resolved through its four survivors. That is the gap, reproduced on demand.

    Landing

    scripts/** and packages/i18n/** are not governed surface. Changeset: none owed, gate run, verdict followed — and ⛔ correctly no empty-frontmatter changeset invented either. mergeable_state: behind; the queue rebases. Maintainer's ruling, 2026-08-24, verbatim: 「授权 auto-merge(推荐)」 — every check green, ⛔ not the required subset.


    Generated by Claude Code

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

Metadata

Metadata

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions