@kontextmind/kxm 0.7.40 → 0.7.44
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 +45 -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-hub.mjs +36 -0
- package/scripts/kxm-runtime-supervisor.mjs +3 -3
- package/scripts/package-surfaces.mjs +39 -0
- 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
|
@@ -16,33 +16,33 @@ import {
|
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
KxmConfigError,
|
|
20
|
+
KxmSchemaRegistry,
|
|
21
|
+
loadKxmProject,
|
|
22
22
|
assertNoRegisteredGates,
|
|
23
23
|
parseRestrictedYaml,
|
|
24
24
|
type JsonObject,
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
} from "./
|
|
25
|
+
type KxmConfigIssue,
|
|
26
|
+
type KxmConfigOptions,
|
|
27
|
+
type KxmProjectBundle,
|
|
28
|
+
} from "./project-config.ts";
|
|
29
29
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type
|
|
38
|
-
type
|
|
39
|
-
type
|
|
40
|
-
} from "./
|
|
41
|
-
import {
|
|
42
|
-
import type {
|
|
30
|
+
CURRENT_KXM_TEMPLATE_VARIANT,
|
|
31
|
+
KXM_TEMPLATE_ID,
|
|
32
|
+
KXM_TEMPLATE_PROVENANCE_PATH,
|
|
33
|
+
renderKxmTemplate,
|
|
34
|
+
resolveKxmTemplateBaseline,
|
|
35
|
+
SUPPORTED_KXM_TEMPLATE_VARIANTS,
|
|
36
|
+
kxmContentSha256,
|
|
37
|
+
type KxmRenderedTemplate,
|
|
38
|
+
type KxmTemplateProvenance,
|
|
39
|
+
type KxmTemplateVariant,
|
|
40
|
+
} from "./template.ts";
|
|
41
|
+
import { computeKxmResourcePermissionDiff } from "./permission.ts";
|
|
42
|
+
import type { KxmResourceKind } from "./project-config.ts";
|
|
43
43
|
|
|
44
44
|
/** Map a managed template path to its resource kind for permission diffing. */
|
|
45
|
-
function resourceKindForTemplatePath(path: string):
|
|
45
|
+
function resourceKindForTemplatePath(path: string): KxmResourceKind {
|
|
46
46
|
if (path === ".kxm/project.yaml") return "project";
|
|
47
47
|
if (path === ".kxm/gates.yaml") return "gate-registry";
|
|
48
48
|
if (path.endsWith("repo.yaml")) return "repository";
|
|
@@ -58,11 +58,11 @@ const MAX_MANAGED_FILE_BYTES = 256 * 1024;
|
|
|
58
58
|
const MAX_MANAGED_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
59
59
|
const PORTABLE_PATH = /^(?:\.|(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*[. ](?:\/|$))[^/\\\u0000-\u001F]+(?:\/[^/\\\u0000-\u001F]+)*)$/;
|
|
60
60
|
|
|
61
|
-
export type
|
|
61
|
+
export type KxmTemplateClassification = "unchanged" | "user-only" | "template-only" | "converged" | "conflict";
|
|
62
62
|
|
|
63
|
-
export interface
|
|
63
|
+
export interface KxmTemplateChange {
|
|
64
64
|
path: string;
|
|
65
|
-
classification:
|
|
65
|
+
classification: KxmTemplateClassification;
|
|
66
66
|
baseSha256?: string;
|
|
67
67
|
localSha256?: string;
|
|
68
68
|
targetSha256?: string;
|
|
@@ -71,7 +71,7 @@ export interface VnextTemplateChange {
|
|
|
71
71
|
reason?: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export interface
|
|
74
|
+
export interface KxmTemplateRepairPlan {
|
|
75
75
|
projectRoot: string;
|
|
76
76
|
projectId: string;
|
|
77
77
|
projectName: string;
|
|
@@ -79,11 +79,11 @@ export interface VnextTemplateRepairPlan {
|
|
|
79
79
|
targetTemplateRevision: string;
|
|
80
80
|
changesRequired: boolean;
|
|
81
81
|
canApply: boolean;
|
|
82
|
-
changes: readonly
|
|
83
|
-
issues: readonly
|
|
82
|
+
changes: readonly KxmTemplateChange[];
|
|
83
|
+
issues: readonly KxmConfigIssue[];
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
interface
|
|
86
|
+
interface KxmInitOperationFile {
|
|
87
87
|
path: string;
|
|
88
88
|
observedSha256: string | null;
|
|
89
89
|
targetSha256: string;
|
|
@@ -91,7 +91,7 @@ interface VnextInitOperationFile {
|
|
|
91
91
|
action: "create" | "replace" | "none";
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
interface
|
|
94
|
+
interface KxmInitOperation {
|
|
95
95
|
schema: "kxm.init-operation.v1";
|
|
96
96
|
operationId: string;
|
|
97
97
|
kind: "create" | "repair";
|
|
@@ -99,26 +99,26 @@ interface VnextInitOperation {
|
|
|
99
99
|
projectRoot: string;
|
|
100
100
|
projectId: string;
|
|
101
101
|
projectName: string;
|
|
102
|
-
templateId: typeof
|
|
102
|
+
templateId: typeof KXM_TEMPLATE_ID;
|
|
103
103
|
sourceTemplateRevision: string | null;
|
|
104
104
|
targetTemplateRevision: string;
|
|
105
105
|
planSha256: string;
|
|
106
|
-
files: readonly
|
|
106
|
+
files: readonly KxmInitOperationFile[];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export interface
|
|
109
|
+
export interface KxmOperationResult {
|
|
110
110
|
kind: "create" | "repair";
|
|
111
111
|
resumed: boolean;
|
|
112
|
-
bundle:
|
|
112
|
+
bundle: KxmProjectBundle;
|
|
113
113
|
files: readonly string[];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export interface
|
|
116
|
+
export interface KxmRepairRuntimeOptions {
|
|
117
117
|
schemasDir?: string;
|
|
118
118
|
repositoryBindings?: Readonly<Record<string, string>>;
|
|
119
119
|
registeredExecutors?: Iterable<string>;
|
|
120
120
|
registeredToolPresets?: Iterable<string>;
|
|
121
|
-
templateVariant?:
|
|
121
|
+
templateVariant?: KxmTemplateVariant;
|
|
122
122
|
testFaultAt?: "prepared" | "first-resource" | "provenance" | "verified";
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -126,12 +126,12 @@ function compareCodeUnits(left: string, right: string): number {
|
|
|
126
126
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function repairIssue(code: string, file: string, message: string, phase:
|
|
129
|
+
function repairIssue(code: string, file: string, message: string, phase: KxmConfigIssue["phase"] = "semantic"): KxmConfigIssue {
|
|
130
130
|
return { phase, code, file, message };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function fail(code: string, file: string, message: string, phase:
|
|
134
|
-
throw new
|
|
133
|
+
function fail(code: string, file: string, message: string, phase: KxmConfigIssue["phase"] = "semantic"): never {
|
|
134
|
+
throw new KxmConfigError([repairIssue(code, file, message, phase)]);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function syncDirectory(path: string): void {
|
|
@@ -189,7 +189,7 @@ function transactionRoot(projectRoot: string): string {
|
|
|
189
189
|
return join(resolve(projectRoot), TRANSACTION_NAME);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
export function
|
|
192
|
+
export function kxmInitTransactionPath(projectRoot: string): string {
|
|
193
193
|
return transactionRoot(projectRoot);
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -211,7 +211,7 @@ function backupFile(projectRoot: string, portablePath: string): string {
|
|
|
211
211
|
|
|
212
212
|
function portableManagedPath(path: string): boolean {
|
|
213
213
|
return path.startsWith(".kxm/")
|
|
214
|
-
&& path !==
|
|
214
|
+
&& path !== KXM_TEMPLATE_PROVENANCE_PATH
|
|
215
215
|
&& path.length <= 1024
|
|
216
216
|
&& !path.includes("\\")
|
|
217
217
|
&& !isAbsolute(path)
|
|
@@ -239,81 +239,81 @@ function readOptionalManaged(projectRoot: string, path: string): Buffer | undefi
|
|
|
239
239
|
return readRegularBounded(file, path);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
function readProjectAndProvenance(projectRoot: string, schemasDir?: string): { project: JsonObject; provenance:
|
|
242
|
+
function readProjectAndProvenance(projectRoot: string, schemasDir?: string): { project: JsonObject; provenance: KxmTemplateProvenance; provenanceBytes: Buffer } | undefined {
|
|
243
243
|
const projectFile = join(projectRoot, ".kxm", "project.yaml");
|
|
244
|
-
const provenanceFile = join(projectRoot, ...
|
|
244
|
+
const provenanceFile = join(projectRoot, ...KXM_TEMPLATE_PROVENANCE_PATH.split("/"));
|
|
245
245
|
if (!existsSync(projectFile) || !existsSync(provenanceFile)) return undefined;
|
|
246
246
|
const projectBytes = readOptionalManaged(projectRoot, ".kxm/project.yaml");
|
|
247
|
-
const provenanceBytes = readOptionalManaged(projectRoot,
|
|
247
|
+
const provenanceBytes = readOptionalManaged(projectRoot, KXM_TEMPLATE_PROVENANCE_PATH);
|
|
248
248
|
if (!projectBytes || !provenanceBytes) return undefined;
|
|
249
249
|
const project = parseRestrictedYaml(projectBytes, ".kxm/project.yaml");
|
|
250
|
-
const provenanceValue = parseRestrictedYaml(provenanceBytes,
|
|
251
|
-
const registry = new
|
|
250
|
+
const provenanceValue = parseRestrictedYaml(provenanceBytes, KXM_TEMPLATE_PROVENANCE_PATH);
|
|
251
|
+
const registry = new KxmSchemaRegistry(schemasDir);
|
|
252
252
|
const issues = [
|
|
253
253
|
...registry.validate("project", project, ".kxm/project.yaml"),
|
|
254
|
-
...registry.validateTemplateProvenance(provenanceValue,
|
|
254
|
+
...registry.validateTemplateProvenance(provenanceValue, KXM_TEMPLATE_PROVENANCE_PATH),
|
|
255
255
|
];
|
|
256
|
-
if (issues.length > 0) throw new
|
|
257
|
-
const provenance =
|
|
258
|
-
if (!provenance) fail("template_provenance_revision_invalid",
|
|
256
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
257
|
+
const provenance = resolveKxmTemplateBaseline(provenanceValue);
|
|
258
|
+
if (!provenance) fail("template_provenance_revision_invalid", KXM_TEMPLATE_PROVENANCE_PATH, "template provenance does not exactly match a supported built-in baseline");
|
|
259
259
|
if (project.id !== provenance.inputs.projectId) {
|
|
260
|
-
fail("template_provenance_project_mismatch",
|
|
260
|
+
fail("template_provenance_project_mismatch", KXM_TEMPLATE_PROVENANCE_PATH, "template provenance belongs to a different project identity");
|
|
261
261
|
}
|
|
262
262
|
const seen = new Set<string>();
|
|
263
263
|
let prior = "";
|
|
264
264
|
let totalBytes = 0;
|
|
265
265
|
for (const record of provenance.files) {
|
|
266
|
-
if (!portableManagedPath(record.path)) fail("template_provenance_path_invalid",
|
|
266
|
+
if (!portableManagedPath(record.path)) fail("template_provenance_path_invalid", KXM_TEMPLATE_PROVENANCE_PATH, `managed path ${record.path} is invalid`, "path");
|
|
267
267
|
const folded = record.path.toLocaleLowerCase("en-US");
|
|
268
|
-
if (seen.has(folded)) fail("template_provenance_path_collision",
|
|
269
|
-
if (prior && compareCodeUnits(prior, record.path) >= 0) fail("template_provenance_order_invalid",
|
|
268
|
+
if (seen.has(folded)) fail("template_provenance_path_collision", KXM_TEMPLATE_PROVENANCE_PATH, `managed path ${record.path} collides after case folding`, "path");
|
|
269
|
+
if (prior && compareCodeUnits(prior, record.path) >= 0) fail("template_provenance_order_invalid", KXM_TEMPLATE_PROVENANCE_PATH, "managed files are not in strict code-unit order");
|
|
270
270
|
totalBytes += record.bytes;
|
|
271
271
|
seen.add(folded);
|
|
272
272
|
prior = record.path;
|
|
273
273
|
}
|
|
274
274
|
if (provenance.files.length > MAX_MANAGED_FILES || totalBytes > MAX_MANAGED_TOTAL_BYTES) {
|
|
275
|
-
fail("template_provenance_bounds_exceeded",
|
|
275
|
+
fail("template_provenance_bounds_exceeded", KXM_TEMPLATE_PROVENANCE_PATH, "managed template manifest exceeds bounded file or byte limits", "parse");
|
|
276
276
|
}
|
|
277
277
|
return { project, provenance, provenanceBytes };
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/** Classify exact whole-file B/L/T states without writing or inferring a missing baseline. */
|
|
281
|
-
export function
|
|
281
|
+
export function planKxmTemplateRepair(
|
|
282
282
|
projectRoot: string,
|
|
283
|
-
options:
|
|
284
|
-
):
|
|
283
|
+
options: KxmRepairRuntimeOptions = {},
|
|
284
|
+
): KxmTemplateRepairPlan | undefined {
|
|
285
285
|
assertNoRegisteredGates(options);
|
|
286
286
|
const root = resolve(projectRoot);
|
|
287
287
|
const source = readProjectAndProvenance(root, options.schemasDir);
|
|
288
288
|
if (!source) return undefined;
|
|
289
|
-
const target =
|
|
289
|
+
const target = renderKxmTemplate(
|
|
290
290
|
source.provenance.inputs.projectId,
|
|
291
291
|
source.provenance.inputs.projectName,
|
|
292
|
-
options.templateVariant ??
|
|
292
|
+
options.templateVariant ?? CURRENT_KXM_TEMPLATE_VARIANT,
|
|
293
293
|
);
|
|
294
294
|
const baseByPath = new Map(source.provenance.files.map((record) => [record.path, record]));
|
|
295
295
|
const targetByPath = new Map(target.provenance.files.map((record) => [record.path, record]));
|
|
296
296
|
const paths = [...new Set([...baseByPath.keys(), ...targetByPath.keys()])].sort(compareCodeUnits);
|
|
297
|
-
const sourceRenderer =
|
|
298
|
-
.map((variant) =>
|
|
297
|
+
const sourceRenderer = SUPPORTED_KXM_TEMPLATE_VARIANTS
|
|
298
|
+
.map((variant) => renderKxmTemplate(source.provenance.inputs.projectId, source.provenance.inputs.projectName, variant))
|
|
299
299
|
.find((candidate) => candidate.templateRevision === source.provenance.templateRevision);
|
|
300
|
-
const expectedProvenance = sourceRenderer?.files.get(
|
|
301
|
-
const issues:
|
|
302
|
-
if (!expectedProvenance ||
|
|
300
|
+
const expectedProvenance = sourceRenderer?.files.get(KXM_TEMPLATE_PROVENANCE_PATH);
|
|
301
|
+
const issues: KxmConfigIssue[] = [];
|
|
302
|
+
if (!expectedProvenance || kxmContentSha256(source.provenanceBytes) !== kxmContentSha256(expectedProvenance)) {
|
|
303
303
|
issues.push(repairIssue(
|
|
304
304
|
"template_provenance_user_modified",
|
|
305
|
-
|
|
305
|
+
KXM_TEMPLATE_PROVENANCE_PATH,
|
|
306
306
|
"template provenance formatting or bytes differ from the supported baseline and require review",
|
|
307
307
|
));
|
|
308
308
|
}
|
|
309
|
-
const changes = paths.map((path):
|
|
309
|
+
const changes = paths.map((path): KxmTemplateChange => {
|
|
310
310
|
const base = baseByPath.get(path);
|
|
311
311
|
const targetRecord = targetByPath.get(path);
|
|
312
312
|
const local = readOptionalManaged(root, path);
|
|
313
|
-
const localSha256 = local &&
|
|
313
|
+
const localSha256 = local && kxmContentSha256(local);
|
|
314
314
|
const baseSha256 = base?.sha256;
|
|
315
315
|
if (base && localSha256 === base.sha256 && local?.byteLength !== base.bytes) {
|
|
316
|
-
fail("template_provenance_bytes_invalid",
|
|
316
|
+
fail("template_provenance_bytes_invalid", KXM_TEMPLATE_PROVENANCE_PATH, `recorded byte count for ${path} does not match its baseline content`);
|
|
317
317
|
}
|
|
318
318
|
const targetSha256 = targetRecord?.sha256;
|
|
319
319
|
if (baseSha256 === localSha256 && localSha256 === targetSha256) {
|
|
@@ -336,7 +336,7 @@ export function planVnextTemplateRepair(
|
|
|
336
336
|
const baseValue = base ? sourceRenderer?.values.get(path) : undefined;
|
|
337
337
|
const targetValue = target.values.get(path);
|
|
338
338
|
const detail = baseValue && targetValue
|
|
339
|
-
?
|
|
339
|
+
? computeKxmResourcePermissionDiff(resourceKindForTemplatePath(path), path, baseValue, targetValue)
|
|
340
340
|
.map((change) => `${change.path} ${change.field} ${change.direction}`)
|
|
341
341
|
.join("; ")
|
|
342
342
|
: "";
|
|
@@ -403,8 +403,8 @@ export function planVnextTemplateRepair(
|
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
function operationPlanSha(operation: Omit<
|
|
407
|
-
return
|
|
406
|
+
function operationPlanSha(operation: Omit<KxmInitOperation, "planSha256" | "phase">): string {
|
|
407
|
+
return kxmContentSha256(JSON.stringify({
|
|
408
408
|
kind: operation.kind,
|
|
409
409
|
projectRoot: operation.projectRoot,
|
|
410
410
|
projectId: operation.projectId,
|
|
@@ -416,7 +416,7 @@ function operationPlanSha(operation: Omit<VnextInitOperation, "planSha256" | "ph
|
|
|
416
416
|
}));
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
function writeOperation(projectRoot: string, operation:
|
|
419
|
+
function writeOperation(projectRoot: string, operation: KxmInitOperation): void {
|
|
420
420
|
const root = transactionRoot(projectRoot);
|
|
421
421
|
const file = operationFile(projectRoot);
|
|
422
422
|
const temporary = join(root, `.operation-${operation.operationId}-${randomUUID()}.tmp`);
|
|
@@ -429,13 +429,13 @@ function writeOperation(projectRoot: string, operation: VnextInitOperation): voi
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
function updatePhase(projectRoot: string, operation:
|
|
432
|
+
function updatePhase(projectRoot: string, operation: KxmInitOperation, phase: KxmInitOperation["phase"]): KxmInitOperation {
|
|
433
433
|
const updated = { ...operation, phase };
|
|
434
434
|
writeOperation(projectRoot, updated);
|
|
435
435
|
return updated;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
function readOperation(projectRoot: string, schemasDir?: string):
|
|
438
|
+
function readOperation(projectRoot: string, schemasDir?: string): KxmInitOperation | undefined {
|
|
439
439
|
const root = transactionRoot(projectRoot);
|
|
440
440
|
if (!existsSync(root)) return undefined;
|
|
441
441
|
const stat = lstatSync(root);
|
|
@@ -448,10 +448,10 @@ function readOperation(projectRoot: string, schemasDir?: string): VnextInitOpera
|
|
|
448
448
|
fail("init_transaction_record_missing", TRANSACTION_NAME, "initialization transaction without an operation record contains unrecognized state");
|
|
449
449
|
}
|
|
450
450
|
const value = parseRestrictedYaml(readRegularBounded(file, `${TRANSACTION_NAME}/${OPERATION_FILE}`), `${TRANSACTION_NAME}/${OPERATION_FILE}`);
|
|
451
|
-
const registry = new
|
|
451
|
+
const registry = new KxmSchemaRegistry(schemasDir);
|
|
452
452
|
const issues = registry.validateInitOperation(value, `${TRANSACTION_NAME}/${OPERATION_FILE}`);
|
|
453
|
-
if (issues.length > 0) throw new
|
|
454
|
-
const operation = value as unknown as
|
|
453
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
454
|
+
const operation = value as unknown as KxmInitOperation;
|
|
455
455
|
if (!sameHostPath(operation.projectRoot, projectRoot)) fail("init_transaction_project_mismatch", TRANSACTION_NAME, "operation belongs to a different project root");
|
|
456
456
|
const expectedPlanSha = operationPlanSha(operation);
|
|
457
457
|
if (operation.planSha256 !== expectedPlanSha) fail("init_transaction_plan_hash_invalid", TRANSACTION_NAME, "operation plan hash does not match its immutable intent");
|
|
@@ -469,7 +469,7 @@ function readOperation(projectRoot: string, schemasDir?: string): VnextInitOpera
|
|
|
469
469
|
return operation;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
export function
|
|
472
|
+
export function hasKxmInitTransaction(projectRoot: string): boolean {
|
|
473
473
|
return existsSync(transactionRoot(projectRoot));
|
|
474
474
|
}
|
|
475
475
|
|
|
@@ -484,12 +484,12 @@ function writeTarget(projectRoot: string, path: string, bytes: Buffer): void {
|
|
|
484
484
|
writeDurableNew(file, bytes, 0o600);
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
function writeAndVerifyBackups(projectRoot: string, operation:
|
|
487
|
+
function writeAndVerifyBackups(projectRoot: string, operation: KxmInitOperation): void {
|
|
488
488
|
if (operation.kind !== "repair") return;
|
|
489
489
|
for (const entry of operation.files) {
|
|
490
490
|
if (entry.observedSha256 === null) continue;
|
|
491
491
|
const current = readOptionalManaged(projectRoot, entry.path);
|
|
492
|
-
if (!current ||
|
|
492
|
+
if (!current || kxmContentSha256(current) !== entry.observedSha256) {
|
|
493
493
|
fail("repair_preimage_changed", entry.path, "destination changed before transaction preparation; local bytes were preserved");
|
|
494
494
|
}
|
|
495
495
|
const file = backupFile(projectRoot, entry.path);
|
|
@@ -498,14 +498,14 @@ function writeAndVerifyBackups(projectRoot: string, operation: VnextInitOperatio
|
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
function writeKnownBackups(projectRoot: string, operation:
|
|
501
|
+
function writeKnownBackups(projectRoot: string, operation: KxmInitOperation): void {
|
|
502
502
|
if (operation.kind !== "repair" || !operation.sourceTemplateRevision) return;
|
|
503
503
|
const source = rendererForRevision(operation, operation.sourceTemplateRevision);
|
|
504
504
|
if (!source) fail("init_transaction_source_unknown", TRANSACTION_NAME, "operation source is not a supported built-in template");
|
|
505
505
|
for (const entry of operation.files) {
|
|
506
506
|
if (entry.observedSha256 === null) continue;
|
|
507
507
|
const bytes = source.files.get(entry.path);
|
|
508
|
-
if (!bytes ||
|
|
508
|
+
if (!bytes || kxmContentSha256(bytes) !== entry.observedSha256) {
|
|
509
509
|
fail("init_transaction_backup_source_invalid", entry.path, "supported source template cannot reproduce the pinned preimage");
|
|
510
510
|
}
|
|
511
511
|
const file = backupFile(projectRoot, entry.path);
|
|
@@ -514,29 +514,29 @@ function writeKnownBackups(projectRoot: string, operation: VnextInitOperation):
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
function verifyBackups(projectRoot: string, operation:
|
|
517
|
+
function verifyBackups(projectRoot: string, operation: KxmInitOperation): void {
|
|
518
518
|
if (operation.kind !== "repair") return;
|
|
519
519
|
for (const entry of operation.files) {
|
|
520
520
|
if (entry.observedSha256 === null) continue;
|
|
521
521
|
const bytes = readOptionalManaged(join(transactionRoot(projectRoot), "backups"), entry.path);
|
|
522
|
-
if (!bytes ||
|
|
522
|
+
if (!bytes || kxmContentSha256(bytes) !== entry.observedSha256) {
|
|
523
523
|
fail("init_transaction_backup_invalid", entry.path, "pinned preimage backup is missing or corrupt");
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
function verifyTargetArtifacts(projectRoot: string, operation:
|
|
528
|
+
function verifyTargetArtifacts(projectRoot: string, operation: KxmInitOperation): void {
|
|
529
529
|
let total = 0;
|
|
530
530
|
for (const entry of operation.files) {
|
|
531
531
|
const bytes = readTarget(projectRoot, entry.path);
|
|
532
532
|
if (!bytes) fail("init_transaction_target_missing", entry.path, "pinned target artifact is missing");
|
|
533
533
|
total += bytes.byteLength;
|
|
534
|
-
if (
|
|
534
|
+
if (kxmContentSha256(bytes) !== entry.targetSha256) fail("init_transaction_target_mismatch", entry.path, "pinned target artifact hash does not match the operation");
|
|
535
535
|
}
|
|
536
536
|
if (operation.files.length > MAX_MANAGED_FILES || total > MAX_MANAGED_TOTAL_BYTES) fail("init_transaction_bounds_exceeded", TRANSACTION_NAME, "operation exceeds bounded file or byte limits", "parse");
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
function loaderOptions(options:
|
|
539
|
+
function loaderOptions(options: KxmRepairRuntimeOptions): KxmConfigOptions {
|
|
540
540
|
assertNoRegisteredGates(options);
|
|
541
541
|
return {
|
|
542
542
|
...(options.schemasDir === undefined ? {} : { schemasDir: options.schemasDir }),
|
|
@@ -550,7 +550,7 @@ function sourceConfigFiles(projectRoot: string): string[] {
|
|
|
550
550
|
const fixed = [
|
|
551
551
|
".kxm/project.yaml",
|
|
552
552
|
".kxm/gates.yaml",
|
|
553
|
-
|
|
553
|
+
KXM_TEMPLATE_PROVENANCE_PATH,
|
|
554
554
|
".kxm/project/env.yaml",
|
|
555
555
|
".kxm/repo/repo.yaml",
|
|
556
556
|
".kxm/repo/env.yaml",
|
|
@@ -587,7 +587,7 @@ function absoluteMemberBindings(projectRoot: string, project: JsonObject, explic
|
|
|
587
587
|
return bindings;
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
function prepareShadow(projectRoot: string, operation:
|
|
590
|
+
function prepareShadow(projectRoot: string, operation: KxmInitOperation, options: KxmRepairRuntimeOptions): void {
|
|
591
591
|
const shadow = shadowRoot(projectRoot);
|
|
592
592
|
mkdirSync(shadow, { recursive: true, mode: 0o700 });
|
|
593
593
|
let total = 0;
|
|
@@ -613,20 +613,20 @@ function prepareShadow(projectRoot: string, operation: VnextInitOperation, optio
|
|
|
613
613
|
}
|
|
614
614
|
const project = parseRestrictedYaml(readFileSync(join(shadow, ".kxm", "project.yaml")), ".kxm/project.yaml");
|
|
615
615
|
const bound = absoluteMemberBindings(projectRoot, project, options.repositoryBindings);
|
|
616
|
-
|
|
616
|
+
loadKxmProject(shadow, { ...loaderOptions(options), repositoryBindings: bound });
|
|
617
617
|
const provenance = readProjectAndProvenance(shadow, options.schemasDir)?.provenance;
|
|
618
618
|
if (!provenance || provenance.templateRevision !== operation.targetTemplateRevision) {
|
|
619
|
-
fail("repair_shadow_provenance_invalid",
|
|
619
|
+
fail("repair_shadow_provenance_invalid", KXM_TEMPLATE_PROVENANCE_PATH, "prospective template provenance does not match the pinned target");
|
|
620
620
|
}
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
function createOperation(
|
|
624
624
|
projectRoot: string,
|
|
625
625
|
kind: "create" | "repair",
|
|
626
|
-
rendered:
|
|
627
|
-
files: readonly
|
|
626
|
+
rendered: KxmRenderedTemplate,
|
|
627
|
+
files: readonly KxmInitOperationFile[],
|
|
628
628
|
sourceTemplateRevision: string | null,
|
|
629
|
-
):
|
|
629
|
+
): KxmInitOperation {
|
|
630
630
|
const root = resolve(projectRoot);
|
|
631
631
|
const operationWithoutHash = {
|
|
632
632
|
schema: "kxm.init-operation.v1" as const,
|
|
@@ -635,7 +635,7 @@ function createOperation(
|
|
|
635
635
|
projectRoot: root,
|
|
636
636
|
projectId: rendered.projectId,
|
|
637
637
|
projectName: rendered.projectName,
|
|
638
|
-
templateId:
|
|
638
|
+
templateId: KXM_TEMPLATE_ID as typeof KXM_TEMPLATE_ID,
|
|
639
639
|
sourceTemplateRevision,
|
|
640
640
|
targetTemplateRevision: rendered.templateRevision,
|
|
641
641
|
files: [...files].sort((left, right) => compareCodeUnits(left.path, right.path)),
|
|
@@ -647,19 +647,19 @@ function createOperation(
|
|
|
647
647
|
};
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
function validateOperationRecord(operation:
|
|
650
|
+
function validateOperationRecord(operation: KxmInitOperation, schemasDir?: string): void {
|
|
651
651
|
const label = `${TRANSACTION_NAME}/${OPERATION_FILE}`;
|
|
652
652
|
const value = parseRestrictedYaml(Buffer.from(JSON.stringify(operation), "utf8"), label);
|
|
653
|
-
const issues = new
|
|
654
|
-
if (issues.length > 0) throw new
|
|
653
|
+
const issues = new KxmSchemaRegistry(schemasDir).validateInitOperation(value, label);
|
|
654
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
function prepareOperation(
|
|
658
658
|
projectRoot: string,
|
|
659
|
-
operation:
|
|
660
|
-
rendered:
|
|
661
|
-
options:
|
|
662
|
-
):
|
|
659
|
+
operation: KxmInitOperation,
|
|
660
|
+
rendered: KxmRenderedTemplate,
|
|
661
|
+
options: KxmRepairRuntimeOptions,
|
|
662
|
+
): KxmInitOperation {
|
|
663
663
|
const transaction = transactionRoot(projectRoot);
|
|
664
664
|
validateOperationRecord(operation, options.schemasDir);
|
|
665
665
|
validateOperationIntent(projectRoot, operation);
|
|
@@ -670,7 +670,7 @@ function prepareOperation(
|
|
|
670
670
|
try {
|
|
671
671
|
for (const entry of operation.files) {
|
|
672
672
|
const bytes = rendered.files.get(entry.path);
|
|
673
|
-
if (!bytes ||
|
|
673
|
+
if (!bytes || kxmContentSha256(bytes) !== entry.targetSha256) fail("template_target_unavailable", entry.path, "renderer cannot reproduce the pinned target bytes");
|
|
674
674
|
writeTarget(projectRoot, entry.path, bytes);
|
|
675
675
|
}
|
|
676
676
|
writeAndVerifyBackups(projectRoot, operation);
|
|
@@ -700,7 +700,7 @@ function ensureDestinationParents(projectRoot: string, path: string): void {
|
|
|
700
700
|
function destinationSha(projectRoot: string, path: string): string | undefined {
|
|
701
701
|
const file = join(projectRoot, ...path.split("/"));
|
|
702
702
|
if (!existsSync(file)) return undefined;
|
|
703
|
-
return
|
|
703
|
+
return kxmContentSha256(readRegularBounded(file, path));
|
|
704
704
|
}
|
|
705
705
|
|
|
706
706
|
function requireConditionalLinkSupport(directory: string, operationId: string, path: string): void {
|
|
@@ -718,17 +718,17 @@ function requireConditionalLinkSupport(directory: string, operationId: string, p
|
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
-
function atomicInstallTarget(projectRoot: string, operation:
|
|
721
|
+
function atomicInstallTarget(projectRoot: string, operation: KxmInitOperation, entry: KxmInitOperationFile): void {
|
|
722
722
|
const destination = join(projectRoot, ...entry.path.split("/"));
|
|
723
723
|
ensureDestinationParents(projectRoot, entry.path);
|
|
724
724
|
const directory = dirname(destination);
|
|
725
725
|
const temporaryPrefix = `.kxm-repair-${operation.operationId}-`;
|
|
726
|
-
const pathKey =
|
|
726
|
+
const pathKey = kxmContentSha256(entry.path).slice("sha256:".length, "sha256:".length + 16);
|
|
727
727
|
const displaced = join(directory, `${temporaryPrefix}${pathKey}.preimage`);
|
|
728
728
|
if (existsSync(displaced)) {
|
|
729
729
|
const stat = lstatSync(displaced);
|
|
730
730
|
if (stat.isSymbolicLink() || !stat.isFile()) fail("repair_preimage_temporary_invalid", entry.path, "stale displaced preimage is not a regular file", "path");
|
|
731
|
-
if (entry.observedSha256 === null ||
|
|
731
|
+
if (entry.observedSha256 === null || kxmContentSha256(readFileSync(displaced)) !== entry.observedSha256) {
|
|
732
732
|
fail("repair_preimage_temporary_mismatch", entry.path, "stale displaced preimage does not match the pinned operation");
|
|
733
733
|
}
|
|
734
734
|
if (!existsSync(destination)) {
|
|
@@ -762,7 +762,7 @@ function atomicInstallTarget(projectRoot: string, operation: VnextInitOperation,
|
|
|
762
762
|
if (entry.observedSha256 !== null) {
|
|
763
763
|
renameSync(destination, displaced);
|
|
764
764
|
syncDirectory(directory);
|
|
765
|
-
if (
|
|
765
|
+
if (kxmContentSha256(readFileSync(displaced)) !== entry.observedSha256) {
|
|
766
766
|
if (!existsSync(destination)) {
|
|
767
767
|
try { linkSync(displaced, destination); }
|
|
768
768
|
catch { fail("repair_preimage_restore_blocked", entry.path, "changed destination was displaced but could not be conditionally restored; its bytes remain in the operation preimage file"); }
|
|
@@ -795,7 +795,7 @@ function atomicInstallTarget(projectRoot: string, operation: VnextInitOperation,
|
|
|
795
795
|
if (destinationSha(projectRoot, entry.path) !== entry.targetSha256) fail("repair_install_verification_failed", entry.path, "installed target hash does not match the pinned operation");
|
|
796
796
|
}
|
|
797
797
|
|
|
798
|
-
function installedCreateMatches(projectRoot: string, operation:
|
|
798
|
+
function installedCreateMatches(projectRoot: string, operation: KxmInitOperation): boolean {
|
|
799
799
|
const configRoot = join(projectRoot, ".kxm");
|
|
800
800
|
if (!existsSync(configRoot)) return false;
|
|
801
801
|
const actual: string[] = [];
|
|
@@ -833,7 +833,7 @@ function syncTreeDirectories(root: string): void {
|
|
|
833
833
|
for (const directory of directories.reverse()) syncDirectory(directory);
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
-
function rebuildCreateShadow(projectRoot: string, operation:
|
|
836
|
+
function rebuildCreateShadow(projectRoot: string, operation: KxmInitOperation, options: KxmRepairRuntimeOptions): void {
|
|
837
837
|
const shadow = shadowRoot(projectRoot);
|
|
838
838
|
rmSync(shadow, { recursive: true, force: true });
|
|
839
839
|
prepareShadow(projectRoot, operation, options);
|
|
@@ -846,10 +846,10 @@ function rebuildCreateShadow(projectRoot: string, operation: VnextInitOperation,
|
|
|
846
846
|
|
|
847
847
|
function applyOperation(
|
|
848
848
|
projectRoot: string,
|
|
849
|
-
original:
|
|
850
|
-
options:
|
|
849
|
+
original: KxmInitOperation,
|
|
850
|
+
options: KxmRepairRuntimeOptions,
|
|
851
851
|
resumed: boolean,
|
|
852
|
-
):
|
|
852
|
+
): KxmOperationResult {
|
|
853
853
|
assertNoRegisteredGates(options);
|
|
854
854
|
let operation = original;
|
|
855
855
|
const effectiveOptions = options;
|
|
@@ -866,23 +866,23 @@ function applyOperation(
|
|
|
866
866
|
fail("create_resume_conflict", ".kxm", "existing project configuration does not match the pinned create operation");
|
|
867
867
|
}
|
|
868
868
|
} else {
|
|
869
|
-
const resources = operation.files.filter((entry) => entry.path !==
|
|
869
|
+
const resources = operation.files.filter((entry) => entry.path !== KXM_TEMPLATE_PROVENANCE_PATH && entry.action !== "none");
|
|
870
870
|
let installed = 0;
|
|
871
871
|
for (const entry of resources) {
|
|
872
872
|
atomicInstallTarget(projectRoot, operation, entry);
|
|
873
873
|
installed += 1;
|
|
874
874
|
if (installed === 1 && options.testFaultAt === "first-resource") throw new Error("injected init fault after first resource");
|
|
875
875
|
}
|
|
876
|
-
|
|
877
|
-
const provenance = operation.files.find((entry) => entry.path ===
|
|
876
|
+
loadKxmProject(projectRoot, loaderOptions(effectiveOptions));
|
|
877
|
+
const provenance = operation.files.find((entry) => entry.path === KXM_TEMPLATE_PROVENANCE_PATH);
|
|
878
878
|
if (!provenance) fail("repair_provenance_target_missing", TRANSACTION_NAME, "repair operation has no final provenance target");
|
|
879
879
|
atomicInstallTarget(projectRoot, operation, provenance);
|
|
880
880
|
if (options.testFaultAt === "provenance") throw new Error("injected init fault after provenance");
|
|
881
881
|
}
|
|
882
|
-
const bundle =
|
|
882
|
+
const bundle = loadKxmProject(projectRoot, loaderOptions(effectiveOptions));
|
|
883
883
|
const source = readProjectAndProvenance(projectRoot, effectiveOptions.schemasDir);
|
|
884
884
|
if (!source || source.provenance.templateRevision !== operation.targetTemplateRevision) {
|
|
885
|
-
fail("init_operation_verification_failed",
|
|
885
|
+
fail("init_operation_verification_failed", KXM_TEMPLATE_PROVENANCE_PATH, "installed provenance does not match the pinned transaction");
|
|
886
886
|
}
|
|
887
887
|
operation = updatePhase(projectRoot, operation, "verified");
|
|
888
888
|
if (options.testFaultAt === "verified") throw new Error("injected init fault after verification");
|
|
@@ -891,16 +891,16 @@ function applyOperation(
|
|
|
891
891
|
return { kind: operation.kind, resumed, bundle, files };
|
|
892
892
|
}
|
|
893
893
|
|
|
894
|
-
export function
|
|
894
|
+
export function prepareAndApplyKxmCreate(
|
|
895
895
|
projectRoot: string,
|
|
896
|
-
rendered:
|
|
897
|
-
options:
|
|
898
|
-
):
|
|
896
|
+
rendered: KxmRenderedTemplate,
|
|
897
|
+
options: KxmRepairRuntimeOptions = {},
|
|
898
|
+
): KxmOperationResult {
|
|
899
899
|
assertNoRegisteredGates(options);
|
|
900
|
-
const files = [...rendered.files.entries()].map(([path, bytes]):
|
|
900
|
+
const files = [...rendered.files.entries()].map(([path, bytes]): KxmInitOperationFile => ({
|
|
901
901
|
path,
|
|
902
902
|
observedSha256: null,
|
|
903
|
-
targetSha256:
|
|
903
|
+
targetSha256: kxmContentSha256(bytes),
|
|
904
904
|
classification: "template-only",
|
|
905
905
|
action: "create",
|
|
906
906
|
}));
|
|
@@ -910,20 +910,20 @@ export function prepareAndApplyVnextCreate(
|
|
|
910
910
|
return applyOperation(projectRoot, prepared, options, false);
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
-
export function
|
|
914
|
-
plan:
|
|
915
|
-
options:
|
|
916
|
-
):
|
|
913
|
+
export function prepareAndApplyKxmRepair(
|
|
914
|
+
plan: KxmTemplateRepairPlan,
|
|
915
|
+
options: KxmRepairRuntimeOptions = {},
|
|
916
|
+
): KxmOperationResult {
|
|
917
917
|
assertNoRegisteredGates(options);
|
|
918
918
|
if (!plan.canApply) fail("template_repair_not_applicable", ".kxm", "template repair has conflicts, policy changes, or no safe template-only update");
|
|
919
|
-
const rendered =
|
|
919
|
+
const rendered = renderKxmTemplate(plan.projectId, plan.projectName, options.templateVariant ?? CURRENT_KXM_TEMPLATE_VARIANT);
|
|
920
920
|
if (rendered.templateRevision !== plan.targetTemplateRevision) fail("template_repair_target_changed", ".kxm", "template target changed after planning");
|
|
921
921
|
const actionable = plan.changes.filter((change) => change.classification === "template-only");
|
|
922
|
-
const provenanceBytes = rendered.files.get(
|
|
923
|
-
if (!provenanceBytes) fail("template_provenance_target_missing",
|
|
924
|
-
const sourceProvenance = readOptionalManaged(plan.projectRoot,
|
|
925
|
-
if (!sourceProvenance) fail("template_provenance_missing",
|
|
926
|
-
const files:
|
|
922
|
+
const provenanceBytes = rendered.files.get(KXM_TEMPLATE_PROVENANCE_PATH);
|
|
923
|
+
if (!provenanceBytes) fail("template_provenance_target_missing", KXM_TEMPLATE_PROVENANCE_PATH, "renderer did not produce provenance");
|
|
924
|
+
const sourceProvenance = readOptionalManaged(plan.projectRoot, KXM_TEMPLATE_PROVENANCE_PATH);
|
|
925
|
+
if (!sourceProvenance) fail("template_provenance_missing", KXM_TEMPLATE_PROVENANCE_PATH, "repair requires recorded provenance");
|
|
926
|
+
const files: KxmInitOperationFile[] = actionable.map((change) => ({
|
|
927
927
|
path: change.path,
|
|
928
928
|
observedSha256: change.localSha256 ?? null,
|
|
929
929
|
targetSha256: change.targetSha256 as string,
|
|
@@ -931,9 +931,9 @@ export function prepareAndApplyVnextRepair(
|
|
|
931
931
|
action: change.action,
|
|
932
932
|
}));
|
|
933
933
|
files.push({
|
|
934
|
-
path:
|
|
935
|
-
observedSha256:
|
|
936
|
-
targetSha256:
|
|
934
|
+
path: KXM_TEMPLATE_PROVENANCE_PATH,
|
|
935
|
+
observedSha256: kxmContentSha256(sourceProvenance),
|
|
936
|
+
targetSha256: kxmContentSha256(provenanceBytes),
|
|
937
937
|
classification: "template-only",
|
|
938
938
|
action: "replace",
|
|
939
939
|
});
|
|
@@ -943,20 +943,20 @@ export function prepareAndApplyVnextRepair(
|
|
|
943
943
|
return applyOperation(plan.projectRoot, prepared, options, false);
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
-
function rendererForRevision(operation:
|
|
947
|
-
for (const variant of
|
|
948
|
-
const rendered =
|
|
946
|
+
function rendererForRevision(operation: KxmInitOperation, revision: string): KxmRenderedTemplate | undefined {
|
|
947
|
+
for (const variant of SUPPORTED_KXM_TEMPLATE_VARIANTS) {
|
|
948
|
+
const rendered = renderKxmTemplate(operation.projectId, operation.projectName, variant);
|
|
949
949
|
if (rendered.templateRevision === revision) return rendered;
|
|
950
950
|
}
|
|
951
951
|
return undefined;
|
|
952
952
|
}
|
|
953
953
|
|
|
954
|
-
function rendererForOperation(operation:
|
|
954
|
+
function rendererForOperation(operation: KxmInitOperation): KxmRenderedTemplate | undefined {
|
|
955
955
|
return rendererForRevision(operation, operation.targetTemplateRevision);
|
|
956
956
|
}
|
|
957
957
|
|
|
958
958
|
/** Re-derive every executable operation field from immutable built-in templates. */
|
|
959
|
-
function validateOperationIntent(projectRoot: string, operation:
|
|
959
|
+
function validateOperationIntent(projectRoot: string, operation: KxmInitOperation): void {
|
|
960
960
|
const target = rendererForOperation(operation);
|
|
961
961
|
if (!target) fail("init_transaction_target_unknown", TRANSACTION_NAME, "operation target is not a supported built-in template");
|
|
962
962
|
const entries = new Map(operation.files.map((entry) => [entry.path, entry]));
|
|
@@ -969,7 +969,7 @@ function validateOperationIntent(projectRoot: string, operation: VnextInitOperat
|
|
|
969
969
|
const entry = entries.get(path);
|
|
970
970
|
if (!entry
|
|
971
971
|
|| entry.observedSha256 !== null
|
|
972
|
-
|| entry.targetSha256 !==
|
|
972
|
+
|| entry.targetSha256 !== kxmContentSha256(bytes)
|
|
973
973
|
|| entry.classification !== "template-only"
|
|
974
974
|
|| entry.action !== "create") {
|
|
975
975
|
fail("init_transaction_create_file_invalid", path, "create operation file does not match its built-in target");
|
|
@@ -997,18 +997,18 @@ function validateOperationIntent(projectRoot: string, operation: VnextInitOperat
|
|
|
997
997
|
}
|
|
998
998
|
if (base.sha256 !== next.sha256) safeChangedPaths.push(path);
|
|
999
999
|
}
|
|
1000
|
-
const provenanceEntry = entries.get(
|
|
1001
|
-
const sourceProvenanceBytes = source.files.get(
|
|
1002
|
-
const targetProvenanceBytes = target.files.get(
|
|
1000
|
+
const provenanceEntry = entries.get(KXM_TEMPLATE_PROVENANCE_PATH);
|
|
1001
|
+
const sourceProvenanceBytes = source.files.get(KXM_TEMPLATE_PROVENANCE_PATH) as Buffer;
|
|
1002
|
+
const targetProvenanceBytes = target.files.get(KXM_TEMPLATE_PROVENANCE_PATH) as Buffer;
|
|
1003
1003
|
if (!provenanceEntry
|
|
1004
|
-
|| provenanceEntry.observedSha256 !==
|
|
1005
|
-
|| provenanceEntry.targetSha256 !==
|
|
1004
|
+
|| provenanceEntry.observedSha256 !== kxmContentSha256(sourceProvenanceBytes)
|
|
1005
|
+
|| provenanceEntry.targetSha256 !== kxmContentSha256(targetProvenanceBytes)
|
|
1006
1006
|
|| provenanceEntry.classification !== "template-only"
|
|
1007
1007
|
|| provenanceEntry.action !== "replace") {
|
|
1008
|
-
fail("init_transaction_provenance_intent_invalid",
|
|
1008
|
+
fail("init_transaction_provenance_intent_invalid", KXM_TEMPLATE_PROVENANCE_PATH, "repair provenance action does not match supported source and target templates");
|
|
1009
1009
|
}
|
|
1010
1010
|
for (const [path, entry] of entries) {
|
|
1011
|
-
if (path ===
|
|
1011
|
+
if (path === KXM_TEMPLATE_PROVENANCE_PATH) continue;
|
|
1012
1012
|
const base = sourceRecords.get(path);
|
|
1013
1013
|
const next = targetRecords.get(path);
|
|
1014
1014
|
if (!base || !next || base.sha256 === next.sha256
|
|
@@ -1028,9 +1028,9 @@ function validateOperationIntent(projectRoot: string, operation: VnextInitOperat
|
|
|
1028
1028
|
|
|
1029
1029
|
function finishPreparingOperation(
|
|
1030
1030
|
projectRoot: string,
|
|
1031
|
-
operation:
|
|
1032
|
-
options:
|
|
1033
|
-
):
|
|
1031
|
+
operation: KxmInitOperation,
|
|
1032
|
+
options: KxmRepairRuntimeOptions,
|
|
1033
|
+
): KxmInitOperation {
|
|
1034
1034
|
const rendered = rendererForOperation(operation);
|
|
1035
1035
|
if (!rendered) fail("init_transaction_renderer_unavailable", TRANSACTION_NAME, "this binary cannot reproduce the pinned pre-application target");
|
|
1036
1036
|
for (const directory of ["targets", "backups", "shadow"]) {
|
|
@@ -1038,7 +1038,7 @@ function finishPreparingOperation(
|
|
|
1038
1038
|
}
|
|
1039
1039
|
for (const entry of operation.files) {
|
|
1040
1040
|
const bytes = rendered.files.get(entry.path);
|
|
1041
|
-
if (!bytes ||
|
|
1041
|
+
if (!bytes || kxmContentSha256(bytes) !== entry.targetSha256) fail("template_target_unavailable", entry.path, "renderer cannot reproduce the pinned target bytes");
|
|
1042
1042
|
writeTarget(projectRoot, entry.path, bytes);
|
|
1043
1043
|
}
|
|
1044
1044
|
writeKnownBackups(projectRoot, operation);
|
|
@@ -1050,10 +1050,10 @@ function finishPreparingOperation(
|
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
1052
|
/** Resume only a validated fixed transaction; filesystem truth wins over its phase hint. */
|
|
1053
|
-
export function
|
|
1053
|
+
export function resumeKxmInitTransaction(
|
|
1054
1054
|
projectRoot: string,
|
|
1055
|
-
options:
|
|
1056
|
-
):
|
|
1055
|
+
options: KxmRepairRuntimeOptions = {},
|
|
1056
|
+
): KxmOperationResult | undefined {
|
|
1057
1057
|
assertNoRegisteredGates(options);
|
|
1058
1058
|
let operation = readOperation(projectRoot, options.schemasDir);
|
|
1059
1059
|
if (!operation) {
|
|
@@ -1070,7 +1070,7 @@ export function resumeVnextInitTransaction(
|
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
1072
|
/** Remove a verified transaction only after associated local binding intent is durable. */
|
|
1073
|
-
export function
|
|
1073
|
+
export function commitKxmInitTransaction(projectRoot: string, schemasDir?: string): void {
|
|
1074
1074
|
const operation = readOperation(projectRoot, schemasDir);
|
|
1075
1075
|
if (!operation || operation.phase !== "verified") {
|
|
1076
1076
|
fail("init_transaction_not_verified", TRANSACTION_NAME, "initialization transaction cannot commit before installed state is verified");
|
|
@@ -1084,7 +1084,7 @@ export function commitVnextInitTransaction(projectRoot: string, schemasDir?: str
|
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
1086
|
/** Validate a pending transaction for dry-run reporting without writing or cleanup. */
|
|
1087
|
-
export function
|
|
1087
|
+
export function inspectKxmInitTransaction(projectRoot: string, schemasDir?: string): KxmInitOperation | undefined {
|
|
1088
1088
|
const operation = readOperation(projectRoot, schemasDir);
|
|
1089
1089
|
if (operation && operation.phase !== "preparing") {
|
|
1090
1090
|
verifyTargetArtifacts(projectRoot, operation);
|