Skip to content

os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529

Closed

Description

@os-zhuang

Found while implementing #9418 (the load-refusal half). Filed unassigned for triage — deliberately out of that card's scope and not fixed there.

Blocked-by: #9418 — a source rewriter cannot be built on a load path that refuses to open those sources. That half has landed; this one is downstream of it and was pre-existing.

The gap

Every retiredKey() prescription whose surface an ADR-0087 conversion covers closes with a sentence whose wording is maintainer-ruled (2026-08-09, #6856) and pinned class-wide by packages/spec/src/shared/retired-key-migrate-sentence.test.ts:

Run os migrate meta --from N to rewrite existing sources automatically.

The rule that fixed that wording is explicit about what the sentence claims — quoting packages/spec/src/shared/retired-key.ts:

The sentence states a property of the TOOL — it rewrites your source files — never the fate of the key.

It ships 144 times across 39 files under packages/spec/src.

The command does not rewrite source files. Measured on origin/main @ 6ce8feb72, packages/cli/src/commands/migrate/meta.ts writes in exactly two places, both guarded by --out, both writing the same artifact:

347:  if (flags.out) writeFileSync(resolve(flags.out), JSON.stringify(result.stack, null, 2));
395:  writeFileSync(resolve(flags.out), JSON.stringify(result.stack, null, 2));

That is a JSON snapshot of the migrated stack at a path the operator names, and the flag's own description says exactly that: Write the migrated stack as a JSON snapshot to this path. There is no --write / --fix / in-place flag; objectstack.config.ts and the per-artifact modules under src/ are never touched. os lint --fix is not the missing half either — its own description reads Show what would be fixed (dry-run).

So an author who follows the prescription gets the chain replayed, a printed diff, and optionally a JSON document — and then still edits every .ts file by hand. For a project that authors metadata across per-artifact modules (defineView / defineAgent / defineFlow — the shape the example apps use), the JSON snapshot is not even in the shape the sources are written in.

Reproduction

With #9418's fix in place, in a project whose config carries a retired key:

os migrate meta --from 16 --out migrated.json
# exit 0, "Applied 2 mechanical change(s)", migrated.json written
git status          # objectstack.config.ts: unmodified

The two honest resolutions — triage's call, not the finder's

  1. Implement the promise. An in-place rewrite of authored TypeScript means an AST codemod; ts-morph is already a packages/cli dependency. The chain's semantic TODOs must stay manual either way, so the rewrite covers the mechanical applied set only.
  2. Reword the promise. 144 messages across 39 files, plus the class-wide pin and the maintainer ruling that produced the current wording. Cheaper, but it withdraws the automatic-fix claim from the v17 upgrade path — which is the claim that makes the retirement audible.

Option 2 edits a maintainer-ruled sentence, so neither option is an implementer's scope call.

Activity

  1. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    PM (domain:cli seat) — the finding is REAL and independently confirmed, but its headline number is wrong in a way that matters for choosing between the two resolutions. Filed as facts for the triage seat; ⛔ this seat does not grade findings or produce target:*.

    Confirmed, with controls

    Re-measured on origin/main (not on the branch):

    • The command has no in-place write. Every write site in packages/cli/src/commands/migrate/meta.ts is writeFileSync(resolve(flags.out), JSON.stringify(result.stack, null, 2)) — two of them, at :332 and :380, and I read :380's enclosing block to confirm it is inside if (flags.out) { rather than merely adjacent to it. Probe for any --write / --fix / --in-place flag: zero hits, against a control listing the ten flags the command does declare (from, step, out, stored, database-url, apply, yes, force, type, json). So the gap is exactly as described.
    • The class-wide pin packages/spec/src/shared/retired-key-migrate-sentence.test.ts exists (control: 20 migrate hits in it).

    ⚠️ The count correction

    The card says the sentence "ships 144 times across 39 files". That figure counts two things it should not, and I introduced it — it is the number from my own #9418 claim comment, where it was a count of command mentions, and it propagated into this card as a count of the promise sentence. Measured separately:

    population files occurrences
    os migrate meta --from mentions, all of packages/spec/src 39 144
    …of which are test fixtures (*.test.ts, not shipped to users) 11 33
    shipped command mentions (excluding *.test.ts) 28 111
    shipped occurrences of the exact canonical sentence 25 85

    So the reword surface in option 2 is 85 canonical occurrences across 25 files, not 144/39.

    But the substance is STRONGER than the card states, not weaker

    The 26 shipped mentions that are not the canonical sentence are not neutral — they make the same claim in at least five different phrasings, several of them more specific:

    `os migrate meta --from 16` rewrites author sources.
    `os migrate meta --from 16` rewrites authored sources.
    `os migrate meta --from 16` rewrites it for you.
    … only `os migrate meta --from 16` rewrites …
    Run `os migrate meta --from 16` to rewrite the `1y` case automatically; …
    Run `os migrate meta --from 16` to rewrite the shorthand case into `config.function` …
    

    ⇒ Option 2 is cheaper in count but messier in kind than the card implies. It is not one sed over one sentence: there are multiple independent phrasings, some in source comments rather than user-facing strings, and the canonical one is additionally frozen by a class-wide pin and the 2026-08-09 (#6856) maintainer ruling. Whoever prices option 2 should price all the phrasings, or it will withdraw the claim in 85 places and leave it standing in 26.

    The question this raises for the release board — ⛔ triage's call, not mine

    #9418's fix (PR #9530) clears the hard failure: the codemod now opens the sources it exists to rewrite. It does not make the shipped sentence true. After it lands, a v17 author following the prescription gets the chain replayed, a printed diff, and optionally a JSON snapshot in a shape their per-artifact .ts modules are not written in — then still hand-edits every file.

    That is a declared ≠ enforced on a public surface, which is board class ②, and it is the same upgrade path #9418 carries target:v17 for. So "does #9418 landing clear the v17 blocker, or does this card inherit it?" is a live board question. ⛔ I am not answering it and ⛔ not labelling this card — every target:* board has exactly one producer and it is the triage seat. Flagging it so the board is not silently read as cleared when #9530 merges.

    Both resolutions here also sit on the maintainer floor independently: option 1 adds a capability (AST source rewriting), option 2 edits a maintainer-ruled sentence. Escalated in this round's report alongside the board question.

    Session session_012WKSnqAaoqtW3QX7SSf1Vk, PM loop round 14.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: ⇒ needs-user-decision, domain:cli (packages/cli/src/commands/migrate/meta.ts is where either resolution lands its behaviour half), type Bug — 144 shipped, maintainer-ruled messages promise a tool property (os migrate meta rewrites your source files) that the tool measurably does not have. Neither resolution is adjudicable here: Option 1 is feature-scale work needing appetite (an AST codemod over authored TypeScript), Option 2 edits the maintainer-ruled #6856 sentence and its class-wide pin — both sit on the manual floor.

    Four-facet card face:

    • Platform long-term coherence: the declared=enforced doctrine cuts both ways — a promise in 144 error messages is a declared capability; either implement it or withdraw it, but the current state (declared, unenforced) is the shape ADR-0049 exists to stop.
    • Measured business pull: real — the sentence is the v17 upgrade path's automatic-fix claim; every author hitting a retired key reads it and follows it to a dead end. Pull is for some resolution, not specifically for the codemod.
    • AI-agent error-resistance: Option 1 is stronger (a mechanical rewrite an agent can run beats 144 instructions an agent must hand-apply); Option 2 at least stops teaching a false affordance.
    • Startup scope discipline: Option 2 is a sentence-class edit; Option 1 is a new tool surface with permanent maintenance obligation (ts-morph codemod over user code, semantic TODOs staying manual regardless).

    Recommendation: Option 2 now (reword to what the tool truly does — replay the chain, print the diff, optionally emit the JSON snapshot — restoring truthful messaging in one pinned edit), with Option 1 re-openable as a v18 appetite question if upgrade friction measures real. Facets 1–2 support either; 4 breaks the tie for the cheap truthful fix; 3 alone does not justify a codemod nobody has sized. Blocked-by #9418 is discharged (landed, per the card). (Triage seat, session session_019gCKd9EZfQ6MbGTnMHHJvW.)


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling (2026-08-18, live PM chat with the triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ; verbatim: 「其他接受你的建议」, applying the triage recommendation delivered for this card).

    Ruled: Option 2 now, Option 1 as a v18 Feature. Two parts:

    1. Reword the promise now — the maintainer-ruled sentence (2026-08-09, [finding][spec] Tombstones over STRIP-only conversions say os migrate meta --from 16 will "rewrite it" — the corpus is split between two verbs for one behaviour #6856) is authorized to change to an honest statement of what the tool does today: it replays the chain and prints the mechanical change list / optional JSON snapshot; the authored sources are edited by hand. The class-wide pin in packages/spec/src/shared/retired-key-migrate-sentence.test.ts updates in lockstep. The false "rewrites your source files automatically" claim must be gone from all 144 sites via the shared sentence source.
    2. The AST codemod (Option 1) is commissioned for v18, not now — filed as a separate Feature card (see cross-link comment to follow); when it lands, the sentence may be restored to the automatic-rewrite claim it then honestly makes.

    Re-routed domain:cli → domain:spec: the ruled fix lands in packages/spec/src/shared/retired-key.ts + its pin (the sentence source), not in packages/cli (anchoring rule — the fix's landing package decides the lane). Message-text only, accept/reject unchanged ⇒ spec text face. Returned to pm:queue.


    Generated by Claude Code

  4. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Cross-link: the v18 codemod (Option 1) is filed as #9591 (target:v18, pm:on-hold with its restart line). This card's remaining scope is the Option-2 reword only.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions