@kontextmind/kxm 0.7.39 → 0.7.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +41 -7
- package/docs/README.md +5 -3
- package/docs/agent-skills.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/configuration.md +28 -5
- package/docs/{vnext → contracts}/README.md +7 -7
- package/docs/{vnext → contracts}/architecture.md +2 -2
- package/docs/{vnext → contracts}/migration.md +25 -25
- package/docs/{vnext → contracts}/routing.md +2 -2
- package/docs/{vnext → contracts}/synchronization.md +1 -1
- package/docs/{vnext → contracts}/validation.md +7 -7
- package/docs/getting-started.md +1 -1
- package/docs/kb/qa-authentik-authentication.md +47 -0
- package/docs/kb/qa-extension-install-and-hub-bootstrap.md +68 -0
- package/docs/kb/qa-hub-on-a-public-host.md +31 -0
- package/docs/kb/qa-sqlite-vs-duckdb.md +18 -0
- package/docs/kb/qa-what-the-hub-stores.md +47 -0
- package/docs/packages.md +91 -0
- package/docs/templates/architecture.md +1 -1
- package/docs/test-matrix.md +6 -6
- package/docs/tui-components.md +131 -0
- package/examples/README.md +2 -2
- package/examples/{vnext → project}/.kxm/agents/critic-3.yaml +1 -1
- package/examples/{vnext → project}/README.md +4 -4
- package/package.json +18 -8
- package/packages/core/tui/CHANGELOG.md +19 -0
- package/packages/core/tui/LICENSE +21 -0
- package/packages/core/tui/README.md +45 -0
- package/packages/core/tui/dist/index.js +1439 -0
- package/packages/core/tui/src/adapters/pi.ts +73 -0
- package/packages/core/tui/src/adapters/terminal.ts +106 -0
- package/packages/core/tui/src/exports/index.ts +30 -0
- package/packages/core/tui/src/services/registry.ts +274 -0
- package/packages/core/tui/src/tui/keys.ts +130 -0
- package/packages/core/tui/src/tui/layout.ts +117 -0
- package/packages/core/tui/src/tui/panel.ts +435 -0
- package/packages/core/tui/src/tui/panelComponent.ts +208 -0
- package/packages/core/tui/src/tui/render.ts +309 -0
- package/packages/core/tui/src/tui/theme.ts +145 -0
- package/packages/core/tui/src/types/surface.ts +407 -0
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +681 -553
- package/plugins/kxm/dist/extension.js +18 -18
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/{vnext-runtime-supervisor.js → runtime-supervisor.js} +553 -553
- package/plugins/kxm/dist/runtime.js +608 -608
- package/plugins/kxm/dist/server.js +13 -13
- package/plugins/kxm/package.json +2 -2
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +2 -2
- package/plugins/kxm/skills/kxm-runs/SKILL.md +3 -3
- package/plugins/kxm/skills/kxm-session/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +1 -1
- package/plugins/kxm/src/autocomplete.ts +4 -4
- package/plugins/kxm/src/{vnext-bindings.ts → bindings.ts} +48 -48
- package/plugins/kxm/src/cli/hub.ts +3 -3
- package/plugins/kxm/src/cli/{vnext.ts → project.ts} +106 -106
- package/plugins/kxm/src/cli/roles.ts +9 -9
- package/plugins/kxm/src/cli/system.ts +2 -2
- package/plugins/kxm/src/cli/tasks.ts +8 -8
- package/plugins/kxm/src/cli/workflows.ts +8 -8
- package/plugins/kxm/src/cli.ts +47 -47
- package/plugins/kxm/src/config.ts +69 -14
- package/plugins/kxm/src/database.ts +4 -4
- package/plugins/kxm/src/{vnext-engine-artifacts.ts → engine-artifacts.ts} +7 -7
- package/plugins/kxm/src/{vnext-engine-command.ts → engine-command.ts} +40 -40
- package/plugins/kxm/src/{vnext-engine-compile.ts → engine-compile.ts} +64 -64
- package/plugins/kxm/src/{vnext-engine-evidence.ts → engine-evidence.ts} +26 -26
- package/plugins/kxm/src/{vnext-engine-fold.ts → engine-fold.ts} +99 -99
- package/plugins/kxm/src/{vnext-engine-gate-records.ts → engine-gate-records.ts} +105 -105
- package/plugins/kxm/src/{vnext-engine-plan.ts → engine-plan.ts} +69 -69
- package/plugins/kxm/src/{vnext-engine.ts → engine.ts} +502 -502
- package/plugins/kxm/src/gate-hash.ts +10 -0
- package/plugins/kxm/src/{vnext-harness.ts → harness.ts} +1 -1
- package/plugins/kxm/src/hub.ts +1 -1
- package/plugins/kxm/src/init-guide-setup.ts +3 -3
- package/plugins/kxm/src/{vnext-init.ts → init.ts} +93 -93
- package/plugins/kxm/src/kxm-install-kind.ts +1 -1
- package/plugins/kxm/src/kxm-update-config.ts +2 -2
- package/plugins/kxm/src/local-snapshot.ts +23 -23
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/{vnext-migrate.ts → migrate.ts} +123 -123
- package/plugins/kxm/src/model-inventory.ts +1 -1
- package/plugins/kxm/src/{vnext-oneshot-evidence.ts → oneshot-evidence.ts} +2 -2
- package/plugins/kxm/src/{vnext-oneshot-process.ts → oneshot-process.ts} +4 -4
- package/plugins/kxm/src/{vnext-oneshot-producer.ts → oneshot-producer.ts} +24 -24
- package/plugins/kxm/src/{vnext-permission.ts → permission.ts} +86 -86
- package/plugins/kxm/src/{vnext-pi-producer.ts → pi-producer.ts} +13 -13
- package/plugins/kxm/src/{vnext-config.ts → project-config.ts} +177 -177
- package/plugins/kxm/src/{vnext-repair.ts → repair.ts} +159 -159
- package/plugins/kxm/src/restricted-yaml.d.mts +3 -3
- package/plugins/kxm/src/restricted-yaml.mjs +4 -4
- package/plugins/kxm/src/{vnext-runtime-owner.ts → runtime-owner.ts} +35 -35
- package/plugins/kxm/src/{vnext-runtime.ts → runtime-service.ts} +136 -136
- package/plugins/kxm/src/{vnext-runtime-store.ts → runtime-store.ts} +145 -145
- package/plugins/kxm/src/{vnext-runtime-supervisor.ts → runtime-supervisor.ts} +80 -80
- package/plugins/kxm/src/runtime.ts +6 -6
- package/plugins/kxm/src/session-work.ts +3 -3
- package/plugins/kxm/src/studio-layout.ts +6 -6
- package/plugins/kxm/src/{vnext-template.ts → template.ts} +33 -33
- package/plugins/kxm/src/tui.ts +10 -24
- package/schemas/{vnext/README.md → README.md} +3 -3
- package/schemas/{vnext/agent.schema.json → agent.schema.json} +1 -1
- package/schemas/{vnext/assignment-result.schema.json → assignment-result.schema.json} +1 -1
- package/schemas/{vnext/backup-manifest.schema.json → backup-manifest.schema.json} +1 -1
- package/schemas/{vnext/candidate.schema.json → candidate.schema.json} +1 -1
- package/schemas/{vnext/common.schema.json → common.schema.json} +2 -2
- package/schemas/{vnext/context-candidate.schema.json → context-candidate.schema.json} +1 -1
- package/schemas/{vnext/context-packet.schema.json → context-packet.schema.json} +1 -1
- package/schemas/{vnext/delivery-manifest.schema.json → delivery-manifest.schema.json} +1 -1
- package/schemas/{vnext/drive-receipt.schema.json → drive-receipt.schema.json} +2 -2
- package/schemas/{vnext/environment.schema.json → environment.schema.json} +1 -1
- package/schemas/{vnext/gate-registry.schema.json → gate-registry.schema.json} +1 -1
- package/schemas/{vnext/handoff-manifest.schema.json → handoff-manifest.schema.json} +1 -1
- package/schemas/{vnext/init-operation.schema.json → init-operation.schema.json} +1 -1
- package/schemas/{vnext/local-repository-bindings.schema.json → local-repository-bindings.schema.json} +1 -1
- package/schemas/{vnext/memory-record.schema.json → memory-record.schema.json} +1 -1
- package/schemas/{vnext/migration-decision.schema.json → migration-decision.schema.json} +1 -1
- package/schemas/{vnext/migration-plan.schema.json → migration-plan.schema.json} +1 -1
- package/schemas/{vnext/migration-receipt.schema.json → migration-receipt.schema.json} +1 -1
- package/schemas/{vnext/model.schema.json → model.schema.json} +1 -1
- package/schemas/{vnext/modes.schema.json → modes.schema.json} +1 -1
- package/schemas/{vnext/permission-diff.schema.json → permission-diff.schema.json} +1 -1
- package/schemas/policy-draft/README.md +2 -2
- package/schemas/{vnext/prices.schema.json → prices.schema.json} +1 -1
- package/schemas/{vnext/project.schema.json → project.schema.json} +1 -1
- package/schemas/{vnext/repository.schema.json → repository.schema.json} +1 -1
- package/schemas/{vnext/role.schema.json → role.schema.json} +1 -1
- package/schemas/{vnext/run-event.schema.json → run-event.schema.json} +1 -1
- package/schemas/{vnext/session-brief.schema.json → session-brief.schema.json} +2 -2
- package/schemas/{vnext/sync-event.schema.json → sync-event.schema.json} +1 -1
- package/schemas/{vnext/template-provenance.schema.json → template-provenance.schema.json} +1 -1
- package/schemas/{vnext/workflow.schema.json → workflow.schema.json} +1 -1
- package/scripts/build-package.mjs +59 -0
- package/scripts/build-runtime.mjs +3 -2
- package/scripts/check-generated.mjs +2 -1
- package/scripts/check-versions.mjs +12 -1
- package/scripts/docker-install-smoke.mjs +268 -0
- package/scripts/emit-codex-artifacts.mjs +1 -1
- package/scripts/kxm-bump-version.mjs +53 -3
- package/scripts/kxm-runtime-supervisor.mjs +3 -3
- package/scripts/package-surfaces.mjs +39 -0
- package/scripts/roster-policy.mjs +1 -1
- package/plugins/kxm/src/vnext-gate-hash.ts +0 -10
- /package/docs/{vnext → contracts}/effects-and-recovery.md +0 -0
- /package/docs/{vnext → contracts}/lifecycles.md +0 -0
- /package/docs/{vnext → contracts}/terminology.md +0 -0
- /package/examples/{vnext → project}/.kxm/agents/coordinator.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/critic-1.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/critic-2.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/implementer.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/planner.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/reproducer.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/reviewer.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/gates.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/critic-claude.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/critic-gemini.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/critic-grok.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/implementation.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/primary.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/prices.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/project/env.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/project.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/repo/repo.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/workflows/default.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/workflows/fix.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/workflows/improve.yaml +0 -0
- /package/examples/{vnext → project}/records/assignment-result-recorded.json +0 -0
- /package/examples/{vnext → project}/records/assignment-result.json +0 -0
- /package/examples/{vnext → project}/records/context-candidate.json +0 -0
- /package/examples/{vnext → project}/records/delivery-manifest.json +0 -0
- /package/examples/{vnext → project}/records/effect-uncertainty-resolved-sync.json +0 -0
- /package/examples/{vnext → project}/records/effect-uncertainty-resolved.json +0 -0
- /package/examples/{vnext → project}/records/run-created.json +0 -0
- /package/examples/{vnext → project}/records/sync-event.json +0 -0
- /package/examples/{vnext → project}/repositories/api/.kxm/repo/env.yaml +0 -0
- /package/examples/{vnext → project}/repositories/api/.kxm/repo/repo.yaml +0 -0
- /package/examples/{vnext → project}/repositories/web/.kxm/repo/repo.yaml +0 -0
|
@@ -3,25 +3,25 @@ import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, open
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { stringify } from "yaml";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
KxmConfigError,
|
|
7
|
+
KxmSchemaRegistry,
|
|
8
8
|
discoverGitRoot,
|
|
9
9
|
legacyConfigFilesAt,
|
|
10
|
-
|
|
10
|
+
loadKxmProject,
|
|
11
11
|
migrationReceiptSelfHash,
|
|
12
12
|
parseRestrictedYaml,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
readKxmMigrationReceipt,
|
|
14
|
+
validateKxmResources,
|
|
15
|
+
verifyKxmMigrationReceiptTarget,
|
|
16
|
+
kxmCanonicalJson,
|
|
17
|
+
KXM_MIGRATION_RECEIPT_PATH,
|
|
18
18
|
type JsonObject,
|
|
19
19
|
type JsonValue,
|
|
20
|
-
type
|
|
21
|
-
type
|
|
22
|
-
type
|
|
23
|
-
} from "./
|
|
24
|
-
import {
|
|
20
|
+
type KxmConfigIssue,
|
|
21
|
+
type KxmConfigOptions,
|
|
22
|
+
type KxmResourceKind,
|
|
23
|
+
} from "./project-config.ts";
|
|
24
|
+
import { withKxmLocalBindingLock, type KxmLocalBindingLock, type KxmLocalBindingStoreOptions } from "./bindings.ts";
|
|
25
25
|
|
|
26
26
|
/* ------------------------------------------------------------------ *
|
|
27
27
|
* Bounded legacy JSON reading (duplicate keys rejected, depth bounded)
|
|
@@ -32,12 +32,12 @@ const MAX_LEGACY_JSON_DEPTH = 32;
|
|
|
32
32
|
const MAX_LEGACY_JSON_NODES = 32_768;
|
|
33
33
|
const MAX_LEGACY_SOURCES = 64;
|
|
34
34
|
|
|
35
|
-
function migrateIssue(phase:
|
|
35
|
+
function migrateIssue(phase: KxmConfigIssue["phase"], code: string, file: string, message: string): KxmConfigIssue {
|
|
36
36
|
return { phase, code, file, message };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function migrateFail(code: string, file: string, message: string): never {
|
|
40
|
-
throw new
|
|
40
|
+
throw new KxmConfigError([migrateIssue("parse", code, file, message)]);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** JSON.parse with duplicate-key rejection and structural bounds. */
|
|
@@ -168,7 +168,7 @@ function detectDuplicateKeys(text: string, label: string): void {
|
|
|
168
168
|
* Legacy source model
|
|
169
169
|
* ------------------------------------------------------------------ */
|
|
170
170
|
|
|
171
|
-
export interface
|
|
171
|
+
export interface KxmLegacySource {
|
|
172
172
|
path: string;
|
|
173
173
|
sha256: string;
|
|
174
174
|
bytes: number;
|
|
@@ -180,23 +180,23 @@ function sha256Of(input: string | Uint8Array): string {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** Read every legacy configuration file with bounds and exact duplicate-key rejection. */
|
|
183
|
-
export function
|
|
183
|
+
export function readKxmLegacySources(root: string): KxmLegacySource[] {
|
|
184
184
|
const files = legacyConfigFilesAt(root);
|
|
185
185
|
if (files.length > MAX_LEGACY_SOURCES) {
|
|
186
|
-
throw new
|
|
186
|
+
throw new KxmConfigError([migrateIssue("discovery", "legacy_source_limit", ".kxm/config", `legacy configuration exceeds ${MAX_LEGACY_SOURCES} files`)]);
|
|
187
187
|
}
|
|
188
188
|
return files.map((path) => {
|
|
189
189
|
const absolute = join(root, ...path.split("/"));
|
|
190
190
|
const stat = lstatSync(absolute);
|
|
191
191
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
192
|
-
throw new
|
|
192
|
+
throw new KxmConfigError([migrateIssue("path", "legacy_source_invalid", path, "legacy configuration must be a regular file, not a link")]);
|
|
193
193
|
}
|
|
194
194
|
const buffer = readFileSync(absolute);
|
|
195
195
|
let text: string;
|
|
196
196
|
try {
|
|
197
197
|
text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
198
198
|
} catch {
|
|
199
|
-
throw new
|
|
199
|
+
throw new KxmConfigError([migrateIssue("parse", "invalid_utf8", path, "document is not valid UTF-8")]);
|
|
200
200
|
}
|
|
201
201
|
return {
|
|
202
202
|
path,
|
|
@@ -207,8 +207,8 @@ export function readVnextLegacySources(root: string): VnextLegacySource[] {
|
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
export function legacySourceDigest(sources: readonly
|
|
211
|
-
return sha256Of(
|
|
210
|
+
export function legacySourceDigest(sources: readonly KxmLegacySource[]): string {
|
|
211
|
+
return sha256Of(kxmCanonicalJson(sources.map((source) => ({ path: source.path, sha256: source.sha256, bytes: source.bytes }))));
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/* ------------------------------------------------------------------ *
|
|
@@ -227,7 +227,7 @@ const KNOWN_STAGE_FIELDS = new Set(["id", "label", "instructions", "requiredEvid
|
|
|
227
227
|
const KNOWN_POLICY_FIELDS = new Set(["kind", "minProducers", "eligibleAgents", "acceptedStatuses", "degradation"]);
|
|
228
228
|
const KNOWN_TRANSITION_FIELDS = new Set(["target", "maxTransitions"]);
|
|
229
229
|
|
|
230
|
-
export interface
|
|
230
|
+
export interface KxmMigrationAmbiguity {
|
|
231
231
|
key: string;
|
|
232
232
|
category: "unsupported" | "identity" | "step-kind" | "terminal-status" | "transition-budget" | "evidence-policy" | "permission" | "secret-reference";
|
|
233
233
|
sourcePath: string;
|
|
@@ -235,20 +235,20 @@ export interface VnextMigrationAmbiguity {
|
|
|
235
235
|
allowedValues: readonly (string | number)[];
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
export interface
|
|
238
|
+
export interface KxmMigrationUnmapped {
|
|
239
239
|
sourcePath: string;
|
|
240
240
|
jsonPointer: string;
|
|
241
241
|
valueSha256: string;
|
|
242
242
|
sensitive: boolean;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
export interface
|
|
245
|
+
export interface KxmMigrationRename {
|
|
246
246
|
from: string;
|
|
247
247
|
to: string;
|
|
248
248
|
kind: "agent" | "model" | "workflow" | "step" | "evidence" | "outcome";
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
export interface
|
|
251
|
+
export interface KxmMigrationPermissionChange {
|
|
252
252
|
sourcePath: string;
|
|
253
253
|
targetPath: string;
|
|
254
254
|
direction: "narrowing" | "expansion" | "unknown";
|
|
@@ -257,13 +257,13 @@ export interface VnextMigrationPermissionChange {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
interface MutablePlan {
|
|
260
|
-
ambiguities:
|
|
260
|
+
ambiguities: KxmMigrationAmbiguity[];
|
|
261
261
|
invalidDecisions: string[];
|
|
262
262
|
decisionKeys: Set<string>;
|
|
263
263
|
decisionAllowedValues: Map<string, readonly (string | number)[]>;
|
|
264
|
-
unmapped:
|
|
265
|
-
renames:
|
|
266
|
-
permissionChanges:
|
|
264
|
+
unmapped: KxmMigrationUnmapped[];
|
|
265
|
+
renames: KxmMigrationRename[];
|
|
266
|
+
permissionChanges: KxmMigrationPermissionChange[];
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
function compareCodeUnits(left: string, right: string): number {
|
|
@@ -317,12 +317,12 @@ function recordUnmapped(plan: MutablePlan, sourcePath: string, pointer: string,
|
|
|
317
317
|
// not be recoverable by offline guessing against a committed plan. Record
|
|
318
318
|
// only the serialized byte length as proof something was dropped.
|
|
319
319
|
const digestInput = sensitive
|
|
320
|
-
? { redacted: true, bytes: Buffer.byteLength(
|
|
320
|
+
? { redacted: true, bytes: Buffer.byteLength(kxmCanonicalJson(value === undefined ? null : value), "utf8") }
|
|
321
321
|
: (value === undefined ? null : value);
|
|
322
322
|
plan.unmapped.push({
|
|
323
323
|
sourcePath,
|
|
324
324
|
jsonPointer: pointer,
|
|
325
|
-
valueSha256: sha256Of(
|
|
325
|
+
valueSha256: sha256Of(kxmCanonicalJson(digestInput)),
|
|
326
326
|
sensitive,
|
|
327
327
|
});
|
|
328
328
|
}
|
|
@@ -348,7 +348,7 @@ function mapIdentity(
|
|
|
348
348
|
plan: MutablePlan,
|
|
349
349
|
sourcePath: string,
|
|
350
350
|
raw: string,
|
|
351
|
-
kind:
|
|
351
|
+
kind: KxmMigrationRename["kind"],
|
|
352
352
|
taken: Map<string, string>,
|
|
353
353
|
decisions: Readonly<Record<string, JsonValue>>,
|
|
354
354
|
dropPointer?: string,
|
|
@@ -387,7 +387,7 @@ function mapIdentity(
|
|
|
387
387
|
`identity:${kind}:${keyFragment(raw)}`,
|
|
388
388
|
"identity",
|
|
389
389
|
sourcePath,
|
|
390
|
-
`${kind} name ${JSON.stringify(elide(raw, 200))} cannot be normalized to a
|
|
390
|
+
`${kind} name ${JSON.stringify(elide(raw, 200))} cannot be normalized to a KXM identifier; supply a decision with the target identifier`,
|
|
391
391
|
["<identifier>"],
|
|
392
392
|
decisions,
|
|
393
393
|
);
|
|
@@ -474,7 +474,7 @@ function decisionValueAllowed(key: string, value: JsonValue | undefined, allowed
|
|
|
474
474
|
function resolveDecision(
|
|
475
475
|
plan: MutablePlan,
|
|
476
476
|
key: string,
|
|
477
|
-
category:
|
|
477
|
+
category: KxmMigrationAmbiguity["category"],
|
|
478
478
|
sourcePath: string,
|
|
479
479
|
message: string,
|
|
480
480
|
allowedValues: readonly (string | number)[],
|
|
@@ -510,7 +510,7 @@ interface ConvertedAgents {
|
|
|
510
510
|
|
|
511
511
|
function convertAgentsRoster(
|
|
512
512
|
plan: MutablePlan,
|
|
513
|
-
source:
|
|
513
|
+
source: KxmLegacySource,
|
|
514
514
|
decisions: Readonly<Record<string, JsonValue>>,
|
|
515
515
|
agentNames: Map<string, string>,
|
|
516
516
|
): ConvertedAgents {
|
|
@@ -618,7 +618,7 @@ function convertAgentsRoster(
|
|
|
618
618
|
}
|
|
619
619
|
const thinking = asString(record.thinking);
|
|
620
620
|
if (thinking) {
|
|
621
|
-
//
|
|
621
|
+
// KXM agents select models; thinking belongs to a model profile. Emit
|
|
622
622
|
// a per-agent profile when the model mapped cleanly.
|
|
623
623
|
const selector = asObject(agent.model as JsonValue | undefined);
|
|
624
624
|
if (selector && typeof selector.profile !== "string") {
|
|
@@ -662,7 +662,7 @@ const IMPLEMENTED_GATES = new Set(["test", "scm-delivery"]);
|
|
|
662
662
|
|
|
663
663
|
function convertGatesRoster(
|
|
664
664
|
plan: MutablePlan,
|
|
665
|
-
source:
|
|
665
|
+
source: KxmLegacySource,
|
|
666
666
|
decisions: Readonly<Record<string, JsonValue>>,
|
|
667
667
|
): void {
|
|
668
668
|
const root = asObject(source.value);
|
|
@@ -696,7 +696,7 @@ function convertGatesRoster(
|
|
|
696
696
|
`gate:${keyFragment(name)}`,
|
|
697
697
|
"unsupported",
|
|
698
698
|
source.path,
|
|
699
|
-
`gate ${JSON.stringify(elide(name, 200))} has no registered deterministic runner in
|
|
699
|
+
`gate ${JSON.stringify(elide(name, 200))} has no registered deterministic runner in KXM; it is preserved in the migration report only`,
|
|
700
700
|
["report-only"],
|
|
701
701
|
decisions,
|
|
702
702
|
);
|
|
@@ -721,7 +721,7 @@ function mapStageRef(context: StageContext, raw: string): string {
|
|
|
721
721
|
|
|
722
722
|
function convertTransition(
|
|
723
723
|
plan: MutablePlan,
|
|
724
|
-
source:
|
|
724
|
+
source: KxmLegacySource,
|
|
725
725
|
context: StageContext,
|
|
726
726
|
stageId: string,
|
|
727
727
|
stageKey: string,
|
|
@@ -746,13 +746,13 @@ function convertTransition(
|
|
|
746
746
|
const sourceIndex = context.stageIds.indexOf(stageId);
|
|
747
747
|
if (target === "$terminal") {
|
|
748
748
|
// Legacy $terminal always completes the run successfully regardless of
|
|
749
|
-
// outcome;
|
|
749
|
+
// outcome; KXM requires an explicit terminal status.
|
|
750
750
|
const chosen = resolveDecision(
|
|
751
751
|
plan,
|
|
752
752
|
`terminal:${context.workflowKey}:${stageKey}:${outcome}`,
|
|
753
753
|
"terminal-status",
|
|
754
754
|
source.path,
|
|
755
|
-
`legacy transition ${context.workflowId}/${stageId} outcome ${outcome} ends the run; choose the
|
|
755
|
+
`legacy transition ${context.workflowId}/${stageId} outcome ${outcome} ends the run; choose the KXM terminal status (legacy semantics completed the run even on failure outcomes)`,
|
|
756
756
|
outcome === "passed" ? ["completed"] : ["completed", "failed", "cancelled"],
|
|
757
757
|
decisions,
|
|
758
758
|
);
|
|
@@ -803,7 +803,7 @@ function convertTransition(
|
|
|
803
803
|
`backedge:${context.workflowKey}:${stageKey}:${outcome}`,
|
|
804
804
|
"transition-budget",
|
|
805
805
|
source.path,
|
|
806
|
-
`back-edge ${context.workflowId}/${stageId} -> ${targetId} (outcome ${outcome}) has no per-edge budget in legacy;
|
|
806
|
+
`back-edge ${context.workflowId}/${stageId} -> ${targetId} (outcome ${outcome}) has no per-edge budget in legacy; KXM requires maxTransitions (1-100)`,
|
|
807
807
|
[2, 3, 4, 5],
|
|
808
808
|
decisions,
|
|
809
809
|
);
|
|
@@ -816,7 +816,7 @@ function convertTransition(
|
|
|
816
816
|
|
|
817
817
|
function convertWorkflowFile(
|
|
818
818
|
plan: MutablePlan,
|
|
819
|
-
source:
|
|
819
|
+
source: KxmLegacySource,
|
|
820
820
|
decisions: Readonly<Record<string, JsonValue>>,
|
|
821
821
|
agentIds: ReadonlyMap<string, JsonObject>,
|
|
822
822
|
agentRawNames: ReadonlyMap<string, string>,
|
|
@@ -847,7 +847,7 @@ function convertWorkflowFile(
|
|
|
847
847
|
: `preview-${(normalizeIdentifier(rawId) || "unnamed").slice(0, 46).replace(/[-_]+$/u, "")}-${createHash("sha256").update(`${rawId}#${source.path}#${definitionIndex}`, "utf8").digest("hex").slice(0, 8)}`;
|
|
848
848
|
const workflowKey = keyFragment(rawId);
|
|
849
849
|
|
|
850
|
-
// Hub/webhook-specific fields have no
|
|
850
|
+
// Hub/webhook-specific fields have no KXM workflow equivalent; the
|
|
851
851
|
// trigger surface is a later phase. Record them (secrets hashed).
|
|
852
852
|
for (const field of ["source", "project", "event", "filter", "delivery", "ttlMs", "promptTemplate"] as const) {
|
|
853
853
|
if (definition[field] !== undefined) recordUnmapped(plan, source.path, `${basePointer}/${field}`, definition[field], false);
|
|
@@ -860,7 +860,7 @@ function convertWorkflowFile(
|
|
|
860
860
|
`secret:${workflowKey}:${field}`,
|
|
861
861
|
"secret-reference",
|
|
862
862
|
source.path,
|
|
863
|
-
`workflow ${workflowId} field ${field} configures a webhook secret;
|
|
863
|
+
`workflow ${workflowId} field ${field} configures a webhook secret; KXM workflows never embed secrets — confirm the field is dropped and re-granted through scoped secret references when the trigger surface lands`,
|
|
864
864
|
["drop"],
|
|
865
865
|
decisions,
|
|
866
866
|
);
|
|
@@ -986,7 +986,7 @@ function convertWorkflowFile(
|
|
|
986
986
|
`instructions:${workflowKey}:${stepKey}`,
|
|
987
987
|
"unsupported",
|
|
988
988
|
source.path,
|
|
989
|
-
`stage ${stepId} has no instructions;
|
|
989
|
+
`stage ${stepId} has no instructions; KXM agent steps require executable instructions — confirm the stage is dropped`,
|
|
990
990
|
["drop"],
|
|
991
991
|
decisions,
|
|
992
992
|
);
|
|
@@ -1161,8 +1161,8 @@ function convertWorkflowFile(
|
|
|
1161
1161
|
"evidence-policy",
|
|
1162
1162
|
source.path,
|
|
1163
1163
|
acceptedStatusesMalformed
|
|
1164
|
-
? `evidence policy ${policyKey} acceptedStatuses is malformed (not an array) and was preserved in the report; confirm mapping the policy to
|
|
1165
|
-
: `evidence policy ${policyKey} accepted statuses ${JSON.stringify(elide(accepted.map((candidate) => String(candidate)).join(","), 200))} cannot map losslessly; confirm mapping to
|
|
1164
|
+
? `evidence policy ${policyKey} acceptedStatuses is malformed (not an array) and was preserved in the report; confirm mapping the policy to KXM 'passed'`
|
|
1165
|
+
: `evidence policy ${policyKey} accepted statuses ${JSON.stringify(elide(accepted.map((candidate) => String(candidate)).join(","), 200))} cannot map losslessly; confirm mapping to KXM 'passed'`,
|
|
1166
1166
|
["passed"],
|
|
1167
1167
|
decisions,
|
|
1168
1168
|
);
|
|
@@ -1183,7 +1183,7 @@ function convertWorkflowFile(
|
|
|
1183
1183
|
sourcePath: source.path,
|
|
1184
1184
|
targetPath: `.kxm/workflows/${workflowId}.yaml`,
|
|
1185
1185
|
direction: "expansion",
|
|
1186
|
-
summary: `step ${stepId}: assignment pool widens from the single legacy coordinator to coordinator plus evidence producers so
|
|
1186
|
+
summary: `step ${stepId}: assignment pool widens from the single legacy coordinator to coordinator plus evidence producers so KXM producer evidence stays in scope`,
|
|
1187
1187
|
decisionKey: `evidence:${workflowKey}:${stepKey}:${policyKey}`,
|
|
1188
1188
|
});
|
|
1189
1189
|
resolveDecision(
|
|
@@ -1223,7 +1223,7 @@ function convertWorkflowFile(
|
|
|
1223
1223
|
}
|
|
1224
1224
|
if (requiredEvidence.length > 0) step.requiredEvidence = requiredEvidence;
|
|
1225
1225
|
if (policyProducers.size > 0) {
|
|
1226
|
-
//
|
|
1226
|
+
// KXM evidence producers must execute within the step assignment
|
|
1227
1227
|
// pool: widen it beyond the legacy single-coordinator shape.
|
|
1228
1228
|
const pool = [stepAgent, ...[...policyProducers].filter((candidate) => candidate !== stepAgent).sort(compareCodeUnits)];
|
|
1229
1229
|
step.assignments = {
|
|
@@ -1246,7 +1246,7 @@ function convertWorkflowFile(
|
|
|
1246
1246
|
continue;
|
|
1247
1247
|
}
|
|
1248
1248
|
if (seenOutcomes.has(outcome)) {
|
|
1249
|
-
// Two legacy outcome keys normalize to the same
|
|
1249
|
+
// Two legacy outcome keys normalize to the same KXM outcome;
|
|
1250
1250
|
// never merge their edges last-wins.
|
|
1251
1251
|
recordUnmapped(plan, source.path, `${pointer}/on/${pointerEscape(rawOutcome)}`, rawTransition, false);
|
|
1252
1252
|
continue;
|
|
@@ -1270,7 +1270,7 @@ function convertWorkflowFile(
|
|
|
1270
1270
|
}
|
|
1271
1271
|
step.on = transitions;
|
|
1272
1272
|
if (typeof stage.maxTransitions === "number") {
|
|
1273
|
-
// Legacy per-stage transition budgets have no
|
|
1273
|
+
// Legacy per-stage transition budgets have no KXM equivalent;
|
|
1274
1274
|
// per-edge budgets and the global limit cover the same bound.
|
|
1275
1275
|
recordUnmapped(plan, source.path, `${pointer}/maxTransitions`, stage.maxTransitions, false);
|
|
1276
1276
|
}
|
|
@@ -1302,7 +1302,7 @@ function convertWorkflowFile(
|
|
|
1302
1302
|
recordUnmapped(plan, source.path, `${basePointer}/maxTransitions`, definition.maxTransitions, false);
|
|
1303
1303
|
}
|
|
1304
1304
|
if (workflow.limits === undefined) {
|
|
1305
|
-
//
|
|
1305
|
+
// KXM requires a global budget whenever any back-edge exists.
|
|
1306
1306
|
const hasBackEdge = steps.some((step) => Object.values(asObject(step.on as JsonValue) ?? {}).some((rule) => {
|
|
1307
1307
|
const target = typeof rule === "string" ? rule : asObject(rule)?.target;
|
|
1308
1308
|
return typeof target === "string" && target !== "$terminal" && stageIds.indexOf(target) <= stageIds.indexOf(String(step.id));
|
|
@@ -1313,7 +1313,7 @@ function convertWorkflowFile(
|
|
|
1313
1313
|
`budget:${workflowKey}`,
|
|
1314
1314
|
"transition-budget",
|
|
1315
1315
|
source.path,
|
|
1316
|
-
`workflow ${workflowId} has back-edges but no legacy global maxTransitions;
|
|
1316
|
+
`workflow ${workflowId} has back-edges but no legacy global maxTransitions; KXM requires limits.maxTransitions (1-1000)`,
|
|
1317
1317
|
[28],
|
|
1318
1318
|
decisions,
|
|
1319
1319
|
);
|
|
@@ -1368,18 +1368,18 @@ function convertWorkflowFile(
|
|
|
1368
1368
|
|
|
1369
1369
|
/* ------------------------- plan assembly -------------------------- */
|
|
1370
1370
|
|
|
1371
|
-
export interface
|
|
1371
|
+
export interface KxmMigrationPlanResult {
|
|
1372
1372
|
plan: JsonObject;
|
|
1373
1373
|
resources: Map<string, JsonObject>;
|
|
1374
|
-
ambiguities: readonly
|
|
1374
|
+
ambiguities: readonly KxmMigrationAmbiguity[];
|
|
1375
1375
|
decisionKeys: ReadonlySet<string>;
|
|
1376
1376
|
decisionAllowedValues: ReadonlyMap<string, readonly (string | number)[]>;
|
|
1377
1377
|
invalidDecisions: readonly string[];
|
|
1378
1378
|
decisions: Readonly<Record<string, JsonValue>>;
|
|
1379
|
-
sources: readonly
|
|
1379
|
+
sources: readonly KxmLegacySource[];
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
|
-
export interface
|
|
1382
|
+
export interface KxmMigrationOptions extends KxmConfigOptions {
|
|
1383
1383
|
decisions?: Readonly<Record<string, JsonValue>>;
|
|
1384
1384
|
projectId?: string;
|
|
1385
1385
|
projectName?: string;
|
|
@@ -1395,18 +1395,18 @@ function sortedRecord<T>(map: ReadonlyMap<string, T>, prefix: string): Map<strin
|
|
|
1395
1395
|
function migrationGitRoot(start: string): string {
|
|
1396
1396
|
const root = discoverGitRoot(start);
|
|
1397
1397
|
if (!root) {
|
|
1398
|
-
throw new
|
|
1398
|
+
throw new KxmConfigError([migrateIssue("discovery", "git_root_required", ".", "kxm migrate must run inside the authoritative Git worktree")]);
|
|
1399
1399
|
}
|
|
1400
1400
|
return root;
|
|
1401
1401
|
}
|
|
1402
1402
|
|
|
1403
1403
|
/** Convert legacy configuration into a deterministic, schema-shaped migration plan. */
|
|
1404
|
-
export function
|
|
1404
|
+
export function planKxmMigration(projectRoot: string, options: KxmMigrationOptions = {}): KxmMigrationPlanResult {
|
|
1405
1405
|
const root = migrationGitRoot(projectRoot);
|
|
1406
|
-
const registry = new
|
|
1407
|
-
const sources =
|
|
1406
|
+
const registry = new KxmSchemaRegistry(options.schemasDir);
|
|
1407
|
+
const sources = readKxmLegacySources(root);
|
|
1408
1408
|
if (sources.length === 0) {
|
|
1409
|
-
throw new
|
|
1409
|
+
throw new KxmConfigError([migrateIssue("discovery", "legacy_sources_missing", ".kxm/config", "no legacy configuration files found to migrate")]);
|
|
1410
1410
|
}
|
|
1411
1411
|
const decisions = options.decisions ?? {};
|
|
1412
1412
|
const plan: MutablePlan = { ambiguities: [], invalidDecisions: [], decisionKeys: new Set(), decisionAllowedValues: new Map(), unmapped: [], renames: [], permissionChanges: [] };
|
|
@@ -1449,11 +1449,11 @@ export function planVnextMigration(projectRoot: string, options: VnextMigrationO
|
|
|
1449
1449
|
|
|
1450
1450
|
const projectId = options.projectId ?? `prj_mig${sha256Of(legacySourceDigest(sources)).slice(7, 39)}`;
|
|
1451
1451
|
if (!PROJECT_ID.test(projectId)) {
|
|
1452
|
-
throw new
|
|
1452
|
+
throw new KxmConfigError([migrateIssue("semantic", "project_id_invalid", ".kxm/project.yaml", "project ID must satisfy the kxm.project.v1 opaque ID grammar and use the prj_ prefix")]);
|
|
1453
1453
|
}
|
|
1454
1454
|
const projectName = options.projectName ?? "Migrated legacy project";
|
|
1455
1455
|
if (projectName.length < 1 || projectName.length > 120 || /[\u0000\r\n]/.test(projectName)) {
|
|
1456
|
-
throw new
|
|
1456
|
+
throw new KxmConfigError([migrateIssue("semantic", "project_name_invalid", ".kxm/project.yaml", "project name must contain 1-120 characters on one line")]);
|
|
1457
1457
|
}
|
|
1458
1458
|
|
|
1459
1459
|
const resources = new Map<string, JsonObject>();
|
|
@@ -1484,8 +1484,8 @@ export function planVnextMigration(projectRoot: string, options: VnextMigrationO
|
|
|
1484
1484
|
|
|
1485
1485
|
// Validate every converted resource against its exact schema before the
|
|
1486
1486
|
// plan is considered applicable.
|
|
1487
|
-
const schemaIssues:
|
|
1488
|
-
const kindOf = (path: string):
|
|
1487
|
+
const schemaIssues: KxmConfigIssue[] = [];
|
|
1488
|
+
const kindOf = (path: string): KxmResourceKind => path === ".kxm/project.yaml" ? "project"
|
|
1489
1489
|
: path.endsWith("repo.yaml") ? "repository"
|
|
1490
1490
|
: path.startsWith(".kxm/agents/") ? "agent"
|
|
1491
1491
|
: path.startsWith(".kxm/models/") ? "model"
|
|
@@ -1493,14 +1493,14 @@ export function planVnextMigration(projectRoot: string, options: VnextMigrationO
|
|
|
1493
1493
|
const idOf = (path: string): string | undefined => path === ".kxm/project.yaml" || path.endsWith("repo.yaml")
|
|
1494
1494
|
? undefined
|
|
1495
1495
|
: path.slice(path.lastIndexOf("/") + 1, -".yaml".length);
|
|
1496
|
-
const semanticResources = new Map<string, { kind:
|
|
1496
|
+
const semanticResources = new Map<string, { kind: KxmResourceKind; id?: string; value: JsonObject }>();
|
|
1497
1497
|
for (const [path, value] of [...resources.entries()].sort(([left], [right]) => compareCodeUnits(left, right))) {
|
|
1498
1498
|
const kind = kindOf(path);
|
|
1499
1499
|
schemaIssues.push(...registry.validate(kind, value, path));
|
|
1500
1500
|
const id = idOf(path);
|
|
1501
1501
|
semanticResources.set(path, kind === "repository" ? { kind, id: "control", value } : { kind, ...(id === undefined ? {} : { id }), value });
|
|
1502
1502
|
}
|
|
1503
|
-
if (schemaIssues.length > 0) throw new
|
|
1503
|
+
if (schemaIssues.length > 0) throw new KxmConfigError(schemaIssues);
|
|
1504
1504
|
// Full semantic validation gates a resolved plan: while ambiguities remain,
|
|
1505
1505
|
// the intermediate resource set may legitimately be incomplete (for example
|
|
1506
1506
|
// a workflow whose coordinator decision is still pending), and the operator
|
|
@@ -1508,8 +1508,8 @@ export function planVnextMigration(projectRoot: string, options: VnextMigrationO
|
|
|
1508
1508
|
// Apply always re-plans and requires zero ambiguities, so this check still
|
|
1509
1509
|
// runs before any write.
|
|
1510
1510
|
if (plan.ambiguities.length === 0) {
|
|
1511
|
-
const semanticIssues =
|
|
1512
|
-
if (semanticIssues.length > 0) throw new
|
|
1511
|
+
const semanticIssues = validateKxmResources(semanticResources, options);
|
|
1512
|
+
if (semanticIssues.length > 0) throw new KxmConfigError(semanticIssues);
|
|
1513
1513
|
}
|
|
1514
1514
|
|
|
1515
1515
|
const planRecord: JsonObject = {
|
|
@@ -1537,7 +1537,7 @@ export function planVnextMigration(projectRoot: string, options: VnextMigrationO
|
|
|
1537
1537
|
canApply: plan.ambiguities.length === 0,
|
|
1538
1538
|
};
|
|
1539
1539
|
const planIssues = registry.validateMigrationPlan(planRecord, "kxm.migration-plan.v1");
|
|
1540
|
-
if (planIssues.length > 0) throw new
|
|
1540
|
+
if (planIssues.length > 0) throw new KxmConfigError(planIssues);
|
|
1541
1541
|
|
|
1542
1542
|
return {
|
|
1543
1543
|
plan: planRecord,
|
|
@@ -1553,14 +1553,14 @@ export function planVnextMigration(projectRoot: string, options: VnextMigrationO
|
|
|
1553
1553
|
|
|
1554
1554
|
/* ------------------------- decisions file -------------------------- */
|
|
1555
1555
|
|
|
1556
|
-
export function
|
|
1556
|
+
export function readKxmMigrationDecisions(path: string, schemasDir?: string): JsonObject {
|
|
1557
1557
|
if (!existsSync(path)) {
|
|
1558
|
-
throw new
|
|
1558
|
+
throw new KxmConfigError([migrateIssue("discovery", "decisions_missing", path, "migration decisions file does not exist")]);
|
|
1559
1559
|
}
|
|
1560
|
-
const registry = new
|
|
1560
|
+
const registry = new KxmSchemaRegistry(schemasDir);
|
|
1561
1561
|
const value = parseRestrictedYaml(readFileSync(path), path);
|
|
1562
1562
|
const issues = registry.validateMigrationDecision(value, path);
|
|
1563
|
-
if (issues.length > 0) throw new
|
|
1563
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
1564
1564
|
return value;
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
@@ -1572,7 +1572,7 @@ function resolutionsOf(decisions: JsonObject): Record<string, JsonValue> {
|
|
|
1572
1572
|
|
|
1573
1573
|
/* ------------------------- apply / verify -------------------------- */
|
|
1574
1574
|
|
|
1575
|
-
export interface
|
|
1575
|
+
export interface KxmMigrationApplyResult {
|
|
1576
1576
|
action: "applied" | "planned" | "already-migrated";
|
|
1577
1577
|
projectRoot: string;
|
|
1578
1578
|
receiptPath?: string;
|
|
@@ -1581,7 +1581,7 @@ export interface VnextMigrationApplyResult {
|
|
|
1581
1581
|
plan?: JsonObject;
|
|
1582
1582
|
}
|
|
1583
1583
|
|
|
1584
|
-
export interface
|
|
1584
|
+
export interface KxmMigrationApplyOptions extends KxmMigrationOptions {
|
|
1585
1585
|
dryRun?: boolean;
|
|
1586
1586
|
decisionsFile?: string;
|
|
1587
1587
|
localStateRoot?: string;
|
|
@@ -1621,34 +1621,34 @@ function writeDurable(path: string, content: Buffer): void {
|
|
|
1621
1621
|
}
|
|
1622
1622
|
|
|
1623
1623
|
/** Apply a reviewed migration: stage, validate the complete target, install, and receipt. */
|
|
1624
|
-
export function
|
|
1624
|
+
export function applyKxmMigration(projectRoot: string, options: KxmMigrationApplyOptions = {}): KxmMigrationApplyResult {
|
|
1625
1625
|
const root = migrationGitRoot(projectRoot);
|
|
1626
|
-
const storeOptions:
|
|
1626
|
+
const storeOptions: KxmLocalBindingStoreOptions = {
|
|
1627
1627
|
...(options.localStateRoot === undefined ? {} : { stateRoot: options.localStateRoot }),
|
|
1628
1628
|
...(options.schemasDir === undefined ? {} : { schemasDir: options.schemasDir }),
|
|
1629
1629
|
};
|
|
1630
1630
|
|
|
1631
|
-
const execute = (lock:
|
|
1632
|
-
const receiptAbsolute = join(root, ...
|
|
1631
|
+
const execute = (lock: KxmLocalBindingLock | undefined): KxmMigrationApplyResult => {
|
|
1632
|
+
const receiptAbsolute = join(root, ...KXM_MIGRATION_RECEIPT_PATH.split("/"));
|
|
1633
1633
|
// Already-migrated detection precedes planning and decision checks: a
|
|
1634
1634
|
// validated receipt makes re-apply an idempotent no-op even when legacy
|
|
1635
1635
|
// sources were later removed or no decisions are supplied.
|
|
1636
1636
|
if (existsSync(receiptAbsolute)) {
|
|
1637
1637
|
const stat = lstatSync(receiptAbsolute);
|
|
1638
1638
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
1639
|
-
throw new
|
|
1639
|
+
throw new KxmConfigError([migrateIssue("path", "migration_receipt_invalid", KXM_MIGRATION_RECEIPT_PATH, "migration receipt must be a regular file, not a link")]);
|
|
1640
1640
|
}
|
|
1641
|
-
const existing =
|
|
1641
|
+
const existing = loadKxmProject(root, options);
|
|
1642
1642
|
return {
|
|
1643
1643
|
action: "already-migrated",
|
|
1644
1644
|
projectRoot: root,
|
|
1645
|
-
receiptPath:
|
|
1645
|
+
receiptPath: KXM_MIGRATION_RECEIPT_PATH,
|
|
1646
1646
|
configRevision: existing.configRevision,
|
|
1647
1647
|
files: [],
|
|
1648
1648
|
};
|
|
1649
1649
|
}
|
|
1650
1650
|
|
|
1651
|
-
const decisionsRecord = options.decisionsFile ?
|
|
1651
|
+
const decisionsRecord = options.decisionsFile ? readKxmMigrationDecisions(options.decisionsFile, options.schemasDir) : undefined;
|
|
1652
1652
|
const decisions = decisionsRecord ? resolutionsOf(decisionsRecord) : (options.decisions ?? {});
|
|
1653
1653
|
// Plan once with the real decisions: resolveDecision validates every
|
|
1654
1654
|
// supplied value inline, so an invalid value can never masquerade as a
|
|
@@ -1656,16 +1656,16 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1656
1656
|
// universe the operator reviewed; decision keys are accepted from the
|
|
1657
1657
|
// union of both plans because binding decisions (coordinator, identities)
|
|
1658
1658
|
// can make plan-1 keys unnecessary without invalidating the review.
|
|
1659
|
-
const first =
|
|
1659
|
+
const first = planKxmMigration(root, { ...options, decisions });
|
|
1660
1660
|
if (decisionsRecord) {
|
|
1661
1661
|
if (decisionsRecord.projectId !== first.plan.projectId) {
|
|
1662
|
-
throw new
|
|
1662
|
+
throw new KxmConfigError([migrateIssue("semantic", "decision_project_mismatch", options.decisionsFile ?? "<decisions>", "decisions file projectId does not match the recomputed plan")]);
|
|
1663
1663
|
}
|
|
1664
1664
|
if (decisionsRecord.projectName !== first.plan.projectName) {
|
|
1665
|
-
throw new
|
|
1665
|
+
throw new KxmConfigError([migrateIssue("semantic", "decision_project_mismatch", options.decisionsFile ?? "<decisions>", "decisions file projectName does not match the recomputed plan")]);
|
|
1666
1666
|
}
|
|
1667
1667
|
if (decisionsRecord.sourceDigest !== first.plan.sourceDigest) {
|
|
1668
|
-
throw new
|
|
1668
|
+
throw new KxmConfigError([migrateIssue("semantic", "decision_source_changed", options.decisionsFile ?? "<decisions>", "legacy sources changed after the decisions file was reviewed")]);
|
|
1669
1669
|
}
|
|
1670
1670
|
}
|
|
1671
1671
|
if (Object.keys(decisions).length > 0) {
|
|
@@ -1674,7 +1674,7 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1674
1674
|
// e.g. a coordinator choice superseded by a raw-name binding) are
|
|
1675
1675
|
// validated against the universe's allowed values here. Anything else
|
|
1676
1676
|
// was never part of any review.
|
|
1677
|
-
const universe =
|
|
1677
|
+
const universe = planKxmMigration(root, { ...options, decisions: {} });
|
|
1678
1678
|
const unknown: string[] = [];
|
|
1679
1679
|
const invalid: string[] = [];
|
|
1680
1680
|
for (const [key, value] of Object.entries(decisions)) {
|
|
@@ -1687,11 +1687,11 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1687
1687
|
}
|
|
1688
1688
|
}
|
|
1689
1689
|
if (unknown.length > 0) {
|
|
1690
|
-
throw new
|
|
1690
|
+
throw new KxmConfigError(unknown.sort(compareCodeUnits).map((key) => migrateIssue("semantic", "decision_unknown", key, "decision key does not match any current ambiguity")));
|
|
1691
1691
|
}
|
|
1692
1692
|
const allInvalid = [...new Set([...invalid, ...first.invalidDecisions])].sort(compareCodeUnits);
|
|
1693
1693
|
if (allInvalid.length > 0) {
|
|
1694
|
-
throw new
|
|
1694
|
+
throw new KxmConfigError(allInvalid.map((key) => migrateIssue("semantic", "decision_value_invalid", key, "decision value is not one of the plan's allowed values")));
|
|
1695
1695
|
}
|
|
1696
1696
|
}
|
|
1697
1697
|
if (first.ambiguities.length > 0) {
|
|
@@ -1700,14 +1700,14 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1700
1700
|
|
|
1701
1701
|
const files = [...first.resources.entries()].sort(([left], [right]) => compareCodeUnits(left, right))
|
|
1702
1702
|
.map(([path, value]) => ({ path, bytes: Buffer.from(stringify(value, { lineWidth: 0 }), "utf8") }));
|
|
1703
|
-
// Refuse to overwrite any existing
|
|
1703
|
+
// Refuse to overwrite any existing KXM state or legacy inputs.
|
|
1704
1704
|
const collisions = files.filter((file) => existsSync(join(root, ...file.path.split("/"))));
|
|
1705
1705
|
if (collisions.length > 0) {
|
|
1706
|
-
throw new
|
|
1706
|
+
throw new KxmConfigError(collisions.map((file) => migrateIssue("semantic", "migration_target_exists", file.path, "target resource already exists; refusing to overwrite")));
|
|
1707
1707
|
}
|
|
1708
1708
|
// Never write through a linked or non-directory parent component: walk
|
|
1709
1709
|
// every target parent from the project root before the first write.
|
|
1710
|
-
const parentIssues:
|
|
1710
|
+
const parentIssues: KxmConfigIssue[] = [];
|
|
1711
1711
|
for (const file of files) {
|
|
1712
1712
|
const segments = file.path.split("/");
|
|
1713
1713
|
let current = root;
|
|
@@ -1721,7 +1721,7 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1721
1721
|
}
|
|
1722
1722
|
}
|
|
1723
1723
|
}
|
|
1724
|
-
if (parentIssues.length > 0) throw new
|
|
1724
|
+
if (parentIssues.length > 0) throw new KxmConfigError(parentIssues);
|
|
1725
1725
|
|
|
1726
1726
|
if (options.dryRun) {
|
|
1727
1727
|
// Preflight is complete (decisions, collisions, linked parents); a
|
|
@@ -1741,29 +1741,29 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1741
1741
|
writeDurable(absolute, file.bytes);
|
|
1742
1742
|
}
|
|
1743
1743
|
// Validate the complete installed bundle before issuing the receipt.
|
|
1744
|
-
const bundle =
|
|
1744
|
+
const bundle = loadKxmProject(root, { ...options, allowUnreceiptedLegacyConfig: true });
|
|
1745
1745
|
const receipt: JsonObject = {
|
|
1746
1746
|
schema: "kxm.migration-receipt.v1",
|
|
1747
1747
|
migrationId: `mig_${sha256Of(`${first.plan.sourceDigest}\n${bundle.configRevision}`).slice(7, 39)}`,
|
|
1748
1748
|
projectId: String(first.plan.projectId),
|
|
1749
1749
|
sourceDigest: String(first.plan.sourceDigest),
|
|
1750
|
-
decisionDigest: sha256Of(
|
|
1750
|
+
decisionDigest: sha256Of(kxmCanonicalJson((decisionsRecord ?? { schema: "kxm.migration-decision.v1", projectId: first.plan.projectId, projectName: first.plan.projectName, sourceDigest: first.plan.sourceDigest, resolutions: decisions }) as JsonObject)),
|
|
1751
1751
|
configRevision: bundle.configRevision,
|
|
1752
1752
|
sources: first.plan.sources as JsonValue,
|
|
1753
1753
|
resources: files.map((file) => ({ path: file.path, sha256: sha256Of(file.bytes), bytes: file.bytes.byteLength })),
|
|
1754
1754
|
};
|
|
1755
1755
|
receipt.receiptSha256 = migrationReceiptSelfHash(receipt);
|
|
1756
|
-
const registry = new
|
|
1757
|
-
const receiptIssues = registry.validateMigrationReceipt(receipt,
|
|
1758
|
-
if (receiptIssues.length > 0) throw new
|
|
1756
|
+
const registry = new KxmSchemaRegistry(options.schemasDir);
|
|
1757
|
+
const receiptIssues = registry.validateMigrationReceipt(receipt, KXM_MIGRATION_RECEIPT_PATH);
|
|
1758
|
+
if (receiptIssues.length > 0) throw new KxmConfigError(receiptIssues);
|
|
1759
1759
|
written.push(receiptAbsolute);
|
|
1760
1760
|
writeDurable(receiptAbsolute, Buffer.from(stringify(receipt, { lineWidth: 0 }), "utf8"));
|
|
1761
1761
|
// The receipt must make the mixed tree loadable.
|
|
1762
|
-
const verified =
|
|
1762
|
+
const verified = loadKxmProject(root, options);
|
|
1763
1763
|
return {
|
|
1764
1764
|
action: "applied",
|
|
1765
1765
|
projectRoot: root,
|
|
1766
|
-
receiptPath:
|
|
1766
|
+
receiptPath: KXM_MIGRATION_RECEIPT_PATH,
|
|
1767
1767
|
configRevision: verified.configRevision,
|
|
1768
1768
|
files: files.map((file) => file.path),
|
|
1769
1769
|
plan: first.plan,
|
|
@@ -1780,67 +1780,67 @@ export function applyVnextMigration(projectRoot: string, options: VnextMigration
|
|
|
1780
1780
|
};
|
|
1781
1781
|
|
|
1782
1782
|
if (options.dryRun) return execute(undefined);
|
|
1783
|
-
return
|
|
1783
|
+
return withKxmLocalBindingLock(root, storeOptions, (lock) => execute(lock));
|
|
1784
1784
|
}
|
|
1785
1785
|
|
|
1786
1786
|
/* ------------------------- verify ---------------------------------- */
|
|
1787
1787
|
|
|
1788
|
-
export interface
|
|
1788
|
+
export interface KxmMigrationVerifyResult {
|
|
1789
1789
|
ok: boolean;
|
|
1790
1790
|
projectRoot: string;
|
|
1791
1791
|
receiptPath: string;
|
|
1792
1792
|
configRevision?: string;
|
|
1793
|
-
issues: readonly
|
|
1793
|
+
issues: readonly KxmConfigIssue[];
|
|
1794
1794
|
}
|
|
1795
1795
|
|
|
1796
1796
|
/** Re-check a migration receipt against current legacy sources and the target bundle. No writes. */
|
|
1797
|
-
export function
|
|
1797
|
+
export function verifyKxmMigration(projectRoot: string, options: KxmConfigOptions = {}): KxmMigrationVerifyResult {
|
|
1798
1798
|
const root = migrationGitRoot(projectRoot);
|
|
1799
|
-
const receiptAbsolute = join(root, ...
|
|
1799
|
+
const receiptAbsolute = join(root, ...KXM_MIGRATION_RECEIPT_PATH.split("/"));
|
|
1800
1800
|
if (!existsSync(receiptAbsolute)) {
|
|
1801
1801
|
return {
|
|
1802
1802
|
ok: false,
|
|
1803
1803
|
projectRoot: root,
|
|
1804
|
-
receiptPath:
|
|
1805
|
-
issues: [migrateIssue("discovery", "migration_receipt_missing",
|
|
1804
|
+
receiptPath: KXM_MIGRATION_RECEIPT_PATH,
|
|
1805
|
+
issues: [migrateIssue("discovery", "migration_receipt_missing", KXM_MIGRATION_RECEIPT_PATH, "no migration receipt exists")],
|
|
1806
1806
|
};
|
|
1807
1807
|
}
|
|
1808
1808
|
try {
|
|
1809
|
-
const bundle =
|
|
1809
|
+
const bundle = loadKxmProject(root, options);
|
|
1810
1810
|
const receipt = bundle.migrationReceipt;
|
|
1811
1811
|
if (!receipt) {
|
|
1812
1812
|
if (legacyConfigFilesAt(root).length === 0) {
|
|
1813
1813
|
// Every legacy source was removed after migration: the receipt was
|
|
1814
|
-
// never consulted at load. Read it directly;
|
|
1814
|
+
// never consulted at load. Read it directly; readKxmMigrationReceipt
|
|
1815
1815
|
// surfaces one migration_source_missing issue per declared source.
|
|
1816
|
-
const direct =
|
|
1816
|
+
const direct = readKxmMigrationReceipt(root, options);
|
|
1817
1817
|
return {
|
|
1818
1818
|
ok: false,
|
|
1819
1819
|
projectRoot: root,
|
|
1820
|
-
receiptPath:
|
|
1820
|
+
receiptPath: KXM_MIGRATION_RECEIPT_PATH,
|
|
1821
1821
|
configRevision: bundle.configRevision,
|
|
1822
|
-
issues: direct.issues.length > 0 ? direct.issues : [migrateIssue("semantic", "migration_receipt_invalid",
|
|
1822
|
+
issues: direct.issues.length > 0 ? direct.issues : [migrateIssue("semantic", "migration_receipt_invalid", KXM_MIGRATION_RECEIPT_PATH, "receipt did not validate")],
|
|
1823
1823
|
};
|
|
1824
1824
|
}
|
|
1825
1825
|
return {
|
|
1826
1826
|
ok: false,
|
|
1827
1827
|
projectRoot: root,
|
|
1828
|
-
receiptPath:
|
|
1828
|
+
receiptPath: KXM_MIGRATION_RECEIPT_PATH,
|
|
1829
1829
|
configRevision: bundle.configRevision,
|
|
1830
|
-
issues: [migrateIssue("semantic", "migration_receipt_invalid",
|
|
1830
|
+
issues: [migrateIssue("semantic", "migration_receipt_invalid", KXM_MIGRATION_RECEIPT_PATH, "receipt did not validate during project load")],
|
|
1831
1831
|
};
|
|
1832
1832
|
}
|
|
1833
|
-
const targetIssues =
|
|
1833
|
+
const targetIssues = verifyKxmMigrationReceiptTarget(root, receipt, bundle.resources, bundle.configRevision);
|
|
1834
1834
|
return {
|
|
1835
1835
|
ok: targetIssues.length === 0,
|
|
1836
1836
|
projectRoot: root,
|
|
1837
|
-
receiptPath:
|
|
1837
|
+
receiptPath: KXM_MIGRATION_RECEIPT_PATH,
|
|
1838
1838
|
configRevision: bundle.configRevision,
|
|
1839
1839
|
issues: targetIssues,
|
|
1840
1840
|
};
|
|
1841
1841
|
} catch (error) {
|
|
1842
|
-
if (error instanceof
|
|
1843
|
-
return { ok: false, projectRoot: root, receiptPath:
|
|
1842
|
+
if (error instanceof KxmConfigError) {
|
|
1843
|
+
return { ok: false, projectRoot: root, receiptPath: KXM_MIGRATION_RECEIPT_PATH, issues: error.issues };
|
|
1844
1844
|
}
|
|
1845
1845
|
throw error;
|
|
1846
1846
|
}
|