@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,985 @@
|
|
|
1
|
+
export const packageExportSubpaths = (packageJson) => {
|
|
2
|
+
const exported = packageJson.exports;
|
|
3
|
+
if (typeof exported === "string") return ["."];
|
|
4
|
+
if (exported === null || typeof exported !== "object" || Array.isArray(exported)) return ["."];
|
|
5
|
+
return Object.keys(exported)
|
|
6
|
+
.filter((key) => key === "." || key.startsWith("./"))
|
|
7
|
+
.sort((left, right) => left.localeCompare(right));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const createPackageBoundaryChecks = ({
|
|
11
|
+
fs,
|
|
12
|
+
path,
|
|
13
|
+
execFileSync,
|
|
14
|
+
repoRoot,
|
|
15
|
+
read,
|
|
16
|
+
readJson,
|
|
17
|
+
walk,
|
|
18
|
+
compare,
|
|
19
|
+
isRecord,
|
|
20
|
+
failIfAny,
|
|
21
|
+
ruleConstraints,
|
|
22
|
+
graphWorkspacePackageRecords,
|
|
23
|
+
graphPackageSourceImportEdges,
|
|
24
|
+
graphPackageImportCycles,
|
|
25
|
+
sourceModuleGraph,
|
|
26
|
+
moduleGraphOracleFailures,
|
|
27
|
+
importSpecifierRecords,
|
|
28
|
+
distributionExportEntries,
|
|
29
|
+
distributionClosureForRoots,
|
|
30
|
+
distributionUnitFinding,
|
|
31
|
+
packageUnitOptionalPeerEntries,
|
|
32
|
+
formatDistributionFinding,
|
|
33
|
+
checkModuleBuckets,
|
|
34
|
+
moduleAmbientForPath,
|
|
35
|
+
allowedAmbientImports,
|
|
36
|
+
packageUnitsRegistryPath,
|
|
37
|
+
distributionRootsRegistryPath,
|
|
38
|
+
}) => {
|
|
39
|
+
const packagePathMatches = (packagePath, prefix) =>
|
|
40
|
+
packagePath === prefix || packagePath.startsWith(`${prefix}/`);
|
|
41
|
+
|
|
42
|
+
const packageMatchesConstraint = (record, names = [], pathPrefixes = []) =>
|
|
43
|
+
names.includes(record.name) ||
|
|
44
|
+
pathPrefixes.some((prefix) => packagePathMatches(record.path, prefix));
|
|
45
|
+
|
|
46
|
+
const packageConstraintNameFailures = ({ ruleId, constraints, records }) => {
|
|
47
|
+
const liveNames = new Set(records.map((record) => record.name));
|
|
48
|
+
const failures = [];
|
|
49
|
+
for (const [index, constraint] of (constraints.forbiddenEdges ?? []).entries()) {
|
|
50
|
+
if (!isRecord(constraint)) continue;
|
|
51
|
+
for (const key of [
|
|
52
|
+
"fromPackageNames",
|
|
53
|
+
"allowedTargetPackageNames",
|
|
54
|
+
"forbiddenTargetPackageNames",
|
|
55
|
+
]) {
|
|
56
|
+
const names = constraint[key];
|
|
57
|
+
if (!Array.isArray(names)) continue;
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
if (typeof name !== "string" || liveNames.has(name)) continue;
|
|
60
|
+
failures.push(
|
|
61
|
+
`${ruleId}: constraints.forbiddenEdges[${index}].${key} references non-workspace package ${name}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return failures;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sourceFileMatchesPublicSubpath = (packagePath, subpath, sourceFile) => {
|
|
70
|
+
if (typeof packagePath !== "string" || typeof subpath !== "string") return false;
|
|
71
|
+
if (subpath === "." || !subpath.startsWith("./")) return false;
|
|
72
|
+
const sourceBase = `${packagePath}/src/${subpath.slice(2)}`;
|
|
73
|
+
return (
|
|
74
|
+
sourceFile === `${sourceBase}.ts` ||
|
|
75
|
+
sourceFile === `${sourceBase}.tsx` ||
|
|
76
|
+
sourceFile === `${sourceBase}/index.ts` ||
|
|
77
|
+
sourceFile === `${sourceBase}/index.tsx` ||
|
|
78
|
+
sourceFile.startsWith(`${sourceBase}/`)
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const packageUnitOptionalPeerAllowsEdge = ({ registry, edge }) => {
|
|
83
|
+
if (!isRecord(registry) || !Array.isArray(registry.packageUnits)) return false;
|
|
84
|
+
const fromName = edge.from?.name;
|
|
85
|
+
const fromPath = edge.from?.path;
|
|
86
|
+
const toName = edge.to?.name;
|
|
87
|
+
const sourceFile = edge.file ?? edge.fromFile;
|
|
88
|
+
if (
|
|
89
|
+
typeof fromName !== "string" ||
|
|
90
|
+
typeof fromPath !== "string" ||
|
|
91
|
+
typeof toName !== "string" ||
|
|
92
|
+
typeof sourceFile !== "string"
|
|
93
|
+
) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return registry.packageUnits.filter(isRecord).some((unit) => {
|
|
98
|
+
if (unit.targetSourcePackageName !== fromName) return false;
|
|
99
|
+
if (!Array.isArray(unit.publicSubpaths)) return false;
|
|
100
|
+
return unit.publicSubpaths.filter(isRecord).some((subpath) => {
|
|
101
|
+
const optionalPeers = Array.isArray(subpath.optionalPeers) ? subpath.optionalPeers : [];
|
|
102
|
+
return (
|
|
103
|
+
optionalPeers.includes(toName) &&
|
|
104
|
+
sourceFileMatchesPublicSubpath(fromPath, subpath.subpath, sourceFile)
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const checkForbiddenPackageEdges = ({ ruleId, constraints, edges, failures }) => {
|
|
111
|
+
const packageUnits = packageUnitsRegistry();
|
|
112
|
+
for (const edge of edges) {
|
|
113
|
+
for (const constraint of constraints.forbiddenEdges ?? []) {
|
|
114
|
+
if (
|
|
115
|
+
!packageMatchesConstraint(
|
|
116
|
+
edge.from,
|
|
117
|
+
constraint.fromPackageNames ?? [],
|
|
118
|
+
constraint.fromPackagePathPrefixes ?? [],
|
|
119
|
+
)
|
|
120
|
+
) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
packageMatchesConstraint(
|
|
125
|
+
edge.to,
|
|
126
|
+
constraint.allowedTargetPackageNames ?? [],
|
|
127
|
+
constraint.allowedTargetPackagePathPrefixes ?? [],
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (
|
|
133
|
+
packageMatchesConstraint(
|
|
134
|
+
edge.to,
|
|
135
|
+
constraint.forbiddenTargetPackageNames ?? [],
|
|
136
|
+
constraint.forbiddenTargetPackagePathPrefixes ?? [],
|
|
137
|
+
)
|
|
138
|
+
) {
|
|
139
|
+
if (packageUnitOptionalPeerAllowsEdge({ registry: packageUnits, edge })) continue;
|
|
140
|
+
failures.push(
|
|
141
|
+
`${edge.file}: ${ruleId}: ${edge.from.name} must not import downstream package ${edge.specifier} (${edge.to.path})`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const checkPackageImportDag = ({ ruleId, label }) => {
|
|
149
|
+
const constraints = ruleConstraints(ruleId);
|
|
150
|
+
const records = graphWorkspacePackageRecords(repoRoot).filter(
|
|
151
|
+
(record) => typeof record.name === "string" && record.name.startsWith("@agent-os/"),
|
|
152
|
+
);
|
|
153
|
+
const edges = graphPackageSourceImportEdges(repoRoot, records);
|
|
154
|
+
const failures = [];
|
|
155
|
+
|
|
156
|
+
failures.push(...packageConstraintNameFailures({ ruleId, constraints, records }));
|
|
157
|
+
|
|
158
|
+
for (const cycle of graphPackageImportCycles(records, edges)) {
|
|
159
|
+
failures.push(`${ruleId}: package cycle ${cycle.join(" -> ")}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
checkForbiddenPackageEdges({ ruleId, constraints, edges, failures });
|
|
163
|
+
|
|
164
|
+
failIfAny(label, failures);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const checkSubstrateImportDag = () =>
|
|
168
|
+
checkPackageImportDag({ ruleId: "substrate-import-dag", label: "substrate import DAG" });
|
|
169
|
+
|
|
170
|
+
const checkConvergenceImportDag = () => {
|
|
171
|
+
checkPackageImportDag({
|
|
172
|
+
ruleId: "convergence-import-dag",
|
|
173
|
+
label: "convergence import DAG",
|
|
174
|
+
});
|
|
175
|
+
checkModuleBuckets();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const checkModuleGraphOracle = () => {
|
|
179
|
+
const records = graphWorkspacePackageRecords(repoRoot).filter(
|
|
180
|
+
(record) => typeof record.name === "string" && record.name.startsWith("@agent-os/"),
|
|
181
|
+
);
|
|
182
|
+
const graph = sourceModuleGraph(repoRoot, records);
|
|
183
|
+
const failures = moduleGraphOracleFailures(repoRoot, records);
|
|
184
|
+
failIfAny("module graph oracle", failures);
|
|
185
|
+
console.log(
|
|
186
|
+
`module graph oracle covered ${graph.files.length} source files and ${graph.edges.length} internal module edges`,
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const packageUnitsRegistry = () => readJson(packageUnitsRegistryPath);
|
|
191
|
+
const distributionRootsRegistry = () => readJson(distributionRootsRegistryPath);
|
|
192
|
+
|
|
193
|
+
const packageUnitRecords = () =>
|
|
194
|
+
(packageUnitsRegistry().packageUnits ?? []).filter(
|
|
195
|
+
(unit) =>
|
|
196
|
+
isRecord(unit) &&
|
|
197
|
+
typeof unit.id === "string" &&
|
|
198
|
+
typeof unit.targetSourcePackageName === "string" &&
|
|
199
|
+
typeof unit.publicPackageName === "string",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const packageUnitSourceNames = () =>
|
|
203
|
+
new Set(packageUnitRecords().map((unit) => unit.targetSourcePackageName));
|
|
204
|
+
|
|
205
|
+
const packageUnitPublicNames = () =>
|
|
206
|
+
new Set(packageUnitRecords().map((unit) => unit.publicPackageName));
|
|
207
|
+
|
|
208
|
+
const packageUnitSourcePathByName = () => {
|
|
209
|
+
const unitNames = packageUnitSourceNames();
|
|
210
|
+
return new Map(
|
|
211
|
+
graphWorkspacePackageRecords(repoRoot)
|
|
212
|
+
.filter((record) => unitNames.has(record.name))
|
|
213
|
+
.map((record) => [record.name, record.path]),
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const allowedToolingSurface = (pkg) =>
|
|
218
|
+
isRecord(pkg) &&
|
|
219
|
+
typeof pkg.path === "string" &&
|
|
220
|
+
pkg.path.startsWith("tooling/") &&
|
|
221
|
+
pkg.published === false;
|
|
222
|
+
|
|
223
|
+
const packageUnitExportSpecifiers = () => {
|
|
224
|
+
const sourceSpecifiers = new Set();
|
|
225
|
+
const publicSpecifiers = new Set();
|
|
226
|
+
for (const unit of packageUnitRecords()) {
|
|
227
|
+
const unitPath = packageUnitSourcePathByName().get(unit.targetSourcePackageName);
|
|
228
|
+
if (typeof unitPath !== "string") continue;
|
|
229
|
+
const manifest = readJson(`${unitPath}/package.json`);
|
|
230
|
+
for (const exportSubpath of packageExportSubpaths(manifest)) {
|
|
231
|
+
const suffix = exportSubpath === "." ? "" : `/${exportSubpath.slice(2)}`;
|
|
232
|
+
sourceSpecifiers.add(`${unit.targetSourcePackageName}${suffix}`);
|
|
233
|
+
publicSpecifiers.add(`${unit.publicPackageName}${suffix}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { sourceSpecifiers, publicSpecifiers };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const consumerFacingSpecifierFiles = () => [
|
|
240
|
+
"README.md",
|
|
241
|
+
"docs/usage-surfaces.md",
|
|
242
|
+
"docs/runtime-packages.md",
|
|
243
|
+
...walk("docs/tutorials").filter((file) => file.endsWith(".md")),
|
|
244
|
+
...walk("docs/guides").filter((file) => file.endsWith(".md")),
|
|
245
|
+
...walk("docs/concepts").filter((file) => file.endsWith(".md")),
|
|
246
|
+
"skills/agentos/SKILL.md",
|
|
247
|
+
"skills/agentos-release/SKILL.md",
|
|
248
|
+
"skills/agentos/references/package-map.md",
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const packageSpecifierPattern =
|
|
252
|
+
/@(?:agent-os|yansirplus)\/(?:\*|[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*(?:\/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)*)/gu;
|
|
253
|
+
|
|
254
|
+
const textPosition = (content, index) => {
|
|
255
|
+
const prefix = content.slice(0, index);
|
|
256
|
+
const lines = prefix.split("\n");
|
|
257
|
+
return { line: lines.length, column: lines.at(-1).length + 1 };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const consumerFacingSpecifierFailuresForContent = ({
|
|
261
|
+
file,
|
|
262
|
+
content,
|
|
263
|
+
sourceSpecifiers,
|
|
264
|
+
publicSpecifiers,
|
|
265
|
+
toolingSourceSpecifiers,
|
|
266
|
+
}) => {
|
|
267
|
+
const failures = [];
|
|
268
|
+
for (const match of content.matchAll(packageSpecifierPattern)) {
|
|
269
|
+
const specifier = match[0];
|
|
270
|
+
const allowed = specifier.startsWith("@yansirplus/")
|
|
271
|
+
? publicSpecifiers.has(specifier)
|
|
272
|
+
: sourceSpecifiers.has(specifier) || toolingSourceSpecifiers.has(specifier);
|
|
273
|
+
if (allowed) continue;
|
|
274
|
+
const position = textPosition(content, match.index ?? 0);
|
|
275
|
+
failures.push(
|
|
276
|
+
`${file}:${position.line}:${position.column}: obsolete consumer-facing package specifier ${specifier}`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return failures;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const consumerFacingSpecifierFailures = () => {
|
|
283
|
+
const { sourceSpecifiers, publicSpecifiers } = packageUnitExportSpecifiers();
|
|
284
|
+
const toolingSourceSpecifiers = new Set(
|
|
285
|
+
(readJson("docs/surface.json").packages ?? [])
|
|
286
|
+
.filter((pkg) => allowedToolingSurface(pkg) && typeof pkg.name === "string")
|
|
287
|
+
.map((pkg) => pkg.name),
|
|
288
|
+
);
|
|
289
|
+
const failures = [];
|
|
290
|
+
for (const file of consumerFacingSpecifierFiles()) {
|
|
291
|
+
if (!fs.existsSync(path.join(repoRoot, file))) continue;
|
|
292
|
+
const content = read(file);
|
|
293
|
+
failures.push(
|
|
294
|
+
...consumerFacingSpecifierFailuresForContent({
|
|
295
|
+
file,
|
|
296
|
+
content,
|
|
297
|
+
sourceSpecifiers,
|
|
298
|
+
publicSpecifiers,
|
|
299
|
+
toolingSourceSpecifiers,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return failures.sort(compare);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const markdownLinkPattern = /!?\[[^\]\n]*\]\(([^)\n]+)\)/gu;
|
|
307
|
+
const externalMarkdownTargetPattern = /^(?:[a-z][a-z0-9+.-]*:|#|\/)/iu;
|
|
308
|
+
|
|
309
|
+
const normalizeMarkdownTarget = (rawTarget) => {
|
|
310
|
+
const target = rawTarget.trim().split(/\s+/u)[0]?.replace(/^<|>$/gu, "") ?? "";
|
|
311
|
+
if (target.length === 0 || externalMarkdownTargetPattern.test(target)) return undefined;
|
|
312
|
+
return target.split("#")[0];
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const markdownTargetCandidates = (file, target) => {
|
|
316
|
+
const decoded = decodeURI(target);
|
|
317
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(file), decoded));
|
|
318
|
+
if (base.endsWith("/")) return [`${base}index.md`];
|
|
319
|
+
if (path.posix.extname(base).length > 0) return [base];
|
|
320
|
+
return [base, `${base}.md`, `${base}/index.md`];
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const markdownLinkFailuresForContent = ({ file, content }) => {
|
|
324
|
+
const failures = [];
|
|
325
|
+
for (const match of content.matchAll(markdownLinkPattern)) {
|
|
326
|
+
const target = normalizeMarkdownTarget(match[1]);
|
|
327
|
+
if (target === undefined) continue;
|
|
328
|
+
let candidates;
|
|
329
|
+
try {
|
|
330
|
+
candidates = markdownTargetCandidates(file, target);
|
|
331
|
+
} catch {
|
|
332
|
+
const position = textPosition(content, match.index ?? 0);
|
|
333
|
+
failures.push(
|
|
334
|
+
`${file}:${position.line}:${position.column}: markdown link target ${target} is not a valid URI path`,
|
|
335
|
+
);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const targetExtension = path.posix.extname(candidates[0] ?? "");
|
|
339
|
+
if (targetExtension.length > 0 && targetExtension !== ".md") continue;
|
|
340
|
+
if (candidates.some((candidate) => fs.existsSync(path.join(repoRoot, candidate)))) continue;
|
|
341
|
+
const position = textPosition(content, match.index ?? 0);
|
|
342
|
+
failures.push(
|
|
343
|
+
`${file}:${position.line}:${position.column}: markdown link target ${target} does not resolve to ${candidates.join(" or ")}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return failures;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const docsLinkIntegrityFailures = () =>
|
|
350
|
+
[
|
|
351
|
+
...walk("docs").filter((file) => file.endsWith(".md")),
|
|
352
|
+
...walk("tooling/docs-site/src/content/docs").filter((file) => file.endsWith(".md")),
|
|
353
|
+
]
|
|
354
|
+
.flatMap((file) => markdownLinkFailuresForContent({ file, content: read(file) }))
|
|
355
|
+
.sort(compare);
|
|
356
|
+
|
|
357
|
+
const checkDocsLinkIntegrity = () => {
|
|
358
|
+
failIfAny("docs link integrity", docsLinkIntegrityFailures());
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const specifierAllowedByPackageUnits = (specifier, unitNames = packageUnitSourceNames()) =>
|
|
362
|
+
[...unitNames].some((name) => specifierMatchesPackage(specifier, name));
|
|
363
|
+
|
|
364
|
+
const packageUnitDocSources = () => {
|
|
365
|
+
const unitNames = packageUnitSourceNames();
|
|
366
|
+
const allowedApiSources = new Set();
|
|
367
|
+
const allowedPackageDocs = new Set();
|
|
368
|
+
for (const pkg of readJson("docs/surface.json").packages ?? []) {
|
|
369
|
+
if (!isRecord(pkg)) continue;
|
|
370
|
+
if (!unitNames.has(pkg.name) && !allowedToolingSurface(pkg)) continue;
|
|
371
|
+
if (typeof pkg.apiSource === "string") allowedApiSources.add(pkg.apiSource);
|
|
372
|
+
if (typeof pkg.slug === "string") allowedPackageDocs.add(`docs/packages/${pkg.slug}.md`);
|
|
373
|
+
}
|
|
374
|
+
return { allowedApiSources, allowedPackageDocs };
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const packageUnitOptionalPeerFindings = () => {
|
|
378
|
+
const findings = [];
|
|
379
|
+
for (const unit of packageUnitRecords()) {
|
|
380
|
+
for (const { peer, subpath } of packageUnitOptionalPeerEntries(unit)) {
|
|
381
|
+
if (!peer.startsWith("@agent-os/")) continue;
|
|
382
|
+
findings.push(
|
|
383
|
+
formatDistributionFinding(
|
|
384
|
+
distributionUnitFinding({
|
|
385
|
+
kind: "package-unit-internal-optional-peer",
|
|
386
|
+
unit,
|
|
387
|
+
message:
|
|
388
|
+
"internal @agent-os modules must be package-local subpaths, not package-unit optional peers",
|
|
389
|
+
specifier: peer,
|
|
390
|
+
target: subpath,
|
|
391
|
+
}),
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return findings;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const obsoletePublicPackageFailures = () => {
|
|
400
|
+
const failures = [];
|
|
401
|
+
const unitNames = packageUnitSourceNames();
|
|
402
|
+
const unitPaths = new Set(
|
|
403
|
+
[...packageUnitSourcePathByName().values()].filter(
|
|
404
|
+
(unitPath) => typeof unitPath === "string",
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
const unitPublicNames = packageUnitPublicNames();
|
|
408
|
+
const surfacePackages = readJson("docs/surface.json").packages ?? [];
|
|
409
|
+
const surfaceByPath = new Map(
|
|
410
|
+
surfacePackages
|
|
411
|
+
.filter((pkg) => isRecord(pkg) && typeof pkg.path === "string")
|
|
412
|
+
.map((pkg) => [pkg.path, pkg]),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
for (const packageJson of walk("packages").filter((file) => file.endsWith("/package.json"))) {
|
|
416
|
+
const packagePath = packageJson.slice(0, -"/package.json".length);
|
|
417
|
+
const manifest = readJson(packageJson);
|
|
418
|
+
if (!manifest.name?.startsWith("@agent-os/")) continue;
|
|
419
|
+
if (!unitNames.has(manifest.name)) {
|
|
420
|
+
failures.push(
|
|
421
|
+
`${packageJson}: obsolete source package ${manifest.name} is not declared by architecture/package-units.json`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (!unitPaths.has(packagePath)) {
|
|
425
|
+
failures.push(`${packageJson}: package path is outside final package-unit roots`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const packageJson of walk("tooling").filter((file) => file.endsWith("/package.json"))) {
|
|
430
|
+
const packagePath = packageJson.slice(0, -"/package.json".length);
|
|
431
|
+
const manifest = readJson(packageJson);
|
|
432
|
+
if (!manifest.name?.startsWith("@agent-os/")) continue;
|
|
433
|
+
const surface = surfaceByPath.get(packagePath);
|
|
434
|
+
if (!allowedToolingSurface(surface)) {
|
|
435
|
+
failures.push(`${packageJson}: tooling package must be private docs/tooling surface only`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const pkg of surfacePackages) {
|
|
440
|
+
if (!isRecord(pkg)) {
|
|
441
|
+
failures.push("docs/surface.json: package entries must be objects");
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const label = `docs/surface.json:${pkg.name ?? pkg.path ?? "package"}`;
|
|
445
|
+
if (allowedToolingSurface(pkg)) continue;
|
|
446
|
+
if (!unitNames.has(pkg.name)) {
|
|
447
|
+
failures.push(`${label}: obsolete package remains in docs/surface.json`);
|
|
448
|
+
}
|
|
449
|
+
if (pkg.published === true && !unitNames.has(pkg.name)) {
|
|
450
|
+
failures.push(`${label}: obsolete published package remains public`);
|
|
451
|
+
}
|
|
452
|
+
if (
|
|
453
|
+
typeof pkg.path === "string" &&
|
|
454
|
+
pkg.path.startsWith("packages/") &&
|
|
455
|
+
!unitPaths.has(pkg.path)
|
|
456
|
+
) {
|
|
457
|
+
failures.push(`${label}: package path is outside final package-unit roots`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const { allowedApiSources, allowedPackageDocs } = packageUnitDocSources();
|
|
462
|
+
for (const file of walk("docs/api").filter((entry) => entry.endsWith(".md"))) {
|
|
463
|
+
if (!allowedApiSources.has(file)) failures.push(`${file}: obsolete API intent page remains`);
|
|
464
|
+
}
|
|
465
|
+
for (const file of walk("docs/packages").filter((entry) => entry.endsWith(".md"))) {
|
|
466
|
+
if (!allowedPackageDocs.has(file)) failures.push(`${file}: obsolete package doc remains`);
|
|
467
|
+
}
|
|
468
|
+
for (const file of walk("tooling/docs-site/src/content/docs/api").filter((entry) =>
|
|
469
|
+
entry.endsWith(".md"),
|
|
470
|
+
)) {
|
|
471
|
+
const source = `docs/api/${path.basename(file)}`;
|
|
472
|
+
if (!allowedApiSources.has(source)) {
|
|
473
|
+
failures.push(`${file}: obsolete docs-site API projection remains`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
for (const file of walk("tooling/docs-site/src/content/docs/packages").filter((entry) =>
|
|
477
|
+
entry.endsWith(".md"),
|
|
478
|
+
)) {
|
|
479
|
+
const source = `docs/packages/${path.basename(file)}`;
|
|
480
|
+
if (!allowedPackageDocs.has(source)) {
|
|
481
|
+
failures.push(`${file}: obsolete docs-site package projection remains`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const file of walk("packages").filter((entry) =>
|
|
486
|
+
/\/(?:README|PUBLIC_API)\.md$/u.test(entry),
|
|
487
|
+
)) {
|
|
488
|
+
if (![...unitPaths].some((unitPath) => file.startsWith(`${unitPath}/`))) {
|
|
489
|
+
failures.push(`${file}: obsolete generated package doc remains`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const sourceAliases = readJson("tsconfig.source-paths.json").compilerOptions?.paths ?? {};
|
|
494
|
+
for (const specifier of Object.keys(sourceAliases)) {
|
|
495
|
+
if (!specifier.startsWith("@agent-os/")) continue;
|
|
496
|
+
if (!specifierAllowedByPackageUnits(specifier, unitNames)) {
|
|
497
|
+
failures.push(`${specifier}: obsolete source alias remains`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
for (const unitName of unitNames) {
|
|
502
|
+
const unitPath = packageUnitSourcePathByName().get(unitName);
|
|
503
|
+
if (typeof unitPath !== "string") {
|
|
504
|
+
failures.push(`${String(unitName)}: package unit source package is missing from workspace`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const manifest = readJson(`${unitPath}/package.json`);
|
|
508
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
509
|
+
for (const dependencyName of Object.keys(manifest[section] ?? {})) {
|
|
510
|
+
if (!dependencyName.startsWith("@agent-os/")) continue;
|
|
511
|
+
if (!unitNames.has(dependencyName)) {
|
|
512
|
+
failures.push(
|
|
513
|
+
`${unitPath}/package.json:${section}: obsolete internal dependency ${dependencyName}`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
for (const [unitName, unitPath] of packageUnitSourcePathByName()) {
|
|
521
|
+
if (typeof unitName !== "string") continue;
|
|
522
|
+
if (typeof unitPath !== "string") continue;
|
|
523
|
+
for (const file of walk(unitPath).filter((entry) =>
|
|
524
|
+
/\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/u.test(entry),
|
|
525
|
+
)) {
|
|
526
|
+
const source = read(file);
|
|
527
|
+
for (const importRecord of importSpecifierRecords(source, file)) {
|
|
528
|
+
if (!importRecord.specifier.startsWith("@agent-os/")) continue;
|
|
529
|
+
if (specifierAllowedByPackageUnits(importRecord.specifier, unitNames)) continue;
|
|
530
|
+
failures.push(
|
|
531
|
+
`${file}:${importRecord.line}:${importRecord.column}: obsolete import specifier ${importRecord.specifier} in final package ${unitName}`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const ownerRegistry = ownerIdRegistry();
|
|
538
|
+
for (const [index, owner] of (ownerRegistry.owners ?? []).entries()) {
|
|
539
|
+
if (!isRecord(owner) || !Array.isArray(owner.sourcePackageNames)) continue;
|
|
540
|
+
for (const sourcePackageName of owner.sourcePackageNames) {
|
|
541
|
+
if (typeof sourcePackageName !== "string" || !sourcePackageName.startsWith("@agent-os/")) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (!unitNames.has(sourcePackageName)) {
|
|
545
|
+
failures.push(
|
|
546
|
+
`architecture/owner-ids.json:owners[${index}]: obsolete sourcePackageName ${sourcePackageName}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const unit of packageUnitRecords()) {
|
|
553
|
+
for (const publicName of [unit.publicPackageName]) {
|
|
554
|
+
if (!unitPublicNames.has(publicName)) {
|
|
555
|
+
failures.push(
|
|
556
|
+
`${unit.id}: public package ${publicName} is not a package-unit public name`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
failures.push(...consumerFacingSpecifierFailures());
|
|
563
|
+
|
|
564
|
+
return failures.sort(compare);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const distributionMinimalityFailures = () => [
|
|
568
|
+
...packageUnitOptionalPeerFindings(),
|
|
569
|
+
...obsoletePublicPackageFailures(),
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const checkNoObsoletePublicPackages = () => {
|
|
573
|
+
failIfAny("no obsolete public packages", obsoletePublicPackageFailures());
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const sourceSpecifierForPublicSubpath = (unit, publicSpecifier) => {
|
|
577
|
+
if (
|
|
578
|
+
typeof publicSpecifier !== "string" ||
|
|
579
|
+
typeof unit.publicPackageName !== "string" ||
|
|
580
|
+
typeof unit.targetSourcePackageName !== "string" ||
|
|
581
|
+
(publicSpecifier !== unit.publicPackageName &&
|
|
582
|
+
!publicSpecifier.startsWith(`${unit.publicPackageName}/`))
|
|
583
|
+
) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
return `${unit.targetSourcePackageName}${publicSpecifier.slice(unit.publicPackageName.length)}`;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const subpathForSourceSpecifier = (unit, sourceSpecifier) => {
|
|
590
|
+
if (
|
|
591
|
+
typeof sourceSpecifier !== "string" ||
|
|
592
|
+
typeof unit.targetSourcePackageName !== "string" ||
|
|
593
|
+
(sourceSpecifier !== unit.targetSourcePackageName &&
|
|
594
|
+
!sourceSpecifier.startsWith(`${unit.targetSourcePackageName}/`))
|
|
595
|
+
) {
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
const suffix = sourceSpecifier.slice(unit.targetSourcePackageName.length);
|
|
599
|
+
return suffix.length === 0 ? "." : `.${suffix}`;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const publicSpecifierForSourceSpecifier = (unit, sourceSpecifier) => {
|
|
603
|
+
const subpath = subpathForSourceSpecifier(unit, sourceSpecifier);
|
|
604
|
+
if (subpath === undefined) return undefined;
|
|
605
|
+
if (subpath === ".") return unit.publicPackageName;
|
|
606
|
+
if (!subpath.startsWith("./")) return undefined;
|
|
607
|
+
return `${unit.publicPackageName}/${subpath.slice(2)}`;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const packageUnitPublicSpecifiers = () => {
|
|
611
|
+
const specifiers = new Set();
|
|
612
|
+
for (const unit of packageUnitRecords()) {
|
|
613
|
+
for (const entry of unit.publicSubpaths ?? []) {
|
|
614
|
+
if (!isRecord(entry) || typeof entry.subpath !== "string") continue;
|
|
615
|
+
if (entry.subpath === ".") {
|
|
616
|
+
specifiers.add(unit.publicPackageName);
|
|
617
|
+
} else if (entry.subpath.startsWith("./")) {
|
|
618
|
+
specifiers.add(`${unit.publicPackageName}/${entry.subpath.slice(2)}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return specifiers;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const packageUnitPublicSpecifierForSource = (sourceSpecifier) => {
|
|
626
|
+
for (const unit of packageUnitRecords()) {
|
|
627
|
+
const publicSpecifier = publicSpecifierForSourceSpecifier(unit, sourceSpecifier);
|
|
628
|
+
if (publicSpecifier !== undefined) return publicSpecifier;
|
|
629
|
+
}
|
|
630
|
+
return undefined;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const specifierMatchesPackage = (specifier, packageName) =>
|
|
634
|
+
specifier === packageName || specifier.startsWith(`${packageName}/`);
|
|
635
|
+
|
|
636
|
+
const specifierMatchesForbidden = (specifier, forbidden) =>
|
|
637
|
+
forbidden.endsWith(":")
|
|
638
|
+
? specifier.startsWith(forbidden)
|
|
639
|
+
: specifier === forbidden || specifier.startsWith(`${forbidden}/`);
|
|
640
|
+
|
|
641
|
+
const graphSourceByFile = (graph) =>
|
|
642
|
+
new Map(
|
|
643
|
+
graph.files.map((entry) => [
|
|
644
|
+
entry.file,
|
|
645
|
+
fs.readFileSync(path.join(repoRoot, entry.file), "utf8"),
|
|
646
|
+
]),
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const exportEntriesBySubpath = (record) => {
|
|
650
|
+
const manifest = readJson(`${record.path}/package.json`);
|
|
651
|
+
return new Map(
|
|
652
|
+
distributionExportEntries(record, manifest).map((entry) => [entry.subpath, entry]),
|
|
653
|
+
);
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const packageUnitRecordsBySourceName = (records) =>
|
|
657
|
+
new Map(records.map((record) => [record.name, record]));
|
|
658
|
+
|
|
659
|
+
const subpathNoLeakPackageFilter = (args) => {
|
|
660
|
+
if (args.length === 0) return undefined;
|
|
661
|
+
if (args.length === 2 && args[0] === "--package" && typeof args[1] === "string") return args[1];
|
|
662
|
+
throw new Error("subpath-no-leak: expected optional --package <source-package-name>");
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const checkSubpathNoLeak = (args = []) => {
|
|
666
|
+
const packageFilter = subpathNoLeakPackageFilter(args);
|
|
667
|
+
const records = graphWorkspacePackageRecords(repoRoot).filter(
|
|
668
|
+
(record) => typeof record.name === "string" && record.name.startsWith("@agent-os/"),
|
|
669
|
+
);
|
|
670
|
+
const recordsByName = packageUnitRecordsBySourceName(records);
|
|
671
|
+
const graph = sourceModuleGraph(repoRoot, records);
|
|
672
|
+
const sourceByFile = graphSourceByFile(graph);
|
|
673
|
+
const failures = [];
|
|
674
|
+
for (const unit of packageUnitsRegistry().packageUnits ?? []) {
|
|
675
|
+
if (!isRecord(unit) || typeof unit.targetSourcePackageName !== "string") continue;
|
|
676
|
+
if (packageFilter !== undefined && unit.targetSourcePackageName !== packageFilter) continue;
|
|
677
|
+
const record = recordsByName.get(unit.targetSourcePackageName);
|
|
678
|
+
if (record === undefined) continue;
|
|
679
|
+
const entriesBySubpath = exportEntriesBySubpath(record);
|
|
680
|
+
const rootEntry = entriesBySubpath.get(".");
|
|
681
|
+
if (rootEntry === undefined) {
|
|
682
|
+
failures.push(`${unit.id}: package root export is missing`);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const samePackageEdges = graph.edges.filter(
|
|
686
|
+
(edge) => edge.from.name === record.name && edge.to.name === record.name,
|
|
687
|
+
);
|
|
688
|
+
const rootClosure = distributionClosureForRoots(rootEntry.targets, samePackageEdges);
|
|
689
|
+
const subpathOnlyPeers = new Set(
|
|
690
|
+
(unit.publicSubpaths ?? [])
|
|
691
|
+
.filter((subpath) => isRecord(subpath) && subpath.subpath !== ".")
|
|
692
|
+
.flatMap((subpath) =>
|
|
693
|
+
Array.isArray(subpath.optionalPeers)
|
|
694
|
+
? subpath.optionalPeers.filter((peer) => typeof peer === "string")
|
|
695
|
+
: [],
|
|
696
|
+
),
|
|
697
|
+
);
|
|
698
|
+
for (const file of [...rootClosure].sort(compare)) {
|
|
699
|
+
const source = sourceByFile.get(file);
|
|
700
|
+
if (source === undefined) continue;
|
|
701
|
+
for (const importRecord of importSpecifierRecords(source, file)) {
|
|
702
|
+
for (const peer of subpathOnlyPeers) {
|
|
703
|
+
if (specifierMatchesPackage(importRecord.specifier, peer)) {
|
|
704
|
+
failures.push(
|
|
705
|
+
`${String(file)}:${importRecord.line}:${importRecord.column}: subpath-no-leak: root closure imports subpath-only peer ${String(peer)} via ${importRecord.specifier}`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const subpath of (unit.publicSubpaths ?? []).filter(
|
|
712
|
+
(entry) => isRecord(entry) && entry.subpath !== ".",
|
|
713
|
+
)) {
|
|
714
|
+
const entry = entriesBySubpath.get(subpath.subpath);
|
|
715
|
+
if (entry === undefined) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
for (const target of entry.targets) {
|
|
719
|
+
if (rootClosure.has(target)) {
|
|
720
|
+
failures.push(
|
|
721
|
+
`${target}: subpath-no-leak: package root closure reaches ${unit.id} ${subpath.subpath}`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
failIfAny("subpath no leak", failures);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const profileTypeNames = (ambient) => {
|
|
731
|
+
if (ambient === "cloudflare-worker") return ["@cloudflare/workers-types"];
|
|
732
|
+
if (ambient === "node") return ["node"];
|
|
733
|
+
return [];
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const profileCacheDir = (batch, profileId) =>
|
|
737
|
+
path.join(repoRoot, ".cache", "profile-verification", batch, profileId);
|
|
738
|
+
|
|
739
|
+
const profileEntrySource = (specifiers) =>
|
|
740
|
+
specifiers
|
|
741
|
+
.map((specifier, index) => [
|
|
742
|
+
`import * as profile${index} from ${JSON.stringify(specifier)};`,
|
|
743
|
+
`void profile${index};`,
|
|
744
|
+
])
|
|
745
|
+
.flat()
|
|
746
|
+
.join("\n") + "\n";
|
|
747
|
+
|
|
748
|
+
const runProfileTypecheck = ({ batch, profile, sourceSpecifiers }) => {
|
|
749
|
+
const dir = profileCacheDir(batch, profile.id);
|
|
750
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
751
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
752
|
+
const entryPath = path.join(dir, "entry.ts");
|
|
753
|
+
const configPath = path.join(dir, "tsconfig.json");
|
|
754
|
+
fs.writeFileSync(entryPath, profileEntrySource(sourceSpecifiers));
|
|
755
|
+
const sourcePathConfig = readJson("tsconfig.source-paths.json").compilerOptions ?? {};
|
|
756
|
+
const baseUrl = path.relative(dir, repoRoot).split(path.sep).join("/") || ".";
|
|
757
|
+
fs.writeFileSync(
|
|
758
|
+
configPath,
|
|
759
|
+
`${JSON.stringify(
|
|
760
|
+
{
|
|
761
|
+
compilerOptions: {
|
|
762
|
+
...sourcePathConfig,
|
|
763
|
+
baseUrl,
|
|
764
|
+
target: "ES2022",
|
|
765
|
+
module: "ESNext",
|
|
766
|
+
moduleResolution: "Bundler",
|
|
767
|
+
strict: true,
|
|
768
|
+
skipLibCheck: true,
|
|
769
|
+
noEmit: true,
|
|
770
|
+
types: profileTypeNames(profile.ambient),
|
|
771
|
+
},
|
|
772
|
+
include: ["entry.ts"],
|
|
773
|
+
},
|
|
774
|
+
null,
|
|
775
|
+
2,
|
|
776
|
+
)}\n`,
|
|
777
|
+
);
|
|
778
|
+
try {
|
|
779
|
+
execFileSync(path.join(repoRoot, "node_modules", ".bin", "tsc"), ["-p", configPath], {
|
|
780
|
+
cwd: repoRoot,
|
|
781
|
+
encoding: "utf8",
|
|
782
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
783
|
+
});
|
|
784
|
+
return [];
|
|
785
|
+
} catch (error) {
|
|
786
|
+
return [
|
|
787
|
+
`${profile.id}: profile-verification typecheck failed\n${error.stdout ?? ""}${error.stderr ?? ""}`,
|
|
788
|
+
];
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const absoluteFiles = (dir) => {
|
|
793
|
+
if (!fs.existsSync(dir)) return [];
|
|
794
|
+
const files = [];
|
|
795
|
+
const visit = (current) => {
|
|
796
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
797
|
+
const target = path.join(current, entry.name);
|
|
798
|
+
if (entry.isDirectory()) {
|
|
799
|
+
visit(target);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (entry.isFile()) files.push(target);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
visit(dir);
|
|
806
|
+
return files.sort(compare);
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const relativeImportFrom = (fromDir, targetFile) => {
|
|
810
|
+
const relative = path
|
|
811
|
+
.relative(fromDir, path.join(repoRoot, targetFile))
|
|
812
|
+
.split(path.sep)
|
|
813
|
+
.join("/");
|
|
814
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const runProfileBundle = ({ batch, profile, bundleFiles, forbiddenSpecifiers }) => {
|
|
818
|
+
const dir = profileCacheDir(batch, profile.id);
|
|
819
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
820
|
+
const entryPath = path.join(dir, "entry.ts");
|
|
821
|
+
const outDir = path.join(dir, "bundle");
|
|
822
|
+
fs.writeFileSync(
|
|
823
|
+
entryPath,
|
|
824
|
+
profileEntrySource(bundleFiles.map((file) => relativeImportFrom(dir, file))),
|
|
825
|
+
);
|
|
826
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
827
|
+
const args = [
|
|
828
|
+
"build",
|
|
829
|
+
entryPath,
|
|
830
|
+
"--outdir",
|
|
831
|
+
outDir,
|
|
832
|
+
"--target",
|
|
833
|
+
profile.ambient === "node" ? "node" : "browser",
|
|
834
|
+
];
|
|
835
|
+
if (profile.ambient === "cloudflare-worker") args.push("--external", "cloudflare:*");
|
|
836
|
+
if (profile.ambient !== "node") args.push("--external", "node:*");
|
|
837
|
+
try {
|
|
838
|
+
execFileSync("bun", args, {
|
|
839
|
+
cwd: repoRoot,
|
|
840
|
+
encoding: "utf8",
|
|
841
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
842
|
+
});
|
|
843
|
+
} catch (error) {
|
|
844
|
+
return [
|
|
845
|
+
`${profile.id}: profile-verification bundle failed\n${error.stdout ?? ""}${error.stderr ?? ""}`,
|
|
846
|
+
];
|
|
847
|
+
}
|
|
848
|
+
const failures = [];
|
|
849
|
+
for (const file of absoluteFiles(outDir).filter((entry) => entry.endsWith(".js"))) {
|
|
850
|
+
const text = fs.readFileSync(file, "utf8");
|
|
851
|
+
for (const forbidden of forbiddenSpecifiers) {
|
|
852
|
+
if (text.includes(forbidden)) {
|
|
853
|
+
failures.push(
|
|
854
|
+
`${toRepoPath(file)}: profile-verification bundle contains forbidden specifier ${forbidden}`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return failures;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const selectedSourceSpecifiersForProfileUnit = ({ profile, unit }) =>
|
|
863
|
+
(profile.selectedSubpaths ?? [])
|
|
864
|
+
.map((specifier) => sourceSpecifierForPublicSubpath(unit, specifier))
|
|
865
|
+
.filter((specifier) => typeof specifier === "string");
|
|
866
|
+
|
|
867
|
+
const profileVerificationFindings = ({ batch }) => {
|
|
868
|
+
const packageUnits = packageUnitsRegistry().packageUnits ?? [];
|
|
869
|
+
const roots = distributionRootsRegistry();
|
|
870
|
+
const unitIds =
|
|
871
|
+
batch === "runtime"
|
|
872
|
+
? new Set(["runtime"])
|
|
873
|
+
: batch === "client"
|
|
874
|
+
? new Set(["client"])
|
|
875
|
+
: new Set(packageUnits.filter(isRecord).map((unit) => unit.id));
|
|
876
|
+
const units = packageUnits.filter((unit) => isRecord(unit) && unitIds.has(unit.id));
|
|
877
|
+
const records = graphWorkspacePackageRecords(repoRoot).filter(
|
|
878
|
+
(record) => typeof record.name === "string" && record.name.startsWith("@agent-os/"),
|
|
879
|
+
);
|
|
880
|
+
const recordsByName = packageUnitRecordsBySourceName(records);
|
|
881
|
+
const graph = sourceModuleGraph(repoRoot, records);
|
|
882
|
+
const sourceByFile = graphSourceByFile(graph);
|
|
883
|
+
const failures = [];
|
|
884
|
+
for (const unit of units) {
|
|
885
|
+
const record = recordsByName.get(unit.targetSourcePackageName);
|
|
886
|
+
if (record === undefined) {
|
|
887
|
+
failures.push(`${unit.id}: source package ${unit.targetSourcePackageName} is missing`);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const entriesBySubpath = exportEntriesBySubpath(record);
|
|
891
|
+
for (const profile of roots.targetProfiles ?? []) {
|
|
892
|
+
if (!isRecord(profile) || !(profile.packageUnits ?? []).includes(unit.id)) continue;
|
|
893
|
+
const sourceSpecifiers = selectedSourceSpecifiersForProfileUnit({ profile, unit });
|
|
894
|
+
if (sourceSpecifiers.length === 0) {
|
|
895
|
+
failures.push(`${profile.id}:${unit.id}: profile selects no subpath for package unit`);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const rootFiles = [];
|
|
899
|
+
for (const specifier of sourceSpecifiers) {
|
|
900
|
+
const subpath = subpathForSourceSpecifier(unit, specifier);
|
|
901
|
+
const entry = subpath === undefined ? undefined : entriesBySubpath.get(subpath);
|
|
902
|
+
if (entry === undefined) {
|
|
903
|
+
failures.push(`${profile.id}:${specifier}: selected subpath is not exported`);
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
rootFiles.push(...entry.targets);
|
|
907
|
+
}
|
|
908
|
+
const closure = distributionClosureForRoots(rootFiles, graph.edges);
|
|
909
|
+
const allowedAmbients = allowedAmbientImports().get(profile.ambient) ?? new Set();
|
|
910
|
+
for (const file of [...closure].sort(compare)) {
|
|
911
|
+
const ambient = moduleAmbientForPath(file);
|
|
912
|
+
if (!allowedAmbients.has(ambient)) {
|
|
913
|
+
failures.push(
|
|
914
|
+
`${String(file)}: profile-verification:${profile.id}: ${profile.ambient} profile links ${ambient} module`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const source = sourceByFile.get(file);
|
|
918
|
+
if (source === undefined) continue;
|
|
919
|
+
for (const importRecord of importSpecifierRecords(source, file)) {
|
|
920
|
+
for (const forbidden of profile.forbiddenSpecifiers ?? []) {
|
|
921
|
+
if (specifierMatchesForbidden(importRecord.specifier, forbidden)) {
|
|
922
|
+
failures.push(
|
|
923
|
+
`${String(file)}:${importRecord.line}:${importRecord.column}: profile-verification:${profile.id}: forbidden specifier ${importRecord.specifier}`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
failures.push(
|
|
930
|
+
...runProfileTypecheck({ batch, profile, sourceSpecifiers }),
|
|
931
|
+
...runProfileBundle({
|
|
932
|
+
batch,
|
|
933
|
+
profile,
|
|
934
|
+
bundleFiles: rootFiles,
|
|
935
|
+
forbiddenSpecifiers: profile.forbiddenSpecifiers ?? [],
|
|
936
|
+
}),
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return failures;
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
const checkProfileVerification = (args = []) => {
|
|
944
|
+
if (args.length !== 2 || args[0] !== "--batch") {
|
|
945
|
+
throw new Error("profile-verification: expected --batch <batch>");
|
|
946
|
+
}
|
|
947
|
+
const batch = args[1];
|
|
948
|
+
if (batch !== "runtime" && batch !== "client") {
|
|
949
|
+
throw new Error(`profile-verification: unsupported batch ${batch}`);
|
|
950
|
+
}
|
|
951
|
+
failIfAny("profile verification", profileVerificationFindings({ batch }));
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
packageConstraintNameFailures,
|
|
956
|
+
packageUnitOptionalPeerAllowsEdge,
|
|
957
|
+
consumerFacingSpecifierFailuresForContent,
|
|
958
|
+
markdownLinkFailuresForContent,
|
|
959
|
+
obsoletePublicPackageFailures,
|
|
960
|
+
distributionMinimalityFailures,
|
|
961
|
+
consumerFacingSpecifierFailures,
|
|
962
|
+
checkDocsLinkIntegrity,
|
|
963
|
+
packageUnitOptionalPeerFindings,
|
|
964
|
+
checkNoObsoletePublicPackages,
|
|
965
|
+
packageUnitsRegistry,
|
|
966
|
+
distributionRootsRegistry,
|
|
967
|
+
packageUnitRecords,
|
|
968
|
+
packageUnitSourceNames,
|
|
969
|
+
packageUnitPublicNames,
|
|
970
|
+
packageUnitSourcePathByName,
|
|
971
|
+
packageUnitPublicSpecifiers,
|
|
972
|
+
packageUnitPublicSpecifierForSource,
|
|
973
|
+
sourceSpecifierForPublicSubpath,
|
|
974
|
+
subpathForSourceSpecifier,
|
|
975
|
+
selectedSourceSpecifiersForProfileUnit,
|
|
976
|
+
runProfileTypecheck,
|
|
977
|
+
specifierAllowedByPackageUnits,
|
|
978
|
+
specifierMatchesPackage,
|
|
979
|
+
checkSubstrateImportDag,
|
|
980
|
+
checkConvergenceImportDag,
|
|
981
|
+
checkModuleGraphOracle,
|
|
982
|
+
checkSubpathNoLeak,
|
|
983
|
+
checkProfileVerification,
|
|
984
|
+
};
|
|
985
|
+
};
|