@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
|
@@ -5,65 +5,65 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
|
|
|
5
5
|
import { Ajv2020, type ErrorObject, type ValidateFunction } from "ajv/dist/2020.js";
|
|
6
6
|
import {
|
|
7
7
|
RestrictedYamlError,
|
|
8
|
-
|
|
8
|
+
KXM_YAML_LIMITS,
|
|
9
9
|
parseRestrictedYaml as parseRestrictedYamlShared,
|
|
10
|
-
type
|
|
10
|
+
type KxmYamlLimits,
|
|
11
11
|
} from "./restricted-yaml.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveKxmTemplateBaseline } from "./template.ts";
|
|
13
13
|
import { findKxmRepoRoot } from "./repo-root.ts";
|
|
14
|
-
import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS, validateHarnessModelPair } from "./
|
|
14
|
+
import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS, validateHarnessModelPair } from "./harness.ts";
|
|
15
15
|
|
|
16
16
|
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
17
17
|
export type JsonObject = { [key: string]: JsonValue };
|
|
18
|
-
export type {
|
|
19
|
-
export {
|
|
18
|
+
export type { KxmYamlLimits };
|
|
19
|
+
export { KXM_YAML_LIMITS };
|
|
20
20
|
|
|
21
|
-
export type
|
|
21
|
+
export type KxmValidationPhase = "discovery" | "parse" | "schema" | "path" | "reference" | "semantic";
|
|
22
22
|
|
|
23
|
-
export interface
|
|
24
|
-
phase:
|
|
23
|
+
export interface KxmConfigIssue {
|
|
24
|
+
phase: KxmValidationPhase;
|
|
25
25
|
code: string;
|
|
26
26
|
file: string;
|
|
27
27
|
message: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export class
|
|
31
|
-
readonly issues: readonly
|
|
30
|
+
export class KxmConfigError extends Error {
|
|
31
|
+
readonly issues: readonly KxmConfigIssue[];
|
|
32
32
|
|
|
33
|
-
constructor(issues: readonly
|
|
33
|
+
constructor(issues: readonly KxmConfigIssue[]) {
|
|
34
34
|
const sorted = sortIssues(issues);
|
|
35
35
|
super(sorted.map((issue) => `${issue.file}: ${issue.code}: ${issue.message}`).join("\n"));
|
|
36
|
-
this.name = "
|
|
36
|
+
this.name = "KxmConfigError";
|
|
37
37
|
this.issues = sorted;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export type
|
|
41
|
+
export type KxmResourceKind = "project" | "repository" | "agent" | "model" | "environment" | "workflow" | "gate-registry";
|
|
42
42
|
|
|
43
|
-
export interface
|
|
44
|
-
kind:
|
|
43
|
+
export interface KxmResource {
|
|
44
|
+
kind: KxmResourceKind;
|
|
45
45
|
id?: string;
|
|
46
46
|
file: string;
|
|
47
47
|
logicalPath: string;
|
|
48
48
|
value: JsonObject;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export interface
|
|
51
|
+
export interface KxmProjectBundle {
|
|
52
52
|
projectRoot: string;
|
|
53
|
-
project:
|
|
54
|
-
repositories: ReadonlyMap<string,
|
|
55
|
-
agents: ReadonlyMap<string,
|
|
56
|
-
models: ReadonlyMap<string,
|
|
57
|
-
workflows: ReadonlyMap<string,
|
|
58
|
-
environments: readonly
|
|
59
|
-
gateRegistry?:
|
|
53
|
+
project: KxmResource;
|
|
54
|
+
repositories: ReadonlyMap<string, KxmResource>;
|
|
55
|
+
agents: ReadonlyMap<string, KxmResource>;
|
|
56
|
+
models: ReadonlyMap<string, KxmResource>;
|
|
57
|
+
workflows: ReadonlyMap<string, KxmResource>;
|
|
58
|
+
environments: readonly KxmResource[];
|
|
59
|
+
gateRegistry?: KxmResource;
|
|
60
60
|
templateProvenance?: JsonObject;
|
|
61
61
|
migrationReceipt?: JsonObject;
|
|
62
|
-
resources: readonly
|
|
62
|
+
resources: readonly KxmResource[];
|
|
63
63
|
configRevision: string;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export interface
|
|
66
|
+
export interface KxmConfigOptions {
|
|
67
67
|
schemasDir?: string;
|
|
68
68
|
repositoryBindings?: Readonly<Record<string, string>>;
|
|
69
69
|
registeredExecutors?: Iterable<string>;
|
|
@@ -73,24 +73,24 @@ export interface VnextConfigOptions {
|
|
|
73
73
|
allowUnreceiptedLegacyConfig?: boolean;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export type
|
|
76
|
+
export type KxmInitializationMode = "create" | "migrate" | "repair" | "ready";
|
|
77
77
|
|
|
78
|
-
export interface
|
|
79
|
-
mode:
|
|
78
|
+
export interface KxmInitializationPlan {
|
|
79
|
+
mode: KxmInitializationMode;
|
|
80
80
|
inspectedFrom: string;
|
|
81
81
|
projectRoot?: string;
|
|
82
82
|
legacyRoot?: string;
|
|
83
83
|
changesRequired: boolean;
|
|
84
|
-
issues: readonly
|
|
84
|
+
issues: readonly KxmConfigIssue[];
|
|
85
85
|
legacyInputs: readonly string[];
|
|
86
86
|
configRevision?: string;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function
|
|
90
|
-
return join(findKxmRepoRoot(import.meta.url), "schemas"
|
|
89
|
+
function defaultKxmSchemaDir(): string {
|
|
90
|
+
return join(findKxmRepoRoot(import.meta.url), "schemas");
|
|
91
91
|
}
|
|
92
|
-
const DEFAULT_SCHEMA_DIR =
|
|
93
|
-
const RESOURCE_SCHEMA: Readonly<Record<
|
|
92
|
+
const DEFAULT_SCHEMA_DIR = defaultKxmSchemaDir();
|
|
93
|
+
const RESOURCE_SCHEMA: Readonly<Record<KxmResourceKind, { identity: string; file: string }>> = Object.freeze({
|
|
94
94
|
project: { identity: "kxm.project.v1", file: "project.schema.json" },
|
|
95
95
|
repository: { identity: "kxm.repository.v1", file: "repository.schema.json" },
|
|
96
96
|
agent: { identity: "kxm.agent.v1", file: "agent.schema.json" },
|
|
@@ -115,35 +115,35 @@ function compareCodeUnits(left: string, right: string): number {
|
|
|
115
115
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function sortIssues(issues: readonly
|
|
118
|
+
function sortIssues(issues: readonly KxmConfigIssue[]): KxmConfigIssue[] {
|
|
119
119
|
return [...issues].sort((left, right) => compareCodeUnits(left.file, right.file)
|
|
120
120
|
|| compareCodeUnits(left.phase, right.phase)
|
|
121
121
|
|| compareCodeUnits(left.code, right.code)
|
|
122
122
|
|| compareCodeUnits(left.message, right.message));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function issue(phase:
|
|
125
|
+
function issue(phase: KxmValidationPhase, code: string, file: string, message: string): KxmConfigIssue {
|
|
126
126
|
return { phase, code, file, message };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function fail(phase:
|
|
130
|
-
throw new
|
|
129
|
+
function fail(phase: KxmValidationPhase, code: string, file: string, message: string): never {
|
|
130
|
+
throw new KxmConfigError([issue(phase, code, file, message)]);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function isJsonObject(value: JsonValue): value is JsonObject {
|
|
134
134
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** Parse the restricted, JSON-compatible YAML profile used by all
|
|
137
|
+
/** Parse the restricted, JSON-compatible YAML profile used by all KXM configuration. */
|
|
138
138
|
export function parseRestrictedYaml(
|
|
139
139
|
input: string | Uint8Array,
|
|
140
140
|
label = "<yaml>",
|
|
141
|
-
limits: Readonly<
|
|
141
|
+
limits: Readonly<KxmYamlLimits> = KXM_YAML_LIMITS,
|
|
142
142
|
): JsonObject {
|
|
143
143
|
try {
|
|
144
144
|
return parseRestrictedYamlShared(input, label, limits);
|
|
145
145
|
} catch (error) {
|
|
146
|
-
if (error instanceof RestrictedYamlError) throw new
|
|
146
|
+
if (error instanceof RestrictedYamlError) throw new KxmConfigError(error.issues);
|
|
147
147
|
throw error;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -154,10 +154,10 @@ function readJsonObject(file: string): Record<string, unknown> {
|
|
|
154
154
|
return parsed as Record<string, unknown>;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
export class
|
|
157
|
+
export class KxmSchemaRegistry {
|
|
158
158
|
readonly schemasDir: string;
|
|
159
159
|
readonly ajv: Ajv2020;
|
|
160
|
-
readonly validators = new Map<
|
|
160
|
+
readonly validators = new Map<KxmResourceKind, ValidateFunction>();
|
|
161
161
|
readonly localBindingsValidator: ValidateFunction;
|
|
162
162
|
readonly templateProvenanceValidator: ValidateFunction;
|
|
163
163
|
readonly initOperationValidator: ValidateFunction;
|
|
@@ -194,20 +194,20 @@ export class VnextSchemaRegistry {
|
|
|
194
194
|
this.ajv.addSchema(readJsonObject(join(this.schemasDir, permissionDiffFile)));
|
|
195
195
|
this.ajv.addSchema(readJsonObject(join(this.schemasDir, runEventFile)));
|
|
196
196
|
this.ajv.addSchema(readJsonObject(join(this.schemasDir, driveReceiptFile)));
|
|
197
|
-
for (const [kind, definition] of Object.entries(RESOURCE_SCHEMA) as [
|
|
198
|
-
const validator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
197
|
+
for (const [kind, definition] of Object.entries(RESOURCE_SCHEMA) as [KxmResourceKind, { identity: string; file: string }][]) {
|
|
198
|
+
const validator = this.ajv.getSchema(`https://schemas.kxm.dev/${definition.file}`);
|
|
199
199
|
if (!validator) throw new Error(`schema did not compile: ${definition.file}`);
|
|
200
200
|
this.validators.set(kind, validator);
|
|
201
201
|
}
|
|
202
|
-
const localBindingsValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
203
|
-
const templateProvenanceValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
204
|
-
const initOperationValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
205
|
-
const migrationPlanValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
206
|
-
const migrationDecisionValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
207
|
-
const migrationReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
208
|
-
const permissionDiffValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
209
|
-
const runEventValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
210
|
-
const driveReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev
|
|
202
|
+
const localBindingsValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${localBindingsFile}`);
|
|
203
|
+
const templateProvenanceValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${templateProvenanceFile}`);
|
|
204
|
+
const initOperationValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${initOperationFile}`);
|
|
205
|
+
const migrationPlanValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${migrationPlanFile}`);
|
|
206
|
+
const migrationDecisionValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${migrationDecisionFile}`);
|
|
207
|
+
const migrationReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${migrationReceiptFile}`);
|
|
208
|
+
const permissionDiffValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${permissionDiffFile}`);
|
|
209
|
+
const runEventValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${runEventFile}`);
|
|
210
|
+
const driveReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${driveReceiptFile}`);
|
|
211
211
|
if (!localBindingsValidator) throw new Error(`schema did not compile: ${localBindingsFile}`);
|
|
212
212
|
if (!templateProvenanceValidator) throw new Error(`schema did not compile: ${templateProvenanceFile}`);
|
|
213
213
|
if (!initOperationValidator) throw new Error(`schema did not compile: ${initOperationFile}`);
|
|
@@ -228,50 +228,50 @@ export class VnextSchemaRegistry {
|
|
|
228
228
|
this.driveReceiptValidator = driveReceiptValidator;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
validate(kind:
|
|
231
|
+
validate(kind: KxmResourceKind, value: JsonObject, file: string): KxmConfigIssue[] {
|
|
232
232
|
const definition = RESOURCE_SCHEMA[kind];
|
|
233
233
|
if (value.schema !== definition.identity) {
|
|
234
234
|
return [issue("schema", "schema_identity_mismatch", file, `expected ${definition.identity}, received ${String(value.schema)}`)];
|
|
235
235
|
}
|
|
236
236
|
const validator = this.validators.get(kind);
|
|
237
|
-
if (!validator) throw new Error(`missing
|
|
237
|
+
if (!validator) throw new Error(`missing KXM validator for ${kind}`);
|
|
238
238
|
if (validator(value)) return [];
|
|
239
239
|
return (validator.errors ?? []).map((error) => schemaIssue(file, error));
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
validateLocalBindings(value: JsonObject, file: string):
|
|
242
|
+
validateLocalBindings(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
243
243
|
return this.validateAuxiliary(value, file, "kxm.local-repository-bindings.v1", this.localBindingsValidator);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
validateTemplateProvenance(value: JsonObject, file: string):
|
|
246
|
+
validateTemplateProvenance(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
247
247
|
return this.validateAuxiliary(value, file, "kxm.template-provenance.v1", this.templateProvenanceValidator);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
validateInitOperation(value: JsonObject, file: string):
|
|
250
|
+
validateInitOperation(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
251
251
|
return this.validateAuxiliary(value, file, "kxm.init-operation.v1", this.initOperationValidator);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
validateMigrationPlan(value: JsonObject, file: string):
|
|
254
|
+
validateMigrationPlan(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
255
255
|
return this.validateAuxiliary(value, file, "kxm.migration-plan.v1", this.migrationPlanValidator);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
validateMigrationDecision(value: JsonObject, file: string):
|
|
258
|
+
validateMigrationDecision(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
259
259
|
return this.validateAuxiliary(value, file, "kxm.migration-decision.v1", this.migrationDecisionValidator);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
validateMigrationReceipt(value: JsonObject, file: string):
|
|
262
|
+
validateMigrationReceipt(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
263
263
|
return this.validateAuxiliary(value, file, "kxm.migration-receipt.v1", this.migrationReceiptValidator);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
validatePermissionDiff(value: JsonObject, file: string):
|
|
266
|
+
validatePermissionDiff(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
267
267
|
return this.validateAuxiliary(value, file, "kxm.permission-diff.v1", this.permissionDiffValidator);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
validateDriveReceipt(value: JsonObject, file: string):
|
|
270
|
+
validateDriveReceipt(value: JsonObject, file: string): KxmConfigIssue[] {
|
|
271
271
|
return this.validateAuxiliary(value, file, "kxm.drive-receipt.v1", this.driveReceiptValidator);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
private validateAuxiliary(value: JsonObject, file: string, identity: string, validator: ValidateFunction):
|
|
274
|
+
private validateAuxiliary(value: JsonObject, file: string, identity: string, validator: ValidateFunction): KxmConfigIssue[] {
|
|
275
275
|
if (value.schema !== identity) {
|
|
276
276
|
return [issue("schema", "schema_identity_mismatch", file, `expected ${identity}, received ${String(value.schema)}`)];
|
|
277
277
|
}
|
|
@@ -280,13 +280,13 @@ export class VnextSchemaRegistry {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
let cachedRunEventRegistry:
|
|
283
|
+
let cachedRunEventRegistry: KxmSchemaRegistry | undefined;
|
|
284
284
|
|
|
285
285
|
/** Validate a local run event against `kxm.run-event.v1`. Throws `run_event_invalid`. */
|
|
286
286
|
export function validateRunEvent(value: unknown, file: string): void {
|
|
287
|
-
const registry = (cachedRunEventRegistry ??= new
|
|
287
|
+
const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
|
|
288
288
|
if (!registry.runEventValidator(value)) {
|
|
289
|
-
throw new
|
|
289
|
+
throw new KxmConfigError([issue(
|
|
290
290
|
"schema",
|
|
291
291
|
"run_event_invalid",
|
|
292
292
|
file,
|
|
@@ -297,9 +297,9 @@ export function validateRunEvent(value: unknown, file: string): void {
|
|
|
297
297
|
|
|
298
298
|
/** Validate a drive receipt against `kxm.drive-receipt.v1`. Throws `drive_receipt_invalid`. */
|
|
299
299
|
export function validateDriveReceipt(value: unknown, file: string): void {
|
|
300
|
-
const registry = (cachedRunEventRegistry ??= new
|
|
300
|
+
const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
|
|
301
301
|
if (!value || typeof value !== "object" || Array.isArray(value) || (value as JsonObject).schema !== "kxm.drive-receipt.v1") {
|
|
302
|
-
throw new
|
|
302
|
+
throw new KxmConfigError([issue(
|
|
303
303
|
"schema",
|
|
304
304
|
"drive_receipt_invalid",
|
|
305
305
|
file,
|
|
@@ -307,7 +307,7 @@ export function validateDriveReceipt(value: unknown, file: string): void {
|
|
|
307
307
|
)]);
|
|
308
308
|
}
|
|
309
309
|
if (!registry.driveReceiptValidator(value)) {
|
|
310
|
-
throw new
|
|
310
|
+
throw new KxmConfigError([issue(
|
|
311
311
|
"schema",
|
|
312
312
|
"drive_receipt_invalid",
|
|
313
313
|
file,
|
|
@@ -316,7 +316,7 @@ export function validateDriveReceipt(value: unknown, file: string): void {
|
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
function schemaIssue(file: string, error: ErrorObject):
|
|
319
|
+
function schemaIssue(file: string, error: ErrorObject): KxmConfigIssue {
|
|
320
320
|
const location = error.instancePath || "/";
|
|
321
321
|
const suffix = error.params && "additionalProperty" in error.params
|
|
322
322
|
? ` (${String(error.params.additionalProperty)})`
|
|
@@ -346,7 +346,7 @@ function containedHostPath(root: string, candidate: string): boolean {
|
|
|
346
346
|
return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`));
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
function portableBindingIssue(root: string, pathHint: string, repositoryId: string):
|
|
349
|
+
function portableBindingIssue(root: string, pathHint: string, repositoryId: string): KxmConfigIssue | undefined {
|
|
350
350
|
let current = root;
|
|
351
351
|
for (const segment of pathHint === "." ? [] : pathHint.split("/")) {
|
|
352
352
|
current = join(current, segment);
|
|
@@ -368,7 +368,7 @@ function portableBindingIssue(root: string, pathHint: string, repositoryId: stri
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
/** The portable relative-path rule every project-owned pathHint must satisfy. */
|
|
371
|
-
export function
|
|
371
|
+
export function kxmPortablePath(path: string): boolean {
|
|
372
372
|
return portablePath(path);
|
|
373
373
|
}
|
|
374
374
|
|
|
@@ -386,12 +386,12 @@ function portablePath(path: string): boolean {
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/** The identifier rule every path-derived resource identity must satisfy. */
|
|
389
|
-
export function
|
|
389
|
+
export function kxmResourceIdentifier(id: string): boolean {
|
|
390
390
|
return id.length <= 64 && IDENTIFIER.test(id) && !WINDOWS_RESERVED.test(id);
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
function resourceIdentifier(id: string): boolean {
|
|
394
|
-
return
|
|
394
|
+
return kxmResourceIdentifier(id);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
function displayPath(root: string, file: string): string {
|
|
@@ -400,14 +400,14 @@ function displayPath(root: string, file: string): string {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
function readResource(
|
|
403
|
-
registry:
|
|
403
|
+
registry: KxmSchemaRegistry,
|
|
404
404
|
root: string,
|
|
405
405
|
file: string,
|
|
406
406
|
logicalPath: string,
|
|
407
|
-
kind:
|
|
407
|
+
kind: KxmResourceKind,
|
|
408
408
|
id?: string,
|
|
409
409
|
containmentRoot = root,
|
|
410
|
-
):
|
|
410
|
+
): KxmResource {
|
|
411
411
|
const label = displayPath(root, file);
|
|
412
412
|
let stat;
|
|
413
413
|
try {
|
|
@@ -429,11 +429,11 @@ function readResource(
|
|
|
429
429
|
if (!stat.isFile()) fail("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
430
430
|
const value = parseRestrictedYaml(readFileSync(file), label);
|
|
431
431
|
const issues = registry.validate(kind, value, label);
|
|
432
|
-
if (issues.length > 0) throw new
|
|
432
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
433
433
|
return { kind, ...(id === undefined ? {} : { id }), file, logicalPath, value };
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
function readTemplateProvenance(registry:
|
|
436
|
+
function readTemplateProvenance(registry: KxmSchemaRegistry, root: string): JsonObject | undefined {
|
|
437
437
|
const file = join(root, ".kxm", "template-provenance.yaml");
|
|
438
438
|
if (!existsSync(file)) return undefined;
|
|
439
439
|
const label = ".kxm/template-provenance.yaml";
|
|
@@ -453,8 +453,8 @@ function readTemplateProvenance(registry: VnextSchemaRegistry, root: string): Js
|
|
|
453
453
|
}
|
|
454
454
|
const value = parseRestrictedYaml(readFileSync(file), label);
|
|
455
455
|
const issues = registry.validateTemplateProvenance(value, label);
|
|
456
|
-
if (issues.length > 0) throw new
|
|
457
|
-
if (!
|
|
456
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
457
|
+
if (!resolveKxmTemplateBaseline(value)) {
|
|
458
458
|
fail("semantic", "template_provenance_revision_invalid", label, "template provenance does not exactly match a supported built-in baseline");
|
|
459
459
|
}
|
|
460
460
|
const files = Array.isArray(value.files) ? value.files : [];
|
|
@@ -482,19 +482,19 @@ function readTemplateProvenance(registry: VnextSchemaRegistry, root: string): Js
|
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
function listNamedResources(
|
|
485
|
-
registry:
|
|
485
|
+
registry: KxmSchemaRegistry,
|
|
486
486
|
root: string,
|
|
487
487
|
directory: string,
|
|
488
488
|
logicalDirectory: string,
|
|
489
489
|
kind: "agent" | "model" | "workflow",
|
|
490
|
-
): Map<string,
|
|
491
|
-
const resources = new Map<string,
|
|
490
|
+
): Map<string, KxmResource> {
|
|
491
|
+
const resources = new Map<string, KxmResource>();
|
|
492
492
|
if (!existsSync(directory)) return resources;
|
|
493
493
|
const stat = lstatSync(directory);
|
|
494
494
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
495
495
|
fail("path", "resource_directory_invalid", displayPath(root, directory), "resource directory must be a regular directory");
|
|
496
496
|
}
|
|
497
|
-
const issues:
|
|
497
|
+
const issues: KxmConfigIssue[] = [];
|
|
498
498
|
const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => compareCodeUnits(left.name, right.name));
|
|
499
499
|
for (const entry of entries) {
|
|
500
500
|
if (entry.isDirectory()) {
|
|
@@ -521,11 +521,11 @@ function listNamedResources(
|
|
|
521
521
|
const resource = readResource(registry, root, join(directory, entry.name), `${logicalDirectory}/${id}.yaml`, kind, id);
|
|
522
522
|
resources.set(id, resource);
|
|
523
523
|
} catch (error) {
|
|
524
|
-
if (error instanceof
|
|
524
|
+
if (error instanceof KxmConfigError) issues.push(...error.issues);
|
|
525
525
|
else throw error;
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
|
-
if (issues.length > 0) throw new
|
|
528
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
529
529
|
return resources;
|
|
530
530
|
}
|
|
531
531
|
|
|
@@ -550,7 +550,7 @@ function names(value: JsonValue | undefined): string[] {
|
|
|
550
550
|
return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : [];
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
-
function mapResource(resource:
|
|
553
|
+
function mapResource(resource: KxmResource, map: Map<string, KxmResource>, issues: KxmConfigIssue[]): void {
|
|
554
554
|
const id = resource.id;
|
|
555
555
|
if (!id) return;
|
|
556
556
|
const collision = [...map.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
@@ -561,7 +561,7 @@ function mapResource(resource: VnextResource, map: Map<string, VnextResource>, i
|
|
|
561
561
|
map.set(id, resource);
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
-
function validatePortablePaths(resource:
|
|
564
|
+
function validatePortablePaths(resource: KxmResource, issues: KxmConfigIssue[]): void {
|
|
565
565
|
const candidates: { path: string; label: string }[] = [];
|
|
566
566
|
if (resource.kind === "project") {
|
|
567
567
|
for (const candidate of valuesOf(resource.value, "repositories")) {
|
|
@@ -587,11 +587,11 @@ function validatePortablePaths(resource: VnextResource, issues: VnextConfigIssue
|
|
|
587
587
|
|
|
588
588
|
function selectorCandidates(
|
|
589
589
|
selectorValue: JsonValue | undefined,
|
|
590
|
-
models: ReadonlyMap<string,
|
|
590
|
+
models: ReadonlyMap<string, KxmResource>,
|
|
591
591
|
file: string,
|
|
592
592
|
label: string,
|
|
593
|
-
issues:
|
|
594
|
-
):
|
|
593
|
+
issues: KxmConfigIssue[],
|
|
594
|
+
): KxmResource[] {
|
|
595
595
|
const selector = objectValue(selectorValue);
|
|
596
596
|
if (!selector) return [];
|
|
597
597
|
const profile = stringValue(selector.profile);
|
|
@@ -616,7 +616,7 @@ interface ModelCandidate {
|
|
|
616
616
|
model?: string;
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
function modelCandidates(selectorValue: JsonValue | undefined, models: ReadonlyMap<string,
|
|
619
|
+
function modelCandidates(selectorValue: JsonValue | undefined, models: ReadonlyMap<string, KxmResource>): ModelCandidate[] {
|
|
620
620
|
const selector = objectValue(selectorValue);
|
|
621
621
|
if (!selector) return [];
|
|
622
622
|
const profile = stringValue(selector.profile);
|
|
@@ -656,8 +656,8 @@ function compatibleModel(left: ModelCandidate, right: ModelCandidate): boolean {
|
|
|
656
656
|
function diversityCandidates(
|
|
657
657
|
stepSelector: JsonValue | undefined,
|
|
658
658
|
allowedAgents: readonly string[],
|
|
659
|
-
agents: ReadonlyMap<string,
|
|
660
|
-
models: ReadonlyMap<string,
|
|
659
|
+
agents: ReadonlyMap<string, KxmResource>,
|
|
660
|
+
models: ReadonlyMap<string, KxmResource>,
|
|
661
661
|
): ModelCandidate[] {
|
|
662
662
|
const stepCandidates = modelCandidates(stepSelector, models);
|
|
663
663
|
const resolved: ModelCandidate[] = [];
|
|
@@ -672,7 +672,7 @@ function diversityCandidates(
|
|
|
672
672
|
return [...new Map(resolved.map((candidate) => [candidate.key, candidate])).values()];
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
function validateEnvironment(resource:
|
|
675
|
+
function validateEnvironment(resource: KxmResource, issues: KxmConfigIssue[]): void {
|
|
676
676
|
const values = objectValue(resource.value.values) ?? {};
|
|
677
677
|
const secretNames = new Set<string>();
|
|
678
678
|
for (const candidate of valuesOf(resource.value, "secrets")) {
|
|
@@ -694,7 +694,7 @@ function accessRank(value: JsonValue | undefined): number {
|
|
|
694
694
|
return value === "write" ? 2 : value === "read" ? 1 : 0;
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
function validateToolScope(agent:
|
|
697
|
+
function validateToolScope(agent: KxmResource, step: JsonObject, file: string, stepId: string, issues: KxmConfigIssue[]): void {
|
|
698
698
|
const requested = objectValue(step.tools);
|
|
699
699
|
if (!requested) return;
|
|
700
700
|
const ceiling = objectValue(agent.value.tools);
|
|
@@ -718,7 +718,7 @@ function validateToolScope(agent: VnextResource, step: JsonObject, file: string,
|
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
-
function validateToolPolicy(resource:
|
|
721
|
+
function validateToolPolicy(resource: KxmResource, policy: JsonObject | undefined, label: string, issues: KxmConfigIssue[]): void {
|
|
722
722
|
if (!policy) return;
|
|
723
723
|
const allowed = new Set(names(policy.allow));
|
|
724
724
|
for (const tool of names(policy.deny)) {
|
|
@@ -727,11 +727,11 @@ function validateToolPolicy(resource: VnextResource, policy: JsonObject | undefi
|
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
/** Validate an in-memory resource set with the same semantic rules as a loaded project. */
|
|
730
|
-
export function
|
|
731
|
-
resources: ReadonlyMap<string, { kind:
|
|
732
|
-
options:
|
|
733
|
-
):
|
|
734
|
-
const make = (logicalPath: string, kind:
|
|
730
|
+
export function validateKxmResources(
|
|
731
|
+
resources: ReadonlyMap<string, { kind: KxmResourceKind; id?: string; value: JsonObject }>,
|
|
732
|
+
options: KxmConfigOptions = {},
|
|
733
|
+
): KxmConfigIssue[] {
|
|
734
|
+
const make = (logicalPath: string, kind: KxmResourceKind, id: string | undefined, value: JsonObject): KxmResource => ({
|
|
735
735
|
kind,
|
|
736
736
|
...(id === undefined ? {} : { id }),
|
|
737
737
|
file: logicalPath,
|
|
@@ -739,13 +739,13 @@ export function validateVnextResources(
|
|
|
739
739
|
value,
|
|
740
740
|
});
|
|
741
741
|
assertNoRegisteredGates(options);
|
|
742
|
-
let gateRegistry:
|
|
743
|
-
let project:
|
|
744
|
-
const repositories = new Map<string,
|
|
745
|
-
const agents = new Map<string,
|
|
746
|
-
const models = new Map<string,
|
|
747
|
-
const workflows = new Map<string,
|
|
748
|
-
const environments:
|
|
742
|
+
let gateRegistry: KxmResource | undefined;
|
|
743
|
+
let project: KxmResource | undefined;
|
|
744
|
+
const repositories = new Map<string, KxmResource>();
|
|
745
|
+
const agents = new Map<string, KxmResource>();
|
|
746
|
+
const models = new Map<string, KxmResource>();
|
|
747
|
+
const workflows = new Map<string, KxmResource>();
|
|
748
|
+
const environments: KxmResource[] = [];
|
|
749
749
|
for (const [logicalPath, resource] of resources) {
|
|
750
750
|
const made = make(logicalPath, resource.kind, resource.id, resource.value);
|
|
751
751
|
if (resource.kind === "project") project = made;
|
|
@@ -761,12 +761,12 @@ export function validateVnextResources(
|
|
|
761
761
|
}
|
|
762
762
|
|
|
763
763
|
function validateAgentScope(
|
|
764
|
-
agent:
|
|
764
|
+
agent: KxmResource,
|
|
765
765
|
step: JsonObject,
|
|
766
766
|
repositories: ReadonlySet<string>,
|
|
767
767
|
file: string,
|
|
768
768
|
stepId: string,
|
|
769
|
-
issues:
|
|
769
|
+
issues: KxmConfigIssue[],
|
|
770
770
|
): void {
|
|
771
771
|
validateToolScope(agent, step, file, stepId, issues);
|
|
772
772
|
const agentRepositories = objectValue(agent.value.repositories) ?? {};
|
|
@@ -800,12 +800,12 @@ function transition(value: JsonValue): { target?: string; maxTransitions?: numbe
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
function validateWorkflow(
|
|
803
|
-
workflow:
|
|
804
|
-
agents: ReadonlyMap<string,
|
|
805
|
-
models: ReadonlyMap<string,
|
|
803
|
+
workflow: KxmResource,
|
|
804
|
+
agents: ReadonlyMap<string, KxmResource>,
|
|
805
|
+
models: ReadonlyMap<string, KxmResource>,
|
|
806
806
|
repositories: ReadonlySet<string>,
|
|
807
807
|
gates: ReadonlySet<string> | undefined,
|
|
808
|
-
issues:
|
|
808
|
+
issues: KxmConfigIssue[],
|
|
809
809
|
): void {
|
|
810
810
|
const file = workflow.logicalPath;
|
|
811
811
|
const coordinator = stringValue(workflow.value.coordinator) ?? "coordinator";
|
|
@@ -1029,7 +1029,7 @@ function validateWorkflow(
|
|
|
1029
1029
|
}
|
|
1030
1030
|
}
|
|
1031
1031
|
|
|
1032
|
-
function validateModelReferences(models: ReadonlyMap<string,
|
|
1032
|
+
function validateModelReferences(models: ReadonlyMap<string, KxmResource>, issues: KxmConfigIssue[]): void {
|
|
1033
1033
|
for (const model of models.values()) {
|
|
1034
1034
|
for (const [index, fallback] of valuesOf(model.value, "fallbacks").entries()) {
|
|
1035
1035
|
selectorCandidates(fallback, models, model.logicalPath, `fallbacks[${index}]`, issues);
|
|
@@ -1058,17 +1058,17 @@ function validateModelReferences(models: ReadonlyMap<string, VnextResource>, iss
|
|
|
1058
1058
|
}
|
|
1059
1059
|
|
|
1060
1060
|
function validateBundle(
|
|
1061
|
-
project:
|
|
1062
|
-
repositories: ReadonlyMap<string,
|
|
1063
|
-
agents: ReadonlyMap<string,
|
|
1064
|
-
models: ReadonlyMap<string,
|
|
1065
|
-
workflows: ReadonlyMap<string,
|
|
1066
|
-
environments: readonly
|
|
1067
|
-
options:
|
|
1068
|
-
gateRegistry?:
|
|
1061
|
+
project: KxmResource,
|
|
1062
|
+
repositories: ReadonlyMap<string, KxmResource>,
|
|
1063
|
+
agents: ReadonlyMap<string, KxmResource>,
|
|
1064
|
+
models: ReadonlyMap<string, KxmResource>,
|
|
1065
|
+
workflows: ReadonlyMap<string, KxmResource>,
|
|
1066
|
+
environments: readonly KxmResource[],
|
|
1067
|
+
options: KxmConfigOptions,
|
|
1068
|
+
gateRegistry?: KxmResource,
|
|
1069
1069
|
projectRoot?: string,
|
|
1070
|
-
):
|
|
1071
|
-
const issues:
|
|
1070
|
+
): KxmConfigIssue[] {
|
|
1071
|
+
const issues: KxmConfigIssue[] = [];
|
|
1072
1072
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
|
|
1073
1073
|
const repositoryIds = new Set<string>();
|
|
1074
1074
|
const foldedRepositories = new Map<string, string>();
|
|
@@ -1168,7 +1168,7 @@ function validateBundle(
|
|
|
1168
1168
|
}
|
|
1169
1169
|
}
|
|
1170
1170
|
} catch (error) {
|
|
1171
|
-
if (error instanceof
|
|
1171
|
+
if (error instanceof KxmConfigError) {
|
|
1172
1172
|
issues.push(...error.issues);
|
|
1173
1173
|
}
|
|
1174
1174
|
}
|
|
@@ -1178,17 +1178,17 @@ function validateBundle(
|
|
|
1178
1178
|
return sortIssues(issues);
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
|
-
export function
|
|
1181
|
+
export function kxmCanonicalJson(value: JsonValue): string {
|
|
1182
1182
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1183
|
-
if (Array.isArray(value)) return `[${value.map((candidate) =>
|
|
1184
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${
|
|
1183
|
+
if (Array.isArray(value)) return `[${value.map((candidate) => kxmCanonicalJson(candidate)).join(",")}]`;
|
|
1184
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${kxmCanonicalJson(value[key] as JsonValue)}`).join(",")}}`;
|
|
1185
1185
|
}
|
|
1186
1186
|
|
|
1187
1187
|
function canonicalize(value: JsonValue): string {
|
|
1188
|
-
return
|
|
1188
|
+
return kxmCanonicalJson(value);
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
-
function bundleRevision(resources: readonly
|
|
1191
|
+
function bundleRevision(resources: readonly KxmResource[]): string {
|
|
1192
1192
|
const hash = createHash("sha256");
|
|
1193
1193
|
for (const resource of [...resources].sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath))) {
|
|
1194
1194
|
hash.update(resource.logicalPath, "utf8");
|
|
@@ -1218,8 +1218,8 @@ export function discoverGitRoot(start = process.cwd()): string | undefined {
|
|
|
1218
1218
|
return existsSync(root) && lstatSync(root).isDirectory() ? root : undefined;
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
-
/** Find the authoritative
|
|
1222
|
-
export function
|
|
1221
|
+
/** Find the authoritative KXM project only at the nearest Git worktree root. */
|
|
1222
|
+
export function discoverKxmProjectRoot(start = process.cwd()): string | undefined {
|
|
1223
1223
|
const gitRoot = discoverGitRoot(start);
|
|
1224
1224
|
return gitRoot && existsSync(join(gitRoot, ".kxm", "project.yaml")) ? gitRoot : undefined;
|
|
1225
1225
|
}
|
|
@@ -1227,23 +1227,23 @@ export function discoverVnextProjectRoot(start = process.cwd()): string | undefi
|
|
|
1227
1227
|
/** The former caller-supplied gate allowlist cannot substitute for reviewed YAML. */
|
|
1228
1228
|
export function assertNoRegisteredGates(options: object): void {
|
|
1229
1229
|
if ("registeredGates" in options) {
|
|
1230
|
-
throw new
|
|
1230
|
+
throw new KxmConfigError([issue("semantic", "registered_gates_removed", ".kxm/gates.yaml", "registeredGates was removed; declare gates in .kxm/gates.yaml")]);
|
|
1231
1231
|
}
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
|
-
/** Load and semantically validate one complete, path-derived
|
|
1235
|
-
export function
|
|
1234
|
+
/** Load and semantically validate one complete, path-derived KXM configuration bundle. */
|
|
1235
|
+
export function loadKxmProject(projectRoot: string, options: KxmConfigOptions = {}): KxmProjectBundle {
|
|
1236
1236
|
assertNoRegisteredGates(options);
|
|
1237
1237
|
const root = resolve(projectRoot);
|
|
1238
1238
|
let migrationReceipt: JsonObject | undefined;
|
|
1239
1239
|
if (legacyConfigFilesAt(root).length > 0 && options.allowUnreceiptedLegacyConfig !== true) {
|
|
1240
|
-
const receiptCheck =
|
|
1241
|
-
if (receiptCheck.issues.length > 0) throw new
|
|
1240
|
+
const receiptCheck = readKxmMigrationReceipt(root, options);
|
|
1241
|
+
if (receiptCheck.issues.length > 0) throw new KxmConfigError(receiptCheck.issues);
|
|
1242
1242
|
migrationReceipt = receiptCheck.receipt;
|
|
1243
1243
|
}
|
|
1244
|
-
const registry = new
|
|
1244
|
+
const registry = new KxmSchemaRegistry(options.schemasDir);
|
|
1245
1245
|
const project = readResource(registry, root, join(root, ".kxm", "project.yaml"), ".kxm/project.yaml", "project");
|
|
1246
|
-
const earlyIssues:
|
|
1246
|
+
const earlyIssues: KxmConfigIssue[] = [];
|
|
1247
1247
|
validatePortablePaths(project, earlyIssues);
|
|
1248
1248
|
const templateProvenance = readTemplateProvenance(registry, root);
|
|
1249
1249
|
if (templateProvenance && templateProvenance.inputs && typeof templateProvenance.inputs === "object" && !Array.isArray(templateProvenance.inputs)) {
|
|
@@ -1253,7 +1253,7 @@ export function loadVnextProject(projectRoot: string, options: VnextConfigOption
|
|
|
1253
1253
|
}
|
|
1254
1254
|
}
|
|
1255
1255
|
if (migrationReceipt && stringValue(migrationReceipt.projectId) !== stringValue(project.value.id)) {
|
|
1256
|
-
earlyIssues.push(issue("semantic", "migration_project_mismatch",
|
|
1256
|
+
earlyIssues.push(issue("semantic", "migration_project_mismatch", KXM_MIGRATION_RECEIPT_PATH, "migration receipt belongs to a different project identity"));
|
|
1257
1257
|
}
|
|
1258
1258
|
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories")
|
|
1259
1259
|
.map((candidate) => stringValue(objectValue(candidate)?.id))
|
|
@@ -1265,21 +1265,21 @@ export function loadVnextProject(projectRoot: string, options: VnextConfigOption
|
|
|
1265
1265
|
earlyIssues.push(issue("reference", "repository_binding_unknown", ".kxm/project.yaml", `host-local binding references unknown repository ${repositoryId}`));
|
|
1266
1266
|
}
|
|
1267
1267
|
}
|
|
1268
|
-
if (earlyIssues.length > 0) throw new
|
|
1268
|
+
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
1269
1269
|
|
|
1270
1270
|
const agents = listNamedResources(registry, root, join(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
1271
1271
|
const models = listNamedResources(registry, root, join(root, ".kxm", "models"), ".kxm/models", "model");
|
|
1272
1272
|
const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
1273
1273
|
const gatePath = join(root, ".kxm", "gates.yaml");
|
|
1274
1274
|
const gateRegistry = existsSync(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : undefined;
|
|
1275
|
-
const environments:
|
|
1276
|
-
const repositories = new Map<string,
|
|
1277
|
-
const loadIssues:
|
|
1275
|
+
const environments: KxmResource[] = [];
|
|
1276
|
+
const repositories = new Map<string, KxmResource>();
|
|
1277
|
+
const loadIssues: KxmConfigIssue[] = [];
|
|
1278
1278
|
|
|
1279
1279
|
const projectEnvironment = join(root, ".kxm", "project", "env.yaml");
|
|
1280
1280
|
if (existsSync(projectEnvironment)) {
|
|
1281
1281
|
try { environments.push(readResource(registry, root, projectEnvironment, ".kxm/project/env.yaml", "environment")); }
|
|
1282
|
-
catch (error) { if (error instanceof
|
|
1282
|
+
catch (error) { if (error instanceof KxmConfigError) loadIssues.push(...error.issues); else throw error; }
|
|
1283
1283
|
}
|
|
1284
1284
|
|
|
1285
1285
|
const seenBindings = new Map<string, string>();
|
|
@@ -1358,7 +1358,7 @@ export function loadVnextProject(projectRoot: string, options: VnextConfigOption
|
|
|
1358
1358
|
definitionValidated = repositories.get(repositoryId) === resource;
|
|
1359
1359
|
}
|
|
1360
1360
|
} catch (error) {
|
|
1361
|
-
if (error instanceof
|
|
1361
|
+
if (error instanceof KxmConfigError) loadIssues.push(...error.issues);
|
|
1362
1362
|
else throw error;
|
|
1363
1363
|
}
|
|
1364
1364
|
} else if (required || hasLocalBinding) {
|
|
@@ -1370,14 +1370,14 @@ export function loadVnextProject(projectRoot: string, options: VnextConfigOption
|
|
|
1370
1370
|
loadIssues.push(issue("reference", "repository_environment_without_definition", `.kxm/repositories/${repositoryId}/env.yaml`, `repository ${repositoryId} environment requires a validated matching repo.yaml`));
|
|
1371
1371
|
} else {
|
|
1372
1372
|
try { environments.push(readResource(registry, root, environmentFile, `.kxm/repositories/${repositoryId}/env.yaml`, "environment", undefined, binding)); }
|
|
1373
|
-
catch (error) { if (error instanceof
|
|
1373
|
+
catch (error) { if (error instanceof KxmConfigError) loadIssues.push(...error.issues); else throw error; }
|
|
1374
1374
|
}
|
|
1375
1375
|
}
|
|
1376
1376
|
}
|
|
1377
|
-
if (loadIssues.length > 0) throw new
|
|
1377
|
+
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
1378
1378
|
|
|
1379
1379
|
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
1380
|
-
if (issues.length > 0) throw new
|
|
1380
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
1381
1381
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
|
|
1382
1382
|
.sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
|
|
1383
1383
|
return {
|
|
@@ -1401,15 +1401,15 @@ export function loadVnextProject(projectRoot: string, options: VnextConfigOption
|
|
|
1401
1401
|
* revision and per-resource bytes. Load-time coexistence only pins the
|
|
1402
1402
|
* legacy sources; this stricter check backs `kxm migrate verify`.
|
|
1403
1403
|
*/
|
|
1404
|
-
export function
|
|
1404
|
+
export function verifyKxmMigrationReceiptTarget(
|
|
1405
1405
|
root: string,
|
|
1406
1406
|
receipt: JsonObject,
|
|
1407
|
-
resources: readonly
|
|
1407
|
+
resources: readonly KxmResource[],
|
|
1408
1408
|
configRevision: string,
|
|
1409
|
-
):
|
|
1410
|
-
const issues:
|
|
1409
|
+
): KxmConfigIssue[] {
|
|
1410
|
+
const issues: KxmConfigIssue[] = [];
|
|
1411
1411
|
if (receipt.configRevision !== configRevision) {
|
|
1412
|
-
issues.push(issue("semantic", "migration_target_changed",
|
|
1412
|
+
issues.push(issue("semantic", "migration_target_changed", KXM_MIGRATION_RECEIPT_PATH, "target configuration revision no longer matches the migration receipt"));
|
|
1413
1413
|
}
|
|
1414
1414
|
const declared = new Map<string, JsonObject>();
|
|
1415
1415
|
for (const candidate of Array.isArray(receipt.resources) ? receipt.resources : []) {
|
|
@@ -1459,7 +1459,7 @@ function legacyInputsAt(root: string): string[] {
|
|
|
1459
1459
|
return candidates.filter((candidate) => existsSync(join(root, ...candidate.split("/"))));
|
|
1460
1460
|
}
|
|
1461
1461
|
|
|
1462
|
-
export const
|
|
1462
|
+
export const KXM_MIGRATION_RECEIPT_PATH = ".kxm/migration-receipt.yaml";
|
|
1463
1463
|
const LEGACY_CONFIG_FILES = [".kxm/config/agents.json", ".kxm/config/gates.json"] as const;
|
|
1464
1464
|
|
|
1465
1465
|
/** Individual legacy configuration files (not directories, not runtime state) that a migration receipt must cover. */
|
|
@@ -1496,9 +1496,9 @@ export function legacyConfigFilesAt(root: string): string[] {
|
|
|
1496
1496
|
return files;
|
|
1497
1497
|
}
|
|
1498
1498
|
|
|
1499
|
-
export interface
|
|
1499
|
+
export interface KxmMigrationReceiptCheck {
|
|
1500
1500
|
receipt?: JsonObject;
|
|
1501
|
-
issues:
|
|
1501
|
+
issues: KxmConfigIssue[];
|
|
1502
1502
|
}
|
|
1503
1503
|
|
|
1504
1504
|
function hashFileRecord(root: string, relativePath: string): { path: string; sha256: string; bytes: number } | undefined {
|
|
@@ -1514,7 +1514,7 @@ function hashFileRecord(root: string, relativePath: string): { path: string; sha
|
|
|
1514
1514
|
|
|
1515
1515
|
export function migrationReceiptSelfHash(receipt: JsonObject): string {
|
|
1516
1516
|
const { receiptSha256: _ignored, ...unsigned } = receipt;
|
|
1517
|
-
return `sha256:${createHash("sha256").update(
|
|
1517
|
+
return `sha256:${createHash("sha256").update(kxmCanonicalJson(unsigned as JsonObject), "utf8").digest("hex")}`;
|
|
1518
1518
|
}
|
|
1519
1519
|
|
|
1520
1520
|
/**
|
|
@@ -1522,36 +1522,36 @@ export function migrationReceiptSelfHash(receipt: JsonObject): string {
|
|
|
1522
1522
|
* exact coverage of the legacy configuration files still present. Does not
|
|
1523
1523
|
* compare the target configuration revision (computed by the caller).
|
|
1524
1524
|
*/
|
|
1525
|
-
export function
|
|
1526
|
-
const receiptPath = join(root, ...
|
|
1525
|
+
export function readKxmMigrationReceipt(root: string, options: KxmConfigOptions = {}): KxmMigrationReceiptCheck {
|
|
1526
|
+
const receiptPath = join(root, ...KXM_MIGRATION_RECEIPT_PATH.split("/"));
|
|
1527
1527
|
const legacyFiles = legacyConfigFilesAt(root);
|
|
1528
1528
|
if (!existsSync(receiptPath)) {
|
|
1529
1529
|
return {
|
|
1530
|
-
issues: legacyFiles.map((file) => issue("semantic", "
|
|
1530
|
+
issues: legacyFiles.map((file) => issue("semantic", "legacy_kxm_conflict", file, "legacy and KXM configuration cannot coexist before an accepted migration receipt")),
|
|
1531
1531
|
};
|
|
1532
1532
|
}
|
|
1533
1533
|
const receiptStat = lstatSync(receiptPath);
|
|
1534
1534
|
if (receiptStat.isSymbolicLink() || !receiptStat.isFile()) {
|
|
1535
|
-
return { issues: [issue("path", "migration_receipt_invalid",
|
|
1535
|
+
return { issues: [issue("path", "migration_receipt_invalid", KXM_MIGRATION_RECEIPT_PATH, "migration receipt must be a regular file, not a link")] };
|
|
1536
1536
|
}
|
|
1537
1537
|
let receipt: JsonObject;
|
|
1538
1538
|
try {
|
|
1539
|
-
receipt = parseRestrictedYaml(readFileSync(receiptPath),
|
|
1539
|
+
receipt = parseRestrictedYaml(readFileSync(receiptPath), KXM_MIGRATION_RECEIPT_PATH);
|
|
1540
1540
|
} catch (error) {
|
|
1541
|
-
if (error instanceof
|
|
1541
|
+
if (error instanceof KxmConfigError) return { issues: [...error.issues] };
|
|
1542
1542
|
throw error;
|
|
1543
1543
|
}
|
|
1544
|
-
const registry = new
|
|
1545
|
-
const schemaIssues = registry.validateMigrationReceipt(receipt,
|
|
1544
|
+
const registry = new KxmSchemaRegistry(options.schemasDir);
|
|
1545
|
+
const schemaIssues = registry.validateMigrationReceipt(receipt, KXM_MIGRATION_RECEIPT_PATH);
|
|
1546
1546
|
if (schemaIssues.length > 0) return { issues: schemaIssues };
|
|
1547
1547
|
if (receipt.receiptSha256 !== migrationReceiptSelfHash(receipt)) {
|
|
1548
|
-
return { issues: [issue("semantic", "migration_receipt_hash_mismatch",
|
|
1548
|
+
return { issues: [issue("semantic", "migration_receipt_hash_mismatch", KXM_MIGRATION_RECEIPT_PATH, "receipt self-hash does not match its content")] };
|
|
1549
1549
|
}
|
|
1550
1550
|
const sources = (Array.isArray(receipt.sources) ? receipt.sources : [])
|
|
1551
1551
|
.map((candidate) => (candidate && typeof candidate === "object" && !Array.isArray(candidate) ? (candidate as JsonObject).path : undefined))
|
|
1552
1552
|
.filter((candidate): candidate is string => typeof candidate === "string")
|
|
1553
1553
|
.sort(compareCodeUnits);
|
|
1554
|
-
const issues:
|
|
1554
|
+
const issues: KxmConfigIssue[] = [];
|
|
1555
1555
|
const declared = new Set(sources);
|
|
1556
1556
|
for (const file of legacyFiles) {
|
|
1557
1557
|
if (!declared.has(file)) {
|
|
@@ -1585,15 +1585,15 @@ function discoverLegacyRoot(start: string): { root: string; inputs: string[] } |
|
|
|
1585
1585
|
}
|
|
1586
1586
|
|
|
1587
1587
|
/** Classify init without changing files. Applying create/repair/migration is a separate, explicit operation. */
|
|
1588
|
-
export function
|
|
1588
|
+
export function planKxmInitialization(start = process.cwd(), options: KxmConfigOptions = {}): KxmInitializationPlan {
|
|
1589
1589
|
const inspectedFrom = resolve(start);
|
|
1590
|
-
const projectRoot =
|
|
1590
|
+
const projectRoot = discoverKxmProjectRoot(inspectedFrom);
|
|
1591
1591
|
if (projectRoot) {
|
|
1592
1592
|
try {
|
|
1593
|
-
const bundle =
|
|
1593
|
+
const bundle = loadKxmProject(projectRoot, options);
|
|
1594
1594
|
return { mode: "ready", inspectedFrom, projectRoot, changesRequired: false, issues: [], legacyInputs: [], configRevision: bundle.configRevision };
|
|
1595
1595
|
} catch (error) {
|
|
1596
|
-
if (!(error instanceof
|
|
1596
|
+
if (!(error instanceof KxmConfigError)) throw error;
|
|
1597
1597
|
const legacyInputs = legacyInputsAt(projectRoot);
|
|
1598
1598
|
if (legacyInputs.length > 0) {
|
|
1599
1599
|
return { mode: "migrate", inspectedFrom, projectRoot, legacyRoot: projectRoot, changesRequired: true, issues: error.issues, legacyInputs };
|