@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,971 @@
1
+ export const createArchitectureChecks = ({
2
+ fs,
3
+ path,
4
+ graphWorkspacePackageRecords,
5
+ sourceModuleGraph,
6
+ importSpecifierRecords,
7
+ repoRoot,
8
+ read,
9
+ readJson,
10
+ walk,
11
+ compare,
12
+ isRecord,
13
+ failIfAny,
14
+ ownerIdRegistry,
15
+ ownerIdRegistryFindings,
16
+ packageExportSubpaths,
17
+ }) => {
18
+ const moduleBucketRegistryPath = "architecture/module-buckets.json";
19
+ let moduleBucketRegistryCache;
20
+ const moduleBucketRegistry = () => {
21
+ moduleBucketRegistryCache ??= readJson(moduleBucketRegistryPath);
22
+ return moduleBucketRegistryCache;
23
+ };
24
+
25
+ const pathRuleMatches = (file, rule) => {
26
+ if (!isRecord(rule) || typeof rule.match !== "string") return false;
27
+ if (rule.match === "all") return true;
28
+ if (typeof rule.value !== "string") return false;
29
+ if (rule.match === "prefix") return file.startsWith(rule.value);
30
+ if (rule.match === "contains") return file.includes(rule.value);
31
+ if (rule.match === "suffix") return file.endsWith(rule.value);
32
+ if (rule.match === "regex") return new RegExp(rule.value, "u").test(file);
33
+ return false;
34
+ };
35
+
36
+ const specifierRuleMatches = (specifier, rule) => {
37
+ if (!isRecord(rule) || typeof rule.match !== "string" || typeof rule.value !== "string") {
38
+ return false;
39
+ }
40
+ if (rule.match === "specifier") return specifier === rule.value;
41
+ if (rule.match === "prefix") return specifier.startsWith(rule.value);
42
+ if (rule.match === "specifier-or-subpath") {
43
+ return specifier === rule.value || specifier.startsWith(`${rule.value}/`);
44
+ }
45
+ return false;
46
+ };
47
+
48
+ const moduleRuleClassification = (file, rules, property) => {
49
+ for (const rule of rules) {
50
+ if (pathRuleMatches(file, rule)) return rule[property];
51
+ }
52
+ throw new Error(`${moduleBucketRegistryPath}: no ${property} rule matches ${file}`);
53
+ };
54
+
55
+ const moduleBucketForPath = (file) =>
56
+ moduleRuleClassification(file, moduleBucketRegistry().bucketRules, "bucket");
57
+
58
+ const moduleAmbientForPath = (file) =>
59
+ moduleRuleClassification(file, moduleBucketRegistry().ambientRules, "ambient");
60
+
61
+ const moduleBucketRank = () =>
62
+ new Map(moduleBucketRegistry().buckets.map((bucket) => [bucket.id, bucket.rank]));
63
+
64
+ const allowedAmbientImports = () =>
65
+ new Map(
66
+ moduleBucketRegistry().ambients.map((ambient) => [
67
+ ambient.id,
68
+ new Set(ambient.allowedImports),
69
+ ]),
70
+ );
71
+
72
+ const ejectionBuckets = () =>
73
+ new Set(
74
+ moduleBucketRegistry()
75
+ .buckets.filter((bucket) => bucket.ejection === true)
76
+ .map((bucket) => bucket.id),
77
+ );
78
+
79
+ const externalAmbientForSpecifier = (specifier) => {
80
+ for (const rule of moduleBucketRegistry().externalAmbients) {
81
+ if (specifierRuleMatches(specifier, rule)) return rule.ambient;
82
+ }
83
+ return undefined;
84
+ };
85
+
86
+ const modulePathRuleMatchKinds = new Set(["all", "prefix", "contains", "suffix", "regex"]);
87
+ const moduleSpecifierRuleMatchKinds = new Set(["specifier", "prefix", "specifier-or-subpath"]);
88
+ const moduleBucketFindingKinds = new Set([
89
+ "bucket-dag",
90
+ "ambient-dag",
91
+ "external-ambient",
92
+ "product-ejection",
93
+ ]);
94
+
95
+ const stringArray = (value) =>
96
+ Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
97
+
98
+ const validateModulePathRules = ({ label, rules, property, allowedValues, findings }) => {
99
+ if (!Array.isArray(rules) || rules.length === 0) {
100
+ findings.push(`${moduleBucketRegistryPath}:${label}: must be a non-empty array`);
101
+ return;
102
+ }
103
+ const seen = new Set();
104
+ let hasCatchAll = false;
105
+ for (const [index, rule] of rules.entries()) {
106
+ const ruleLabel = `${moduleBucketRegistryPath}:${label}[${index}]`;
107
+ if (!isRecord(rule)) {
108
+ findings.push(`${ruleLabel}: rule must be an object`);
109
+ continue;
110
+ }
111
+ if (typeof rule.id !== "string" || rule.id.length === 0) {
112
+ findings.push(`${ruleLabel}: id must be a non-empty string`);
113
+ } else if (seen.has(rule.id)) {
114
+ findings.push(`${ruleLabel}: duplicate id ${rule.id}`);
115
+ } else {
116
+ seen.add(rule.id);
117
+ }
118
+ if (!modulePathRuleMatchKinds.has(rule.match)) {
119
+ findings.push(
120
+ `${ruleLabel}: match must be one of ${[...modulePathRuleMatchKinds].join(", ")}`,
121
+ );
122
+ }
123
+ if (rule.match === "all") {
124
+ hasCatchAll = true;
125
+ } else if (typeof rule.value !== "string" || rule.value.length === 0) {
126
+ findings.push(`${ruleLabel}: value must be a non-empty string`);
127
+ }
128
+ if (rule.match === "regex" && typeof rule.value === "string") {
129
+ try {
130
+ new RegExp(rule.value, "u");
131
+ } catch (error) {
132
+ findings.push(`${ruleLabel}: regex is invalid: ${error.message}`);
133
+ }
134
+ }
135
+ if (typeof rule[property] !== "string" || !allowedValues.has(rule[property])) {
136
+ findings.push(`${ruleLabel}: ${property} must reference a declared ${property}`);
137
+ }
138
+ }
139
+ if (!hasCatchAll) {
140
+ findings.push(`${moduleBucketRegistryPath}:${label}: final catch-all rule is required`);
141
+ }
142
+ };
143
+
144
+ const moduleBucketRegistryFindings = (registry) => {
145
+ const findings = [];
146
+ if (!isRecord(registry)) return [`${moduleBucketRegistryPath}: registry must be a JSON object`];
147
+ if (registry.schemaVersion !== 1) {
148
+ findings.push(`${moduleBucketRegistryPath}: schemaVersion must be 1`);
149
+ }
150
+ if (!isRecord(registry.policy)) {
151
+ findings.push(`${moduleBucketRegistryPath}: policy object is required`);
152
+ } else {
153
+ for (const key of ["classification", "ambientIsolation", "productBucket"]) {
154
+ if (typeof registry.policy[key] !== "string" || registry.policy[key].length === 0) {
155
+ findings.push(`${moduleBucketRegistryPath}: policy.${key} must be a non-empty string`);
156
+ }
157
+ }
158
+ }
159
+ if (!isRecord(registry.productEjection)) {
160
+ findings.push(`${moduleBucketRegistryPath}: productEjection object is required`);
161
+ } else {
162
+ if (!stringArray(registry.productEjection.packagePathPrefixes)) {
163
+ findings.push(
164
+ `${moduleBucketRegistryPath}: productEjection.packagePathPrefixes must be a non-empty string array`,
165
+ );
166
+ }
167
+ if (
168
+ typeof registry.productEjection.reason !== "string" ||
169
+ registry.productEjection.reason.length === 0
170
+ ) {
171
+ findings.push(
172
+ `${moduleBucketRegistryPath}: productEjection.reason must be a non-empty string`,
173
+ );
174
+ }
175
+ }
176
+
177
+ const bucketIds = new Set();
178
+ if (!Array.isArray(registry.buckets) || registry.buckets.length === 0) {
179
+ findings.push(`${moduleBucketRegistryPath}: buckets must be a non-empty array`);
180
+ } else {
181
+ const ranks = new Set();
182
+ for (const [index, bucket] of registry.buckets.entries()) {
183
+ const label = `${moduleBucketRegistryPath}:buckets[${index}]`;
184
+ if (!isRecord(bucket)) {
185
+ findings.push(`${label}: bucket must be an object`);
186
+ continue;
187
+ }
188
+ if (typeof bucket.id !== "string" || bucket.id.length === 0) {
189
+ findings.push(`${label}: id must be a non-empty string`);
190
+ } else if (bucketIds.has(bucket.id)) {
191
+ findings.push(`${label}: duplicate id ${bucket.id}`);
192
+ } else {
193
+ bucketIds.add(bucket.id);
194
+ }
195
+ if (!Number.isInteger(bucket.rank) || bucket.rank < 0) {
196
+ findings.push(`${label}: rank must be a non-negative integer`);
197
+ } else if (ranks.has(bucket.rank)) {
198
+ findings.push(`${label}: duplicate rank ${bucket.rank}`);
199
+ } else {
200
+ ranks.add(bucket.rank);
201
+ }
202
+ if (typeof bucket.description !== "string" || bucket.description.length === 0) {
203
+ findings.push(`${label}: description must be a non-empty string`);
204
+ }
205
+ if ("ejection" in bucket && typeof bucket.ejection !== "boolean") {
206
+ findings.push(`${label}: ejection must be boolean when present`);
207
+ }
208
+ }
209
+ }
210
+
211
+ const ambientIds = new Set();
212
+ if (!Array.isArray(registry.ambients) || registry.ambients.length === 0) {
213
+ findings.push(`${moduleBucketRegistryPath}: ambients must be a non-empty array`);
214
+ } else {
215
+ for (const [index, ambient] of registry.ambients.entries()) {
216
+ const label = `${moduleBucketRegistryPath}:ambients[${index}]`;
217
+ if (!isRecord(ambient)) {
218
+ findings.push(`${label}: ambient must be an object`);
219
+ continue;
220
+ }
221
+ if (typeof ambient.id !== "string" || ambient.id.length === 0) {
222
+ findings.push(`${label}: id must be a non-empty string`);
223
+ } else if (ambientIds.has(ambient.id)) {
224
+ findings.push(`${label}: duplicate id ${ambient.id}`);
225
+ } else {
226
+ ambientIds.add(ambient.id);
227
+ }
228
+ if (!stringArray(ambient.allowedImports)) {
229
+ findings.push(`${label}: allowedImports must be a non-empty string array`);
230
+ }
231
+ }
232
+ for (const [index, ambient] of registry.ambients.entries()) {
233
+ if (!isRecord(ambient) || !Array.isArray(ambient.allowedImports)) continue;
234
+ for (const target of ambient.allowedImports) {
235
+ if (!ambientIds.has(target)) {
236
+ findings.push(
237
+ `${moduleBucketRegistryPath}:ambients[${index}]: allowedImports references unknown ambient ${target}`,
238
+ );
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ validateModulePathRules({
245
+ label: "bucketRules",
246
+ rules: registry.bucketRules,
247
+ property: "bucket",
248
+ allowedValues: bucketIds,
249
+ findings,
250
+ });
251
+ validateModulePathRules({
252
+ label: "ambientRules",
253
+ rules: registry.ambientRules,
254
+ property: "ambient",
255
+ allowedValues: ambientIds,
256
+ findings,
257
+ });
258
+
259
+ if (!Array.isArray(registry.externalAmbients)) {
260
+ findings.push(`${moduleBucketRegistryPath}: externalAmbients must be an array`);
261
+ } else {
262
+ const seen = new Set();
263
+ for (const [index, rule] of registry.externalAmbients.entries()) {
264
+ const label = `${moduleBucketRegistryPath}:externalAmbients[${index}]`;
265
+ if (!isRecord(rule)) {
266
+ findings.push(`${label}: rule must be an object`);
267
+ continue;
268
+ }
269
+ if (typeof rule.id !== "string" || rule.id.length === 0) {
270
+ findings.push(`${label}: id must be a non-empty string`);
271
+ } else if (seen.has(rule.id)) {
272
+ findings.push(`${label}: duplicate id ${rule.id}`);
273
+ } else {
274
+ seen.add(rule.id);
275
+ }
276
+ if (!moduleSpecifierRuleMatchKinds.has(rule.match)) {
277
+ findings.push(
278
+ `${label}: match must be one of ${[...moduleSpecifierRuleMatchKinds].join(", ")}`,
279
+ );
280
+ }
281
+ if (typeof rule.value !== "string" || rule.value.length === 0) {
282
+ findings.push(`${label}: value must be a non-empty string`);
283
+ }
284
+ if (typeof rule.ambient !== "string" || !ambientIds.has(rule.ambient)) {
285
+ findings.push(`${label}: ambient must reference a declared ambient`);
286
+ }
287
+ }
288
+ }
289
+
290
+ if (!isRecord(registry.reportMode)) {
291
+ findings.push(`${moduleBucketRegistryPath}: reportMode object is required`);
292
+ } else {
293
+ if (
294
+ typeof registry.reportMode.enforcement !== "string" ||
295
+ registry.reportMode.enforcement.length === 0
296
+ ) {
297
+ findings.push(
298
+ `${moduleBucketRegistryPath}: reportMode.enforcement must be a non-empty string`,
299
+ );
300
+ }
301
+ if (!stringArray(registry.reportMode.findingKinds)) {
302
+ findings.push(
303
+ `${moduleBucketRegistryPath}: reportMode.findingKinds must be a non-empty string array`,
304
+ );
305
+ } else {
306
+ for (const kind of registry.reportMode.findingKinds) {
307
+ if (!moduleBucketFindingKinds.has(kind)) {
308
+ findings.push(
309
+ `${moduleBucketRegistryPath}: reportMode.findingKinds contains unknown kind ${kind}`,
310
+ );
311
+ }
312
+ }
313
+ }
314
+ }
315
+ return findings;
316
+ };
317
+
318
+ const distributionRootsRegistryPath = "architecture/distribution-roots.json";
319
+ const packageUnitsRegistryPath = "architecture/package-units.json";
320
+
321
+ const architectureStringRecordArray = (value) =>
322
+ Array.isArray(value) && value.every((entry) => isRecord(entry));
323
+
324
+ const validateStringRefs = ({ label, values, allowed, noun, findings }) => {
325
+ if (!stringArray(values)) {
326
+ findings.push(`${label}: must be a non-empty string array`);
327
+ return;
328
+ }
329
+ for (const value of values) {
330
+ if (!allowed.has(value)) findings.push(`${label}: unknown ${noun} ${value}`);
331
+ }
332
+ };
333
+
334
+ const validatePeerEntries = ({ label, peers, findings }) => {
335
+ if (!Array.isArray(peers)) {
336
+ findings.push(`${label}: requiredPeers must be an array`);
337
+ return;
338
+ }
339
+ for (const [index, peer] of peers.entries()) {
340
+ const peerLabel = `${label}.requiredPeers[${index}]`;
341
+ if (!isRecord(peer)) {
342
+ findings.push(`${peerLabel}: peer must be an object`);
343
+ continue;
344
+ }
345
+ if (typeof peer.name !== "string" || peer.name.length === 0) {
346
+ findings.push(`${peerLabel}: name must be a non-empty string`);
347
+ }
348
+ if (typeof peer.range !== "string" || peer.range.length === 0) {
349
+ findings.push(`${peerLabel}: range must be a non-empty string`);
350
+ }
351
+ }
352
+ };
353
+
354
+ const expectedPublicPackageNameForSource = (sourcePackageName) =>
355
+ typeof sourcePackageName === "string" && sourcePackageName.startsWith("@agent-os/")
356
+ ? `@yansirplus/${sourcePackageName.slice("@agent-os/".length)}`
357
+ : undefined;
358
+
359
+ const packageUnitsRegistryFindings = ({
360
+ registry,
361
+ bucketIds,
362
+ ambientIds,
363
+ targetProfileIds = new Set(),
364
+ workspacePackageRecordsByName = new Map(),
365
+ }) => {
366
+ const findings = [];
367
+ if (!isRecord(registry)) return [`${packageUnitsRegistryPath}: registry must be a JSON object`];
368
+ if (registry.schemaVersion !== 1) {
369
+ findings.push(`${packageUnitsRegistryPath}: schemaVersion must be 1`);
370
+ }
371
+ if (!isRecord(registry.policy)) {
372
+ findings.push(`${packageUnitsRegistryPath}: policy object is required`);
373
+ } else {
374
+ for (const key of ["packageBoundary", "namespaceSplit", "effectPeer"]) {
375
+ if (typeof registry.policy[key] !== "string" || registry.policy[key].length === 0) {
376
+ findings.push(`${packageUnitsRegistryPath}: policy.${key} must be a non-empty string`);
377
+ }
378
+ }
379
+ }
380
+ if (
381
+ !architectureStringRecordArray(registry.packageUnits) ||
382
+ registry.packageUnits.length === 0
383
+ ) {
384
+ findings.push(`${packageUnitsRegistryPath}: packageUnits must be a non-empty object array`);
385
+ return findings;
386
+ }
387
+ const ids = new Set();
388
+ const publicNames = new Set();
389
+ for (const [index, unit] of registry.packageUnits.entries()) {
390
+ const label = `${packageUnitsRegistryPath}:packageUnits[${index}]`;
391
+ if (typeof unit.id !== "string" || unit.id.length === 0) {
392
+ findings.push(`${label}: id must be a non-empty string`);
393
+ } else if (ids.has(unit.id)) {
394
+ findings.push(`${label}: duplicate id ${unit.id}`);
395
+ } else {
396
+ ids.add(unit.id);
397
+ }
398
+ if (
399
+ typeof unit.targetSourcePackageName !== "string" ||
400
+ !unit.targetSourcePackageName.startsWith("@agent-os/")
401
+ ) {
402
+ findings.push(`${label}: targetSourcePackageName must be an @agent-os/* string`);
403
+ }
404
+ if (
405
+ typeof unit.publicPackageName !== "string" ||
406
+ !unit.publicPackageName.startsWith("@yansirplus/")
407
+ ) {
408
+ findings.push(`${label}: publicPackageName must be an @yansirplus/* string`);
409
+ } else if (publicNames.has(unit.publicPackageName)) {
410
+ findings.push(`${label}: duplicate publicPackageName ${unit.publicPackageName}`);
411
+ } else {
412
+ publicNames.add(unit.publicPackageName);
413
+ }
414
+ const expectedPublicPackageName = expectedPublicPackageNameForSource(
415
+ unit.targetSourcePackageName,
416
+ );
417
+ if (
418
+ expectedPublicPackageName !== undefined &&
419
+ unit.publicPackageName !== expectedPublicPackageName
420
+ ) {
421
+ findings.push(
422
+ `${label}: publicPackageName must be ${expectedPublicPackageName}, the @yansirplus projection of ${unit.targetSourcePackageName}`,
423
+ );
424
+ }
425
+ if (typeof unit.status !== "string" || unit.status.length === 0) {
426
+ findings.push(`${label}: status must be a non-empty string`);
427
+ }
428
+
429
+ if (!isRecord(unit.hardInstallEnvelope)) {
430
+ findings.push(`${label}: hardInstallEnvelope object is required`);
431
+ } else {
432
+ for (const key of [
433
+ "dependencies",
434
+ "installScripts",
435
+ "nativeArtifacts",
436
+ "packageWideMetadata",
437
+ ]) {
438
+ if (!Array.isArray(unit.hardInstallEnvelope[key])) {
439
+ findings.push(`${label}: hardInstallEnvelope.${key} must be an array`);
440
+ }
441
+ }
442
+ validatePeerEntries({
443
+ label: `${label}:hardInstallEnvelope`,
444
+ peers: unit.hardInstallEnvelope.requiredPeers,
445
+ findings,
446
+ });
447
+ }
448
+
449
+ validateStringRefs({
450
+ label: `${label}: runtimeConditions`,
451
+ values: unit.runtimeConditions,
452
+ allowed: ambientIds,
453
+ noun: "ambient",
454
+ findings,
455
+ });
456
+ if (targetProfileIds.size > 0) {
457
+ validateStringRefs({
458
+ label: `${label}: targetProfiles`,
459
+ values: unit.targetProfiles,
460
+ allowed: targetProfileIds,
461
+ noun: "targetProfile",
462
+ findings,
463
+ });
464
+ } else if (!stringArray(unit.targetProfiles)) {
465
+ findings.push(`${label}: targetProfiles must be a non-empty string array`);
466
+ }
467
+
468
+ if (!architectureStringRecordArray(unit.publicSubpaths) || unit.publicSubpaths.length === 0) {
469
+ findings.push(`${label}: publicSubpaths must be a non-empty object array`);
470
+ continue;
471
+ }
472
+ const subpaths = new Set();
473
+ for (const [subpathIndex, subpath] of unit.publicSubpaths.entries()) {
474
+ const subpathLabel = `${label}:publicSubpaths[${subpathIndex}]`;
475
+ if (
476
+ typeof subpath.subpath !== "string" ||
477
+ (subpath.subpath !== "." && !subpath.subpath.startsWith("./"))
478
+ ) {
479
+ findings.push(`${subpathLabel}: subpath must be . or ./name`);
480
+ } else if (subpaths.has(subpath.subpath)) {
481
+ findings.push(`${subpathLabel}: duplicate subpath ${subpath.subpath}`);
482
+ } else {
483
+ subpaths.add(subpath.subpath);
484
+ }
485
+ validateStringRefs({
486
+ label: `${subpathLabel}: moduleBuckets`,
487
+ values: subpath.moduleBuckets,
488
+ allowed: bucketIds,
489
+ noun: "bucket",
490
+ findings,
491
+ });
492
+ if ("targetProfiles" in subpath) {
493
+ validateStringRefs({
494
+ label: `${subpathLabel}: targetProfiles`,
495
+ values: subpath.targetProfiles,
496
+ allowed: targetProfileIds,
497
+ noun: "targetProfile",
498
+ findings,
499
+ });
500
+ } else if (Array.isArray(unit.targetProfiles) && unit.targetProfiles.length > 1) {
501
+ findings.push(
502
+ `${subpathLabel}: targetProfiles must be declared when package unit has multiple targetProfiles`,
503
+ );
504
+ }
505
+ if (!Array.isArray(subpath.optionalPeers)) {
506
+ findings.push(`${subpathLabel}: optionalPeers must be an array`);
507
+ } else if (
508
+ !subpath.optionalPeers.every((peer) => typeof peer === "string" && peer.length > 0)
509
+ ) {
510
+ findings.push(`${subpathLabel}: optionalPeers entries must be non-empty strings`);
511
+ }
512
+ }
513
+ const record = workspacePackageRecordsByName.get(unit.targetSourcePackageName);
514
+ if (record !== undefined) {
515
+ const manifestPath = `${record.path}/package.json`;
516
+ if (!fs.existsSync(path.join(repoRoot, manifestPath))) {
517
+ findings.push(`${label}: source package manifest is missing at ${manifestPath}`);
518
+ } else {
519
+ const actualSubpaths = new Set(packageExportSubpaths(readJson(manifestPath)));
520
+ const declaredSubpaths = new Set(
521
+ unit.publicSubpaths
522
+ .filter(isRecord)
523
+ .map((subpath) => subpath.subpath)
524
+ .filter((subpath) => typeof subpath === "string"),
525
+ );
526
+ for (const subpath of [...actualSubpaths].sort(compare)) {
527
+ if (!declaredSubpaths.has(subpath)) {
528
+ findings.push(
529
+ `${label}: publicSubpaths missing package.json export ${unit.targetSourcePackageName}${subpath === "." ? "" : `/${subpath.slice(2)}`}`,
530
+ );
531
+ }
532
+ }
533
+ for (const subpath of [...declaredSubpaths].sort(compare)) {
534
+ if (!actualSubpaths.has(subpath)) {
535
+ findings.push(
536
+ `${label}: publicSubpaths declares non-exported subpath ${String(subpath)}`,
537
+ );
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+ return findings;
544
+ };
545
+
546
+ const distributionRootsRegistryFindings = ({
547
+ registry,
548
+ packageUnitIds,
549
+ ambientIds,
550
+ packageUnitsById = new Map(),
551
+ }) => {
552
+ const findings = [];
553
+ if (!isRecord(registry)) {
554
+ return [`${distributionRootsRegistryPath}: registry must be a JSON object`];
555
+ }
556
+ if (registry.schemaVersion !== 1) {
557
+ findings.push(`${distributionRootsRegistryPath}: schemaVersion must be 1`);
558
+ }
559
+ if (!isRecord(registry.policy)) {
560
+ findings.push(`${distributionRootsRegistryPath}: policy object is required`);
561
+ } else {
562
+ for (const key of ["rootTruth", "dogfoodWitness", "targetSelection"]) {
563
+ if (typeof registry.policy[key] !== "string" || registry.policy[key].length === 0) {
564
+ findings.push(
565
+ `${distributionRootsRegistryPath}: policy.${key} must be a non-empty string`,
566
+ );
567
+ }
568
+ }
569
+ }
570
+
571
+ if (!architectureStringRecordArray(registry.roots) || registry.roots.length === 0) {
572
+ findings.push(`${distributionRootsRegistryPath}: roots must be a non-empty object array`);
573
+ } else {
574
+ const ids = new Set();
575
+ for (const [index, root] of registry.roots.entries()) {
576
+ const label = `${distributionRootsRegistryPath}:roots[${index}]`;
577
+ if (typeof root.id !== "string" || root.id.length === 0) {
578
+ findings.push(`${label}: id must be a non-empty string`);
579
+ } else if (ids.has(root.id)) {
580
+ findings.push(`${label}: duplicate id ${root.id}`);
581
+ } else {
582
+ ids.add(root.id);
583
+ }
584
+ if (root.kind !== "public-package") {
585
+ findings.push(`${label}: kind must be public-package`);
586
+ }
587
+ if (typeof root.packageUnit !== "string" || !packageUnitIds.has(root.packageUnit)) {
588
+ findings.push(`${label}: packageUnit must reference a package unit`);
589
+ }
590
+ if (
591
+ typeof root.publicPackageName !== "string" ||
592
+ !root.publicPackageName.startsWith("@yansirplus/")
593
+ ) {
594
+ findings.push(`${label}: publicPackageName must be an @yansirplus/* string`);
595
+ }
596
+ const unit = packageUnitsById.get(root.packageUnit);
597
+ if (
598
+ unit !== undefined &&
599
+ typeof unit.publicPackageName === "string" &&
600
+ root.publicPackageName !== unit.publicPackageName
601
+ ) {
602
+ findings.push(
603
+ `${label}: publicPackageName must equal package unit ${root.packageUnit} publicPackageName ${unit.publicPackageName}`,
604
+ );
605
+ }
606
+ if (typeof root.consumerRoot !== "string" || root.consumerRoot.length === 0) {
607
+ findings.push(`${label}: consumerRoot must be a non-empty string`);
608
+ }
609
+ }
610
+ }
611
+
612
+ if (
613
+ !architectureStringRecordArray(registry.targetProfiles) ||
614
+ registry.targetProfiles.length === 0
615
+ ) {
616
+ findings.push(
617
+ `${distributionRootsRegistryPath}: targetProfiles must be a non-empty object array`,
618
+ );
619
+ } else {
620
+ const ids = new Set();
621
+ for (const [index, profile] of registry.targetProfiles.entries()) {
622
+ const label = `${distributionRootsRegistryPath}:targetProfiles[${index}]`;
623
+ if (typeof profile.id !== "string" || profile.id.length === 0) {
624
+ findings.push(`${label}: id must be a non-empty string`);
625
+ } else if (ids.has(profile.id)) {
626
+ findings.push(`${label}: duplicate id ${profile.id}`);
627
+ } else {
628
+ ids.add(profile.id);
629
+ }
630
+ if (typeof profile.ambient !== "string" || !ambientIds.has(profile.ambient)) {
631
+ findings.push(`${label}: ambient must reference a module ambient`);
632
+ }
633
+ validateStringRefs({
634
+ label: `${label}: packageUnits`,
635
+ values: profile.packageUnits,
636
+ allowed: packageUnitIds,
637
+ noun: "packageUnit",
638
+ findings,
639
+ });
640
+ if (!stringArray(profile.selectedSubpaths)) {
641
+ findings.push(`${label}: selectedSubpaths must be a non-empty string array`);
642
+ } else {
643
+ const allowedPublicSpecifiers = new Set();
644
+ const explicitProfileSpecifiers = new Set();
645
+ const explicitUnitPublicSpecifiers = new Set();
646
+ for (const unitId of Array.isArray(profile.packageUnits) ? profile.packageUnits : []) {
647
+ const unit = packageUnitsById.get(unitId);
648
+ if (!isRecord(unit) || typeof unit.publicPackageName !== "string") continue;
649
+ for (const subpath of Array.isArray(unit.publicSubpaths) ? unit.publicSubpaths : []) {
650
+ if (!isRecord(subpath) || typeof subpath.subpath !== "string") continue;
651
+ const specifier =
652
+ subpath.subpath === "."
653
+ ? unit.publicPackageName
654
+ : `${unit.publicPackageName}/${subpath.subpath.slice(2)}`;
655
+ allowedPublicSpecifiers.add(specifier);
656
+ if (Array.isArray(subpath.targetProfiles)) {
657
+ explicitUnitPublicSpecifiers.add(specifier);
658
+ if (subpath.targetProfiles.includes(profile.id)) {
659
+ explicitProfileSpecifiers.add(specifier);
660
+ }
661
+ }
662
+ }
663
+ }
664
+ const selected = new Set(profile.selectedSubpaths);
665
+ for (const expected of explicitProfileSpecifiers) {
666
+ if (!selected.has(expected)) {
667
+ findings.push(
668
+ `${label}: selectedSubpaths is missing ${String(expected)}, which package-units assigns to targetProfile ${profile.id}`,
669
+ );
670
+ }
671
+ }
672
+ for (const specifier of profile.selectedSubpaths) {
673
+ if (allowedPublicSpecifiers.has(specifier)) continue;
674
+ findings.push(
675
+ `${label}: selectedSubpaths includes ${specifier}, which is not exported by the selected packageUnits`,
676
+ );
677
+ }
678
+ for (const specifier of profile.selectedSubpaths) {
679
+ if (
680
+ explicitUnitPublicSpecifiers.has(specifier) &&
681
+ !explicitProfileSpecifiers.has(specifier)
682
+ ) {
683
+ findings.push(
684
+ `${label}: selectedSubpaths includes ${specifier}, which package-units does not assign to targetProfile ${profile.id}`,
685
+ );
686
+ }
687
+ }
688
+ }
689
+ if (!Array.isArray(profile.forbiddenSpecifiers)) {
690
+ findings.push(`${label}: forbiddenSpecifiers must be an array`);
691
+ } else if (
692
+ !profile.forbiddenSpecifiers.every(
693
+ (specifier) => typeof specifier === "string" && specifier.length > 0,
694
+ )
695
+ ) {
696
+ findings.push(`${label}: forbiddenSpecifiers entries must be non-empty strings`);
697
+ }
698
+ }
699
+ }
700
+
701
+ if (
702
+ !architectureStringRecordArray(registry.dogfoodRoots) ||
703
+ registry.dogfoodRoots.length === 0
704
+ ) {
705
+ findings.push(
706
+ `${distributionRootsRegistryPath}: dogfoodRoots must be a non-empty object array`,
707
+ );
708
+ } else {
709
+ for (const [index, root] of registry.dogfoodRoots.entries()) {
710
+ const label = `${distributionRootsRegistryPath}:dogfoodRoots[${index}]`;
711
+ for (const key of ["id", "kind", "path", "witnessLevel", "gate"]) {
712
+ if (typeof root[key] !== "string" || root[key].length === 0) {
713
+ findings.push(`${label}: ${key} must be a non-empty string`);
714
+ }
715
+ }
716
+ if (!stringArray(root.requiredCapabilities)) {
717
+ findings.push(`${label}: requiredCapabilities must be a non-empty string array`);
718
+ }
719
+ }
720
+ }
721
+ return findings;
722
+ };
723
+
724
+ const moduleBucketFindingsForEdges = (edges) => {
725
+ const findings = [];
726
+ const rankByBucket = moduleBucketRank();
727
+ const importsByAmbient = allowedAmbientImports();
728
+ for (const edge of edges) {
729
+ const fromBucket = moduleBucketForPath(edge.fromFile);
730
+ const toBucket = moduleBucketForPath(edge.toFile);
731
+ const fromRank = rankByBucket.get(fromBucket);
732
+ const toRank = rankByBucket.get(toBucket);
733
+ if (fromRank !== undefined && toRank !== undefined && fromRank < toRank) {
734
+ findings.push({
735
+ kind: "bucket-dag",
736
+ file: edge.fromFile,
737
+ target: edge.toFile,
738
+ specifier: edge.specifier,
739
+ message: `${fromBucket} module imports downstream ${toBucket} module`,
740
+ });
741
+ }
742
+
743
+ const fromAmbient = moduleAmbientForPath(edge.fromFile);
744
+ const toAmbient = moduleAmbientForPath(edge.toFile);
745
+ if (!(importsByAmbient.get(fromAmbient) ?? new Set()).has(toAmbient)) {
746
+ findings.push({
747
+ kind: "ambient-dag",
748
+ file: edge.fromFile,
749
+ target: edge.toFile,
750
+ specifier: edge.specifier,
751
+ message: `${fromAmbient} module imports ${toAmbient} module`,
752
+ });
753
+ }
754
+ }
755
+ return findings;
756
+ };
757
+
758
+ const moduleBucketExternalFindings = (records) => {
759
+ const findings = [];
760
+ const importsByAmbient = allowedAmbientImports();
761
+ for (const record of records) {
762
+ for (const file of walk(`${record.path}/src`).filter((entry) =>
763
+ /\.(?:ts|tsx|mts|cts)$/u.test(entry),
764
+ )) {
765
+ const source = read(file);
766
+ const ambient = moduleAmbientForPath(file);
767
+ for (const importRecord of importSpecifierRecords(source, file)) {
768
+ const targetAmbient = externalAmbientForSpecifier(importRecord.specifier);
769
+ if (targetAmbient === undefined) continue;
770
+ if ((importsByAmbient.get(ambient) ?? new Set()).has(targetAmbient)) continue;
771
+ findings.push({
772
+ kind: "external-ambient",
773
+ file,
774
+ target: targetAmbient,
775
+ specifier: importRecord.specifier,
776
+ message: `${ambient} module imports ${targetAmbient} external specifier`,
777
+ });
778
+ }
779
+ }
780
+ }
781
+ return findings;
782
+ };
783
+
784
+ const moduleProductFindings = (graph) => {
785
+ const ejection = ejectionBuckets();
786
+ const packagePathPrefixes = moduleBucketRegistry().productEjection.packagePathPrefixes;
787
+ return graph.files
788
+ .filter(
789
+ (entry) =>
790
+ packagePathPrefixes.some((prefix) => entry.package.path.startsWith(prefix)) &&
791
+ ejection.has(moduleBucketForPath(entry.file)),
792
+ )
793
+ .map((entry) => ({
794
+ kind: "product-ejection",
795
+ file: entry.file,
796
+ target: "consumer",
797
+ specifier: entry.package.name,
798
+ message: "product bucket module must be ejected from final substrate",
799
+ }));
800
+ };
801
+
802
+ const moduleBucketNegativeFixtureFailures = () => {
803
+ const failures = [];
804
+ const edgeFindings = moduleBucketFindingsForEdges([
805
+ {
806
+ fromFile: "packages/core/src/index.ts",
807
+ toFile: "packages/providers/deploy-cloudflare/src/index.ts",
808
+ specifier: "@agent-os/deploy-cloudflare",
809
+ },
810
+ {
811
+ fromFile: "packages/client/src/index.ts",
812
+ toFile: "packages/runtime/src/node/index.ts",
813
+ specifier: "@agent-os/runtime/node",
814
+ },
815
+ ]);
816
+ const edgeKinds = edgeFindings.map((finding) => finding.kind);
817
+ for (const kind of ["bucket-dag", "ambient-dag"]) {
818
+ if (!edgeKinds.includes(kind)) {
819
+ failures.push(`edge negative fixture: expected ${kind}, got ${JSON.stringify(edgeKinds)}`);
820
+ }
821
+ }
822
+
823
+ const productFindings = moduleProductFindings({
824
+ files: [
825
+ {
826
+ package: { name: "@agent-os/example", path: "packages/example" },
827
+ file: "packages/example/src/product/widget.ts",
828
+ },
829
+ ],
830
+ });
831
+ if (!productFindings.some((finding) => finding.kind === "product-ejection")) {
832
+ failures.push(
833
+ `product negative fixture: expected product-ejection, got ${JSON.stringify(productFindings)}`,
834
+ );
835
+ }
836
+ return failures;
837
+ };
838
+
839
+ const checkModuleBuckets = (args = []) => {
840
+ const reportOnly = args.length === 1 && args[0] === "--report-only";
841
+ const negativeFixtures = args.length === 1 && args[0] === "--negative-fixtures";
842
+ if (!reportOnly && !negativeFixtures && args.length > 0) {
843
+ throw new Error(`module-buckets: unexpected argument(s): ${args.join(" ")}`);
844
+ }
845
+ if (negativeFixtures) {
846
+ failIfAny("module buckets negative fixtures", moduleBucketNegativeFixtureFailures());
847
+ return;
848
+ }
849
+ const records = graphWorkspacePackageRecords(repoRoot).filter(
850
+ (record) => typeof record.name === "string" && record.name.startsWith("@agent-os/"),
851
+ );
852
+ const graph = sourceModuleGraph(repoRoot, records);
853
+ const rawFindings = [
854
+ ...moduleBucketFindingsForEdges(graph.edges),
855
+ ...moduleBucketExternalFindings(records),
856
+ ...moduleProductFindings(graph),
857
+ ];
858
+ const seenFindings = new Set();
859
+ const findings = rawFindings
860
+ .filter((finding) => {
861
+ const key = `${finding.kind}\0${finding.file}\0${finding.target}\0${finding.specifier}\0${finding.message}`;
862
+ if (seenFindings.has(key)) return false;
863
+ seenFindings.add(key);
864
+ return true;
865
+ })
866
+ .sort(
867
+ (left, right) =>
868
+ compare(left.kind, right.kind) ||
869
+ compare(left.file, right.file) ||
870
+ compare(left.specifier, right.specifier),
871
+ );
872
+ const bucketCounts = new Map();
873
+ const ambientCounts = new Map();
874
+ for (const entry of graph.files) {
875
+ const bucket = moduleBucketForPath(entry.file);
876
+ const ambient = moduleAmbientForPath(entry.file);
877
+ bucketCounts.set(bucket, (bucketCounts.get(bucket) ?? 0) + 1);
878
+ ambientCounts.set(ambient, (ambientCounts.get(ambient) ?? 0) + 1);
879
+ }
880
+ const sortedEntries = (counts) =>
881
+ [...counts.entries()].sort(([left], [right]) => compare(left, right));
882
+ const summary = `module buckets report-only: ${findings.length} finding(s); buckets ${JSON.stringify(Object.fromEntries(sortedEntries(bucketCounts)))}; ambients ${JSON.stringify(Object.fromEntries(sortedEntries(ambientCounts)))}`;
883
+ const lines = findings.map(
884
+ (finding) =>
885
+ `${finding.file}: module-buckets:${finding.kind}: ${finding.message} via ${finding.specifier} -> ${finding.target}`,
886
+ );
887
+ if (reportOnly) {
888
+ console.log(summary);
889
+ for (const line of lines) console.log(line);
890
+ return;
891
+ }
892
+ failIfAny("module buckets", lines);
893
+ };
894
+
895
+ const architectureSourceFindings = () => {
896
+ const workspacePackageRecords = graphWorkspacePackageRecords(repoRoot).filter(
897
+ (record) => typeof record.name === "string" && record.name.startsWith("@agent-os/"),
898
+ );
899
+ const workspacePackageNames = new Set(workspacePackageRecords.map((record) => record.name));
900
+ const workspacePackageRecordsByName = new Map(
901
+ workspacePackageRecords.map((record) => [record.name, record]),
902
+ );
903
+ const moduleBuckets = moduleBucketRegistry();
904
+ const packageUnits = readJson(packageUnitsRegistryPath);
905
+ const distributionRoots = readJson(distributionRootsRegistryPath);
906
+ const packageUnitsById = new Map(
907
+ Array.isArray(packageUnits.packageUnits)
908
+ ? packageUnits.packageUnits
909
+ .filter(isRecord)
910
+ .map((unit) => [unit.id, unit])
911
+ .filter(([id]) => typeof id === "string")
912
+ : [],
913
+ );
914
+ const bucketIds = new Set(
915
+ Array.isArray(moduleBuckets.buckets) ? moduleBuckets.buckets.map((bucket) => bucket.id) : [],
916
+ );
917
+ const ambientIds = new Set(
918
+ Array.isArray(moduleBuckets.ambients)
919
+ ? moduleBuckets.ambients.map((ambient) => ambient.id)
920
+ : [],
921
+ );
922
+ const packageUnitIds = new Set(
923
+ Array.isArray(packageUnits.packageUnits)
924
+ ? packageUnits.packageUnits.map((unit) => unit.id)
925
+ : [],
926
+ );
927
+ const targetProfileIds = new Set(
928
+ Array.isArray(distributionRoots.targetProfiles)
929
+ ? distributionRoots.targetProfiles.map((profile) => profile.id)
930
+ : [],
931
+ );
932
+ return [
933
+ ...ownerIdRegistryFindings({ registry: ownerIdRegistry(), workspacePackageNames }),
934
+ ...moduleBucketRegistryFindings(moduleBuckets),
935
+ ...packageUnitsRegistryFindings({
936
+ registry: packageUnits,
937
+ bucketIds,
938
+ ambientIds,
939
+ targetProfileIds,
940
+ workspacePackageRecordsByName,
941
+ }),
942
+ ...distributionRootsRegistryFindings({
943
+ registry: distributionRoots,
944
+ packageUnitIds,
945
+ ambientIds,
946
+ packageUnitsById,
947
+ }),
948
+ ];
949
+ };
950
+
951
+ const checkArchitectureSources = () => {
952
+ failIfAny("architecture sources", architectureSourceFindings());
953
+ };
954
+
955
+ return {
956
+ moduleBucketRegistryPath,
957
+ distributionRootsRegistryPath,
958
+ packageUnitsRegistryPath,
959
+ moduleBucketRegistry,
960
+ moduleBucketForPath,
961
+ moduleAmbientForPath,
962
+ allowedAmbientImports,
963
+ moduleBucketRegistryFindings,
964
+ packageUnitsRegistryFindings,
965
+ distributionRootsRegistryFindings,
966
+ moduleBucketFindingsForEdges,
967
+ moduleBucketNegativeFixtureFailures,
968
+ checkModuleBuckets,
969
+ checkArchitectureSources,
970
+ };
971
+ };