@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.
Files changed (47) hide show
  1. package/PUBLIC_API.md +22 -0
  2. package/README.md +34 -0
  3. package/dist/build/agent-authoring/config.d.ts +177 -0
  4. package/dist/build/agent-authoring/config.js +607 -0
  5. package/dist/build/agent-authoring/manifest-compiler.d.ts +159 -0
  6. package/dist/build/agent-authoring/manifest-compiler.js +737 -0
  7. package/dist/build/agent-authoring/shared.d.ts +10 -0
  8. package/dist/build/agent-authoring/shared.js +57 -0
  9. package/dist/build/agent-authoring/static-target.d.ts +59 -0
  10. package/dist/build/agent-authoring/static-target.js +1857 -0
  11. package/dist/build/agent-authoring.d.ts +9 -0
  12. package/dist/build/agent-authoring.js +5 -0
  13. package/dist/build/build-cli.d.ts +2 -0
  14. package/dist/build/build-cli.js +264 -0
  15. package/dist/check/algorithmic/architecture-checks.mjs +971 -0
  16. package/dist/check/algorithmic/client-boundary-checks.mjs +337 -0
  17. package/dist/check/algorithmic/convergence-smoke-checks.mjs +608 -0
  18. package/dist/check/algorithmic/distribution-checks.mjs +919 -0
  19. package/dist/check/algorithmic/owner-checks.mjs +647 -0
  20. package/dist/check/algorithmic/package-boundary-checks.mjs +985 -0
  21. package/dist/check/algorithmic/projection-boundary-checks.mjs +302 -0
  22. package/dist/check/algorithmic/repo-surface-checks.mjs +267 -0
  23. package/dist/check/algorithmic/runtime-structural-checks.mjs +264 -0
  24. package/dist/check/algorithmic/source-alias-checks.mjs +106 -0
  25. package/dist/check/algorithmic/static-target-checks.mjs +447 -0
  26. package/dist/check/algorithmic-checks.mjs +482 -0
  27. package/dist/check/check-coverage.mjs +231 -0
  28. package/dist/check/command-runner.mjs +22 -0
  29. package/dist/check/default-gate.mjs +51 -0
  30. package/dist/check/gate-selector.mjs +305 -0
  31. package/dist/check/manifest-rules.mjs +223 -0
  32. package/dist/check/package-graph.mjs +464 -0
  33. package/dist/generate/generate-agent-docs.mjs +435 -0
  34. package/dist/generate/generate-carrier-reference.mjs +514 -0
  35. package/dist/generate/generate-docs.mjs +345 -0
  36. package/dist/generate/generate-effect-skill-manifests.mjs +193 -0
  37. package/dist/generate/project-docs-site.mjs +190 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +25 -0
  40. package/dist/lib/agent-docs-model.mjs +888 -0
  41. package/dist/lib/boundary-rules.mjs +63 -0
  42. package/dist/lib/capability-routes.mjs +354 -0
  43. package/dist/lib/projection-sink.mjs +113 -0
  44. package/dist/lib/public-api-model.mjs +306 -0
  45. package/dist/main.mjs +233 -0
  46. package/dist/runner.mjs +127 -0
  47. package/package.json +32 -0
@@ -0,0 +1,223 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { runAlgorithmicChecker } from "./algorithmic-checks.mjs";
5
+ import { runCommand } from "./command-runner.mjs";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
8
+
9
+ const allowedEngines = new Set([
10
+ "text",
11
+ "json",
12
+ "importBoundary",
13
+ "generatedProjection",
14
+ "proofClass",
15
+ "algorithmic",
16
+ ]);
17
+
18
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
19
+ const stringArray = (value) =>
20
+ Array.isArray(value) && value.every((item) => typeof item === "string");
21
+
22
+ const read = (relativePath) => fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
23
+ const readJson = (relativePath) => JSON.parse(read(relativePath));
24
+
25
+ const walkFiles = (relativePath) => {
26
+ const absolutePath = path.join(repoRoot, relativePath);
27
+ if (!fs.existsSync(absolutePath)) return [];
28
+ const stat = fs.statSync(absolutePath);
29
+ if (stat.isFile()) return [relativePath];
30
+ const files = [];
31
+ for (const entry of fs.readdirSync(absolutePath, { withFileTypes: true })) {
32
+ const child = path.join(relativePath, entry.name);
33
+ if (entry.isDirectory()) files.push(...walkFiles(child));
34
+ if (entry.isFile()) files.push(child);
35
+ }
36
+ return files.map((file) => file.split(path.sep).join("/"));
37
+ };
38
+
39
+ const getJsonPointer = (value, pointer) => {
40
+ if (pointer === "" || pointer === "/") return value;
41
+ return pointer
42
+ .split("/")
43
+ .slice(1)
44
+ .reduce((current, segment) => {
45
+ if (current === undefined || current === null) return undefined;
46
+ const key = segment.replaceAll("~1", "/").replaceAll("~0", "~");
47
+ return current[key];
48
+ }, value);
49
+ };
50
+
51
+ const assertProofClasses = (ruleId, proofClasses) => {
52
+ if (!stringArray(proofClasses) || proofClasses.length === 0) {
53
+ throw new Error(`${ruleId}: proofClass acceptance requires non-empty proofClasses`);
54
+ }
55
+ };
56
+
57
+ const collectTextFailures = (rule) => {
58
+ const failures = [];
59
+ for (const assertion of rule.acceptance.assertions ?? []) {
60
+ const content = read(assertion.path);
61
+ for (const token of assertion.contains ?? []) {
62
+ if (!content.includes(token))
63
+ failures.push(`${assertion.path}: missing ${JSON.stringify(token)}`);
64
+ }
65
+ for (const token of assertion.notContains ?? []) {
66
+ if (content.includes(token))
67
+ failures.push(`${assertion.path}: forbidden ${JSON.stringify(token)}`);
68
+ }
69
+ for (const pattern of assertion.matches ?? []) {
70
+ if (!new RegExp(pattern, "u").test(content)) {
71
+ failures.push(`${assertion.path}: missing pattern ${pattern}`);
72
+ }
73
+ }
74
+ for (const pattern of assertion.notMatches ?? []) {
75
+ if (new RegExp(pattern, "u").test(content)) {
76
+ failures.push(`${assertion.path}: forbidden pattern ${pattern}`);
77
+ }
78
+ }
79
+ }
80
+ return failures;
81
+ };
82
+
83
+ const collectJsonFailures = (rule) => {
84
+ const failures = [];
85
+ for (const assertion of rule.acceptance.assertions ?? []) {
86
+ const value = getJsonPointer(readJson(assertion.path), assertion.pointer ?? "/");
87
+ if ("equals" in assertion && JSON.stringify(value) !== JSON.stringify(assertion.equals)) {
88
+ failures.push(
89
+ `${assertion.path}${assertion.pointer ?? ""}: expected ${JSON.stringify(assertion.equals)}`,
90
+ );
91
+ }
92
+ if (assertion.keysExactly !== undefined) {
93
+ const actual = isRecord(value)
94
+ ? Object.keys(value).sort((left, right) => left.localeCompare(right))
95
+ : [];
96
+ const expected = [...assertion.keysExactly].sort((left, right) => left.localeCompare(right));
97
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
98
+ failures.push(
99
+ `${assertion.path}${assertion.pointer ?? ""}: keys must be exactly ${expected.join(", ")}`,
100
+ );
101
+ }
102
+ }
103
+ if (assertion.requiredKeys !== undefined) {
104
+ const actual = isRecord(value) ? new Set(Object.keys(value)) : new Set();
105
+ for (const key of assertion.requiredKeys) {
106
+ if (!actual.has(key))
107
+ failures.push(`${assertion.path}${assertion.pointer ?? ""}: missing key ${key}`);
108
+ }
109
+ }
110
+ }
111
+ return failures;
112
+ };
113
+
114
+ const importSpecifiers = (source) => {
115
+ const specifiers = [];
116
+ const regex =
117
+ /\b(?:import|export)\s+(?:[^"'`]*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/gu;
118
+ for (const match of source.matchAll(regex)) specifiers.push(match[1] ?? match[2]);
119
+ return specifiers;
120
+ };
121
+
122
+ const collectImportBoundaryFailures = (rule) => {
123
+ const failures = [];
124
+ const roots = rule.acceptance.roots ?? [];
125
+ const forbiddenSpecPrefixes = rule.acceptance.forbiddenSpecPrefixes ?? [];
126
+ const forbiddenRelativeRoots = (rule.acceptance.forbiddenRelativeRoots ?? []).map((entry) =>
127
+ path.resolve(repoRoot, entry),
128
+ );
129
+ for (const root of roots) {
130
+ for (const file of walkFiles(root).filter((entry) => /\.(?:mjs|js|ts|tsx)$/u.test(entry))) {
131
+ const source = read(file);
132
+ for (const specifier of importSpecifiers(source)) {
133
+ if (forbiddenSpecPrefixes.some((prefix) => specifier.startsWith(prefix))) {
134
+ failures.push(`${file}: forbidden package import ${specifier}`);
135
+ }
136
+ if (specifier.startsWith(".")) {
137
+ const resolved = path.resolve(path.dirname(path.join(repoRoot, file)), specifier);
138
+ if (
139
+ forbiddenRelativeRoots.some(
140
+ (rootPath) => resolved === rootPath || resolved.startsWith(`${rootPath}${path.sep}`),
141
+ )
142
+ ) {
143
+ failures.push(`${file}: forbidden relative import ${specifier}`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return failures;
150
+ };
151
+
152
+ export const validateRuleAcceptance = (rule, failures) => {
153
+ if (!isRecord(rule.acceptance)) {
154
+ failures.push(`${rule.id}: missing acceptance`);
155
+ return;
156
+ }
157
+ if (!allowedEngines.has(rule.acceptance.engine)) {
158
+ failures.push(`${rule.id}: acceptance.engine must be one of ${[...allowedEngines].join(", ")}`);
159
+ }
160
+ if (rule.acceptance.engine === "proofClass") {
161
+ try {
162
+ assertProofClasses(rule.id, rule.acceptance.proofClasses);
163
+ } catch (error) {
164
+ failures.push(error instanceof Error ? error.message : String(error));
165
+ }
166
+ }
167
+ if (rule.acceptance.engine === "algorithmic") {
168
+ if (typeof rule.acceptance.checker !== "string" || rule.acceptance.checker.length === 0) {
169
+ failures.push(`${rule.id}: algorithmic acceptance requires checker`);
170
+ }
171
+ if (typeof rule.acceptance.reason !== "string" || rule.acceptance.reason.length === 0) {
172
+ failures.push(`${rule.id}: algorithmic acceptance requires reason`);
173
+ }
174
+ if (rule.acceptance.packageCommands !== undefined) {
175
+ failures.push(`${rule.id}: algorithmic acceptance must not execute packageCommands`);
176
+ }
177
+ }
178
+ if (rule.acceptance.engine === "generatedProjection") {
179
+ if (
180
+ typeof rule.acceptance.command !== "string" ||
181
+ !/\s--check(?:\s|$)/u.test(rule.acceptance.command)
182
+ ) {
183
+ failures.push(`${rule.id}: generatedProjection acceptance requires a --check command`);
184
+ }
185
+ }
186
+ };
187
+
188
+ export const runRuleAcceptance = async (rule) => {
189
+ switch (rule.acceptance.engine) {
190
+ case "proofClass":
191
+ assertProofClasses(rule.id, rule.acceptance.proofClasses);
192
+ return;
193
+ case "algorithmic":
194
+ await runAlgorithmicChecker(rule.acceptance.checker);
195
+ return;
196
+ case "text": {
197
+ const failures = collectTextFailures(rule);
198
+ if (failures.length > 0) throw new Error(failures.join("\n"));
199
+ return;
200
+ }
201
+ case "json": {
202
+ const failures = collectJsonFailures(rule);
203
+ if (failures.length > 0) throw new Error(failures.join("\n"));
204
+ return;
205
+ }
206
+ case "importBoundary": {
207
+ const failures = collectImportBoundaryFailures(rule);
208
+ if (failures.length > 0) throw new Error(failures.join("\n"));
209
+ return;
210
+ }
211
+ case "generatedProjection":
212
+ if (
213
+ typeof rule.acceptance.command !== "string" ||
214
+ !/\s--check(?:\s|$)/u.test(rule.acceptance.command)
215
+ ) {
216
+ throw new Error(`${rule.id}: generatedProjection acceptance requires a --check command`);
217
+ }
218
+ runCommand(rule.acceptance.command, { cwd: repoRoot });
219
+ return;
220
+ default:
221
+ throw new Error(`${rule.id}: unsupported acceptance engine ${rule.acceptance.engine}`);
222
+ }
223
+ };
@@ -0,0 +1,464 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+
5
+ const compare = (left, right) => left.localeCompare(right);
6
+ const readJsonFile = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
7
+ const sourceModuleFilePattern = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/u;
8
+ const internalPackageNamePattern = /^@agent-os\/[^/]+/u;
9
+
10
+ export const walkFiles = (repoRoot, relativePath, options = {}) => {
11
+ const absolutePath = path.join(repoRoot, relativePath);
12
+ if (!fs.existsSync(absolutePath)) return [];
13
+ const stat = fs.statSync(absolutePath);
14
+ if (stat.isFile()) return [relativePath];
15
+ const ignored = options.ignored ?? new Set(["node_modules", "dist", ".wrangler", ".turbo"]);
16
+ const files = [];
17
+ for (const entry of fs.readdirSync(absolutePath, { withFileTypes: true })) {
18
+ if (entry.isDirectory() && ignored.has(entry.name)) continue;
19
+ const child = path.join(relativePath, entry.name);
20
+ if (entry.isDirectory()) files.push(...walkFiles(repoRoot, child, options));
21
+ if (entry.isFile()) files.push(child.split(path.sep).join("/"));
22
+ }
23
+ return files.sort(compare);
24
+ };
25
+
26
+ export const workspacePackageRecords = (repoRoot) => {
27
+ const rootPackage = readJsonFile(path.join(repoRoot, "package.json"));
28
+ const workspaces = Array.isArray(rootPackage.workspaces)
29
+ ? rootPackage.workspaces
30
+ : Array.isArray(rootPackage.workspaces?.packages)
31
+ ? rootPackage.workspaces.packages
32
+ : [];
33
+ const records = [];
34
+
35
+ for (const workspace of workspaces) {
36
+ if (typeof workspace !== "string") continue;
37
+ if (workspace.endsWith("/*")) {
38
+ const base = workspace.slice(0, -2);
39
+ const baseDir = path.join(repoRoot, base);
40
+ if (!fs.existsSync(baseDir)) continue;
41
+ for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
42
+ if (!entry.isDirectory()) continue;
43
+ const packagePath = `${base}/${entry.name}`;
44
+ const packageJsonPath = path.join(repoRoot, packagePath, "package.json");
45
+ if (!fs.existsSync(packageJsonPath)) continue;
46
+ records.push({ name: readJsonFile(packageJsonPath).name, path: packagePath });
47
+ }
48
+ continue;
49
+ }
50
+
51
+ const packageJsonPath = path.join(repoRoot, workspace, "package.json");
52
+ if (!fs.existsSync(packageJsonPath)) continue;
53
+ records.push({ name: readJsonFile(packageJsonPath).name, path: workspace });
54
+ }
55
+
56
+ return records.sort((left, right) => left.path.localeCompare(right.path));
57
+ };
58
+
59
+ const scriptKindForFile = (fileName) => {
60
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
61
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
62
+ if (fileName.endsWith(".js") || fileName.endsWith(".mjs") || fileName.endsWith(".cjs")) {
63
+ return ts.ScriptKind.JS;
64
+ }
65
+ return ts.ScriptKind.TS;
66
+ };
67
+
68
+ const moduleSpecifierPosition = (sourceFile, node) => {
69
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
70
+ return { line: position.line + 1, column: position.character + 1 };
71
+ };
72
+
73
+ const pushImportRecord = (sourceFile, records, node, specifier, importKind, syntaxKind) => {
74
+ records.push({
75
+ specifier,
76
+ importKind,
77
+ syntaxKind,
78
+ ...moduleSpecifierPosition(sourceFile, node),
79
+ });
80
+ };
81
+
82
+ export const importSpecifierRecords = (content, fileName = "agentos-check.ts") => {
83
+ const sourceFile = ts.createSourceFile(
84
+ fileName,
85
+ content,
86
+ ts.ScriptTarget.Latest,
87
+ true,
88
+ scriptKindForFile(fileName),
89
+ );
90
+ const records = [];
91
+ const visit = (node) => {
92
+ if (
93
+ ts.isImportDeclaration(node) &&
94
+ node.moduleSpecifier !== undefined &&
95
+ ts.isStringLiteralLike(node.moduleSpecifier)
96
+ ) {
97
+ pushImportRecord(
98
+ sourceFile,
99
+ records,
100
+ node.moduleSpecifier,
101
+ node.moduleSpecifier.text,
102
+ node.importClause?.isTypeOnly === true ? "type" : "value",
103
+ "import",
104
+ );
105
+ }
106
+ if (
107
+ ts.isExportDeclaration(node) &&
108
+ node.moduleSpecifier !== undefined &&
109
+ ts.isStringLiteralLike(node.moduleSpecifier)
110
+ ) {
111
+ pushImportRecord(
112
+ sourceFile,
113
+ records,
114
+ node.moduleSpecifier,
115
+ node.moduleSpecifier.text,
116
+ node.isTypeOnly ? "type" : "export",
117
+ "export",
118
+ );
119
+ }
120
+ if (
121
+ ts.isCallExpression(node) &&
122
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
123
+ node.arguments.length === 1 &&
124
+ ts.isStringLiteralLike(node.arguments[0])
125
+ ) {
126
+ pushImportRecord(
127
+ sourceFile,
128
+ records,
129
+ node.arguments[0],
130
+ node.arguments[0].text,
131
+ "dynamic",
132
+ "dynamic-import",
133
+ );
134
+ }
135
+ if (
136
+ ts.isImportEqualsDeclaration(node) &&
137
+ ts.isExternalModuleReference(node.moduleReference) &&
138
+ ts.isStringLiteralLike(node.moduleReference.expression)
139
+ ) {
140
+ pushImportRecord(
141
+ sourceFile,
142
+ records,
143
+ node.moduleReference.expression,
144
+ node.moduleReference.expression.text,
145
+ node.isTypeOnly ? "type" : "value",
146
+ "import-equals",
147
+ );
148
+ }
149
+ if (
150
+ ts.isImportTypeNode(node) &&
151
+ ts.isLiteralTypeNode(node.argument) &&
152
+ ts.isStringLiteralLike(node.argument.literal)
153
+ ) {
154
+ pushImportRecord(
155
+ sourceFile,
156
+ records,
157
+ node.argument.literal,
158
+ node.argument.literal.text,
159
+ "type",
160
+ "import-type",
161
+ );
162
+ }
163
+ ts.forEachChild(node, visit);
164
+ };
165
+ visit(sourceFile);
166
+ return records;
167
+ };
168
+
169
+ export const importSpecifiers = (content, fileName = "agentos-check.ts") =>
170
+ importSpecifierRecords(content, fileName).map((record) => record.specifier);
171
+
172
+ export const packageFromInternalSpecifier = (recordsByName, specifier) => {
173
+ if (!specifier.startsWith("@agent-os/")) return undefined;
174
+ const [scope, name] = specifier.split("/");
175
+ if (scope !== "@agent-os" || name === undefined) return undefined;
176
+ return recordsByName.get(`${scope}/${name}`);
177
+ };
178
+
179
+ const toRepoPath = (repoRoot, absolutePath) =>
180
+ path.relative(repoRoot, absolutePath).split(path.sep).join("/");
181
+
182
+ const isInsideRepo = (repoRoot, absolutePath) => {
183
+ const relativePath = path.relative(repoRoot, absolutePath);
184
+ return (
185
+ relativePath.length > 0 && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)
186
+ );
187
+ };
188
+
189
+ export const owningPackageForFile = (records, file) =>
190
+ records
191
+ .filter((record) => file === record.path || file.startsWith(`${record.path}/`))
192
+ .sort((left, right) => right.path.length - left.path.length)[0];
193
+
194
+ export const packageSourceFiles = (repoRoot, record) =>
195
+ walkFiles(repoRoot, `${record.path}/src`).filter((entry) => sourceModuleFilePattern.test(entry));
196
+
197
+ const diagnosticText = (diagnostic) =>
198
+ ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
199
+
200
+ const fallbackCompilerOptions = (repoRoot) => {
201
+ const config = readJsonFile(path.join(repoRoot, "tsconfig.source-paths.json"));
202
+ const converted = ts.convertCompilerOptionsFromJson(
203
+ {
204
+ ...config.compilerOptions,
205
+ allowJs: true,
206
+ module: "ESNext",
207
+ moduleResolution: "Bundler",
208
+ resolveJsonModule: true,
209
+ target: "ES2022",
210
+ },
211
+ repoRoot,
212
+ "tsconfig.source-paths.json",
213
+ );
214
+ if (converted.errors.length > 0) {
215
+ throw new Error(converted.errors.map(diagnosticText).join("\n"));
216
+ }
217
+ return converted.options;
218
+ };
219
+
220
+ const compilerOptionsLoader = (repoRoot) => {
221
+ const byConfig = new Map();
222
+ const fallback = () => fallbackCompilerOptions(repoRoot);
223
+ return (sourceFile) => {
224
+ const absoluteSourceFile = path.join(repoRoot, sourceFile);
225
+ const configPath = ts.findConfigFile(path.dirname(absoluteSourceFile), (file) =>
226
+ ts.sys.fileExists(file),
227
+ );
228
+ if (configPath === undefined) return fallback();
229
+ const cached = byConfig.get(configPath);
230
+ if (cached !== undefined) return cached;
231
+ const parsed = ts.getParsedCommandLineOfConfigFile(
232
+ configPath,
233
+ { allowJs: true, resolveJsonModule: true },
234
+ {
235
+ ...ts.sys,
236
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
237
+ throw new Error(diagnosticText(diagnostic));
238
+ },
239
+ },
240
+ );
241
+ if (parsed === undefined) throw new Error(`${toRepoPath(repoRoot, configPath)} did not parse`);
242
+ if (parsed.errors.length > 0) {
243
+ throw new Error(parsed.errors.map(diagnosticText).join("\n"));
244
+ }
245
+ byConfig.set(configPath, parsed.options);
246
+ return parsed.options;
247
+ };
248
+ };
249
+
250
+ export const resolveModuleSpecifier = (repoRoot, fromFile, specifier, compilerOptions) => {
251
+ const resolved = ts.resolveModuleName(
252
+ specifier,
253
+ path.join(repoRoot, fromFile),
254
+ compilerOptions,
255
+ ts.sys,
256
+ ).resolvedModule;
257
+ if (resolved === undefined) return undefined;
258
+ const resolvedFileName = path.resolve(resolved.resolvedFileName);
259
+ if (!isInsideRepo(repoRoot, resolvedFileName)) return undefined;
260
+ return toRepoPath(repoRoot, resolvedFileName);
261
+ };
262
+
263
+ const edgeKey = (edge) => `${edge.fromFile}\0${edge.specifier}\0${edge.toFile}\0${edge.importKind}`;
264
+
265
+ export const sourceModuleImportEdges = (repoRoot, records) => {
266
+ const compilerOptionsForFile = compilerOptionsLoader(repoRoot);
267
+ const edges = [];
268
+ const seen = new Set();
269
+ for (const from of records) {
270
+ for (const file of packageSourceFiles(repoRoot, from)) {
271
+ const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
272
+ const compilerOptions = compilerOptionsForFile(file);
273
+ for (const importRecord of importSpecifierRecords(source, file)) {
274
+ const toFile = resolveModuleSpecifier(
275
+ repoRoot,
276
+ file,
277
+ importRecord.specifier,
278
+ compilerOptions,
279
+ );
280
+ if (toFile === undefined) continue;
281
+ const to = owningPackageForFile(records, toFile);
282
+ if (to === undefined) continue;
283
+ const edge = {
284
+ from,
285
+ to,
286
+ fromFile: file,
287
+ toFile,
288
+ file,
289
+ specifier: importRecord.specifier,
290
+ importKind: importRecord.importKind,
291
+ syntaxKind: importRecord.syntaxKind,
292
+ line: importRecord.line,
293
+ column: importRecord.column,
294
+ source: "source-module-import",
295
+ };
296
+ const key = edgeKey(edge);
297
+ if (seen.has(key)) continue;
298
+ seen.add(key);
299
+ edges.push(edge);
300
+ }
301
+ }
302
+ }
303
+ return edges.sort(
304
+ (left, right) =>
305
+ compare(left.fromFile, right.fromFile) ||
306
+ compare(left.specifier, right.specifier) ||
307
+ compare(left.toFile, right.toFile) ||
308
+ compare(left.importKind, right.importKind),
309
+ );
310
+ };
311
+
312
+ export const sourceModuleGraph = (repoRoot, records) => ({
313
+ packages: records,
314
+ files: records.flatMap((record) =>
315
+ packageSourceFiles(repoRoot, record).map((file) => ({ package: record, file })),
316
+ ),
317
+ edges: sourceModuleImportEdges(repoRoot, records),
318
+ });
319
+
320
+ export const moduleGraphOracleFailures = (repoRoot, records) => {
321
+ const graphEdges = sourceModuleImportEdges(repoRoot, records);
322
+ const observed = new Set(graphEdges.map(edgeKey));
323
+ const compilerOptionsForFile = compilerOptionsLoader(repoRoot);
324
+ const failures = [];
325
+ for (const from of records) {
326
+ for (const file of packageSourceFiles(repoRoot, from)) {
327
+ const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
328
+ const compilerOptions = compilerOptionsForFile(file);
329
+ for (const importRecord of importSpecifierRecords(source, file)) {
330
+ const toFile = resolveModuleSpecifier(
331
+ repoRoot,
332
+ file,
333
+ importRecord.specifier,
334
+ compilerOptions,
335
+ );
336
+ if (toFile === undefined) continue;
337
+ const to = owningPackageForFile(records, toFile);
338
+ if (to === undefined) continue;
339
+ const expectedKey = edgeKey({
340
+ fromFile: file,
341
+ specifier: importRecord.specifier,
342
+ toFile,
343
+ importKind: importRecord.importKind,
344
+ });
345
+ if (!observed.has(expectedKey)) {
346
+ failures.push(
347
+ `${file}:${importRecord.line}:${importRecord.column}: module graph missed TypeScript-resolved ${importRecord.importKind} edge ${importRecord.specifier} -> ${toFile}`,
348
+ );
349
+ }
350
+ }
351
+ }
352
+ }
353
+ if (!graphEdges.some((edge) => edge.from.name === edge.to.name)) {
354
+ failures.push("module graph must retain at least one same-package edge");
355
+ }
356
+ if (!graphEdges.some((edge) => edge.importKind === "type")) {
357
+ failures.push("module graph must retain type-only edges");
358
+ }
359
+ if (!graphEdges.some((edge) => edge.syntaxKind === "export")) {
360
+ failures.push("module graph must retain re-export edges");
361
+ }
362
+ if (!graphEdges.some((edge) => internalPackageNamePattern.test(edge.specifier))) {
363
+ failures.push("module graph must retain @agent-os alias/subpath edges");
364
+ }
365
+ return failures;
366
+ };
367
+
368
+ export const packageSourceImportEdges = (repoRoot, records) => {
369
+ const seen = new Set();
370
+ return sourceModuleImportEdges(repoRoot, records)
371
+ .filter((edge) => edge.to.name !== edge.from.name)
372
+ .map((edge) => ({
373
+ from: edge.from,
374
+ to: edge.to,
375
+ source: "source-import",
376
+ file: edge.fromFile,
377
+ specifier: edge.specifier,
378
+ importKind: edge.importKind,
379
+ syntaxKind: edge.syntaxKind,
380
+ toFile: edge.toFile,
381
+ }))
382
+ .filter((edge) => {
383
+ const key = `${edge.from.name}\0${edge.to.name}\0${edge.file}\0${edge.specifier}`;
384
+ if (seen.has(key)) return false;
385
+ seen.add(key);
386
+ return true;
387
+ });
388
+ };
389
+
390
+ export const packageManifestDependencyEdges = (repoRoot, records) => {
391
+ const recordsByName = new Map(records.map((record) => [record.name, record]));
392
+ const edges = [];
393
+ for (const from of records) {
394
+ const manifest = readJsonFile(path.join(repoRoot, from.path, "package.json"));
395
+ for (const field of [
396
+ "dependencies",
397
+ "devDependencies",
398
+ "peerDependencies",
399
+ "optionalDependencies",
400
+ ]) {
401
+ for (const name of Object.keys(manifest[field] ?? {})) {
402
+ const to = recordsByName.get(name);
403
+ if (to !== undefined && to.name !== from.name) {
404
+ edges.push({
405
+ from,
406
+ to,
407
+ source: `package-json:${field}`,
408
+ file: `${from.path}/package.json`,
409
+ });
410
+ }
411
+ }
412
+ }
413
+ }
414
+ return edges;
415
+ };
416
+
417
+ export const tsconfigReferenceEdges = (repoRoot, records) => {
418
+ const recordsByPath = new Map(records.map((record) => [record.path, record]));
419
+ const edges = [];
420
+ for (const from of records) {
421
+ const tsconfigPath = path.join(repoRoot, from.path, "tsconfig.json");
422
+ if (!fs.existsSync(tsconfigPath)) continue;
423
+ const tsconfig = readJsonFile(tsconfigPath);
424
+ for (const reference of tsconfig.references ?? []) {
425
+ if (typeof reference?.path !== "string") continue;
426
+ const targetPath = path
427
+ .relative(repoRoot, path.resolve(repoRoot, from.path, reference.path))
428
+ .split(path.sep)
429
+ .join("/");
430
+ const to = recordsByPath.get(targetPath);
431
+ if (to !== undefined && to.name !== from.name) {
432
+ edges.push({ from, to, source: "tsconfig-reference", file: `${from.path}/tsconfig.json` });
433
+ }
434
+ }
435
+ }
436
+ return edges;
437
+ };
438
+
439
+ export const packageImportCycles = (records, edges) => {
440
+ const graph = new Map(records.map((record) => [record.name, []]));
441
+ for (const edge of edges) graph.get(edge.from.name)?.push(edge.to.name);
442
+ for (const targets of graph.values()) targets.sort(compare);
443
+
444
+ const visiting = new Set();
445
+ const visited = new Set();
446
+ const stack = [];
447
+ const cycles = [];
448
+ const visit = (name) => {
449
+ if (visited.has(name)) return;
450
+ if (visiting.has(name)) {
451
+ const index = stack.indexOf(name);
452
+ cycles.push([...stack.slice(index), name]);
453
+ return;
454
+ }
455
+ visiting.add(name);
456
+ stack.push(name);
457
+ for (const target of graph.get(name) ?? []) visit(target);
458
+ stack.pop();
459
+ visiting.delete(name);
460
+ visited.add(name);
461
+ };
462
+ for (const record of records) visit(record.name);
463
+ return cycles;
464
+ };