@yansirplus/cli 0.5.17 → 0.5.19
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/README.md +12 -6
- package/agent-catalog/agentOS/SKILL.md +22 -0
- package/agent-catalog/agentOS/references/agent/decision-graph.json +530 -0
- package/agent-catalog/agentOS/references/agent/errors.json +497 -0
- package/agent-catalog/agentOS/references/agent/invariant-matrix.json +337 -0
- package/agent-catalog/agentOS/references/agent/primitives.json +989 -0
- package/agent-catalog/agentOS/references/agent/recipes.json +109 -0
- package/agent-catalog/agentOS/references/agent/start-here.md +25 -0
- package/agent-catalog/agentOS/references/package-map.md +73 -0
- package/agent-catalog/agentOS/references/provenance.json +251 -0
- package/agent-catalog/agentOS/references/public-api/cli.md +20 -0
- package/agent-catalog/agentOS/references/public-api/client.md +90 -0
- package/agent-catalog/agentOS/references/public-api/core.md +1907 -0
- package/agent-catalog/agentOS/references/public-api/runtime.md +843 -0
- package/dist/build/agent-authoring/config.d.ts +20 -5
- package/dist/build/agent-authoring/config.js +132 -32
- package/dist/build/agent-authoring/manifest-compiler.d.ts +131 -2
- package/dist/build/agent-authoring/manifest-compiler.js +630 -8
- package/dist/build/agent-authoring/shared.d.ts +2 -0
- package/dist/build/agent-authoring/shared.js +2 -0
- package/dist/build/agent-authoring/static-target.d.ts +6 -3
- package/dist/build/agent-authoring/static-target.js +1900 -281
- package/dist/build/agent-authoring.d.ts +3 -3
- package/dist/build/agent-authoring.js +1 -1
- package/dist/build/build-cli.d.ts +1 -1
- package/dist/build/build-cli.js +1629 -26
- package/dist/check/algorithmic/client-boundary-checks.mjs +3 -34
- package/dist/check/algorithmic/convergence-smoke-checks.mjs +652 -6
- package/dist/check/algorithmic/distribution-checks.mjs +8 -7
- package/dist/check/algorithmic/package-boundary-checks.mjs +3 -2
- package/dist/check/algorithmic/repo-surface-checks.mjs +55 -1
- package/dist/check/algorithmic/static-target-checks.mjs +83 -5
- package/dist/check/algorithmic-checks.mjs +10 -17
- package/dist/check/default-gate.mjs +3 -3
- package/dist/check/effect-scan-gate.mjs +121 -0
- package/dist/check/package-graph.mjs +2 -32
- package/dist/consumer-overlay.mjs +1281 -0
- package/dist/lib/public-api-model.mjs +19 -0
- package/dist/lib/repo-source-files.mjs +26 -0
- package/dist/lib/ts-module-loader.mjs +44 -0
- package/dist/lib/workspace-manifest.mjs +77 -0
- package/dist/main.mjs +171 -21
- package/dist/release-status.mjs +515 -0
- package/package.json +8 -4
- package/dist/check/check-coverage.mjs +0 -231
- package/dist/generate/generate-agent-docs.mjs +0 -435
- package/dist/generate/generate-carrier-reference.mjs +0 -514
- package/dist/generate/generate-docs.mjs +0 -345
- package/dist/generate/generate-effect-skill-manifests.mjs +0 -193
- package/dist/generate/project-docs-site.mjs +0 -190
- package/dist/lib/boundary-rules.mjs +0 -63
- package/dist/lib/capability-routes.mjs +0 -354
- package/dist/lib/projection-sink.mjs +0 -113
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
const root = process.cwd();
|
|
8
|
-
const check = process.argv.includes("--check");
|
|
9
|
-
const watch = process.argv.includes("--watch");
|
|
10
|
-
const docsRoot = path.join(root, "docs");
|
|
11
|
-
const targetRoot = path.join(root, "tooling/docs-site/src/content/docs");
|
|
12
|
-
const failures = [];
|
|
13
|
-
|
|
14
|
-
const generatedNotice = (source) =>
|
|
15
|
-
`<!-- generated by packages/cli/src/generate/project-docs-site.mjs; edit ${source} -->`;
|
|
16
|
-
|
|
17
|
-
const walk = (dir) => {
|
|
18
|
-
if (!fs.existsSync(dir)) return [];
|
|
19
|
-
const out = [];
|
|
20
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
-
const full = path.join(dir, entry.name);
|
|
22
|
-
if (entry.isDirectory()) {
|
|
23
|
-
out.push(...walk(full));
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (entry.isFile()) out.push(full);
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const slash = (value) => value.replaceAll(path.sep, "/");
|
|
32
|
-
const relRoot = (file) => slash(path.relative(root, file));
|
|
33
|
-
const escapeYaml = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
34
|
-
|
|
35
|
-
const isProjectedSource = (file) => {
|
|
36
|
-
const rel = slash(path.relative(docsRoot, file));
|
|
37
|
-
if (!file.endsWith(".md")) return false;
|
|
38
|
-
if (rel.startsWith("templates/")) return false;
|
|
39
|
-
return true;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const siteRouteForDocsRel = (docsRel, anchor = "") => {
|
|
43
|
-
const normalized = slash(docsRel).replace(/\.md$/u, "");
|
|
44
|
-
if (normalized === "README") return `/${anchor}`;
|
|
45
|
-
return `/${normalized}/${anchor}`;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const targetRelForDocsRel = (docsRel) => {
|
|
49
|
-
if (docsRel === "README.md") return "index.md";
|
|
50
|
-
return docsRel;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const parseTargetHref = (href, sourceDocsRel) => {
|
|
54
|
-
if (!href.endsWith(".md") && !href.includes(".md#")) return null;
|
|
55
|
-
const [rawPath, rawAnchor] = href.split("#");
|
|
56
|
-
if (!rawPath.endsWith(".md")) return null;
|
|
57
|
-
const sourceDir = path.posix.dirname(sourceDocsRel);
|
|
58
|
-
const targetDocsRel = slash(path.posix.normalize(path.posix.join(sourceDir, rawPath)));
|
|
59
|
-
const anchor = rawAnchor === undefined ? "" : `#${rawAnchor}`;
|
|
60
|
-
return siteRouteForDocsRel(targetDocsRel, anchor);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const rewriteLinksForSite = (text, sourceDocsRel) =>
|
|
64
|
-
text.replace(/\[([^\]\n]+)\]\(([^)\s]+)(\s+"[^"]*")?\)/gu, (full, label, href, title = "") => {
|
|
65
|
-
const rewritten = parseTargetHref(href, sourceDocsRel);
|
|
66
|
-
return rewritten === null ? full : `[${label}](${rewritten}${title})`;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const projectMarkdown = (sourceFile, options = {}) => {
|
|
70
|
-
const docsRel = slash(path.relative(docsRoot, sourceFile));
|
|
71
|
-
const sourceRel = relRoot(sourceFile);
|
|
72
|
-
const source = fs.readFileSync(sourceFile, "utf8").replace(/\s+$/u, "");
|
|
73
|
-
const h1 = source.match(/^# (.+)$/mu);
|
|
74
|
-
const title = h1?.[1] ?? sourceRel;
|
|
75
|
-
const withoutH1 =
|
|
76
|
-
h1 === null ? source : source.replace(/^# .+\r?\n\r?\n?/u, "").replace(/\s+$/u, "");
|
|
77
|
-
const bodyPrefix = options.bodyPrefix ?? "";
|
|
78
|
-
const body = `${bodyPrefix}${rewriteLinksForSite(withoutH1, docsRel)}`;
|
|
79
|
-
return [
|
|
80
|
-
"---",
|
|
81
|
-
`title: "${escapeYaml(title)}"`,
|
|
82
|
-
"---",
|
|
83
|
-
"",
|
|
84
|
-
generatedNotice(sourceRel),
|
|
85
|
-
"",
|
|
86
|
-
body,
|
|
87
|
-
"",
|
|
88
|
-
].join("\n");
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const formatProjectedMarkdown = (expected) => {
|
|
92
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-docs-site-"));
|
|
93
|
-
try {
|
|
94
|
-
for (const [targetRel, content] of expected) {
|
|
95
|
-
const target = path.join(tmpDir, targetRel);
|
|
96
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
97
|
-
fs.writeFileSync(target, `${content.replace(/\s+$/u, "")}\n`);
|
|
98
|
-
}
|
|
99
|
-
const result = spawnSync("vp", ["fmt", tmpDir, "--write"], {
|
|
100
|
-
cwd: root,
|
|
101
|
-
encoding: "utf8",
|
|
102
|
-
});
|
|
103
|
-
if (result.status !== 0) {
|
|
104
|
-
failures.push(
|
|
105
|
-
`docs-site projection formatting failed: ${result.stderr || result.stdout || result.status}`,
|
|
106
|
-
);
|
|
107
|
-
return expected;
|
|
108
|
-
}
|
|
109
|
-
const formatted = new Map();
|
|
110
|
-
for (const targetRel of expected.keys()) {
|
|
111
|
-
formatted.set(targetRel, fs.readFileSync(path.join(tmpDir, targetRel), "utf8"));
|
|
112
|
-
}
|
|
113
|
-
return formatted;
|
|
114
|
-
} finally {
|
|
115
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const removeEmptyDirs = (dir) => {
|
|
120
|
-
if (!fs.existsSync(dir)) return;
|
|
121
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
122
|
-
if (entry.isDirectory()) removeEmptyDirs(path.join(dir, entry.name));
|
|
123
|
-
}
|
|
124
|
-
if (dir !== targetRoot && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const project = () => {
|
|
128
|
-
failures.length = 0;
|
|
129
|
-
const sources = walk(docsRoot).filter(isProjectedSource).sort();
|
|
130
|
-
const expected = new Map();
|
|
131
|
-
|
|
132
|
-
for (const source of sources) {
|
|
133
|
-
const docsRel = slash(path.relative(docsRoot, source));
|
|
134
|
-
const targetRel = targetRelForDocsRel(docsRel);
|
|
135
|
-
expected.set(targetRel, projectMarkdown(source));
|
|
136
|
-
}
|
|
137
|
-
const formattedExpected = formatProjectedMarkdown(expected);
|
|
138
|
-
|
|
139
|
-
for (const [targetRel, content] of formattedExpected) {
|
|
140
|
-
const target = path.join(targetRoot, targetRel);
|
|
141
|
-
const expectedText = `${content.replace(/\s+$/u, "")}\n`;
|
|
142
|
-
if (check) {
|
|
143
|
-
const actual = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
|
144
|
-
if (actual !== expectedText) failures.push(`${slash(path.relative(root, target))} is stale`);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
148
|
-
fs.writeFileSync(target, expectedText);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const actualTargets = walk(targetRoot)
|
|
152
|
-
.filter((file) => file.endsWith(".md"))
|
|
153
|
-
.map((file) => slash(path.relative(targetRoot, file)))
|
|
154
|
-
.sort();
|
|
155
|
-
for (const targetRel of actualTargets) {
|
|
156
|
-
if (!formattedExpected.has(targetRel)) {
|
|
157
|
-
const target = path.join(targetRoot, targetRel);
|
|
158
|
-
if (check) {
|
|
159
|
-
failures.push(`${slash(path.relative(root, target))} is stale extra projection`);
|
|
160
|
-
} else {
|
|
161
|
-
fs.rmSync(target);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (!check) removeEmptyDirs(targetRoot);
|
|
166
|
-
|
|
167
|
-
if (failures.length > 0) {
|
|
168
|
-
console.error(failures.join("\n"));
|
|
169
|
-
process.exitCode = 1;
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
console.log(`projected docs site content for ${expected.size} pages`);
|
|
174
|
-
return true;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
project();
|
|
178
|
-
|
|
179
|
-
if (watch) {
|
|
180
|
-
console.log("watching docs/** for docs-site projection changes");
|
|
181
|
-
let timer = null;
|
|
182
|
-
const schedule = () => {
|
|
183
|
-
if (timer !== null) clearTimeout(timer);
|
|
184
|
-
timer = setTimeout(() => {
|
|
185
|
-
timer = null;
|
|
186
|
-
project();
|
|
187
|
-
}, 100);
|
|
188
|
-
};
|
|
189
|
-
fs.watch(docsRoot, { recursive: true }, schedule);
|
|
190
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
-
|
|
6
|
-
export const readBoundaryRulesSource = (root, failures = []) => {
|
|
7
|
-
const file = "docs/agent/boundary-rules.source.json";
|
|
8
|
-
try {
|
|
9
|
-
const value = JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
|
|
10
|
-
if (!isRecord(value)) {
|
|
11
|
-
failures.push(`${file}: boundary rules source must be an object`);
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
} catch (cause) {
|
|
16
|
-
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
17
|
-
failures.push(`${file}: unable to read boundary rules source: ${reason}`);
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const commandGroupContainsRule = (source, groupId, ruleId, seen = new Set()) => {
|
|
23
|
-
if (!isRecord(source.commandGroups) || seen.has(groupId)) return false;
|
|
24
|
-
const steps = source.commandGroups[groupId];
|
|
25
|
-
if (!Array.isArray(steps)) return false;
|
|
26
|
-
seen.add(groupId);
|
|
27
|
-
return steps.some((step) => {
|
|
28
|
-
if (!isRecord(step)) return false;
|
|
29
|
-
if (step.type === "rule") return step.id === ruleId;
|
|
30
|
-
if (step.type === "group" && typeof step.id === "string") {
|
|
31
|
-
return commandGroupContainsRule(source, step.id, ruleId, seen);
|
|
32
|
-
}
|
|
33
|
-
return false;
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const collectBoundaryRuleMembershipFailures = (root, specs) => {
|
|
38
|
-
const failures = [];
|
|
39
|
-
const source = readBoundaryRulesSource(root, failures);
|
|
40
|
-
if (source === null) return failures;
|
|
41
|
-
|
|
42
|
-
const rules = Array.isArray(source.rules) ? source.rules : [];
|
|
43
|
-
for (const spec of specs) {
|
|
44
|
-
const rule = rules.find((entry) => isRecord(entry) && entry.id === spec.ruleId);
|
|
45
|
-
if (rule === undefined) {
|
|
46
|
-
failures.push(`docs/agent/boundary-rules.source.json: missing boundary rule ${spec.ruleId}`);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (spec.commandGroup !== undefined && rule.commandGroup !== spec.commandGroup) {
|
|
50
|
-
failures.push(
|
|
51
|
-
`docs/agent/boundary-rules.source.json: ${spec.ruleId} must declare commandGroup ${spec.commandGroup}`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
for (const groupId of spec.reachableFrom ?? []) {
|
|
55
|
-
if (!commandGroupContainsRule(source, groupId, spec.ruleId)) {
|
|
56
|
-
failures.push(
|
|
57
|
-
`docs/agent/boundary-rules.source.json: ${groupId} must include boundary rule ${spec.ruleId}`,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return failures;
|
|
63
|
-
};
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
export const generatedCapabilityRuleFields = new Set([
|
|
2
|
-
"allowedPrimitivePackages",
|
|
3
|
-
"coordinationCapabilityKind",
|
|
4
|
-
"coordinationPackage",
|
|
5
|
-
"coverage",
|
|
6
|
-
"docs",
|
|
7
|
-
"invariants",
|
|
8
|
-
"sourceFactOwners",
|
|
9
|
-
"testEvidence",
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
const unique = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
13
|
-
|
|
14
|
-
const coordinationCapabilityKinds = new Set(["composer", "facade", "profile", "projection"]);
|
|
15
|
-
|
|
16
|
-
const consumerFacingCapabilityKinds = new Set(["composer", "facade", "profile"]);
|
|
17
|
-
|
|
18
|
-
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
|
-
|
|
20
|
-
const stringArray = (value) =>
|
|
21
|
-
Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
|
|
22
|
-
|
|
23
|
-
const authoredCapabilityRuleSourceFields = new Set(["schemaVersion", "rules"]);
|
|
24
|
-
|
|
25
|
-
const resolvePrefixOwner = (prefix, namespaceOwners) => {
|
|
26
|
-
const candidates = namespaceOwners.filter(
|
|
27
|
-
(owner) => prefix.startsWith(owner.prefix) || owner.prefix.startsWith(prefix),
|
|
28
|
-
);
|
|
29
|
-
const owners = unique(candidates.map((candidate) => candidate.owner));
|
|
30
|
-
if (owners.length !== 1) {
|
|
31
|
-
return { ok: false, owners, candidates };
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
ok: true,
|
|
35
|
-
owner: owners[0],
|
|
36
|
-
declarations: candidates
|
|
37
|
-
.filter((candidate) => candidate.owner === owners[0])
|
|
38
|
-
.map((candidate) => ({
|
|
39
|
-
prefix: candidate.prefix,
|
|
40
|
-
owner: candidate.owner,
|
|
41
|
-
filePath: candidate.filePath,
|
|
42
|
-
})),
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const primitiveEvidence = (primitive) => {
|
|
47
|
-
if (primitive.testEvidence?.tests !== undefined) {
|
|
48
|
-
return {
|
|
49
|
-
primitive: primitive.id,
|
|
50
|
-
tests: primitive.testEvidence.tests,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
primitive: primitive.id,
|
|
55
|
-
noTestReason: primitive.testEvidence?.noTestReason ?? "missing evidence source",
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const buildCapabilityRouteProjection = ({
|
|
60
|
-
source,
|
|
61
|
-
recipes = [],
|
|
62
|
-
primitives,
|
|
63
|
-
invariants,
|
|
64
|
-
rootScripts,
|
|
65
|
-
namespaceOwners,
|
|
66
|
-
}) => {
|
|
67
|
-
const failures = [];
|
|
68
|
-
const primitiveById = new Map(primitives.map((primitive) => [primitive.id, primitive]));
|
|
69
|
-
const invariantById = new Map(invariants.map((invariant) => [invariant.id, invariant]));
|
|
70
|
-
const scriptNames = new Set(Object.keys(rootScripts));
|
|
71
|
-
|
|
72
|
-
if (!isObject(source)) {
|
|
73
|
-
return { failures: ["capability rules source must be an object"], routes: [] };
|
|
74
|
-
}
|
|
75
|
-
for (const field of Object.keys(source)) {
|
|
76
|
-
if (!authoredCapabilityRuleSourceFields.has(field)) {
|
|
77
|
-
failures.push(`capability rules source must not author field ${field}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (source.schemaVersion !== 1) failures.push("capability rules schemaVersion must be 1");
|
|
81
|
-
if (!Array.isArray(source.rules)) failures.push("capability rules source must contain rules[]");
|
|
82
|
-
|
|
83
|
-
const rules = Array.isArray(source.rules) ? source.rules : [];
|
|
84
|
-
const seenPrimitives = new Set();
|
|
85
|
-
const routes = [];
|
|
86
|
-
|
|
87
|
-
for (const [index, rule] of rules.entries()) {
|
|
88
|
-
const owner =
|
|
89
|
-
isObject(rule) && typeof rule.primitive === "string" ? rule.primitive : `rule[${index}]`;
|
|
90
|
-
|
|
91
|
-
if (!isObject(rule)) {
|
|
92
|
-
failures.push(`${owner} must be an object`);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
for (const field of Object.keys(rule)) {
|
|
97
|
-
if (generatedCapabilityRuleFields.has(field)) {
|
|
98
|
-
failures.push(`${owner} must not author generated field ${field}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const required = [
|
|
103
|
-
"primitive",
|
|
104
|
-
"intents",
|
|
105
|
-
"sourceFactPrefixes",
|
|
106
|
-
"allowedPrimitives",
|
|
107
|
-
"forbiddenWrites",
|
|
108
|
-
"gates",
|
|
109
|
-
];
|
|
110
|
-
for (const field of required) {
|
|
111
|
-
if (!(field in rule)) failures.push(`${owner} missing ${field}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (typeof rule.primitive !== "string" || rule.primitive.length === 0) {
|
|
115
|
-
failures.push(`${owner} primitive must be a non-empty string`);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (seenPrimitives.has(rule.primitive))
|
|
119
|
-
failures.push(`duplicate capability rule ${rule.primitive}`);
|
|
120
|
-
seenPrimitives.add(rule.primitive);
|
|
121
|
-
|
|
122
|
-
const primitive = primitiveById.get(rule.primitive);
|
|
123
|
-
if (primitive === undefined) {
|
|
124
|
-
failures.push(`${rule.primitive} references unknown primitive`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!stringArray(rule.intents) || rule.intents.length === 0) {
|
|
128
|
-
failures.push(`${rule.primitive} intents must be a non-empty string array`);
|
|
129
|
-
}
|
|
130
|
-
if (!stringArray(rule.sourceFactPrefixes) || rule.sourceFactPrefixes.length === 0) {
|
|
131
|
-
failures.push(`${rule.primitive} sourceFactPrefixes must be a non-empty string array`);
|
|
132
|
-
}
|
|
133
|
-
if (!stringArray(rule.allowedPrimitives) || rule.allowedPrimitives.length === 0) {
|
|
134
|
-
failures.push(`${rule.primitive} allowedPrimitives must be a non-empty string array`);
|
|
135
|
-
} else if (!rule.allowedPrimitives.includes(rule.primitive)) {
|
|
136
|
-
failures.push(`${rule.primitive} allowedPrimitives must include its coordination primitive`);
|
|
137
|
-
}
|
|
138
|
-
if (!Array.isArray(rule.forbiddenWrites)) {
|
|
139
|
-
failures.push(`${rule.primitive} forbiddenWrites must be an array`);
|
|
140
|
-
}
|
|
141
|
-
if (!stringArray(rule.gates) || rule.gates.length === 0) {
|
|
142
|
-
failures.push(`${rule.primitive} gates must be a non-empty boundary gate array`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const allowedPrimitives = stringArray(rule.allowedPrimitives) ? rule.allowedPrimitives : [];
|
|
146
|
-
const allowedPrimitiveRecords = [];
|
|
147
|
-
for (const allowedPrimitive of allowedPrimitives) {
|
|
148
|
-
const record = primitiveById.get(allowedPrimitive);
|
|
149
|
-
if (record === undefined) {
|
|
150
|
-
failures.push(
|
|
151
|
-
`${rule.primitive} allowedPrimitives references unknown primitive ${allowedPrimitive}`,
|
|
152
|
-
);
|
|
153
|
-
} else {
|
|
154
|
-
allowedPrimitiveRecords.push(record);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const sourceFactOwners = [];
|
|
159
|
-
for (const prefix of stringArray(rule.sourceFactPrefixes) ? rule.sourceFactPrefixes : []) {
|
|
160
|
-
const resolved = resolvePrefixOwner(prefix, namespaceOwners);
|
|
161
|
-
if (!resolved.ok) {
|
|
162
|
-
failures.push(
|
|
163
|
-
`${rule.primitive} sourceFactPrefixes ${JSON.stringify(prefix)} must resolve to exactly one owner; observed ${JSON.stringify(resolved.owners)}`,
|
|
164
|
-
);
|
|
165
|
-
} else {
|
|
166
|
-
sourceFactOwners.push({
|
|
167
|
-
prefix,
|
|
168
|
-
owner: resolved.owner,
|
|
169
|
-
declarations: resolved.declarations,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const forbiddenWrites = Array.isArray(rule.forbiddenWrites) ? rule.forbiddenWrites : [];
|
|
175
|
-
for (const [writeIndex, write] of forbiddenWrites.entries()) {
|
|
176
|
-
const writeOwner = `${rule.primitive} forbiddenWrites[${writeIndex}]`;
|
|
177
|
-
if (!isObject(write)) {
|
|
178
|
-
failures.push(`${writeOwner} must be an object`);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
for (const field of ["actor", "action", "target", "invariant"]) {
|
|
182
|
-
if (!(field in write)) failures.push(`${writeOwner} missing ${field}`);
|
|
183
|
-
}
|
|
184
|
-
if (typeof write.actor !== "string" || write.actor.length === 0) {
|
|
185
|
-
failures.push(`${writeOwner} actor must be a non-empty string`);
|
|
186
|
-
}
|
|
187
|
-
if (typeof write.action !== "string" || write.action.length === 0) {
|
|
188
|
-
failures.push(`${writeOwner} action must be a non-empty string`);
|
|
189
|
-
}
|
|
190
|
-
if (!isObject(write.target)) {
|
|
191
|
-
failures.push(`${writeOwner} target must be an object`);
|
|
192
|
-
} else {
|
|
193
|
-
if (!["eventPrefix", "surface", "material"].includes(write.target.kind)) {
|
|
194
|
-
failures.push(`${writeOwner} target.kind must be eventPrefix, surface, or material`);
|
|
195
|
-
}
|
|
196
|
-
if (typeof write.target.value !== "string" || write.target.value.length === 0) {
|
|
197
|
-
failures.push(`${writeOwner} target.value must be a non-empty string`);
|
|
198
|
-
}
|
|
199
|
-
if (write.target.kind === "eventPrefix" && typeof write.target.value === "string") {
|
|
200
|
-
const resolved = resolvePrefixOwner(write.target.value, namespaceOwners);
|
|
201
|
-
if (!resolved.ok) {
|
|
202
|
-
failures.push(
|
|
203
|
-
`${writeOwner} target ${JSON.stringify(write.target.value)} must resolve to exactly one owner; observed ${JSON.stringify(resolved.owners)}`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (typeof write.invariant !== "string" || !invariantById.has(write.invariant)) {
|
|
209
|
-
failures.push(
|
|
210
|
-
`${writeOwner} references unknown invariant ${JSON.stringify(write.invariant)}`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (const gate of stringArray(rule.gates) ? rule.gates : []) {
|
|
216
|
-
if (!scriptNames.has(gate))
|
|
217
|
-
failures.push(`${rule.primitive} references unknown boundary gate ${gate}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const sourceOwnerNames = unique(sourceFactOwners.map((entry) => entry.owner));
|
|
221
|
-
if (
|
|
222
|
-
sourceOwnerNames.length > 1 &&
|
|
223
|
-
primitive !== undefined &&
|
|
224
|
-
!coordinationCapabilityKinds.has(primitive.capabilityKind)
|
|
225
|
-
) {
|
|
226
|
-
failures.push(
|
|
227
|
-
`${rule.primitive} spans ${sourceOwnerNames.length} fact owners but coordination primitive kind ${JSON.stringify(
|
|
228
|
-
primitive.capabilityKind,
|
|
229
|
-
)} is not one of ${JSON.stringify([...coordinationCapabilityKinds])}`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
primitive === undefined ||
|
|
235
|
-
failures.some((failure) => failure.startsWith(`${rule.primitive} `))
|
|
236
|
-
) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const invariantIds = unique([
|
|
241
|
-
...allowedPrimitiveRecords.flatMap((record) => record.invariants),
|
|
242
|
-
...forbiddenWrites.flatMap((write) =>
|
|
243
|
-
typeof write?.invariant === "string" && invariantById.has(write.invariant)
|
|
244
|
-
? [write.invariant]
|
|
245
|
-
: [],
|
|
246
|
-
),
|
|
247
|
-
]);
|
|
248
|
-
|
|
249
|
-
routes.push({
|
|
250
|
-
primitive: rule.primitive,
|
|
251
|
-
intents: rule.intents,
|
|
252
|
-
coordinationPackage: primitive.package,
|
|
253
|
-
coordinationCapabilityKind: primitive.capabilityKind,
|
|
254
|
-
sourceFactPrefixes: rule.sourceFactPrefixes,
|
|
255
|
-
sourceFactOwners,
|
|
256
|
-
allowedPrimitives,
|
|
257
|
-
allowedPrimitivePackages: unique(allowedPrimitiveRecords.map((record) => record.package)),
|
|
258
|
-
forbiddenWrites,
|
|
259
|
-
gates: rule.gates,
|
|
260
|
-
invariants: invariantIds,
|
|
261
|
-
docs: unique([
|
|
262
|
-
...allowedPrimitiveRecords.map((record) => record.docs),
|
|
263
|
-
...invariantIds.flatMap((id) => {
|
|
264
|
-
const invariant = invariantById.get(id);
|
|
265
|
-
return invariant === undefined ? [] : [invariant.docs];
|
|
266
|
-
}),
|
|
267
|
-
]),
|
|
268
|
-
testEvidence: allowedPrimitiveRecords.map(primitiveEvidence),
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const coverage = buildCapabilityRouteCoverage({ routes, recipes, primitives });
|
|
273
|
-
failures.push(...coverage.failures);
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
failures,
|
|
277
|
-
routes:
|
|
278
|
-
failures.length === 0
|
|
279
|
-
? routes.sort((left, right) => left.primitive.localeCompare(right.primitive))
|
|
280
|
-
: [],
|
|
281
|
-
coverage: failures.length === 0 ? coverage.summary : { recipes: [], primitives: [] },
|
|
282
|
-
};
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const routeMatchesRecipe = (route, recipe) => {
|
|
286
|
-
const recipePrimitives = new Set(recipe.primitives);
|
|
287
|
-
return (
|
|
288
|
-
recipePrimitives.has(route.primitive) ||
|
|
289
|
-
route.allowedPrimitives.some((primitive) => recipePrimitives.has(primitive))
|
|
290
|
-
);
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const routePrimitivesForRecipe = (routes, recipe) =>
|
|
294
|
-
routes
|
|
295
|
-
.filter((route) => routeMatchesRecipe(route, recipe))
|
|
296
|
-
.map((route) => route.primitive)
|
|
297
|
-
.sort((left, right) => left.localeCompare(right));
|
|
298
|
-
|
|
299
|
-
const buildCapabilityRouteCoverage = ({ routes, recipes, primitives }) => {
|
|
300
|
-
const failures = [];
|
|
301
|
-
const coveredPrimitiveIds = new Set(
|
|
302
|
-
routes.flatMap((route) => [route.primitive, ...route.allowedPrimitives]),
|
|
303
|
-
);
|
|
304
|
-
const recipeCoverage = recipes.map((recipe) => {
|
|
305
|
-
const routePrimitives = routePrimitivesForRecipe(routes, recipe);
|
|
306
|
-
const noRouteReason =
|
|
307
|
-
typeof recipe.noRouteReason === "string" ? recipe.noRouteReason.trim() : undefined;
|
|
308
|
-
if (routePrimitives.length === 0 && noRouteReason === undefined) {
|
|
309
|
-
failures.push(`${recipe.id} must have a capability route or noRouteReason`);
|
|
310
|
-
}
|
|
311
|
-
if (routePrimitives.length > 0 && noRouteReason !== undefined) {
|
|
312
|
-
failures.push(`${recipe.id} has both route coverage and noRouteReason`);
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
id: recipe.id,
|
|
316
|
-
routePrimitives,
|
|
317
|
-
...(noRouteReason === undefined ? {} : { noRouteReason }),
|
|
318
|
-
};
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
const primitiveCoverage = primitives
|
|
322
|
-
.filter((primitive) => consumerFacingCapabilityKinds.has(primitive.capabilityKind))
|
|
323
|
-
.map((primitive) => {
|
|
324
|
-
const routePrimitives = routes
|
|
325
|
-
.filter(
|
|
326
|
-
(route) =>
|
|
327
|
-
route.primitive === primitive.id || route.allowedPrimitives.includes(primitive.id),
|
|
328
|
-
)
|
|
329
|
-
.map((route) => route.primitive)
|
|
330
|
-
.sort((left, right) => left.localeCompare(right));
|
|
331
|
-
const noRouteReason =
|
|
332
|
-
typeof primitive.noRouteReason === "string" ? primitive.noRouteReason.trim() : undefined;
|
|
333
|
-
if (!coveredPrimitiveIds.has(primitive.id) && noRouteReason === undefined) {
|
|
334
|
-
failures.push(`${primitive.id} must have a capability route or noRouteReason`);
|
|
335
|
-
}
|
|
336
|
-
if (coveredPrimitiveIds.has(primitive.id) && noRouteReason !== undefined) {
|
|
337
|
-
failures.push(`${primitive.id} has both route coverage and noRouteReason`);
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
id: primitive.id,
|
|
341
|
-
capabilityKind: primitive.capabilityKind,
|
|
342
|
-
routePrimitives,
|
|
343
|
-
...(noRouteReason === undefined ? {} : { noRouteReason }),
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
failures,
|
|
349
|
-
summary: {
|
|
350
|
-
recipes: recipeCoverage,
|
|
351
|
-
primitives: primitiveCoverage,
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
};
|