@yansirplus/cli 0.5.17
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/PUBLIC_API.md +22 -0
- package/README.md +34 -0
- package/dist/build/agent-authoring/config.d.ts +177 -0
- package/dist/build/agent-authoring/config.js +607 -0
- package/dist/build/agent-authoring/manifest-compiler.d.ts +159 -0
- package/dist/build/agent-authoring/manifest-compiler.js +737 -0
- package/dist/build/agent-authoring/shared.d.ts +10 -0
- package/dist/build/agent-authoring/shared.js +57 -0
- package/dist/build/agent-authoring/static-target.d.ts +59 -0
- package/dist/build/agent-authoring/static-target.js +1857 -0
- package/dist/build/agent-authoring.d.ts +9 -0
- package/dist/build/agent-authoring.js +5 -0
- package/dist/build/build-cli.d.ts +2 -0
- package/dist/build/build-cli.js +264 -0
- package/dist/check/algorithmic/architecture-checks.mjs +971 -0
- package/dist/check/algorithmic/client-boundary-checks.mjs +337 -0
- package/dist/check/algorithmic/convergence-smoke-checks.mjs +608 -0
- package/dist/check/algorithmic/distribution-checks.mjs +919 -0
- package/dist/check/algorithmic/owner-checks.mjs +647 -0
- package/dist/check/algorithmic/package-boundary-checks.mjs +985 -0
- package/dist/check/algorithmic/projection-boundary-checks.mjs +302 -0
- package/dist/check/algorithmic/repo-surface-checks.mjs +267 -0
- package/dist/check/algorithmic/runtime-structural-checks.mjs +264 -0
- package/dist/check/algorithmic/source-alias-checks.mjs +106 -0
- package/dist/check/algorithmic/static-target-checks.mjs +447 -0
- package/dist/check/algorithmic-checks.mjs +482 -0
- package/dist/check/check-coverage.mjs +231 -0
- package/dist/check/command-runner.mjs +22 -0
- package/dist/check/default-gate.mjs +51 -0
- package/dist/check/gate-selector.mjs +305 -0
- package/dist/check/manifest-rules.mjs +223 -0
- package/dist/check/package-graph.mjs +464 -0
- package/dist/generate/generate-agent-docs.mjs +435 -0
- package/dist/generate/generate-carrier-reference.mjs +514 -0
- package/dist/generate/generate-docs.mjs +345 -0
- package/dist/generate/generate-effect-skill-manifests.mjs +193 -0
- package/dist/generate/project-docs-site.mjs +190 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/lib/agent-docs-model.mjs +888 -0
- package/dist/lib/boundary-rules.mjs +63 -0
- package/dist/lib/capability-routes.mjs +354 -0
- package/dist/lib/projection-sink.mjs +113 -0
- package/dist/lib/public-api-model.mjs +306 -0
- package/dist/main.mjs +233 -0
- package/dist/runner.mjs +127 -0
- package/package.json +32 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
export const createOwnerChecks = ({
|
|
2
|
+
ts,
|
|
3
|
+
graphWorkspacePackageRecords,
|
|
4
|
+
repoRoot,
|
|
5
|
+
read,
|
|
6
|
+
walk,
|
|
7
|
+
compare,
|
|
8
|
+
isRecord,
|
|
9
|
+
unwrap,
|
|
10
|
+
callName,
|
|
11
|
+
packageSourceFiles,
|
|
12
|
+
packageTestFiles,
|
|
13
|
+
}) => {
|
|
14
|
+
const ownerCouplingSinkProperties = new Set([
|
|
15
|
+
"boundaryOwner",
|
|
16
|
+
"boundaryOwnerId",
|
|
17
|
+
"boundaryPackageId",
|
|
18
|
+
"claimedBy",
|
|
19
|
+
"factOwnerRef",
|
|
20
|
+
"owner",
|
|
21
|
+
"packageId",
|
|
22
|
+
"settlementId",
|
|
23
|
+
]);
|
|
24
|
+
const ownerIdentityBoundarySinkProperties = new Set([
|
|
25
|
+
"boundaryOwner",
|
|
26
|
+
"boundaryOwnerId",
|
|
27
|
+
"boundaryPackageId",
|
|
28
|
+
"claimedBy",
|
|
29
|
+
"factOwnerRef",
|
|
30
|
+
"owner",
|
|
31
|
+
"settlementId",
|
|
32
|
+
]);
|
|
33
|
+
const packageMetadataNames = new Set(["packageId", "sourcePackageName", "publicPackageName"]);
|
|
34
|
+
|
|
35
|
+
const publicNameForSourcePackage = (name) => name.replace(/^@agent-os\//u, "@yansirplus/");
|
|
36
|
+
|
|
37
|
+
const ownerCouplingPackageNames = () => {
|
|
38
|
+
const sourcePackageNames = new Set(
|
|
39
|
+
graphWorkspacePackageRecords(repoRoot)
|
|
40
|
+
.map((record) => record.name)
|
|
41
|
+
.filter((name) => typeof name === "string" && name.startsWith("@agent-os/")),
|
|
42
|
+
);
|
|
43
|
+
const publicPackageNames = new Set([...sourcePackageNames].map(publicNameForSourcePackage));
|
|
44
|
+
return { sourcePackageNames, publicPackageNames };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ownerCouplingPropertyName = (name) => {
|
|
48
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
|
|
49
|
+
return name.text;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ownerCouplingPosition = (sourceFile, node) => {
|
|
55
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
56
|
+
return { line: position.line + 1, column: position.character + 1 };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const packageMetadataSource = (expression, packageNames) => {
|
|
60
|
+
const unwrapped = unwrap(expression);
|
|
61
|
+
if (ts.isStringLiteralLike(unwrapped)) {
|
|
62
|
+
if (packageNames.sourcePackageNames.has(unwrapped.text)) return "sourcePackageNameLiteral";
|
|
63
|
+
if (packageNames.publicPackageNames.has(unwrapped.text)) return "publicPackageNameLiteral";
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (ts.isIdentifier(unwrapped) && packageMetadataNames.has(unwrapped.text)) {
|
|
67
|
+
return unwrapped.text;
|
|
68
|
+
}
|
|
69
|
+
if (ts.isPropertyAccessExpression(unwrapped) && packageMetadataNames.has(unwrapped.name.text)) {
|
|
70
|
+
return unwrapped.name.text;
|
|
71
|
+
}
|
|
72
|
+
if (ts.isCallExpression(unwrapped) && callName(unwrapped.expression) === "publicPackageName") {
|
|
73
|
+
return "publicPackageName";
|
|
74
|
+
}
|
|
75
|
+
if (ts.isTemplateExpression(unwrapped)) {
|
|
76
|
+
for (const span of unwrapped.templateSpans) {
|
|
77
|
+
const source = packageMetadataSource(span.expression, packageNames);
|
|
78
|
+
if (source !== undefined) return source;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
82
|
+
return (
|
|
83
|
+
packageMetadataSource(unwrapped.left, packageNames) ??
|
|
84
|
+
packageMetadataSource(unwrapped.right, packageNames)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (ts.isConditionalExpression(unwrapped)) {
|
|
88
|
+
return (
|
|
89
|
+
packageMetadataSource(unwrapped.whenTrue, packageNames) ??
|
|
90
|
+
packageMetadataSource(unwrapped.whenFalse, packageNames)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const packageMetadataFindingsForSource = (content, file, packageNames, sinkProperties) => {
|
|
97
|
+
const sourceFile = ts.createSourceFile(
|
|
98
|
+
file,
|
|
99
|
+
content,
|
|
100
|
+
ts.ScriptTarget.Latest,
|
|
101
|
+
true,
|
|
102
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
103
|
+
);
|
|
104
|
+
const findings = [];
|
|
105
|
+
const record = (node, sink, source) => {
|
|
106
|
+
const position = ownerCouplingPosition(sourceFile, node);
|
|
107
|
+
findings.push({
|
|
108
|
+
file,
|
|
109
|
+
line: position.line,
|
|
110
|
+
column: position.column,
|
|
111
|
+
sink,
|
|
112
|
+
source,
|
|
113
|
+
expression: node.getText(sourceFile),
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const visit = (node) => {
|
|
118
|
+
if (ts.isPropertyAssignment(node)) {
|
|
119
|
+
const sink = ownerCouplingPropertyName(node.name);
|
|
120
|
+
const source = packageMetadataSource(node.initializer, packageNames);
|
|
121
|
+
if (sink !== undefined && source !== undefined && sinkProperties.has(sink)) {
|
|
122
|
+
record(node.initializer, sink, source);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (ts.isBinaryExpression(node)) {
|
|
127
|
+
const leftText = node.left.getText(sourceFile);
|
|
128
|
+
const rightText = node.right.getText(sourceFile);
|
|
129
|
+
const leftSource = packageMetadataSource(node.left, packageNames);
|
|
130
|
+
const rightSource = packageMetadataSource(node.right, packageNames);
|
|
131
|
+
if (leftText.includes("factOwnerRef") && rightSource !== undefined) {
|
|
132
|
+
record(node.right, "factOwnerRef", rightSource);
|
|
133
|
+
}
|
|
134
|
+
if (rightText.includes("factOwnerRef") && leftSource !== undefined) {
|
|
135
|
+
record(node.left, "factOwnerRef", leftSource);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ts.forEachChild(node, visit);
|
|
140
|
+
};
|
|
141
|
+
visit(sourceFile);
|
|
142
|
+
return findings;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const ownerCouplingFindingsForSource = (content, file, packageNames) =>
|
|
146
|
+
packageMetadataFindingsForSource(content, file, packageNames, ownerCouplingSinkProperties);
|
|
147
|
+
|
|
148
|
+
const ownerIdentityBoundaryFindingsForSource = (content, file, packageNames) =>
|
|
149
|
+
packageMetadataFindingsForSource(
|
|
150
|
+
content,
|
|
151
|
+
file,
|
|
152
|
+
packageNames,
|
|
153
|
+
ownerIdentityBoundarySinkProperties,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const ownerIdentityBoundaryNegativeFixtures = [
|
|
157
|
+
{
|
|
158
|
+
name: "fact owner from contract package id",
|
|
159
|
+
content: ["const event = {", " factOwnerRef: contract.packageId,", "};", ""],
|
|
160
|
+
expected: [["factOwnerRef", "packageId"]],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "fact owner from source package name",
|
|
164
|
+
content: ["const event = {", " factOwnerRef: contract.sourcePackageName,", "};", ""],
|
|
165
|
+
expected: [["factOwnerRef", "sourcePackageName"]],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "fact owner from public package projection",
|
|
169
|
+
content: [
|
|
170
|
+
"const event = {",
|
|
171
|
+
" factOwnerRef: publicPackageName(contract.packageId),",
|
|
172
|
+
"};",
|
|
173
|
+
"",
|
|
174
|
+
],
|
|
175
|
+
expected: [["factOwnerRef", "publicPackageName"]],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "fact owner from public package literal",
|
|
179
|
+
content: ["const event = {", ' factOwnerRef: "@yansirplus/runtime",', "};", ""],
|
|
180
|
+
expected: [["factOwnerRef", "publicPackageNameLiteral"]],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "settlement id from package metadata",
|
|
184
|
+
content: [
|
|
185
|
+
"const settlement = defineSettlementContract({",
|
|
186
|
+
" settlementId: spec.sourcePackageName,",
|
|
187
|
+
"});",
|
|
188
|
+
"",
|
|
189
|
+
],
|
|
190
|
+
expected: [["settlementId", "sourcePackageName"]],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "extension conflict owner from package metadata",
|
|
194
|
+
content: ["const conflict = {", " claimedBy: declaration.packageId,", "};", ""],
|
|
195
|
+
expected: [["claimedBy", "packageId"]],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "namespace owner from package metadata",
|
|
199
|
+
content: ["const namespace = {", " owner: namespace.sourcePackageName,", "};", ""],
|
|
200
|
+
expected: [["owner", "sourcePackageName"]],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "boundary owner from package metadata",
|
|
204
|
+
content: ["const intent = {", " boundaryOwnerId: boundaryPackage.packageId,", "};", ""],
|
|
205
|
+
expected: [["boundaryOwnerId", "packageId"]],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "legacy boundary package field from package metadata",
|
|
209
|
+
content: ["const intent = {", " boundaryPackageId: boundaryPackage.packageId,", "};", ""],
|
|
210
|
+
expected: [["boundaryPackageId", "packageId"]],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "ledger identity comparison from package metadata",
|
|
214
|
+
content: ["if (committed.factOwnerRef !== contract.packageId) {}", ""],
|
|
215
|
+
expected: [["factOwnerRef", "packageId"]],
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const findingPairs = (findings) => findings.map((finding) => [finding.sink, finding.source]);
|
|
220
|
+
|
|
221
|
+
const ownerIdentityBoundaryNegativeFixtureFailures = (packageNames) => {
|
|
222
|
+
const failures = [];
|
|
223
|
+
for (const fixture of ownerIdentityBoundaryNegativeFixtures) {
|
|
224
|
+
const findings = ownerIdentityBoundaryFindingsForSource(
|
|
225
|
+
fixture.content.join("\n"),
|
|
226
|
+
`negative-fixtures/${fixture.name}.ts`,
|
|
227
|
+
packageNames,
|
|
228
|
+
);
|
|
229
|
+
const actual = findingPairs(findings);
|
|
230
|
+
for (const expected of fixture.expected) {
|
|
231
|
+
if (!actual.some(([sink, source]) => sink === expected[0] && source === expected[1])) {
|
|
232
|
+
failures.push(
|
|
233
|
+
`${fixture.name}: expected ${expected[0]} from ${expected[1]}, got ${JSON.stringify(
|
|
234
|
+
actual,
|
|
235
|
+
)}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return failures;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const ownerCouplingScanFiles = () =>
|
|
244
|
+
[
|
|
245
|
+
...walk("packages").filter((file) => /\.(?:ts|tsx|mts|cts)$/u.test(file)),
|
|
246
|
+
...walk("packages/cli/src").filter((file) => /\.(?:mjs|ts)$/u.test(file)),
|
|
247
|
+
...walk("tooling/distribution").filter((file) => /\.(?:mjs|ts)$/u.test(file)),
|
|
248
|
+
].sort(compare);
|
|
249
|
+
|
|
250
|
+
const checkOwnerCoupling = (args = []) => {
|
|
251
|
+
const reportOnly = args.length === 1 && args[0] === "--report-only";
|
|
252
|
+
if (!reportOnly && args.length > 0) {
|
|
253
|
+
throw new Error(`owner-coupling: unexpected argument(s): ${args.join(" ")}`);
|
|
254
|
+
}
|
|
255
|
+
const packageNames = ownerCouplingPackageNames();
|
|
256
|
+
const findings = ownerCouplingScanFiles().flatMap((file) =>
|
|
257
|
+
ownerCouplingFindingsForSource(read(file), file, packageNames),
|
|
258
|
+
);
|
|
259
|
+
const lines = findings.map(
|
|
260
|
+
(finding) =>
|
|
261
|
+
`${finding.file}:${finding.line}:${finding.column}: owner-coupling: ${finding.sink} reads ${finding.source} via ${finding.expression}`,
|
|
262
|
+
);
|
|
263
|
+
if (reportOnly) {
|
|
264
|
+
console.log(`owner coupling report-only: ${findings.length} finding(s)`);
|
|
265
|
+
for (const line of lines) console.log(line);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
failIfAny("owner coupling", lines);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const checkOwnerIdentityBoundary = (args = []) => {
|
|
272
|
+
const negativeFixtures = args.length === 1 && args[0] === "--negative-fixtures";
|
|
273
|
+
if (!negativeFixtures && args.length > 0) {
|
|
274
|
+
throw new Error(`owner-identity-boundary: unexpected argument(s): ${args.join(" ")}`);
|
|
275
|
+
}
|
|
276
|
+
const packageNames = ownerCouplingPackageNames();
|
|
277
|
+
if (negativeFixtures) {
|
|
278
|
+
failIfAny(
|
|
279
|
+
"owner identity boundary negative fixtures",
|
|
280
|
+
ownerIdentityBoundaryNegativeFixtureFailures(packageNames),
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const findings = ownerCouplingScanFiles().flatMap((file) =>
|
|
285
|
+
ownerIdentityBoundaryFindingsForSource(read(file), file, packageNames),
|
|
286
|
+
);
|
|
287
|
+
failIfAny(
|
|
288
|
+
"owner identity boundary",
|
|
289
|
+
findings.map(
|
|
290
|
+
(finding) =>
|
|
291
|
+
`${finding.file}:${finding.line}:${finding.column}: owner-identity-boundary: ${finding.sink} reads ${finding.source} via ${finding.expression}`,
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const ownerIdRegistryPath = "architecture/owner-ids.json";
|
|
297
|
+
|
|
298
|
+
const ownerIdRegistry = () => readJson(ownerIdRegistryPath);
|
|
299
|
+
|
|
300
|
+
const ownerIdValue = (expression, constStrings) => {
|
|
301
|
+
const unwrapped = unwrap(expression);
|
|
302
|
+
if (ts.isStringLiteralLike(unwrapped)) return unwrapped.text;
|
|
303
|
+
if (ts.isIdentifier(unwrapped)) return constStrings.get(unwrapped.text);
|
|
304
|
+
return undefined;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const ownerIdConstStrings = (sourceFile) => {
|
|
308
|
+
const out = new Map();
|
|
309
|
+
const visit = (node) => {
|
|
310
|
+
if (ts.isVariableStatement(node)) {
|
|
311
|
+
for (const declaration of node.declarationList.declarations) {
|
|
312
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue;
|
|
313
|
+
const value = ownerIdValue(declaration.initializer, out);
|
|
314
|
+
if (value !== undefined) out.set(declaration.name.text, value);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
ts.forEachChild(node, visit);
|
|
318
|
+
};
|
|
319
|
+
visit(sourceFile);
|
|
320
|
+
return out;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const ownerIdObjectProperty = (objectLiteral, name) => {
|
|
324
|
+
for (const property of objectLiteral.properties) {
|
|
325
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
326
|
+
const propertyName = ownerCouplingPropertyName(property.name);
|
|
327
|
+
if (propertyName === name) return property.initializer;
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const ownerIdDeclarationFindingsForSource = ({
|
|
333
|
+
content,
|
|
334
|
+
file,
|
|
335
|
+
registeredOwners,
|
|
336
|
+
workspacePackageNames,
|
|
337
|
+
}) => {
|
|
338
|
+
const sourceFile = ts.createSourceFile(
|
|
339
|
+
file,
|
|
340
|
+
content,
|
|
341
|
+
ts.ScriptTarget.Latest,
|
|
342
|
+
true,
|
|
343
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
344
|
+
);
|
|
345
|
+
const constStrings = ownerIdConstStrings(sourceFile);
|
|
346
|
+
const findings = [];
|
|
347
|
+
const declarationCalls = new Set(["defineCarrier", "defineBoundaryContract", "eventNamespace"]);
|
|
348
|
+
const record = (node, message) => {
|
|
349
|
+
const position = ownerCouplingPosition(sourceFile, node);
|
|
350
|
+
findings.push(`${file}:${position.line}:${position.column}: owner-ids: ${message}`);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const visit = (node) => {
|
|
354
|
+
if (ts.isCallExpression(node) && declarationCalls.has(callName(node.expression))) {
|
|
355
|
+
const [firstArg] = node.arguments;
|
|
356
|
+
if (firstArg === undefined || !ts.isObjectLiteralExpression(unwrap(firstArg))) {
|
|
357
|
+
record(node, `${callName(node.expression)} declaration must use an object literal`);
|
|
358
|
+
} else {
|
|
359
|
+
const objectLiteral = unwrap(firstArg);
|
|
360
|
+
const ownerNode = ownerIdObjectProperty(objectLiteral, "ownerId");
|
|
361
|
+
const sourceNode = ownerIdObjectProperty(objectLiteral, "sourcePackageName");
|
|
362
|
+
const packageNode = ownerIdObjectProperty(objectLiteral, "packageId");
|
|
363
|
+
if (packageNode !== undefined) {
|
|
364
|
+
record(
|
|
365
|
+
packageNode,
|
|
366
|
+
`${callName(node.expression)} declaration must not declare packageId`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const ownerId =
|
|
370
|
+
ownerNode === undefined ? undefined : ownerIdValue(ownerNode, constStrings);
|
|
371
|
+
const sourcePackageName =
|
|
372
|
+
sourceNode === undefined ? undefined : ownerIdValue(sourceNode, constStrings);
|
|
373
|
+
if (ownerId === undefined) {
|
|
374
|
+
record(node, `${callName(node.expression)} declaration requires literal ownerId`);
|
|
375
|
+
} else if (!registeredOwners.has(ownerId)) {
|
|
376
|
+
record(
|
|
377
|
+
ownerNode ?? node,
|
|
378
|
+
`ownerId ${ownerId} is not registered in ${ownerIdRegistryPath}`,
|
|
379
|
+
);
|
|
380
|
+
} else if (registeredOwners.get(ownerId)?.status === "retired") {
|
|
381
|
+
record(ownerNode ?? node, `ownerId ${ownerId} is retired and cannot be declared`);
|
|
382
|
+
}
|
|
383
|
+
if (sourcePackageName === undefined) {
|
|
384
|
+
record(
|
|
385
|
+
node,
|
|
386
|
+
`${callName(node.expression)} declaration requires literal sourcePackageName`,
|
|
387
|
+
);
|
|
388
|
+
} else if (!workspacePackageNames.has(sourcePackageName)) {
|
|
389
|
+
record(
|
|
390
|
+
sourceNode ?? node,
|
|
391
|
+
`sourcePackageName ${sourcePackageName} is not a workspace package`,
|
|
392
|
+
);
|
|
393
|
+
} else if (
|
|
394
|
+
ownerId !== undefined &&
|
|
395
|
+
registeredOwners.has(ownerId) &&
|
|
396
|
+
registeredOwners.get(ownerId)?.status === "active"
|
|
397
|
+
) {
|
|
398
|
+
const owner = registeredOwners.get(ownerId);
|
|
399
|
+
const sources = new Set(owner.sourcePackageNames ?? []);
|
|
400
|
+
if (!sources.has(sourcePackageName)) {
|
|
401
|
+
record(
|
|
402
|
+
sourceNode ?? node,
|
|
403
|
+
`sourcePackageName ${sourcePackageName} is not registered for ownerId ${ownerId}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
ts.forEachChild(node, visit);
|
|
410
|
+
};
|
|
411
|
+
visit(sourceFile);
|
|
412
|
+
return findings;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const coreClaimedNamespaceFindingsForSource = ({
|
|
416
|
+
content,
|
|
417
|
+
file,
|
|
418
|
+
registeredOwners,
|
|
419
|
+
workspacePackageNames,
|
|
420
|
+
}) => {
|
|
421
|
+
const sourceFile = ts.createSourceFile(
|
|
422
|
+
file,
|
|
423
|
+
content,
|
|
424
|
+
ts.ScriptTarget.Latest,
|
|
425
|
+
true,
|
|
426
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
427
|
+
);
|
|
428
|
+
const constStrings = ownerIdConstStrings(sourceFile);
|
|
429
|
+
const findings = [];
|
|
430
|
+
const record = (node, message) => {
|
|
431
|
+
const position = ownerCouplingPosition(sourceFile, node);
|
|
432
|
+
findings.push(`${file}:${position.line}:${position.column}: owner-ids: ${message}`);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const validateNamespaceObject = (objectLiteral) => {
|
|
436
|
+
const ownerNode = ownerIdObjectProperty(objectLiteral, "ownerId");
|
|
437
|
+
const sourceNode = ownerIdObjectProperty(objectLiteral, "sourcePackageName");
|
|
438
|
+
const packageNode = ownerIdObjectProperty(objectLiteral, "packageId");
|
|
439
|
+
if (packageNode !== undefined) {
|
|
440
|
+
record(packageNode, "CORE_CLAIMED_EVENT_NAMESPACES must not declare packageId");
|
|
441
|
+
}
|
|
442
|
+
const ownerId = ownerNode === undefined ? undefined : ownerIdValue(ownerNode, constStrings);
|
|
443
|
+
const sourcePackageName =
|
|
444
|
+
sourceNode === undefined ? undefined : ownerIdValue(sourceNode, constStrings);
|
|
445
|
+
if (ownerId === undefined) {
|
|
446
|
+
record(objectLiteral, "CORE_CLAIMED_EVENT_NAMESPACES entry requires literal ownerId");
|
|
447
|
+
} else if (!registeredOwners.has(ownerId)) {
|
|
448
|
+
record(
|
|
449
|
+
ownerNode ?? objectLiteral,
|
|
450
|
+
`ownerId ${ownerId} is not registered in ${ownerIdRegistryPath}`,
|
|
451
|
+
);
|
|
452
|
+
} else if (registeredOwners.get(ownerId)?.status === "retired") {
|
|
453
|
+
record(ownerNode ?? objectLiteral, `ownerId ${ownerId} is retired and cannot be declared`);
|
|
454
|
+
}
|
|
455
|
+
if (sourcePackageName === undefined) {
|
|
456
|
+
record(
|
|
457
|
+
objectLiteral,
|
|
458
|
+
"CORE_CLAIMED_EVENT_NAMESPACES entry requires literal sourcePackageName",
|
|
459
|
+
);
|
|
460
|
+
} else if (!workspacePackageNames.has(sourcePackageName)) {
|
|
461
|
+
record(
|
|
462
|
+
sourceNode ?? objectLiteral,
|
|
463
|
+
`sourcePackageName ${sourcePackageName} is not a workspace package`,
|
|
464
|
+
);
|
|
465
|
+
} else if (
|
|
466
|
+
ownerId !== undefined &&
|
|
467
|
+
registeredOwners.has(ownerId) &&
|
|
468
|
+
registeredOwners.get(ownerId)?.status === "active"
|
|
469
|
+
) {
|
|
470
|
+
const owner = registeredOwners.get(ownerId);
|
|
471
|
+
const sources = new Set(owner.sourcePackageNames ?? []);
|
|
472
|
+
if (!sources.has(sourcePackageName)) {
|
|
473
|
+
record(
|
|
474
|
+
sourceNode ?? objectLiteral,
|
|
475
|
+
`sourcePackageName ${sourcePackageName} is not registered for ownerId ${ownerId}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const visit = (node) => {
|
|
482
|
+
if (
|
|
483
|
+
ts.isVariableDeclaration(node) &&
|
|
484
|
+
ts.isIdentifier(node.name) &&
|
|
485
|
+
node.name.text === "CORE_CLAIMED_EVENT_NAMESPACES" &&
|
|
486
|
+
node.initializer !== undefined
|
|
487
|
+
) {
|
|
488
|
+
const initializer = unwrap(node.initializer);
|
|
489
|
+
if (!ts.isArrayLiteralExpression(initializer)) {
|
|
490
|
+
record(node, "CORE_CLAIMED_EVENT_NAMESPACES must be an array literal");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
for (const element of initializer.elements) {
|
|
494
|
+
const entry = unwrap(element);
|
|
495
|
+
if (ts.isObjectLiteralExpression(entry)) {
|
|
496
|
+
validateNamespaceObject(entry);
|
|
497
|
+
} else {
|
|
498
|
+
record(element, "CORE_CLAIMED_EVENT_NAMESPACES entries must be object literals");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
ts.forEachChild(node, visit);
|
|
504
|
+
};
|
|
505
|
+
visit(sourceFile);
|
|
506
|
+
return findings;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const ownerIdRegistryFindings = ({ registry, workspacePackageNames }) => {
|
|
510
|
+
const findings = [];
|
|
511
|
+
if (!isRecord(registry)) {
|
|
512
|
+
return [`${ownerIdRegistryPath}: owner registry must be a JSON object`];
|
|
513
|
+
}
|
|
514
|
+
if (registry.schemaVersion !== 1) {
|
|
515
|
+
findings.push(`${ownerIdRegistryPath}: schemaVersion must be 1`);
|
|
516
|
+
}
|
|
517
|
+
if (!isRecord(registry.policy)) {
|
|
518
|
+
findings.push(`${ownerIdRegistryPath}: policy object is required`);
|
|
519
|
+
} else {
|
|
520
|
+
if (registry.policy.allocation !== "append-only") {
|
|
521
|
+
findings.push(`${ownerIdRegistryPath}: policy.allocation must be append-only`);
|
|
522
|
+
}
|
|
523
|
+
for (const key of ["retirement", "namespaceSplit"]) {
|
|
524
|
+
if (typeof registry.policy[key] !== "string" || registry.policy[key].length === 0) {
|
|
525
|
+
findings.push(`${ownerIdRegistryPath}: policy.${key} must be a non-empty string`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!Array.isArray(registry.owners) || registry.owners.length === 0) {
|
|
530
|
+
findings.push(`${ownerIdRegistryPath}: owners must be a non-empty array`);
|
|
531
|
+
return findings;
|
|
532
|
+
}
|
|
533
|
+
const seenOwnerIds = new Set();
|
|
534
|
+
for (const [index, owner] of registry.owners.entries()) {
|
|
535
|
+
const label = `${ownerIdRegistryPath}:owners[${index}]`;
|
|
536
|
+
if (!isRecord(owner)) {
|
|
537
|
+
findings.push(`${label}: owner must be an object`);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (typeof owner.ownerId !== "string" || owner.ownerId.length === 0) {
|
|
541
|
+
findings.push(`${label}: ownerId must be a non-empty string`);
|
|
542
|
+
} else if (seenOwnerIds.has(owner.ownerId)) {
|
|
543
|
+
findings.push(`${label}: duplicate ownerId ${owner.ownerId}`);
|
|
544
|
+
} else {
|
|
545
|
+
seenOwnerIds.add(owner.ownerId);
|
|
546
|
+
}
|
|
547
|
+
if (owner.status !== "active" && owner.status !== "retired") {
|
|
548
|
+
findings.push(`${label}: status must be active or retired`);
|
|
549
|
+
}
|
|
550
|
+
if (owner.status === "active") {
|
|
551
|
+
if (
|
|
552
|
+
!Array.isArray(owner.sourcePackageNames) ||
|
|
553
|
+
owner.sourcePackageNames.length === 0 ||
|
|
554
|
+
!owner.sourcePackageNames.every((name) => typeof name === "string" && name.length > 0)
|
|
555
|
+
) {
|
|
556
|
+
findings.push(
|
|
557
|
+
`${label}: active owner sourcePackageNames must be a non-empty string array`,
|
|
558
|
+
);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if ("retiredSourcePackageNames" in owner) {
|
|
562
|
+
findings.push(`${label}: active owner must not declare retiredSourcePackageNames`);
|
|
563
|
+
}
|
|
564
|
+
for (const sourcePackageName of owner.sourcePackageNames) {
|
|
565
|
+
if (!workspacePackageNames.has(sourcePackageName)) {
|
|
566
|
+
findings.push(
|
|
567
|
+
`${label}: sourcePackageName ${sourcePackageName} is not a workspace package`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (owner.status === "retired") {
|
|
573
|
+
if ("sourcePackageNames" in owner) {
|
|
574
|
+
findings.push(`${label}: retired owner must not declare live sourcePackageNames`);
|
|
575
|
+
}
|
|
576
|
+
if (
|
|
577
|
+
!Array.isArray(owner.retiredSourcePackageNames) ||
|
|
578
|
+
owner.retiredSourcePackageNames.length === 0 ||
|
|
579
|
+
!owner.retiredSourcePackageNames.every(
|
|
580
|
+
(name) => typeof name === "string" && name.length > 0,
|
|
581
|
+
)
|
|
582
|
+
) {
|
|
583
|
+
findings.push(
|
|
584
|
+
`${label}: retiredSourcePackageNames must be a non-empty string array for retired owners`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return findings;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const checkOwnerIds = () => {
|
|
593
|
+
const workspacePackageNames = new Set(
|
|
594
|
+
graphWorkspacePackageRecords(repoRoot)
|
|
595
|
+
.map((record) => record.name)
|
|
596
|
+
.filter((name) => typeof name === "string" && name.startsWith("@agent-os/")),
|
|
597
|
+
);
|
|
598
|
+
const registry = ownerIdRegistry();
|
|
599
|
+
const registryFindings = ownerIdRegistryFindings({ registry, workspacePackageNames });
|
|
600
|
+
const registeredOwners = new Map(
|
|
601
|
+
(Array.isArray(registry.owners) ? registry.owners : [])
|
|
602
|
+
.filter((owner) => isRecord(owner) && typeof owner.ownerId === "string")
|
|
603
|
+
.map((owner) => [owner.ownerId, owner]),
|
|
604
|
+
);
|
|
605
|
+
const declarationFindings = packageSourceFiles().flatMap((file) =>
|
|
606
|
+
ownerIdDeclarationFindingsForSource({
|
|
607
|
+
content: read(file),
|
|
608
|
+
file,
|
|
609
|
+
registeredOwners,
|
|
610
|
+
workspacePackageNames,
|
|
611
|
+
}),
|
|
612
|
+
);
|
|
613
|
+
const retiredTestDeclarationFindings = packageTestFiles().flatMap((file) =>
|
|
614
|
+
ownerIdDeclarationFindingsForSource({
|
|
615
|
+
content: read(file),
|
|
616
|
+
file,
|
|
617
|
+
registeredOwners,
|
|
618
|
+
workspacePackageNames,
|
|
619
|
+
}).filter((finding) => finding.includes(" is retired and cannot be declared")),
|
|
620
|
+
);
|
|
621
|
+
const coreNamespaceFindings = coreClaimedNamespaceFindingsForSource({
|
|
622
|
+
content: read("packages/core/src/errors.ts"),
|
|
623
|
+
file: "packages/core/src/errors.ts",
|
|
624
|
+
registeredOwners,
|
|
625
|
+
workspacePackageNames,
|
|
626
|
+
});
|
|
627
|
+
failIfAny("owner ids", [
|
|
628
|
+
...registryFindings,
|
|
629
|
+
...declarationFindings,
|
|
630
|
+
...retiredTestDeclarationFindings,
|
|
631
|
+
...coreNamespaceFindings,
|
|
632
|
+
]);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
ownerCouplingFindingsForSource,
|
|
637
|
+
ownerIdentityBoundaryFindingsForSource,
|
|
638
|
+
ownerIdentityBoundaryNegativeFixtureFailures,
|
|
639
|
+
ownerIdDeclarationFindingsForSource,
|
|
640
|
+
coreClaimedNamespaceFindingsForSource,
|
|
641
|
+
ownerIdRegistryFindings,
|
|
642
|
+
ownerIdRegistry,
|
|
643
|
+
checkOwnerCoupling,
|
|
644
|
+
checkOwnerIdentityBoundary,
|
|
645
|
+
checkOwnerIds,
|
|
646
|
+
};
|
|
647
|
+
};
|