Skip to content

fix(plugin-dev): the i18n auto-detect resolves translations from packages[] (#15232) - #15282

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-15232-plugin-dev-i18n-package-order
Sep 4, 2026
Merged

fix(plugin-dev): the i18n auto-detect resolves translations from packages[] (#15232)#15282
hotlong merged 6 commits into
mainfrom
claude/issue-15232-plugin-dev-i18n-package-order

Conversation

@hotlong

@hotlong hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15232

Reader program 6/4 of the ADR-0130 D4 option-B ruling on #14512 (comment 5528589044). Split out of the by-shape sweep record #15210; acceptance pin is #15004. The artifact stays additive through this card — composeStacks, packages/spec/src/stack.zod.ts and what every command emits are untouched.

The site

packages/plugins/plugin-dev/src/dev-plugin.ts:526 read options.stack.translations and nothing else. Under option B — every definition carried once inside packages[], no flattened top level — that read returns undefined, the auto-detect concludes "this app declares no copy", and I18nServicePlugin is never registered. Not a crash: the app boots and serves, on the core in-memory i18n fallback, so a developer sees message keys or last release's strings where the app declared real translations. Nothing throws and nothing logs.

Both halves, in the order the card asked for — the ledger row FIRST, red

This site had no row in OPTION_B_LOSSES: the by-shape sweep found it, not the pin. So the loss was ledgered before the reader was touched, and each step was measured.

1 · The row, with the reader still top-level-only → RED (7e9db1fb7, verbatim):

AssertionError: A subsystem lost a collection that the ledger does not carry — this is the
failure #15004 exists to make loud. An option-B artifact reaches it with the collection
ABSENT and NOTHING THROWN.

  B2 · plugin-dev I18nServicePlugin auto-detect over the caller-supplied stack · translations

  LOST     B2 · plugin-dev I18nServicePlugin auto-detect over the caller-supplied stack · translations = none

 Test Files  1 failed (1)
      Tests  1 failed | 5 passed (6)

The other five cases passed in that same run — BASELINE green on the additive shape, the packages[] anti-vacuity control green — which is what makes this a discrimination rather than a broken fixture.

2 · Ledgered → GREEN at 25 rows (64906c620): Tests 6 passed (6).

3 · The reader fixed → RED again, naming the line to delete (e3aca15e2, verbatim):

AssertionError: A ledgered subsystem now SEES its collections under option B — the reader
program moved forward. Delete these lines from OPTION_B_LOSSES:

  B2 · plugin-dev I18nServicePlugin auto-detect over the caller-supplied stack · translations

4 · Row deleted → GREEN at 24 rows: Tests 6 passed (6).

The pin, before and after — stated against the right baseline. Relative to this branch's own merge base (460134af8) the ledger file is byte-identical: git diff 460134af8 HEAD -- packages/cli/test/option-b-reader-acceptance.pin.test.ts is empty, and the branch carries the 24 rows that base carried. ⚠️ main has moved since — #15226 landed and took the ledger to 23 by deleting its own B2 · plugin-security … from-source config row — so "24 → 24" is a fact about this branch's baseline, not about today's main, and is written that way to avoid reading as a claim about the current file. The two do not collide: git merge-tree --write-tree origin/main against this branch's head exits 0 with zero CONFLICT lines (result tree d90f3eb94), measured rather than assumed. The ledger never grew here either way: the row this card added existed only in the intermediate commit, as the thing this fix can be checked against. ⛔ Set equality is untouched, no subsystem stopped being asserted, and no row was added to silence a red. The probe now measures one row more than it did (a reader that is watched and not lost), which is the shape the program ends in.

The fix

DevPlugin.init's 3b decision — detection plus the locales it derives — is now one exported function, devI18nPluginOptions (packages/plugins/plugin-dev/src/dev-i18n.ts). The plugin keeps the dynamic import and its degradation: those are about the optional package being installed, which is a different question from what the stack declares.

  • Flattened top level FIRST, packages[] only where that came back falsy. The caller's original expression (Array.isArray(t) && t.length > 0) is preserved rather than re-expressed, at the top level and per package body — the trap reader program 3/4 — @objectstack/cli: the four config-load boundaries and the gates that key off config.objects #15006 measured. Every artifact the platform emits today answers bit-identically, and that is a property of the merge rather than of the order: composeStacks merges translations with 'concat' (stack.zod.ts:895), so a package that declares copy always leaves a non-empty flattened array too.
  • ⚠️ The same answer is not the same work, and an earlier draft of this body claimed it was. The flattened read short-circuits only when translations is present and NON-EMPTY. Every stack that carries packages[] and declares no i18n at all reaches resolveArtifactPackageOrder on every DevPlugin.init — the ordinary multi-package app that simply does not translate. Contract review measured it with a counting proxy; it is now pinned by a case of this PR's own that counts the reads (2 when the gate is reached, 0 when it short-circuits). Two consequences are load-bearing and are handled below: the cheap limbs are asked first, and the gate's refusals are reachable in ordinary use, so the caller degrades on them instead of dying.
  • stack.packages is not iterated. resolveArtifactPackageOrder (@objectstack/core, ADR-0130 D4+D5, fix(metadata): register a packages[] artifact per package at the metadata door so every object has one owner across every door (#14599) #14643) is the one traversal. Worth stating plainly: for a boolean the ORDER is not observable — what is observable is that the same call is the gate, so a hand-rolled loop would answer "this app declares copy" for an artifact whose package list the registration path refuses.
  • The packages key's absence is checked before the call. D4's second branch returns the caller's own object as the single package body, so an unguarded walk would read the same translations a second time for every single-package stack. Pinned by a getter that counts reads: exactly 1.
  • A malformed packages is refused by the READER, and degraded by the PLUGIN. devI18nPluginOptions raises the same ADR-0112 envelope (code + status: 422) ObjectQL.registerApp raises for that object later in the same boot — asserted on code and status rather than through a bare expect().toThrow(). DevPlugin catches it, prints its own diagnosis and boots on the in-memory fallback; the section below says why. ⚠️ Not every refusal on this path carries the envelope: a dependency cycle between two packages comes out of resolvePluginOrder as a bare Error with no code and no status. That is documented under @throws on both functions and asserted by its own case — an earlier draft of this body said "never a bare throw", which was false about the code rather than about the assertion style.
  • i18n and manifest.translations are untouched. i18n is an artifact ENVELOPE key, not a package-owned collection (derived: ObjectStackDefinitionSchemaAssembledPackageBodySchema, complement pinned by reader program 1/4 — the option-B acceptance probe: a two-package fixture booted through every entry path with the flattened collections absent #15004), so option B does not move those two limbs. translations is the one limb it moves.

The question this site raises: a caller-supplied, already-composed stack

DevPlugin takes its stack from options.stack — a caller-supplied object, with no load boundary this repo owns between the composed config and the reader. The enumeration in #14512 comment 5523741937 lists it as one of three construction sites with nothing to fold at.

Yes, the auto-detect should fire for an already-composed multi-package config, and this fix makes it fire. The evidence, rather than the intuition:

  1. That construction is the documented one and it is what a multi-package app runs. new DevPlugin({ stack: config }) is the example in packages/plugins/plugin-dev/src/index.ts:36. composeStacks has exactly one call site in this repo and it is user configexamples/app-multi-package/objectstack.config.ts:51 — whose own header says os dev "boots the same shape straight from source". So the object handed to DevPlugin is the composed project itself, packages[] and all; there is no artifact door in between to fold the read into.
  2. Because there is no load step, the fold has to live in the reader. For the other cards a loader could in principle normalise the shape before the reader sees it. Here the plugin IS the boundary, so a fix anywhere else would leave this path exactly as broken as it was.
  3. It costs today's ANSWERS nothing — but it is not free, and the earlier draft of this paragraph was wrong. Every artifact and composed config the platform produces today resolves to the same boolean (the 'concat' merge above). What is not true is that the walk never happens: a composed config that declares no i18n reaches the gate every boot. The short-circuit is pinned only for the case it really covers — a stack whose flattened translations is non-empty, which resolves untouched even beside a malformed packages.
  4. The scope stays exactly the caller's object. The fix reads packages[] on the stack it was handed; it does not go looking for a config elsewhere, and it does not consult the singular manifest for collections (@objectstack/verify's bootStack ignores an app's isDefault permission set, while the CLI honours it — after #5491 a dogfood app cannot test its own declared baseline #7001's constraint, and the manifest carries none anyway — measured in A multi-package artifact serializes its metadata twice — the flattened top level and every packages[i] body carry the same definitions #14512 comment 5523603341).

Tests

All at c108823e9 (final commit, pnpm-lock.yaml included — the diff adds one devDependency). The gate-family and lint runs quoted below were taken at 561ae05da; everything re-run after the census fix is marked as such.

  • pnpm --filter @objectstack/plugin-dev testTest Files 7 passed (7) / Tests 72 passed (72), including 14 new cases in dev-i18n-packages-reader.test.ts, all green by name under --reporter=verbose. (An earlier draft of this body said 11 for what were then 10 — miscounted, corrected here and re-counted after the remediation added four.)
  • pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/option-b-reader-acceptance.pin.test.tsTest Files 1 passed (1) / Tests 6 passed (6).
  • Ablation of the original fix, measured at e3aca15e2 when this file held 10 cases rather than 14 (committed first, then mutated): the packages[] limb removed, confirmed on disk before the run — limb occurrences 1 → 0, injected marker 1, and the blob hash moved off HEAD's (8ecb11ff…9d0ccdac…). The suite went 4 failed | 64 passed, failing exactly THE FIX, the empty-top-level case, the malformed-packages refusal and the BOOT case, with the baseline and the negative control still green. Restored with git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven restored: git diff HEAD empty, git status clean, disk hash back to 8ecb11ff…, marker count 0. No rebuild leg was needed and that is checkable rather than assumed — the test imports ./dev-i18n relatively and @objectstack/core is aliased to source in this package's vitest.config.ts, so no dist/ is on that resolution path.
  • Three more ablations, each a single-case discrimination, at c108823e9 (same discipline: every mutation confirmed on disk by occurrence counts in both directions plus a blob-hash move off HEAD's; every restore by git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven by an empty git status, an empty git diff HEAD and matching hashes):
    • remove DevPlugin's catch → exactly 1 failed | 13 passed, the failure being "a project the ADR-0130 D4 gate REFUSES still boots";
    • put the translations limb back first → exactly 1 failed | 13 passed, the failure being "a stack that already declares its locales is answered WITHOUT walking packages[]";
    • reader back to top-level-only (the original defect) → 7 failed | 7 passed, including THE FIX, the reachability case (its proxy counts 0 reads instead of 2), the cycle case and both packages[]-dependent boots.
  • Gate family, derived on the merged tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (10 paths, no stale-tree warning): 53 of 57 exit 0, each exit code captured before any pipe. Includes check:test-source-alias, check:type-source-resolution, check:type-check-debt, check:cross-package-test-inputs, check:undeclared-dep-imports, check:workspace-manifest-cycles, check:turbo-task-graph, check:nul-bytes, check:published-files, check:engine-double-contract. Re-run at c108823e9: check:driver-memory-census, check:nul-bytes and check:test-source-alias all exit 0.
  • NOT MEASURED, not reported as passed — 4 gates, each with its own unmet prerequisite, all CI-owned: check:dual-build-cjs-loads (exit 3, 11 packages have no dist/), check:i18n and check:i18n-coverage (exit 3, the extract configs' build closure), check:i18n-walk-parity (exit 1, packages/cli/dist/utils/i18n-extract.js absent — it reads the walker from built output).
  • pnpm --filter @objectstack/plugin-dev typecheck → exit 0 at c108823e9; check:test-typecheck 0 file(s) / 0 error(s). --listFiles confirms both new plugin-dev files really are in that program (2 of 2), so "typecheck clean" is a statement about them.
  • pnpm --filter @objectstack/cli check:test-typecheck3 file(s) / 28 error(s) / 6 pinned signature(s), byte-identical to the pre-existing test-typecheck-debt.json: the fourth paths rule adds zero diagnostics.
  • pnpm lint (eslint . --no-inline-config) repo-wide → exit 0. No narrowing claimed.
  • Declared narrowing: the @objectstack/cli suite was not run whole, only the pin. Measured rather than argued: the new alias is anchored (/^@objectstack\/plugin-dev$/) so it re-resolves exactly one specifier; grep -rln '@objectstack/plugin-dev' packages/cli/src packages/cli/test returns exactly one file, the probe; tsconfig.test.json is noEmit and vitest does not read its paths (no vite-tsconfig-paths in packages/cli/vitest.config.ts). No existing test's resolution moves. CI runs the suite regardless.

Contract review REJECT → remediated (Q3 + Q4)

The isolated adversarial review passed Q1 (nothing lands on the published surface but the decision itself) and Q2 (patch is right; the changeset stands) and rejected Q3 and Q4. All seven items are in this head. Each was reproduced first, then shown green.

① The reachability claim was false, and it is corrected everywhere it appeared — the two bullets above, this PR's open question, the dev-i18n.ts docblock, and the comment over the short-circuit case (which now says what it actually covers: a stack whose flattened level has something to say). The truth, stated once: every stack carrying packages[] that declares no i18n at all — no i18n config, no manifest.translations, no non-empty top-level translations — reaches the gate on every DevPlugin.init.

② The two missing cases exist now. (a) A real composeStacks(…, { manifest: 'preserve' }) output with no translations anywhere: packages is read twice through a counting proxy — once by this reader's absent-key guard, once inside resolveArtifactPackageOrder — the call answers undefined, and it does not throw. Two reads is the discriminator: a short-circuit reads it zero times, which is exactly what the third ablation above produces. (b) The same stack with a package body the D4 gate refuses, driven through DevPlugin.init: the boot completes, the diagnosis line is printed, and no I18nServicePlugin is constructed.

③ The reproduced regression is closed, posture B. A package manifest still carrying authoring-time glob objects (ManifestSchema's written form; the repo's own CONFIG_GLOBS fixture) is refused by ArtifactPackageSchema by design. Such a project boots today, and on the first cut it would have stopped booting — thrown out of the block whose only job is deciding whether to register a translation service, while new AppPlugin(...) twenty lines above degrades the very same refusal on the very same object to one log line. That inversion had no defence, so DevPlugin now catches, logs its own line naming the metadata defect and carrying the envelope verbatim, and boots on the in-memory fallback. ⛔ Not through reportOptionalLoadFailure — its text says a PACKAGE is installed but failed to initialize, and naming a package for a metadata-shape defect is the mis-attribution #7926 removed from this file. ⛔ And never silently — silence is the failure class this card exists to remove. dev-plugin.ts's AppPlugin try/catch is untouched: whether DevPlugin should refuse malformed metadata at all is a separate maintainer question, filed rather than decided here.

@throws is complete on both functions, including the bare Error for a dependency cycle, now asserted by a case that pins code === undefined and status === undefined alongside the message.

⑤ The three limbs are asked cheapest-first. i18n / manifest.i18n / manifest.translations are property reads that cannot throw; the translations limb is the only one that can reach the gate, so it is asked last. || is commutative, so the answer is unchanged — what changes is that a stack which already declares its locales can no longer be refused over a packages list its answer never needed.

⑦ The guard divergence is recorded, not unilaterally "fixed". This reader treats only an absent packages key as single-package, so packages: {} is refused; the sibling reader in @objectstack/metadata guards with Array.isArray and accepts a non-array. #15226 spells it as this file does, so it is a program-level split. The dev-i18n.ts docblock now names it so the next author does not align one side without ruling the other.

Review item 6 — packages/cli/package.json's published bytes DO change, and no cli changeset is owed

Stated rather than left implicit, because it is true and easy to miss: packages/cli/package.json does change — one devDependency line — and that file is published. Consumers are unaffected: npm does not install a published package's devDependencies, packages/cli's files field ships dist and bin, and nothing in either imports @objectstack/plugin-dev. The precedent is the sibling card's PR #15281, read rather than recalled: it edits packages/cli/tsconfig.test.json, this same probe fixture and the pin, and declares only @objectstack/verify in its changeset. Check Changeset is green on this head.

CI follow-up on this head: check:driver-memory-census

Lint & Repo Gates went red on 561ae05da — a real finding, and mine. The new test copied this package's sibling-harness convention of mocking every optional package absent, and one of those lines was vi.mock('@objectstack/driver-memory', …). That gate counts vi.mock as a declaration, and @objectstack/driver-memory's consumer set is locked by maintainer ruling (#5499 froze investment; #5704 / #6664 ruled each remaining consumer, at two). This file was a third.

Not ledgered. Of the gate's three exits — rule it, migrate it, file it — the answer is migrate, and here migrating means deleting: the mock was redundant, not load-bearing. dev-plugin.ts has exactly one import('@objectstack/driver-memory') (:475) and it sits inside if (enabled('driver')) (:472), while both boots in this test pass services: { driver: false }. Measured rather than argued:

  • reproduced locally at 561ae05da: node scripts/check-driver-memory-census.mjs exit 1 (captured before any pipe), naming line 57;
  • with the line deleted: exit 0OK — every declaration is ledgered, every ledger entry is live … 2 ruled consumer(s), the ledger file itself untouched, and the census back to 12 module binding(s) in 12 file(s);
  • the suite was unchanged at 7 passed (7) / 68 passed (68) (the count at c60f2b44d, before the remediation added four cases), and by name both BOOT cases green at 5ms / 1ms — a real import('@objectstack/driver-memory') does not transform in 5ms, so the timing corroborates the structural reading.

A comment now stands where the mock was, so the next author copying the sibling harness does not re-add it. The other ten vi.mock(absent) lines were checked against every census/ledger gate in scripts/: none of those packages appears in one — driver-memory is the only frozen-consumer package in the set. Worth recording for the next card: check:driver-memory-census is not in the family scripts/pm/dispatch-gates.mjs derives for this change set, which is why the local 57-command run was green and CI saw it first.

Docs drift triage — four hand-written pages flagged, four verdicts

Flagged off the field names on the new DevI18nPluginOptions interface. content/docs/releases/v17.mdx was flagged too and was read only, never touched.

Page · anchor Verdict
content/docs/kernel/services-checklist.mdx:428 (DevPlugin, defaultLocale) Read in full; no edit — and this is the one that mattered. The row says DevPlugin "Auto-wires I18nServicePlugin when the stack declares translations". That sentence is exactly the predicate this PR widens, and it stays true — what changes is that it is now true for a multi-package stack as well. Before this fix the page over-claimed for that shape: the app declared translations and got the fallback anyway. The two samples below it are untouched paths — the production plugin (new I18nServicePlugin({ defaultLocale: 'en', localesDir: './i18n' })) and a stack-less new DevPlugin(), whose i18n block is gated on options.stack and never runs.
content/docs/protocol/kernel/i18n-standard.mdx:355, :980, :986 (defaultLocale, fallbackLocale) False positive. Protocol-level config keys and the key-miss fallback chain. This PR reads those same two keys from the same place (stack.i18n, else manifest.i18n) with the same expression it always used; neither their meaning nor their resolution moves.
content/docs/ui/translations.mdx:57, :126, :214, :222 (defaultLocale) False positive. The authoring-side i18n block and the request-time locale-resolution chain. i18n is an artifact ENVELOPE key that option B does not move, and this PR leaves that limb alone.
content/docs/deployment/environment-variables.mdx:289 (fallbackLocale) False positive. The anchor sits inside OS_SEARCH_PINYIN_ENABLED's description of how its default derives from the stack's configured locales. Untouched by this PR.

The bot's own caveat, taken seriously rather than waved past: packages/plugins/plugin-dev/src/index.ts and packages/cli/vitest.config.ts produced no anchors, so any page documenting them went unscanned this round — and the two new exported symbols live in that index. Checked by hand instead: content/docs/plugins/packages.mdx:360 describes the package at feature level and links to its README, content/docs/kernel/services.mdx:114 describes its no-stubs composition posture, and the README documents usage plus DevPluginOptions (unchanged). None of the three enumerates an export list, so there is no page owing an entry for devI18nPluginOptions / DevI18nPluginOptions.

Why packages/cli gains a test-only edge to @objectstack/plugin-dev

The pin's ledger lives in packages/cli, and #15004's rule is that a row calls a shipped reader rather than re-implementing one — so the probe has to reach plugin-dev's decision. That needs three declarations, all test-layer: a devDependency (nothing published imports it; check:workspace-manifest-cycles green, so the manifest graph stays acyclic), an anchored vitest alias to source, and a star-less paths rule. The alias is the sanctioned remedy — KNOWN_UNALIASED_TEST_IMPORTS in scripts/check-test-source-alias.mjs is shrink-only and was left untouched in both directions; the gate reports the same required set for @objectstack/cli before and after, so crossing into plugin-dev/src adds no unaliased artifact import it did not already carry.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m

…D first (#15232)

The by-shape sweep found this site, not the #15004 pin, so `OPTION_B_LOSSES`
carries no row for it. This commit adds the row and nothing else: the reader
is deliberately still top-level-only, so the pin goes RED naming a subsystem
that loses a collection the ledger does not carry. The fix, and the deletion
of the row, land in the next commit — a row that never existed is a row
nobody can check the fix against.

The row calls the SHIPPED decision rather than re-reading `stack.translations`:
`devI18nPluginOptions` is extracted from `DevPlugin.init`'s 3b block, verbatim
in behaviour, and exported so the probe measures what the plugin itself
decides. `packages/cli` reaches it through an anchored vitest alias to source
(the sanctioned remedy — `KNOWN_UNALIASED_TEST_IMPORTS` is shrink-only) and a
star-less `paths` rule, so the run and the type verdict agree about which
artifact is under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…o check against (#15232)

The row added in the previous commit went RED naming a subsystem the ledger
does not carry — the exact failure #15004 exists to make loud:

  B2 · plugin-dev I18nServicePlugin auto-detect over the caller-supplied
  stack · translations

This records it, which turns the pin green at 25 rows and makes the loss a
measured fact rather than an argument. The next commit fixes the reader and
deletes this line again, so `OPTION_B_LOSSES` ends this branch exactly as it
started it — the ledger never grows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…ackages[]` (#15232)

`DevPlugin.init`'s 3b block read `options.stack.translations` alone, so a
multi-package app under ADR-0130 D4's option-B shape — every definition
carried once inside `packages[]`, no flattened top level — was read as
declaring no copy at all. `I18nServicePlugin` was never registered and `os dev`
served message keys, or last release's strings, from the core in-memory
fallback. Nothing threw and nothing logged.

The detection now reads the flattened top level FIRST and consults each
package body only where that came back falsy, in the order
`resolveArtifactPackageOrder` (`@objectstack/core`, ADR-0130 D4+D5) registers
them. Today's additive artifact answers bit-identically: the original
expression `Array.isArray(t) && t.length > 0` is preserved rather than
re-expressed, and it short-circuits before `packages[]` is touched. A stack
with no `packages` key never reaches the traversal at all, so its
`translations` is still read exactly once. A malformed `packages` raises the
same ADR-0112 envelope the registration path raises for it.

The ledger row added two commits ago goes green and is deleted, so
`OPTION_B_LOSSES` ends this branch at the 24 rows it started with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/plugin-dev, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/cli/vitest.config.ts, packages/plugins/plugin-dev/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx (via fallbackLocale (symbol, a field of interface DevI18nPluginOptions))
  • content/docs/kernel/services-checklist.mdx (via DevPlugin (symbol, a top-level class), defaultLocale (symbol, a field of interface DevI18nPluginOptions))
  • content/docs/protocol/kernel/i18n-standard.mdx (via defaultLocale (symbol, a field of interface DevI18nPluginOptions), fallbackLocale (symbol, a field of interface DevI18nPluginOptions))
  • content/docs/ui/translations.mdx (via defaultLocale (symbol, a field of interface DevI18nPluginOptions))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via DevPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cli/vitest.config.ts, packages/plugins/plugin-dev/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3a4373f4c3ff0b4d2e04a029f632392a0dc762b1packageMentionDocs.

Which tree this was computed on

This run read content/docs from c2cad02094715baa87f5063111e451336eafb6d3 — the merge of head c108823e9473b8a229263f42aa20945e64466ce5 into base 3a4373f4c3ff0b4d2e04a029f632392a0dc762b1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c2cad02094715baa87f5063111e451336eafb6d3 && git checkout c2cad02094715baa87f5063111e451336eafb6d3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a4373f4c3ff0b4d2e04a029f632392a0dc762b1 c108823e9473b8a229263f42aa20945e64466ce5 && git checkout -B drift-repro 3a4373f4c3ff0b4d2e04a029f632392a0dc762b1 && git merge --no-ff c108823e9473b8a229263f42aa20945e64466ce5

node scripts/docs-audit/affected-docs.mjs --json 3a4373f4c3ff0b4d2e04a029f632392a0dc762b1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3a4373f4c3ff0b4d2e04a029f632392a0dc762b1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 4, 2026

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

收到交付。先说一句该说的:四步的顺序你做对了,而且做成了这个程序里最该被照抄的一次 —— 行先入账并让 pin 变红(且同一次运行里另外五个 case 是绿的,所以那是判别力而不是坏夹具)、再修、再删行,最后 git diff origin/main -- packages/cli/test/option-b-reader-acceptance.pin.test.ts 为空。账本从 24 到 24,被观测的行却多了一行。这正是卡面要的形状。

但本 PR 进不了落地队列,原因与代码质量无关:

packages/plugins/plugin-dev/src/index.ts 新增了两个对外符号(devI18nPluginOptions 与类型 DevI18nPluginOptions),而 @objectstack/plugin-dev 只发布 "." 一个入口。契约复审细则的机械底线是:

任何新导出符号已发布载荷上的新键yes,直接锁契约复审档。

这条不依赖接受集的方向,也不依赖改动是否加法式 —— 所以它跟你「今天所有产物逐字节相同」的论证并不冲突,两者都成立。needs:contract-review 已挂在 PR #15282 与卡 #15232 双载体上,由我在复审结论出来后摘。

已起隔离的对抗性契约复审,并且把你在报告里挂旗的那个 posture 问题(畸形 packages[] 是否该从 DevPlugin.init 抛出 —— 你的 A/B/C)作为第四问一并交给它。按细则,复审拿到的只有卡面、#14512 的裁决、本 PR 正文与仓库源码:既不喂我的意见,也不喂你的推荐,否则第二意见就不是第二意见了。所以在它出结论之前,我不在这条评论里对 A/B/C 或对 changeset 级别表态。

在此期间:PR 保持 draft,不要武装 auto-merge。

另外你报告里记的那条 —— 派发单说「assign 自己」而席位契约说 assignee 字段归 PM —— 是第三个席位提这一条了。冲突是真的,归我收口,不是你的问题;你按契约文件处理并把分歧写进报告,是对的做法。我会另开一张卡改派发面,不占本卡。


Generated by Claude Code

…dger locks (#15232)

`check:driver-memory-census` counts `vi.mock` as a DECLARATION, and that
package's consumer set is locked by maintainer ruling (#5499 froze investment,
#5704 / #6664 ruled each remaining consumer at two). The mock copied in from
this package's sibling harnesses made this file a third consumer.

⛔ Not ledgered — MIGRATED, which here means deleted: the line was redundant.
`dev-plugin.ts` has exactly one `import('@objectstack/driver-memory')` and it
sits inside `if (enabled('driver'))`, while both boots in this file pass
`services: { driver: false }`, so the specifier was never reached. Measured
rather than argued: with the line gone the suite is unchanged at
`7 passed (7) / 68 passed (68)`, both BOOT cases green (5ms / 1ms), and the
census gate returns to `OK — 2 ruled consumers` with its ledger untouched.

A comment stands where the mock was, so the next author copying the sibling
harness does not re-add it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

契约复审结论:REJECT(Q1 PASS · Q2 PASS · Q3 REJECT · Q4 REJECT)

隔离的对抗性复审已出具。它在 561ae05da 的独立工作树里做了真实 pnpm install 与真实 vitest 运行,所有断言都是实测而非阅读推断;工作树与分支已清理,仓库未被改动。

先说过了的两问,因为它们改变了我原本可能的偏见:

  • Q1(已发布面)PASS。 stackDeclaresTranslations 确实没被发布 —— 不是推断,是构建后读导出表:tsup.config.ts:5 只有 entry: ['src/index.ts'],package.json 只声明 ".",dist__export 表里只有 DevPlugindevI18nPluginOptions,前者只作为内部 function 存在。你 index.ts 上那句「published surface 是决定,不是它的肢」是真的。
  • Q2(语义化版本)PASS,patch 是对的。 复审扫了最近 600 个动过任意 packages/*/src/index.ts 的提交,只保留纯加法的导出变更(有增无删)且该包自己有 bump 的,得 {minor: 158, patch: 64, major: 4} n=226;其中标题以 fix( 开头的子集是 {patch: 49, minor: 36, major: 2} n=87 —— 对「fix( + 给已发布 index 加一个导出符号」这个结构孪生,patch 是众数。 本程序内四张兄弟卡也一致取 patch。changeset 不用动。

⚠️ 我按细则没有在复审跑之前对这两问表态,现在看是对的:我原本的直觉是 Q2 该走 minor(#15261 那场复审引 655b106 得出的结论)。隔离在这里买到了东西 —— 见下面「一条要归档的分歧」。


Q3 · 3a —— PR 的核心事实主张是假的,而且复审测出来了

PR 正文写:

for every artifact and every composed config the platform produces today the packages[] pass is not even reached

拿真实 composeStacks([...], { manifest: 'preserve' }) 的产出(不带 translations),用 Proxy 数 packages 的访问次数:

composed keys: manifest,objects,packages
has top-level translations? undefined
GATE REACHED for a no-translations composed config? true -> answer false

短路只在顶层 translations 非空时才发生。于是每一个带 packages[] 而没有声明 i18n 的 stack —— 也就是多包应用里不做国际化的那个大多数 —— 现在每次 DevPlugin.init 都要把每个包体走一遍完整的 ADR-0130 D4 解析。

本该钉住这条的那个用例(dev-i18n-packages-reader.test.ts 的「the flattened level answers FIRST」)夹具自己声明了顶层 translations,即它只覆盖了主张恰好成立的那一半。没有任何用例覆盖「加法式多包 stack + 完全没有 translations」。

Q3 · 3b —— 一条已复现的回归:今天能启动的项目,之后起不来

一个 defineStack 合法、composeStacks(preserve) 产出的配置,其中一个包的创作期 manifest 声明了 glob objects(objects: ['./src/objects/*.object.ts'],ManifestSchema 的成文形式,仓库自己在 packages/cli/test/build-multi-package-artifact.e2e.test.ts:123-133CONFIG_GLOBS 做过夹具)—— 这样的包体是 ArtifactPackageSchema 按设计拒绝的(packages/spec/src/assembled-package-body.test.ts:168-176:「the ASSEMBLED entry refuses authoring GLOBS where definitions belong」)。实测:

A2 entry1 parses? false [{"path":["manifest","objects",0],"message":"Invalid input: expected object, received string"}]
A2 reader -> undefined { code: 'INVALID_ARTIFACT_PACKAGE_ENTRY',
  msg: 'Release artifact `packages[1]` is not a package entry (ADR-0130 D4): manifest.objects.0: …' }

这个项目在本 PR 之前 new DevPlugin({ stack: config }) 是能启动的;之后 DevPlugin.init 抛出,dev server 根本起不来 —— 而抛出点是那个唯一职责为「要不要注册一个翻译服务」的代码块。os build 早就用一条专门的编译诊断拒绝同一个项目(那条 e2e 断言 exit 1、路径 packages.0.manifest.objects.0),所以这个拒绝本来就有一个更好的家;新加的这个位置不对、措辞也不对。

Q3 · 3c —— @throws 不完整,而且「绝不裸抛」这句是假的

resolveArtifactPackageOrder 自己的 JSDoc 点名三类抛出,第三类是 resolvePluginOrder依赖成环错误。实测两个包互相依赖:

CYCLE ERROR: Error {"message":"[Kernel] Circular dependency detected: a"}   // code: undefined, status: undefined

裸 Error,没有 code,没有 status —— 正是 PR 正文断言不会发生的那件事(「Asserted on code and status, never a bare throw」)。更重的是:已发布devI18nPluginOptions(dev-i18n.ts:138)一个 @throws 都没有,正文里也完全没提会抛 —— 它成文的契约是「返回 options 或 undefined」。这是一个已发布 API,其唯一被记录的失败模式是沉默。

Q3 · 3d / 3e —— 两条较轻的

  • 3d 求值过早:dev-i18n.ts:143 无条件先算 stackDeclaresTranslations(stack),再看 i18n / manifest.i18n / manifest.translations 三肢。于是一个已经明确声明了 locales 的 stack,会因为一份答案根本用不到的 packages 列表而被拒。
  • 3e 守卫与本程序指定的单一读取器不一致:本 PR 守 packages === undefined || packages === null(packages: {} 会被拒);@objectstack/core 那边守 if (!Array.isArray(artifact.packages)) return artifact;(非数组拒)。同一个程序里两个读取器对同一输入给相反答案。fix(plugin-security): the app default permission set resolves from packages[] (#15007) #15226 与本 PR 一致,所以这是程序级分裂,不是本卡独有的错。

Q4 —— 拒绝姿态:REJECT,理由是记录不实 + 落地行为自相矛盾

复审没有裁定 A 在原则上错。它裁定的是:支撑 A 的记录是错的,而落地行为在同一个文件里自相矛盾且无人辩护。

=== DevPlugin.init outcome ===
threw? true { code: 'INVALID_ARTIFACT_PACKAGE_ENTRY', status: 422, … }
log lines mentioning app metadata: [ 'error   ✘ … failed to initialize — skipping app metadata …' ]

dev-plugin.ts:505new AppPlugin(this.options.stack) 包在 try 里,catch 走 reportOptionalLoadFailure(:512),而那里的源码注释就是这么写的:new AppPlugin(stack) parses the stack definition, so a malformed stack throws HERE」 → 降级成一行日志。

所以这次改动之后,这个插件为了「我要不要注册一个翻译服务」而拒绝得,比为了「我到底要不要注册这个应用的元数据」还要硬。 这个倒置在任何地方都没有被辩护过。


我的处置:走 B,并且不碰 AppPlugin

复审把选择留给维护者。我判断这里不需要维护者裁决,因为要做的不是改政策,而是恢复与已落地行为的一致:

  • 真正会变的是「DevPlugin 到底该不该对畸形元数据硬拒」—— 那个才是维护者的题,我另立卡,不在本卡决定;
  • 本卡该做的只是别让 i18n 探测器比元数据门更严。它今天严,是本 PR 造成的,而且带着一条已复现的回归(3b)。

⛔ 但不是复审否掉的那个 C:reportOptionalLoadFailure 的措辞是「PACKAGE is installed but failed to initialize」,对一个元数据形状缺陷点名一个,正是 #7926 从这个文件里清掉的误归因。要的是 catch + 一条自己的、点名元数据缺陷的响亮日志 + 跳过注册,而不是静默跳过(静默会把本卡刚修的失败类换个形状造回来),也不是借用那条错措辞。

转 PASS 的清单(按序,已发回席位)

  1. [正确性] 把可达性主张改成真话,所有出现处:PR 正文「The fix」第 3 条与「The question this site raises」第 3 条、open question 的选项 A、dev-i18n.ts:89-91 及由它推出的结论、短路测试上方的注释。真话是:每一个带 packages[] 且没有非空顶层 translations 的 stack 都会到达这个门 —— 也就是不做 i18n 的多包应用这个常态。
  2. [正确性] 补上缺的用例:(a) 加法式 composeStacks(preserve)完全没有 translations → 到达 resolveArtifactPackageOrder、答 undefined、不抛;(b) 同一个 stack 配一个被拒的 packages[] → 断言 DevPlugin.init 在选定姿态下实际怎么做。
  3. [正确性] 按上面的 B 收掉 3b 的回归与 3-vs-3b 的分裂。
  4. [正确性] 补全抛出文档:给 devI18nPluginOptions(现在一条都没有)与 stackDeclaresTranslations@throws,含成环那条裸 Error;并从 PR 正文里撤掉「never a bare throw」,或者补一个断言成环行为的用例。
  5. [正确性,小] 三肢按便宜的先排序,让能被 i18n / manifest.i18n / manifest.translations 回答的 stack 不再因一份用不到的 packages 被拒(消掉 3d)。
  6. [机械] 在正文里说明 packages/cli/package.json 的已发布字节确实变了(仅 devDependency,对消费者无影响),以及为什么不欠 cli 的 changeset,引 fix(verify): a multi-package app no longer gets an os verify that measures nothing (#15229) #15281 的先例。
  7. [机械,可选] 与 core 的守卫对齐(Array.isArray vs undefined || null),或明确记录本程序故意带两种守卫。

Q1、Q2 无需任何阻塞性改动。

复审没能推翻、因而整改不得回归的部分

packages: []false 不抛;stack 是数组/函数/null/原始值 → 不抛;Proxy 包裹的包体读得对;单包 stack 对顶层 translations 恰好读一次(守卫真的挡住了 D4 第二分支的重复读);translations 的合并处置是 'concat'(stack.zod.ts:895),所以今天加法式产物上布尔答案逐位相同 —— 「answers bit-identically」成立,塌掉的是「costs nothing」;i18n 属信封键这条正确,ARTIFACT_ENVELOPE_KEYS 的推导与钉子都核过了;locale 表达式重构行为等价;pin 确实未被改动(git diff origin/main 对该文件为空,前后 24 行,集合相等完好)。

标签:needs:contract-reviewPR #15282 与卡 #15232 双载体上继续挂着,由我在整改被复核通过后摘。PR 保持 draft。


Generated by Claude Code

…bs first, document what throws (#15232)

Contract review (#15282) measured three things wrong with the first cut, and
this is all three:

1. REACHABILITY. "Today's artifacts never reach the packages[] pass" was FALSE.
   The flattened read short-circuits only when `translations` is non-empty, so
   every multi-package stack that does not translate reaches the gate on every
   boot. The claim is corrected wherever it appeared and pinned by a case that
   counts the reads (2 when the gate is reached, 0 when it short-circuits).

2. A REPRODUCED REGRESSION. A package manifest still carrying authoring glob
   `objects` is refused by `ArtifactPackageSchema` by design; such a project
   boots today and would have stopped booting on this reader — thrown from the
   block whose only job is deciding whether to register a translation service,
   while `new AppPlugin(...)` twenty lines above degrades the very same refusal
   to a log line. That inversion is indefensible, so `DevPlugin` now catches,
   prints its own line naming the METADATA defect and carrying the envelope
   verbatim, and boots on the in-memory fallback. ⛔ Not `reportOptionalLoadFailure`
   (it names a PACKAGE — the #7926 mis-attribution) and ⛔ never silent.
   `dev-plugin.ts`'s AppPlugin try/catch is untouched: whether DevPlugin should
   refuse malformed metadata at all is a separate maintainer question.

3. EVALUATION ORDER. The three limbs are now asked cheapest-first, so a stack
   that already declares its locales in `i18n` is no longer refused over a
   `packages` list its answer never needed. `||` is commutative — the answer is
   unchanged, only what can throw is.

Both functions gain `@throws`, including the BARE `Error` (no code, no status)
`resolvePluginOrder` raises for a dependency cycle, now asserted by a case.
The guard divergence with the sibling reader (`Array.isArray` vs absent-key) is
recorded as a program-level split rather than unilaterally aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

整改复核:PASS —— 契约复审档关闭,needs:contract-review 双载体已摘

⚠️ 同会话自审声明:本复核由派发本卡的同一个 PM 席位做出,与 dev 席同会话。这不是独立第二意见 —— 独立那一票是先前那场上下文隔离的对抗性复审(评论 5538108541),它给出 REJECT;本条只判定它列的整改是否真的做到了。凡我说「已核」的,都是我自己在 PR head c108823e9 上跑的读数,不是转述报告。

逐条 diff 层面核过的四件事

① pin 未被动过 —— 而我差一点报了个假警,记下来。

我第一次问的是 git diff origin/main pmverify-15282 -- <pin>,得到 66 行改动、分支 24 行 vs main 23 行,看起来像账本被动了。那个问法是错的:它把「分支改了文件」和「main 自己前进了」混在一起。用 merge-base 重问:

merge-base: 460134af8
分支 vs 自身基线:  (空)                    ← 分支一个字没动 pin
main vs 同一基线:  46 insertions, 20 deletions  ← 66 行全是 main 的
git merge-tree origin/main <head>: exit 0, 0 conflicts

那 66 行是 #15226 删掉一行(24→23)与 #15228 加入四条 cli 读用例块造成的。集合相等断言、子系统覆盖、反空洞控制,分支侧全部原样。

② 姿态 B 落在正确的位置,且 dev-plugin.ts:505 未被碰。 catch 住 devI18nPluginOptions 的拒绝、打自己的日志、继续用核心内存 i18n 兜底。注释把两条测量出来的理由都写下了:可达性(带 packages[] 而不声明 i18n 的 stack 每次启动都会走完整个包列表)与倒置(二十行上面 new AppPlugin(...) 解析同一个对象、其拒绝被降级)。

③ 那条日志躲开了 #7926 的误归因,而且说清了后果与去处。 逐字:

✘ the i18n auto-detect could not read this stack's 'packages[]' … This is NOT a missing-package problem and installing anything will not help: the stack's PACKAGE LIST is malformed (ADR-0130 D4), and 'os build' refuses the same project with its own compile diagnostic. Translations this app declares will not be served until it is fixed. The artifact reader reported (verbatim — the framework does not interpret it): …

它主动否定了「装个包就好了」这个最可能的误读,点名 os build 才是有专门诊断的那道门,并且把用户可见后果写出来了。信封原样带出、并声明框架不解释它。

@throws 现在含成环那条裸 Error,两个函数都有,并明说它 带其他拒绝都带的信封 —— 按 err.code / err.status 接的调用方匹配不到,这正是复审 3c 指出的缺口。

⑤ 可达性措辞已改真。 dev-i18n.ts 里唯一残留的「every artifact the platform emits today」现在限定在 「the ANSWER … is bit-identical」 —— 那是复审确认成立的那一半;塌掉的「costs nothing」不再出现。配套那句 refusals documented under @throws are reachable in ORDINARY use rather than only for exotic input — which is why DevPlugin degrades on them instead of dying 是承重的,而且是对的。

driver-memory 普查:迁移,不是记账 —— 这是正确出口

CI 先看到的那条红(check:driver-memory-census,job 100960342355)被按门禁自己给的三条出口里的 migrate it 收掉了:那个 vi.mock('@objectstack/driver-memory') 是从本包兄弟测试照抄来的冗余 —— dev-plugin.ts 对它的唯一 import 在 if (enabled('driver')) 里面,而两个 BOOT 用例都传 services: { driver: false }。删掉即可,普查回到 2 ruled consumers,台账文件一个字没动,套件不变。

⛔ 这正是门禁原话禁止的反面:「Do NOT add an entry to make this green」。席位没有走那条路。

复审要求的关键实测,答案是它

我点名要的第 2(a) 条:门是被到达的,而且调用不抛。 在真实 composeStacks(preserve)、任何层级都没有 translations 的产物上,用 Proxy 数 packages 的读取次数 = 2(一次是本读取器的缺键守卫,一次在 resolveArtifactPackageOrder 内),答 undefined,not.toThrow() 成立。用「2 次」而不是「至少 1 次」做判据是对的:顶层-only 的消融下同一用例以 0 次失败,那正是那句假主张断言的短路。

三次消融各自是单用例判别(去掉 catch → 只有「gate REFUSES still boots」红;translations 肢挪回最前 → 只有「已声明 locales」红;读取器退回顶层-only → 7 红),每次突变都在磁盘上确认(双向计数 + blob hash 移位),每次 restore 都证明过(git status 空、git diff HEAD 空、hash 归位、marker 归零)。

席位自曝的一处门禁派生缺口,已另立卡

check:driver-memory-census is NOT in the family dispatch-gates derives for this change set — that is why the local run was green and CI saw it first; recorded rather than glossed.

主动记而不是抹掉,是对的处置。已单独立卡,不算在本卡账上。

还欠两处,都只在 PR 正文,不阻塞

  1. 正文里的「OPTION_B_LOSSES 24 → 24」在当前 main 上会读成陈述错误 —— 那是相对旧基线的真话。改成「相对基线未改动;其间 main 已由 fix(plugin-security): the app default permission set resolves from packages[] (#15007) #15226 从 24 降到 23」。
  2. 复审第 6 条(packages/cli/package.json 已发布字节确实变了、仅 devDependency、为何不欠 cli 的 changeset,引 fix(verify): a multi-package app no longer gets an os verify that measures nothing (#15229) #15281 先例)我在正文里没找到,补一句。

落地前检


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 4, 2026 10:32
@hotlong
hotlong added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 88a35c2 Sep 4, 2026
45 checks passed
@hotlong
hotlong deleted the claude/issue-15232-plugin-dev-i18n-package-order branch September 4, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reader program 6/4 — @objectstack/plugin-dev: the i18n auto-detect never fires for a multi-package app

2 participants