Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { runLayer1 } from "./validation/layer1.js";
import { runLayer2, type Layer2Mode } from "./validation/layer2.js";
import type { RepairAttempt, RunReport } from "./report/model.js";
import type { JudgeResult, Platform, PlatformDetail, RenamePair, WorkerResult } from "./agents/types.js";
import { applyRenameOverrides, type OverrideOutcome } from "./rename-overrides.js";
import { applyRenameOverrides, syncEntityNames, type OverrideOutcome } from "./rename-overrides.js";
import { isValidSlug, slugToPascal } from "./slug.js";

export type DispatchReportOptions = {
Expand Down Expand Up @@ -77,7 +77,12 @@ export async function dispatch(spec: string, options: DispatchOptions = {}): Pro
else if (o.kind === "noop") trace("dispatch", `rename override: ${o.from}=${o.to} already the planned target — no change`);
else trace("dispatch", `rename override ignored: no planned rename for "${o.from}" (got ${o.from}=${o.to})`);
}
domain = { ...domain, renamePlan: merged.plan };
// Carry the override into entity names too, so the report's entity cards
// agree with its rename plan (the planner names entities after their
// targets). Code generation keys off renamePlan, not entities — this is
// metadata coherence, not a code-affecting change.
const entities = syncEntityNames(domain.entities, merged.outcomes);
domain = { ...domain, renamePlan: merged.plan, entities };
}

// Mirror the substrate's NATIVEAPPTEMPLATE_API_* config to the
Expand Down
23 changes: 22 additions & 1 deletion src/rename-overrides.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RenamePair } from "./agents/types.js";
import type { Entity, RenamePair } from "./agents/types.js";

// Manual rename overrides (ROADMAP §"Optional explicit naming overrides").
// The planner is the sole source of the rename plan; these let a human veto a
Expand Down Expand Up @@ -54,3 +54,24 @@ export function applyRenameOverrides(
}
return { plan: merged, outcomes };
}

// Keep entity names in step with rename-plan overrides. The planner names each
// entity after its rename target (e.g. it emits both `Shopkeeper -> Monitor`
// and an entity `Monitor` that `replaces` Shopkeeper), so a `--rename` that
// changes the target must also rename the entity or the report contradicts its
// own rename plan. Only entities whose name still matches the planner's old
// target are touched — an entity deliberately named differently is left alone.
export function syncEntityNames(
entities: readonly Entity[],
outcomes: readonly OverrideOutcome[],
): readonly Entity[] {
const changes = new Map<string, { was: string; to: string }>();
for (const o of outcomes) {
if (o.kind === "changed") changes.set(o.from, { was: o.was, to: o.to });
}
if (changes.size === 0) return entities;
return entities.map((e) => {
const change = changes.get(e.replaces);
return change && e.name === change.was ? { ...e, name: change.to } : e;
});
}
34 changes: 34 additions & 0 deletions tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,40 @@ test("applyRenameOverrides reports unmatched + noop overrides distinctly", async
]);
});

test("syncEntityNames renames an entity whose name still matches the overridden target", async () => {
const { syncEntityNames } = await import("../src/rename-overrides.js");
const entities = [
{ name: "Household", replaces: "Shop", fields: [{ name: "address", type: "string" as const }] },
{ name: "Monitor", replaces: "Shopkeeper", fields: [], states: ["Active", "Resolved"] as const },
];
const synced = syncEntityNames(entities, [
{ kind: "changed", from: "Shopkeeper", was: "Monitor", to: "Resident" },
]);
assert.equal(synced[0]?.name, "Household"); // untouched
assert.equal(synced[1]?.name, "Resident"); // renamed Monitor -> Resident
// `replaces` and other fields are preserved.
assert.equal(synced[1]?.replaces, "Shopkeeper");
assert.deepEqual(synced[1]?.states, ["Active", "Resolved"]);
});

test("syncEntityNames leaves entities alone when name diverges, no changed outcomes, or no match", async () => {
const { syncEntityNames } = await import("../src/rename-overrides.js");
const entities = [{ name: "Watcher", replaces: "Shopkeeper", fields: [] }];
// Name diverges from the old target → conservative: leave it.
assert.equal(
syncEntityNames(entities, [{ kind: "changed", from: "Shopkeeper", was: "Monitor", to: "Resident" }])[0]?.name,
"Watcher",
);
// No changed outcomes (noop/unmatched only) → returns input untouched.
const noop = syncEntityNames(entities, [{ kind: "noop", from: "Shop", to: "Clinic" }]);
assert.equal(noop, entities);
// Override targets a token no entity replaces → unchanged.
assert.equal(
syncEntityNames(entities, [{ kind: "changed", from: "Shop", was: "Store", to: "Household" }])[0]?.name,
"Watcher",
);
});

test("dispatch applies a rename override end-to-end and surfaces the outcome (stub pipeline)", async () => {
const result = await dispatch("a walk-in clinic queue for small veterinary practices", {
renameOverrides: [{ from: "Shopkeeper", to: "Provider" }],
Expand Down
Loading