@kontextmind/kxm 0.6.0 → 0.7.0
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/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +5 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +118 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +2 -2
- package/docs/kxm-handbook.md +3 -3
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +102 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +19 -0
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +6875 -3672
- package/plugins/kxm/dist/core.js +214 -34
- package/plugins/kxm/dist/extension.js +7721 -85
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5403 -1014
- package/plugins/kxm/dist/server.js +3008 -2268
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/cli.ts +1077 -11
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +356 -7
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +375 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-config.ts +15 -110
- package/plugins/kxm/src/vnext-engine.ts +198 -62
- package/plugins/kxm/src/vnext-harness.ts +263 -81
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
- package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-hub.mjs +6 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JsonObject } from "./restricted-yaml.mjs";
|
|
2
|
+
|
|
3
|
+
export const POLICY_DRAFT_MODEL_SCHEMA: "kxm.model.v2";
|
|
4
|
+
export const POLICY_DRAFT_ROLE_SCHEMA: "kxm.role.v2";
|
|
5
|
+
export const POLICY_DRAFT_PURPOSES: readonly [
|
|
6
|
+
"writer",
|
|
7
|
+
"planner",
|
|
8
|
+
"reviewer-arch",
|
|
9
|
+
"reviewer-cli",
|
|
10
|
+
"experiment",
|
|
11
|
+
];
|
|
12
|
+
export const POLICY_DRAFT_PERMISSIONS: readonly ["edit", "read-only"];
|
|
13
|
+
export const POLICY_DRAFT_STATUSES: readonly ["admitted", "candidate", "retired"];
|
|
14
|
+
|
|
15
|
+
export interface PolicyDraftIssue {
|
|
16
|
+
phase: "parse" | "schema" | "reference" | "semantic";
|
|
17
|
+
code: string;
|
|
18
|
+
file: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PolicyDraftCeiling {
|
|
23
|
+
readonly provider: string;
|
|
24
|
+
readonly roles?: readonly string[];
|
|
25
|
+
readonly permissions: readonly string[];
|
|
26
|
+
readonly models?: readonly string[];
|
|
27
|
+
readonly efforts?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PolicyDraftOptions {
|
|
31
|
+
readonly ceilings: Readonly<Record<string, PolicyDraftCeiling>>;
|
|
32
|
+
readonly nativePiBrakeProviders: readonly string[];
|
|
33
|
+
readonly piAllowedProviders: readonly string[];
|
|
34
|
+
readonly vendorAliases?: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PolicyDraftDocument = JsonObject | string | Uint8Array;
|
|
38
|
+
|
|
39
|
+
export interface PolicyDraftInput {
|
|
40
|
+
readonly models?: Readonly<Record<string, PolicyDraftDocument>> | readonly PolicyDraftDocument[];
|
|
41
|
+
readonly roles?: Readonly<Record<string, PolicyDraftDocument>> | readonly PolicyDraftDocument[];
|
|
42
|
+
readonly evidence?: Readonly<Record<string, string | Uint8Array>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PolicyDraftData {
|
|
46
|
+
readonly models: Readonly<Record<string, JsonObject>>;
|
|
47
|
+
readonly roles: Readonly<Record<string, JsonObject>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type PolicyDraftResult =
|
|
51
|
+
| { readonly ok: true; readonly data: PolicyDraftData }
|
|
52
|
+
| { readonly ok: false; readonly issues: readonly PolicyDraftIssue[] };
|
|
53
|
+
|
|
54
|
+
/** Pure draft validation. Not admission, not a trusted loader, and not file I/O. */
|
|
55
|
+
export function validatePolicyDraft(input: PolicyDraftInput, options: PolicyDraftOptions): PolicyDraftResult;
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { RestrictedYamlError, parseRestrictedYaml } from "./restricted-yaml.mjs";
|
|
3
|
+
|
|
4
|
+
export const POLICY_DRAFT_MODEL_SCHEMA = "kxm.model.v2";
|
|
5
|
+
export const POLICY_DRAFT_ROLE_SCHEMA = "kxm.role.v2";
|
|
6
|
+
export const POLICY_DRAFT_PURPOSES = Object.freeze([
|
|
7
|
+
"writer",
|
|
8
|
+
"planner",
|
|
9
|
+
"reviewer-arch",
|
|
10
|
+
"reviewer-cli",
|
|
11
|
+
"experiment",
|
|
12
|
+
]);
|
|
13
|
+
export const POLICY_DRAFT_PERMISSIONS = Object.freeze(["edit", "read-only"]);
|
|
14
|
+
export const POLICY_DRAFT_STATUSES = Object.freeze(["admitted", "candidate", "retired"]);
|
|
15
|
+
|
|
16
|
+
const IDENTIFIER = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
17
|
+
const ROUTE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
18
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
19
|
+
const TOKEN = /[\s\x00-\x1f]/u;
|
|
20
|
+
const EFFORTS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
21
|
+
const MODES = Object.freeze(["headless", "interactive", "either"]);
|
|
22
|
+
const MODEL_KEYS = Object.freeze([
|
|
23
|
+
"schema", "id", "harness", "model", "vendor", "status", "permissions", "origin",
|
|
24
|
+
"thinking", "tags", "capabilities", "priority", "fallbacks", "limits",
|
|
25
|
+
]);
|
|
26
|
+
const ROLE_KEYS = Object.freeze([
|
|
27
|
+
"schema", "id", "purpose", "permission", "description", "roster",
|
|
28
|
+
"skills", "tools", "produces", "consumes", "policy",
|
|
29
|
+
]);
|
|
30
|
+
const ORIGIN_KEYS = Object.freeze(["source", "sha256"]);
|
|
31
|
+
const LIMIT_KEYS = Object.freeze(["contextTokens", "outputTokens", "timeoutMs"]);
|
|
32
|
+
const TOOL_KEYS = Object.freeze(["preset", "allow", "deny"]);
|
|
33
|
+
const TEMPLATE_KEYS = Object.freeze(["template", "schema"]);
|
|
34
|
+
const POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification"]);
|
|
35
|
+
const ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
|
|
36
|
+
const CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
|
|
37
|
+
|
|
38
|
+
function compareCodeUnits(left, right) {
|
|
39
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sortIssues(issues) {
|
|
43
|
+
return [...issues].sort((left, right) => compareCodeUnits(left.file, right.file)
|
|
44
|
+
|| compareCodeUnits(left.phase, right.phase)
|
|
45
|
+
|| compareCodeUnits(left.code, right.code)
|
|
46
|
+
|| compareCodeUnits(left.message, right.message));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function issue(phase, code, file, message) {
|
|
50
|
+
return { phase, code, file, message };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isObject(value) {
|
|
54
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function own(object, key) {
|
|
58
|
+
return Object.hasOwn(object, key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extraKeys(value, allowed) {
|
|
62
|
+
return Object.keys(value).filter((key) => !allowed.includes(key));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function freezeDeep(value) {
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
for (const child of Object.values(value)) freezeDeep(child);
|
|
68
|
+
Object.freeze(value);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cloneJson(value) {
|
|
74
|
+
return JSON.parse(JSON.stringify(value));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sha256Bytes(input) {
|
|
78
|
+
const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
|
|
79
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function canonicalVendor(value, aliases) {
|
|
83
|
+
if (typeof value !== "string") return "";
|
|
84
|
+
const lower = value.toLowerCase();
|
|
85
|
+
return own(aliases, lower) ? aliases[lower] : lower;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function identifier(value, max = 64) {
|
|
89
|
+
return typeof value === "string" && value.length >= 1 && value.length <= max && IDENTIFIER.test(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function routeId(value) {
|
|
93
|
+
return typeof value === "string" && value.length >= 1 && value.length <= 64 && ROUTE_ID.test(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function nonemptyToken(value, max = 512) {
|
|
97
|
+
return typeof value === "string" && value.length >= 1 && value.length <= max && !TOKEN.test(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function codePointCount(value) {
|
|
101
|
+
return [...value].length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sourcePath(value) {
|
|
105
|
+
if (!nonemptyToken(value)) return false;
|
|
106
|
+
if (value.startsWith("/") || value.includes("\\")) return false;
|
|
107
|
+
const parts = value.split("/");
|
|
108
|
+
return parts.length > 0 && parts.every((part) => part && part !== "." && part !== "..");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function uniqueStringList(value, allowed, { min = 1, max = 64 } = {}) {
|
|
112
|
+
if (!Array.isArray(value) || value.length < min || value.length > max) return false;
|
|
113
|
+
if (new Set(value).size !== value.length) return false;
|
|
114
|
+
return value.every((item) => allowed.includes(item));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function identifierList(value, { maxItems = 32, itemMax = 64 } = {}) {
|
|
118
|
+
if (!Array.isArray(value) || value.length > maxItems) return false;
|
|
119
|
+
if (new Set(value).size !== value.length) return false;
|
|
120
|
+
return value.every((item) => identifier(item, itemMax));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseDocument(value, label, issues) {
|
|
124
|
+
if (typeof value === "string" || value instanceof Uint8Array) {
|
|
125
|
+
try {
|
|
126
|
+
return parseRestrictedYaml(value, label);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof RestrictedYamlError) issues.push(...error.issues);
|
|
129
|
+
else issues.push(issue("parse", "invalid_yaml", label, error instanceof Error ? error.message : "invalid YAML"));
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (isObject(value)) return value;
|
|
134
|
+
issues.push(issue("schema", "root_not_object", label, "resource root must be a mapping"));
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectDocuments(raw, kind, issues) {
|
|
139
|
+
const collected = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
if (raw === undefined) return collected;
|
|
142
|
+
if (Array.isArray(raw)) {
|
|
143
|
+
for (const [index, value] of raw.entries()) {
|
|
144
|
+
const label = `${kind}[${index}]`;
|
|
145
|
+
const document = parseDocument(value, label, issues);
|
|
146
|
+
if (!document) continue;
|
|
147
|
+
const declared = document.id;
|
|
148
|
+
if (!routeId(declared) && !(kind === "roles" && identifier(declared))) {
|
|
149
|
+
issues.push(issue("schema", "missing_id", label, `${kind.slice(0, -1)} identity is missing or invalid`));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (seen.has(declared)) {
|
|
153
|
+
issues.push(issue("schema", kind === "models" ? "duplicate_route_id" : "duplicate_role_id", label, `duplicate ${kind.slice(0, -1)} identity ${declared}`));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
seen.add(declared);
|
|
157
|
+
collected.push({ id: declared, label: `${kind}/${declared}`, document });
|
|
158
|
+
}
|
|
159
|
+
return collected;
|
|
160
|
+
}
|
|
161
|
+
if (!isObject(raw)) {
|
|
162
|
+
issues.push(issue("schema", "root_not_object", kind, `${kind} must be a mapping or array`));
|
|
163
|
+
return collected;
|
|
164
|
+
}
|
|
165
|
+
for (const id of Object.keys(raw).sort(compareCodeUnits)) {
|
|
166
|
+
const label = `${kind}/${id}`;
|
|
167
|
+
if (kind === "models" ? !routeId(id) : !identifier(id)) {
|
|
168
|
+
issues.push(issue("schema", "invalid_id", label, `invalid ${kind.slice(0, -1)} identity`));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (seen.has(id)) {
|
|
172
|
+
issues.push(issue("schema", kind === "models" ? "duplicate_route_id" : "duplicate_role_id", label, `duplicate ${kind.slice(0, -1)} identity ${id}`));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
seen.add(id);
|
|
176
|
+
const document = parseDocument(raw[id], label, issues);
|
|
177
|
+
if (!document) continue;
|
|
178
|
+
if (document.id !== undefined && document.id !== id) {
|
|
179
|
+
issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
collected.push({ id, label, document });
|
|
183
|
+
}
|
|
184
|
+
return collected;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function closedObject(value, allowed, label, issues, code = "schema_additionalProperties") {
|
|
188
|
+
if (!isObject(value)) {
|
|
189
|
+
issues.push(issue("schema", "root_not_object", label, "value must be a mapping"));
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const unexpected = extraKeys(value, allowed);
|
|
193
|
+
if (unexpected.length > 0) {
|
|
194
|
+
issues.push(issue("schema", code, label, `unexpected properties: ${unexpected.join(", ")}`));
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function required(value, keys, label, issues) {
|
|
201
|
+
for (const key of keys) {
|
|
202
|
+
if (!own(value, key)) issues.push(issue("schema", "schema_required", label, `missing ${key}`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function validateOrigin(origin, label, issues) {
|
|
207
|
+
if (!closedObject(origin, ORIGIN_KEYS, `${label}.origin`, issues)) return;
|
|
208
|
+
required(origin, ORIGIN_KEYS, `${label}.origin`, issues);
|
|
209
|
+
if (origin.source !== undefined && !sourcePath(origin.source)) {
|
|
210
|
+
issues.push(issue("schema", "origin_source_invalid", `${label}.origin`, "origin source must be a contained relative path"));
|
|
211
|
+
}
|
|
212
|
+
if (origin.sha256 !== undefined && (typeof origin.sha256 !== "string" || !SHA256.test(origin.sha256))) {
|
|
213
|
+
issues.push(issue("schema", "origin_hash_invalid", `${label}.origin`, "origin sha256 must be a 64-character hex digest"));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validateModelShape(document, id, label, issues) {
|
|
218
|
+
if (!closedObject(document, MODEL_KEYS, label, issues)) return;
|
|
219
|
+
required(document, ["schema", "harness", "model", "vendor", "status", "permissions"], label, issues);
|
|
220
|
+
if (document.schema !== undefined && document.schema !== POLICY_DRAFT_MODEL_SCHEMA) {
|
|
221
|
+
issues.push(issue("schema", "schema_identity_mismatch", label, `expected ${POLICY_DRAFT_MODEL_SCHEMA}, received ${String(document.schema)}`));
|
|
222
|
+
}
|
|
223
|
+
if (document.id !== undefined && document.id !== id) {
|
|
224
|
+
issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
|
|
225
|
+
}
|
|
226
|
+
if (document.harness !== undefined && !identifier(document.harness)) {
|
|
227
|
+
issues.push(issue("schema", "schema_pattern", label, "harness must be an identifier"));
|
|
228
|
+
}
|
|
229
|
+
if (document.model !== undefined && (typeof document.model !== "string" || codePointCount(document.model) < 1 || codePointCount(document.model) > 200)) {
|
|
230
|
+
issues.push(issue("schema", "schema_pattern", label, "model must be a nonempty string"));
|
|
231
|
+
}
|
|
232
|
+
if (document.vendor !== undefined && !nonemptyToken(document.vendor, 64)) {
|
|
233
|
+
issues.push(issue("schema", "schema_pattern", label, "vendor must be a nonempty token"));
|
|
234
|
+
}
|
|
235
|
+
if (document.status !== undefined && !POLICY_DRAFT_STATUSES.includes(document.status)) {
|
|
236
|
+
issues.push(issue("schema", "unsupported_status", label, "status must be admitted, candidate, or retired"));
|
|
237
|
+
}
|
|
238
|
+
if (document.permissions !== undefined && !uniqueStringList(document.permissions, POLICY_DRAFT_PERMISSIONS, { min: 1, max: 2 })) {
|
|
239
|
+
issues.push(issue("schema", "schema_enum", label, "permissions must be unique edit and/or read-only"));
|
|
240
|
+
}
|
|
241
|
+
if (document.harness === "pi" && !own(document, "origin")) {
|
|
242
|
+
issues.push(issue("schema", "schema_required", label, "Pi routes require origin source and sha256"));
|
|
243
|
+
}
|
|
244
|
+
if (own(document, "origin")) validateOrigin(document.origin, label, issues);
|
|
245
|
+
if (document.thinking !== undefined && (typeof document.thinking !== "string" || document.thinking.length < 1 || document.thinking.length > 64)) {
|
|
246
|
+
issues.push(issue("schema", "schema_pattern", label, "thinking must be a short string"));
|
|
247
|
+
}
|
|
248
|
+
if (document.tags !== undefined && !identifierList(document.tags)) {
|
|
249
|
+
issues.push(issue("schema", "schema_pattern", label, "tags must be unique identifiers"));
|
|
250
|
+
}
|
|
251
|
+
if (document.capabilities !== undefined && !identifierList(document.capabilities)) {
|
|
252
|
+
issues.push(issue("schema", "schema_pattern", label, "capabilities must be unique identifiers"));
|
|
253
|
+
}
|
|
254
|
+
if (document.priority !== undefined && (!Number.isInteger(document.priority) || document.priority < -10000 || document.priority > 10000)) {
|
|
255
|
+
issues.push(issue("schema", "schema_type", label, "priority must be an integer in range"));
|
|
256
|
+
}
|
|
257
|
+
if (document.fallbacks !== undefined) {
|
|
258
|
+
if (!Array.isArray(document.fallbacks) || document.fallbacks.length > 8) {
|
|
259
|
+
issues.push(issue("schema", "schema_type", label, "fallbacks must be a bounded array"));
|
|
260
|
+
} else {
|
|
261
|
+
for (const [index, fallback] of document.fallbacks.entries()) {
|
|
262
|
+
if (!isObject(fallback)) {
|
|
263
|
+
issues.push(issue("schema", "root_not_object", `${label}.fallbacks[${index}]`, "fallback must be a mapping"));
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const keys = Object.keys(fallback);
|
|
267
|
+
const profile = keys.length === 1 && own(fallback, "profile") && identifier(fallback.profile);
|
|
268
|
+
const tag = own(fallback, "tag") && identifier(fallback.tag)
|
|
269
|
+
&& extraKeys(fallback, ["tag", "capabilities"]).length === 0
|
|
270
|
+
&& (fallback.capabilities === undefined || identifierList(fallback.capabilities));
|
|
271
|
+
const provider = own(fallback, "provider") && own(fallback, "model")
|
|
272
|
+
&& extraKeys(fallback, ["provider", "model"]).length === 0
|
|
273
|
+
&& identifier(fallback.provider)
|
|
274
|
+
&& typeof fallback.model === "string"
|
|
275
|
+
&& codePointCount(fallback.model) >= 1 && codePointCount(fallback.model) <= 200;
|
|
276
|
+
if (!profile && !tag && !provider) {
|
|
277
|
+
issues.push(issue("schema", "schema_oneOf", `${label}.fallbacks[${index}]`, "fallback does not match a retained v1 selector"));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (document.limits !== undefined) {
|
|
283
|
+
if (!closedObject(document.limits, LIMIT_KEYS, `${label}.limits`, issues)) return;
|
|
284
|
+
for (const key of ["contextTokens", "outputTokens"]) {
|
|
285
|
+
if (document.limits[key] !== undefined && (!Number.isInteger(document.limits[key]) || document.limits[key] < 1)) {
|
|
286
|
+
issues.push(issue("schema", "schema_type", `${label}.limits`, `${key} must be a positive integer`));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (document.limits.timeoutMs !== undefined && (!Number.isInteger(document.limits.timeoutMs) || document.limits.timeoutMs < 0 || document.limits.timeoutMs > 31536000000)) {
|
|
290
|
+
issues.push(issue("schema", "schema_type", `${label}.limits`, "timeoutMs must be a non-negative integer between 0 and 31536000000"));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function validateTemplateList(value, label, issues) {
|
|
296
|
+
if (!Array.isArray(value)) {
|
|
297
|
+
issues.push(issue("schema", "schema_type", label, "must be an array"));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
for (const [index, entry] of value.entries()) {
|
|
301
|
+
const path = `${label}[${index}]`;
|
|
302
|
+
if (!closedObject(entry, TEMPLATE_KEYS, path, issues)) continue;
|
|
303
|
+
if (typeof entry.template !== "string" || !entry.template) {
|
|
304
|
+
issues.push(issue("schema", "schema_required", path, "template is required"));
|
|
305
|
+
}
|
|
306
|
+
if (entry.schema !== undefined && typeof entry.schema !== "string") {
|
|
307
|
+
issues.push(issue("schema", "schema_type", path, "schema must be a string"));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function validateRoleShape(document, id, label, issues) {
|
|
313
|
+
if (!closedObject(document, ROLE_KEYS, label, issues)) return;
|
|
314
|
+
required(document, ["schema", "purpose", "permission", "description", "roster"], label, issues);
|
|
315
|
+
if (document.schema !== undefined && document.schema !== POLICY_DRAFT_ROLE_SCHEMA) {
|
|
316
|
+
issues.push(issue("schema", "schema_identity_mismatch", label, `expected ${POLICY_DRAFT_ROLE_SCHEMA}, received ${String(document.schema)}`));
|
|
317
|
+
}
|
|
318
|
+
if (document.id !== undefined && document.id !== id) {
|
|
319
|
+
issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
|
|
320
|
+
}
|
|
321
|
+
if (document.purpose !== undefined && !POLICY_DRAFT_PURPOSES.includes(document.purpose)) {
|
|
322
|
+
issues.push(issue("schema", "unsupported_purpose", label, "purpose must be a runner role label"));
|
|
323
|
+
}
|
|
324
|
+
if (document.permission !== undefined && !POLICY_DRAFT_PERMISSIONS.includes(document.permission)) {
|
|
325
|
+
issues.push(issue("schema", "schema_enum", label, "permission must be edit or read-only"));
|
|
326
|
+
}
|
|
327
|
+
if (document.description !== undefined && (typeof document.description !== "string" || document.description.length < 1 || document.description.length > 2000)) {
|
|
328
|
+
issues.push(issue("schema", "schema_type", label, "description must be a bounded string"));
|
|
329
|
+
}
|
|
330
|
+
if (document.roster !== undefined) {
|
|
331
|
+
if (!Array.isArray(document.roster) || document.roster.length > 32) {
|
|
332
|
+
issues.push(issue("schema", "schema_type", label, "roster must be a bounded array"));
|
|
333
|
+
} else {
|
|
334
|
+
const seenRoutes = new Set();
|
|
335
|
+
for (const [index, entry] of document.roster.entries()) {
|
|
336
|
+
const path = `${label}.roster[${index}]`;
|
|
337
|
+
if (!closedObject(entry, ROSTER_ENTRY_KEYS, path, issues)) continue;
|
|
338
|
+
if (!routeId(entry.route)) {
|
|
339
|
+
issues.push(issue("schema", "schema_pattern", path, "roster route must be a route identity"));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (seenRoutes.has(entry.route)) {
|
|
343
|
+
issues.push(issue("schema", "duplicate_route_id", path, `duplicate roster route ${entry.route}`));
|
|
344
|
+
}
|
|
345
|
+
seenRoutes.add(entry.route);
|
|
346
|
+
if (entry.effort !== undefined && !EFFORTS.includes(entry.effort)) {
|
|
347
|
+
issues.push(issue("schema", "schema_enum", path, "effort is not a known helper effort"));
|
|
348
|
+
}
|
|
349
|
+
if (entry.mode !== undefined && !MODES.includes(entry.mode)) {
|
|
350
|
+
issues.push(issue("schema", "schema_enum", path, "mode must be headless, interactive, or either"));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (document.skills !== undefined) {
|
|
356
|
+
if (!Array.isArray(document.skills) || document.skills.length > 64
|
|
357
|
+
|| document.skills.some((item) => typeof item !== "string" || item.length < 1 || item.length > 128)) {
|
|
358
|
+
issues.push(issue("schema", "schema_type", label, "skills must be bounded strings"));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (document.tools !== undefined) {
|
|
362
|
+
if (!closedObject(document.tools, TOOL_KEYS, `${label}.tools`, issues)) return;
|
|
363
|
+
if (document.tools.preset !== undefined && typeof document.tools.preset !== "string") {
|
|
364
|
+
issues.push(issue("schema", "schema_type", `${label}.tools`, "preset must be a string"));
|
|
365
|
+
}
|
|
366
|
+
for (const key of ["allow", "deny"]) {
|
|
367
|
+
if (document.tools[key] !== undefined && (!Array.isArray(document.tools[key]) || document.tools[key].some((item) => typeof item !== "string"))) {
|
|
368
|
+
issues.push(issue("schema", "schema_type", `${label}.tools`, `${key} must be a string array`));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (document.produces !== undefined) validateTemplateList(document.produces, `${label}.produces`, issues);
|
|
373
|
+
if (document.consumes !== undefined) validateTemplateList(document.consumes, `${label}.consumes`, issues);
|
|
374
|
+
if (document.policy !== undefined) {
|
|
375
|
+
if (!closedObject(document.policy, POLICY_KEYS, `${label}.policy`, issues)) return;
|
|
376
|
+
if (document.policy.vendorIndependenceRequired !== undefined && typeof document.policy.vendorIndependenceRequired !== "boolean") {
|
|
377
|
+
issues.push(issue("schema", "schema_type", `${label}.policy`, "vendorIndependenceRequired must be boolean"));
|
|
378
|
+
}
|
|
379
|
+
if (document.policy.requiresGateVerification !== undefined && typeof document.policy.requiresGateVerification !== "boolean") {
|
|
380
|
+
issues.push(issue("schema", "schema_type", `${label}.policy`, "requiresGateVerification must be boolean"));
|
|
381
|
+
}
|
|
382
|
+
if (document.policy.maxTransitions !== undefined && (!Number.isInteger(document.policy.maxTransitions) || document.policy.maxTransitions < 1)) {
|
|
383
|
+
issues.push(issue("schema", "schema_type", `${label}.policy`, "maxTransitions must be a positive integer"));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function subset(values, allowed) {
|
|
389
|
+
return values.every((value) => allowed.includes(value));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function validateModelSemantics(model, label, options, evidence, issues) {
|
|
393
|
+
const aliases = options.vendorAliases ?? {};
|
|
394
|
+
const ceiling = options.ceilings[model.harness];
|
|
395
|
+
if (!ceiling) {
|
|
396
|
+
issues.push(issue("semantic", "unsupported_harness", label, `no code-owned ceiling for harness ${String(model.harness)}`));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const vendor = canonicalVendor(model.vendor, aliases);
|
|
400
|
+
if (options.piAllowedProviders.includes(vendor)) {
|
|
401
|
+
issues.push(issue("semantic", "vendor_aggregator", label, "vendor must name the model vendor, not a billing aggregator"));
|
|
402
|
+
}
|
|
403
|
+
if (!subset(model.permissions, ceiling.permissions)) {
|
|
404
|
+
issues.push(issue("semantic", "permission_escalation", label, "route permissions exceed the harness ceiling"));
|
|
405
|
+
}
|
|
406
|
+
if (Array.isArray(ceiling.models) && ceiling.models.length > 0 && !ceiling.models.includes(model.model)) {
|
|
407
|
+
issues.push(issue("semantic", "model_not_in_ceiling", label, "exact model is not listed on the harness ceiling"));
|
|
408
|
+
}
|
|
409
|
+
if (model.harness === "pi") {
|
|
410
|
+
const parts = typeof model.model === "string" ? model.model.split("/") : [];
|
|
411
|
+
if (parts.length < 3 || parts.some((part) => !part) || !options.piAllowedProviders.includes(parts[0])) {
|
|
412
|
+
issues.push(issue("semantic", "unsupported_pi_model", label, "Pi model must use an allowlisted aggregator prefix"));
|
|
413
|
+
} else {
|
|
414
|
+
const prefix = canonicalVendor(parts[1], aliases);
|
|
415
|
+
if (options.nativePiBrakeProviders.includes(prefix) || options.nativePiBrakeProviders.includes(vendor)) {
|
|
416
|
+
issues.push(issue("semantic", "pi_native_vendor_forbidden", label, "native vendor cannot use Pi"));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const origin = model.origin;
|
|
420
|
+
if (isObject(origin) && typeof origin.source === "string" && typeof origin.sha256 === "string") {
|
|
421
|
+
if (!own(evidence, origin.source)) {
|
|
422
|
+
issues.push(issue("semantic", "origin_evidence_missing", label, `missing evidence bytes for ${origin.source}`));
|
|
423
|
+
} else if (sha256Bytes(evidence[origin.source]) !== origin.sha256) {
|
|
424
|
+
issues.push(issue("semantic", "origin_hash_mismatch", label, "origin evidence hash does not match supplied bytes"));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
if (vendor !== ceiling.provider || (typeof model.model === "string" && model.model.includes("/"))) {
|
|
429
|
+
issues.push(issue("semantic", "native_vendor_mismatch", label, "native route vendor or model does not match the harness ceiling"));
|
|
430
|
+
}
|
|
431
|
+
if (own(model, "origin")) {
|
|
432
|
+
issues.push(issue("semantic", "origin_unexpected", label, "origin is only valid for Pi routes"));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function admittedRoute(model) {
|
|
438
|
+
return model.status === "admitted";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function validateRoleSemantics(role, id, label, models, options, issues) {
|
|
442
|
+
if (!Array.isArray(role.roster)) return;
|
|
443
|
+
for (const [index, entry] of role.roster.entries()) {
|
|
444
|
+
const path = `${label}.roster[${index}]`;
|
|
445
|
+
const model = models.get(entry.route);
|
|
446
|
+
if (!model) {
|
|
447
|
+
issues.push(issue("reference", "route_unknown", path, `roster references unknown route ${entry.route}`));
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const ceiling = options.ceilings[model.harness];
|
|
451
|
+
if (!ceiling) continue;
|
|
452
|
+
if (Array.isArray(ceiling.roles) && ceiling.roles.length > 0 && !ceiling.roles.includes(role.purpose)) {
|
|
453
|
+
issues.push(issue("semantic", "purpose_ceiling_mismatch", path, `purpose ${role.purpose} is not allowed on harness ${model.harness}`));
|
|
454
|
+
}
|
|
455
|
+
if (!model.permissions.includes(role.permission)) {
|
|
456
|
+
issues.push(issue("semantic", "permission_escalation", path, "role permission is not granted by the route"));
|
|
457
|
+
}
|
|
458
|
+
if (!ceiling.permissions.includes(role.permission)) {
|
|
459
|
+
issues.push(issue("semantic", "permission_escalation", path, "role permission exceeds the harness ceiling"));
|
|
460
|
+
}
|
|
461
|
+
if (Array.isArray(ceiling.efforts) && ceiling.efforts.length > 0 && entry.effort !== undefined && !ceiling.efforts.includes(entry.effort)) {
|
|
462
|
+
issues.push(issue("semantic", "effort_ceiling_mismatch", path, "effort is not allowed on the harness ceiling"));
|
|
463
|
+
}
|
|
464
|
+
if (!admittedRoute(model)) {
|
|
465
|
+
issues.push(issue("semantic", "route_not_admitted", path, `route ${entry.route} is ${model.status} and is not treated as admitted`));
|
|
466
|
+
}
|
|
467
|
+
if (model.harness === "pi") {
|
|
468
|
+
if (role.permission === "edit" && !["writer", "experiment"].includes(role.purpose)) {
|
|
469
|
+
issues.push(issue("semantic", "permission_escalation", path, "Pi critic/planner cannot edit"));
|
|
470
|
+
}
|
|
471
|
+
if (role.purpose === "writer" && (model.permissions.length !== 1 || model.permissions[0] !== "edit")) {
|
|
472
|
+
issues.push(issue("semantic", "permission_escalation", path, "Pi writer requires edit permission only"));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (id === role.purpose && CRITIC_PURPOSES.includes(role.purpose)) {
|
|
478
|
+
const admitted = role.roster
|
|
479
|
+
.map((entry) => models.get(entry.route))
|
|
480
|
+
.filter((model) => model && admittedRoute(model));
|
|
481
|
+
if (admitted.length !== 1) {
|
|
482
|
+
issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list exactly one admitted route"));
|
|
483
|
+
} else if (admitted[0].permissions.length !== 1 || admitted[0].permissions[0] !== "read-only" || role.permission !== "read-only") {
|
|
484
|
+
issues.push(issue("semantic", "permission_escalation", label, "critic purpose files must be read-only"));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function validateCriticVendors(roles, models, options, issues) {
|
|
490
|
+
const aliases = options.vendorAliases ?? {};
|
|
491
|
+
const critics = [];
|
|
492
|
+
for (const purpose of CRITIC_PURPOSES) {
|
|
493
|
+
const role = roles.get(purpose);
|
|
494
|
+
if (!role || role.purpose !== purpose) continue;
|
|
495
|
+
const admitted = (role.roster ?? [])
|
|
496
|
+
.map((entry) => models.get(entry.route))
|
|
497
|
+
.filter((model) => model && admittedRoute(model));
|
|
498
|
+
if (admitted.length === 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) });
|
|
499
|
+
}
|
|
500
|
+
if (critics.length === 2 && critics[0].vendor === critics[1].vendor) {
|
|
501
|
+
issues.push(issue("semantic", "critic_vendor_collision", "roles/reviewer-arch", "required critics must have independent vendors"));
|
|
502
|
+
}
|
|
503
|
+
const writer = roles.get("writer");
|
|
504
|
+
if (writer && critics.length > 0) {
|
|
505
|
+
for (const entry of writer.roster ?? []) {
|
|
506
|
+
const model = models.get(entry.route);
|
|
507
|
+
if (!model || !admittedRoute(model)) continue;
|
|
508
|
+
const vendor = canonicalVendor(model.vendor, aliases);
|
|
509
|
+
if (critics.some((critic) => critic.vendor === vendor)) {
|
|
510
|
+
issues.push(issue("semantic", "writer_critic_vendor_collision", "roles/writer", "writer and critics must have independent vendors"));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Pure draft validation. Not admission, not a trusted loader, and not file I/O. */
|
|
517
|
+
export function validatePolicyDraft(input, options) {
|
|
518
|
+
const issues = [];
|
|
519
|
+
if (!isObject(input)) {
|
|
520
|
+
return { ok: false, issues: [issue("schema", "root_not_object", "<draft>", "draft input must be a mapping")] };
|
|
521
|
+
}
|
|
522
|
+
const unexpected = extraKeys(input, ["models", "roles", "evidence"]);
|
|
523
|
+
if (unexpected.length > 0) {
|
|
524
|
+
issues.push(issue("schema", "schema_additionalProperties", "<draft>", `unexpected properties: ${unexpected.join(", ")}`));
|
|
525
|
+
}
|
|
526
|
+
if (!isObject(options) || !isObject(options.ceilings)
|
|
527
|
+
|| !Array.isArray(options.nativePiBrakeProviders)
|
|
528
|
+
|| !Array.isArray(options.piAllowedProviders)) {
|
|
529
|
+
issues.push(issue("semantic", "ceilings_required", "<options>", "code-owned ceilings and vendor brakes must be supplied explicitly"));
|
|
530
|
+
return { ok: false, issues: sortIssues(issues) };
|
|
531
|
+
}
|
|
532
|
+
const evidence = isObject(input.evidence) ? input.evidence : {};
|
|
533
|
+
if (input.evidence !== undefined && !isObject(input.evidence)) {
|
|
534
|
+
issues.push(issue("schema", "root_not_object", "evidence", "evidence must be a mapping of source path to bytes"));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const modelDocs = collectDocuments(input.models, "models", issues);
|
|
538
|
+
const roleDocs = collectDocuments(input.roles, "roles", issues);
|
|
539
|
+
for (const entry of modelDocs) validateModelShape(entry.document, entry.id, entry.label, issues);
|
|
540
|
+
for (const entry of roleDocs) validateRoleShape(entry.document, entry.id, entry.label, issues);
|
|
541
|
+
if (issues.length > 0) return { ok: false, issues: sortIssues(issues) };
|
|
542
|
+
|
|
543
|
+
const models = new Map();
|
|
544
|
+
for (const entry of modelDocs) {
|
|
545
|
+
validateModelSemantics(entry.document, entry.label, options, evidence, issues);
|
|
546
|
+
const normalized = cloneJson(entry.document);
|
|
547
|
+
normalized.id = entry.id;
|
|
548
|
+
models.set(entry.id, normalized);
|
|
549
|
+
}
|
|
550
|
+
const roles = new Map();
|
|
551
|
+
for (const entry of roleDocs) {
|
|
552
|
+
validateRoleSemantics(entry.document, entry.id, entry.label, models, options, issues);
|
|
553
|
+
const normalized = cloneJson(entry.document);
|
|
554
|
+
normalized.id = entry.id;
|
|
555
|
+
roles.set(entry.id, normalized);
|
|
556
|
+
}
|
|
557
|
+
validateCriticVendors(roles, models, options, issues);
|
|
558
|
+
if (issues.length > 0) return { ok: false, issues: sortIssues(issues) };
|
|
559
|
+
|
|
560
|
+
const data = freezeDeep({
|
|
561
|
+
models: Object.fromEntries([...models.entries()].sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
562
|
+
roles: Object.fromEntries([...roles.entries()].sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
563
|
+
});
|
|
564
|
+
return { ok: true, data };
|
|
565
|
+
}
|