@namewta/speculo 0.7.4 → 0.7.5

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 (157) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/template/canonical/README.md +208 -63
  4. package/template/canonical/canonical-person-steelman-deliberation.md +518 -0
  5. package/template/skills/engineering-standards-builder/README.md +43 -0
  6. package/template/skills/engineering-standards-builder/SKILL.md +233 -0
  7. package/template/skills/engineering-standards-builder/examples/README.md +15 -0
  8. package/template/skills/engineering-standards-builder/examples/fallback/kotlin-gradle/build.gradle.kts +5 -0
  9. package/template/skills/engineering-standards-builder/examples/fallback/kotlin-gradle/expected.json +12 -0
  10. package/template/skills/engineering-standards-builder/examples/fallback/kotlin-gradle/src/main/kotlin/example/App.kt +2 -0
  11. package/template/skills/engineering-standards-builder/examples/go/service/.golangci.yml +3 -0
  12. package/template/skills/engineering-standards-builder/examples/go/service/cmd/api/main.go +3 -0
  13. package/template/skills/engineering-standards-builder/examples/go/service/expected.json +23 -0
  14. package/template/skills/engineering-standards-builder/examples/go/service/go.mod +3 -0
  15. package/template/skills/engineering-standards-builder/examples/go/service/internal/service/service.go +2 -0
  16. package/template/skills/engineering-standards-builder/examples/go/service/internal/service/service_test.go +3 -0
  17. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/expected.json +22 -0
  18. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/pom.xml +12 -0
  19. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/src/main/java/dev/speculo/orders/OrdersApplication.java +7 -0
  20. package/template/skills/engineering-standards-builder/examples/java/spring-boot-maven/src/test/java/dev/speculo/orders/OrdersApplicationTest.java +3 -0
  21. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/.github/workflows/ci.yml +11 -0
  22. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/apps/web/package.json +11 -0
  23. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/apps/web/src/App.vue +2 -0
  24. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/apps/web/tsconfig.json +8 -0
  25. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/expected.json +22 -0
  26. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/package.json +11 -0
  27. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/pnpm-workspace.yaml +2 -0
  28. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/orders/pom.xml +12 -0
  29. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/orders/src/main/java/dev/speculo/orders/OrdersApplication.java +7 -0
  30. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/worker/go.mod +3 -0
  31. package/template/skills/engineering-standards-builder/examples/polyglot/monorepo/services/worker/main.go +2 -0
  32. package/template/skills/engineering-standards-builder/examples/rust/workspace/Cargo.toml +7 -0
  33. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/cli/Cargo.toml +8 -0
  34. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/cli/src/main.rs +1 -0
  35. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/core/Cargo.toml +5 -0
  36. package/template/skills/engineering-standards-builder/examples/rust/workspace/crates/core/src/lib.rs +3 -0
  37. package/template/skills/engineering-standards-builder/examples/rust/workspace/expected.json +14 -0
  38. package/template/skills/engineering-standards-builder/examples/rust/workspace/rust-toolchain.toml +3 -0
  39. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/expected.json +21 -0
  40. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/package.json +20 -0
  41. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/src/App.test.tsx +2 -0
  42. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/src/App.tsx +1 -0
  43. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/src/main.tsx +3 -0
  44. package/template/skills/engineering-standards-builder/examples/typescript/react-vite/tsconfig.json +10 -0
  45. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/expected.json +22 -0
  46. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/package.json +21 -0
  47. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/src/App.test.ts +2 -0
  48. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/src/App.vue +7 -0
  49. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/src/main.ts +4 -0
  50. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/tsconfig.json +11 -0
  51. package/template/skills/engineering-standards-builder/examples/typescript/vue-vite/vite.config.ts +2 -0
  52. package/template/skills/engineering-standards-builder/manifest.txt +112 -0
  53. package/template/skills/engineering-standards-builder/references/go/00-detection-and-scope.md +37 -0
  54. package/template/skills/engineering-standards-builder/references/go/01-language-and-idioms.md +34 -0
  55. package/template/skills/engineering-standards-builder/references/go/02-modules-packages-and-layout.md +34 -0
  56. package/template/skills/engineering-standards-builder/references/go/03-errors-context-and-concurrency.md +38 -0
  57. package/template/skills/engineering-standards-builder/references/go/04-testing-tooling-quality-gates.md +46 -0
  58. package/template/skills/engineering-standards-builder/references/go/README.md +9 -0
  59. package/template/skills/engineering-standards-builder/references/java/00-detection-and-scope.md +40 -0
  60. package/template/skills/engineering-standards-builder/references/java/01-language-packages-and-api.md +35 -0
  61. package/template/skills/engineering-standards-builder/references/java/02-build-modules-and-dependencies.md +39 -0
  62. package/template/skills/engineering-standards-builder/references/java/03-errors-nullability-resources-concurrency.md +36 -0
  63. package/template/skills/engineering-standards-builder/references/java/04-testing-tooling-quality-gates.md +36 -0
  64. package/template/skills/engineering-standards-builder/references/java/README.md +10 -0
  65. package/template/skills/engineering-standards-builder/references/java/frameworks/spring-boot.md +74 -0
  66. package/template/skills/engineering-standards-builder/references/rules/00-governance-and-precedence.md +52 -0
  67. package/template/skills/engineering-standards-builder/references/rules/01-project-discovery.md +79 -0
  68. package/template/skills/engineering-standards-builder/references/rules/02-evidence-topology-and-scope.md +87 -0
  69. package/template/skills/engineering-standards-builder/references/rules/03-interview-and-decisions.md +77 -0
  70. package/template/skills/engineering-standards-builder/references/rules/04-architecture-and-dependency-boundaries.md +58 -0
  71. package/template/skills/engineering-standards-builder/references/rules/05-files-directories-and-naming.md +53 -0
  72. package/template/skills/engineering-standards-builder/references/rules/06-apis-errors-resources-and-concurrency.md +53 -0
  73. package/template/skills/engineering-standards-builder/references/rules/07-documentation-and-comments.md +46 -0
  74. package/template/skills/engineering-standards-builder/references/rules/08-testing-strategy.md +45 -0
  75. package/template/skills/engineering-standards-builder/references/rules/09-security-configuration-and-data.md +44 -0
  76. package/template/skills/engineering-standards-builder/references/rules/10-performance-observability-and-i18n.md +36 -0
  77. package/template/skills/engineering-standards-builder/references/rules/11-tooling-quality-gates-and-ci.md +69 -0
  78. package/template/skills/engineering-standards-builder/references/rules/12-git-review-and-delivery.md +46 -0
  79. package/template/skills/engineering-standards-builder/references/rules/13-adoption-exceptions-and-ratchets.md +60 -0
  80. package/template/skills/engineering-standards-builder/references/rules/14-generation-contract.md +105 -0
  81. package/template/skills/engineering-standards-builder/references/rules/15-validation-contract.md +65 -0
  82. package/template/skills/engineering-standards-builder/references/rules/16-language-adapter-contract.md +55 -0
  83. package/template/skills/engineering-standards-builder/references/rules/README.md +23 -0
  84. package/template/skills/engineering-standards-builder/references/rust/00-detection-and-scope.md +38 -0
  85. package/template/skills/engineering-standards-builder/references/rust/01-language-api-and-safety.md +35 -0
  86. package/template/skills/engineering-standards-builder/references/rust/02-crates-workspaces-and-layout.md +36 -0
  87. package/template/skills/engineering-standards-builder/references/rust/03-errors-ownership-and-concurrency.md +37 -0
  88. package/template/skills/engineering-standards-builder/references/rust/04-testing-tooling-quality-gates.md +44 -0
  89. package/template/skills/engineering-standards-builder/references/rust/README.md +9 -0
  90. package/template/skills/engineering-standards-builder/references/typescript/00-detection-and-scope.md +58 -0
  91. package/template/skills/engineering-standards-builder/references/typescript/01-language-and-type-system.md +49 -0
  92. package/template/skills/engineering-standards-builder/references/typescript/02-modules-packages-and-runtime-boundaries.md +49 -0
  93. package/template/skills/engineering-standards-builder/references/typescript/03-functions-async-errors-resources.md +42 -0
  94. package/template/skills/engineering-standards-builder/references/typescript/04-testing-tooling-quality-gates.md +51 -0
  95. package/template/skills/engineering-standards-builder/references/typescript/README.md +27 -0
  96. package/template/skills/engineering-standards-builder/references/typescript/app-types/cli.md +11 -0
  97. package/template/skills/engineering-standards-builder/references/typescript/app-types/library.md +11 -0
  98. package/template/skills/engineering-standards-builder/references/typescript/frameworks/react.md +71 -0
  99. package/template/skills/engineering-standards-builder/references/typescript/frameworks/vue.md +131 -0
  100. package/template/skills/engineering-standards-builder/references/typescript/runtimes/browser.md +15 -0
  101. package/template/skills/engineering-standards-builder/references/typescript/runtimes/electron.md +25 -0
  102. package/template/skills/engineering-standards-builder/references/typescript/runtimes/node.md +13 -0
  103. package/template/skills/engineering-standards-builder/scripts/discover-project.mjs +957 -0
  104. package/template/skills/engineering-standards-builder/scripts/self-test.mjs +203 -0
  105. package/template/skills/engineering-standards-builder/scripts/sync-manifest.mjs +105 -0
  106. package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +255 -0
  107. package/template/skills/engineering-standards-builder/scripts/validate-generated-skill.mjs +286 -0
  108. package/template/skills/engineering-standards-builder/templates/README.md +18 -0
  109. package/template/skills/engineering-standards-builder/templates/compatibility/agents-typescript--standards.md +6 -0
  110. package/template/skills/engineering-standards-builder/templates/compatibility/agents-typescript-standards.md +6 -0
  111. package/template/skills/engineering-standards-builder/templates/compatibility/claude-engineering-standards.md +6 -0
  112. package/template/skills/engineering-standards-builder/templates/compatibility/claude-typescript-standards.md +6 -0
  113. package/template/skills/engineering-standards-builder/templates/project-skill/SKILL.md.template +26 -0
  114. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/00-project-profile.md.template +26 -0
  115. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/01-module-map.md.template +27 -0
  116. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/02-decisions-and-exceptions.md.template +31 -0
  117. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/review-checklist.md.template +14 -0
  118. package/template/skills/source-code-zip-skill/SKILL.md +343 -0
  119. package/template/skills/source-code-zip-skill/scripts/zip_source_code.py +638 -0
  120. package/template/workflows/person/INDEX.md +1 -0
  121. package/template/workflows/person/S-steelman-deliberation/S-steelman-deliberation.md +218 -0
  122. package/template/workflows/person/S-steelman-deliberation/_templates/decision-template.md +51 -0
  123. package/template/workflows/person/S-steelman-deliberation/_templates/steelman-dossier-template.md +96 -0
  124. package/template/workflows/person/S-steelman-deliberation/deliberate.md +127 -0
  125. package/template/workflows/person/S-steelman-deliberation/evidence-gate.md +37 -0
  126. package/template/workflows/person/S-steelman-deliberation/judge.md +69 -0
  127. package/template/workflows/person/S-steelman-deliberation/tools/validate-steelman-change.mjs +697 -0
  128. package/template/skills/typescript-standards-builder/README.md +0 -53
  129. package/template/skills/typescript-standards-builder/SKILL.md +0 -245
  130. package/template/skills/typescript-standards-builder/examples/sample-generated-tree.md +0 -30
  131. package/template/skills/typescript-standards-builder/examples/sample-interview-decisions.md +0 -26
  132. package/template/skills/typescript-standards-builder/manifest.txt +0 -29
  133. package/template/skills/typescript-standards-builder/references/00-governance-and-fixed-defaults.md +0 -77
  134. package/template/skills/typescript-standards-builder/references/01-project-discovery.md +0 -100
  135. package/template/skills/typescript-standards-builder/references/02-interview-workflow.md +0 -129
  136. package/template/skills/typescript-standards-builder/references/03-project-architecture-and-directory-layout.md +0 -84
  137. package/template/skills/typescript-standards-builder/references/04-file-directory-and-symbol-naming.md +0 -92
  138. package/template/skills/typescript-standards-builder/references/05-modules-imports-exports-and-dependencies.md +0 -63
  139. package/template/skills/typescript-standards-builder/references/06-typescript-type-system.md +0 -64
  140. package/template/skills/typescript-standards-builder/references/07-functions-async-errors-and-resources.md +0 -42
  141. package/template/skills/typescript-standards-builder/references/08-comments-jsdoc-and-documentation.md +0 -51
  142. package/template/skills/typescript-standards-builder/references/09-testing-strategy.md +0 -58
  143. package/template/skills/typescript-standards-builder/references/10-react-and-frontend.md +0 -39
  144. package/template/skills/typescript-standards-builder/references/11-node-cli-and-cross-platform.md +0 -31
  145. package/template/skills/typescript-standards-builder/references/12-formatting-lint-and-complexity.md +0 -58
  146. package/template/skills/typescript-standards-builder/references/13-configuration-dependencies-and-ci.md +0 -71
  147. package/template/skills/typescript-standards-builder/references/14-security-performance-and-i18n.md +0 -32
  148. package/template/skills/typescript-standards-builder/references/15-git-review-and-delivery.md +0 -28
  149. package/template/skills/typescript-standards-builder/references/16-adoption-exceptions-and-migration.md +0 -61
  150. package/template/skills/typescript-standards-builder/references/17-generation-contract.md +0 -104
  151. package/template/skills/typescript-standards-builder/references/README.md +0 -37
  152. package/template/skills/typescript-standards-builder/templates/agents-compat-skill/SKILL.md +0 -1
  153. package/template/skills/typescript-standards-builder/templates/claude-skill/SKILL.md +0 -1
  154. package/template/skills/typescript-standards-builder/templates/project-skill/SKILL.md.template +0 -34
  155. package/template/skills/typescript-standards-builder/templates/project-skill/references/00-project-profile.md.template +0 -17
  156. package/template/skills/typescript-standards-builder/templates/project-skill/references/10-review-checklist.md +0 -23
  157. package/template/skills/typescript-standards-builder/templates/project-skill/references/11-decisions-and-exceptions.md.template +0 -19
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { spawnSync } from 'node:child_process';
8
+
9
+ function usage() {
10
+ return `Usage: node scripts/self-test.mjs --root <skill-root>\n\n` +
11
+ `Run deterministic scanner fixtures plus generated-Skill validator success and failure cases.\n\n` +
12
+ `Options:\n --root <path> Skill root (required)\n --help Show this help\n`;
13
+ }
14
+
15
+ function parseArgs(argv) {
16
+ let root = null;
17
+ for (let i = 0; i < argv.length; i += 1) {
18
+ const arg = argv[i];
19
+ if (arg === '--help' || arg === '-h') return { help: true };
20
+ if (arg === '--root') {
21
+ const value = argv[i + 1];
22
+ if (!value || value.startsWith('--')) throw new Error('--root requires a value');
23
+ root = value; i += 1; continue;
24
+ }
25
+ throw new Error(`unknown argument: ${arg}`);
26
+ }
27
+ if (!root) throw new Error('--root is required');
28
+ return { root: path.resolve(root) };
29
+ }
30
+
31
+ async function findExpectedFiles(root) {
32
+ const result = [];
33
+ async function visit(directory) {
34
+ const entries = await readdir(directory, { withFileTypes: true });
35
+ entries.sort((a, b) => a.name.localeCompare(b.name, 'en'));
36
+ for (const entry of entries) {
37
+ const abs = path.join(directory, entry.name);
38
+ if (entry.isDirectory()) await visit(abs);
39
+ else if (entry.isFile() && entry.name === 'expected.json') result.push(abs);
40
+ }
41
+ }
42
+ await visit(root);
43
+ return result;
44
+ }
45
+
46
+ function runNode(script, args, options = {}) {
47
+ return spawnSync(process.execPath, [script, ...args], {
48
+ encoding: 'utf8', timeout: options.timeout ?? 60000, maxBuffer: 64 * 1024 * 1024,
49
+ cwd: options.cwd,
50
+ });
51
+ }
52
+
53
+ function assert(condition, message) {
54
+ if (!condition) throw new Error(message);
55
+ }
56
+
57
+ function assertIncludes(actual, expected, label) {
58
+ for (const value of expected ?? []) {
59
+ assert(actual.includes(value), `${label}: expected ${JSON.stringify(value)} in ${JSON.stringify(actual)}`);
60
+ }
61
+ }
62
+
63
+ function assertFixture(inventory, expected, fixtureName) {
64
+ if (expected.topology) assert(inventory.topology === expected.topology, `${fixtureName}: topology expected ${expected.topology}, got ${inventory.topology}`);
65
+ if (expected.minimumModules) assert(inventory.modules.length >= expected.minimumModules, `${fixtureName}: expected at least ${expected.minimumModules} modules, got ${inventory.modules.length}`);
66
+ assertIncludes(inventory.summary.languages, expected.languages, `${fixtureName} summary.languages`);
67
+ assertIncludes(inventory.summary.adapters, expected.adapters, `${fixtureName} summary.adapters`);
68
+ for (const value of expected.absentAdapters ?? []) assert(!inventory.summary.adapters.includes(value), `${fixtureName}: adapter ${value} must be absent`);
69
+ assertIncludes(inventory.summary.frameworks, expected.frameworks, `${fixtureName} summary.frameworks`);
70
+ assertIncludes(inventory.summary.buildSystems, expected.buildSystems, `${fixtureName} summary.buildSystems`);
71
+ assertIncludes(inventory.summary.testSystems, expected.testSystems, `${fixtureName} summary.testSystems`);
72
+ assertIncludes((inventory.ciCommands ?? []).map((item) => item.responsibility), expected.ciResponsibilities, `${fixtureName} ciResponsibilities`);
73
+ if (expected.module) {
74
+ const module = inventory.modules.find((item) => item.path === expected.module.path);
75
+ assert(module, `${fixtureName}: module ${expected.module.path} not found`);
76
+ for (const key of ['languages', 'frameworks', 'buildSystems', 'testSystems', 'runtimes', 'applicationTypes']) {
77
+ assertIncludes(module[key] ?? [], expected.module[key], `${fixtureName} module.${key}`);
78
+ }
79
+ }
80
+ assert(inventory.scan.truncated === false, `${fixtureName}: scan unexpectedly truncated`);
81
+ assert(inventory.modules.every((module) => Array.isArray(module.evidence)), `${fixtureName}: every module must contain evidence`);
82
+ }
83
+
84
+ async function writeGeneratedFixture(projectRoot) {
85
+ await mkdir(path.join(projectRoot, 'src'), { recursive: true });
86
+ await writeFile(path.join(projectRoot, 'package.json'), `${JSON.stringify({
87
+ name: 'generated-validation-fixture', private: true,
88
+ dependencies: { vue: '^3.5.0' }, devDependencies: { typescript: '^5.9.0', vite: '^7.0.0' },
89
+ }, null, 2)}\n`);
90
+ await writeFile(path.join(projectRoot, 'tsconfig.json'), '{"compilerOptions":{"strict":true},"include":["src"]}\n');
91
+ await writeFile(path.join(projectRoot, 'src', 'App.vue'), '<script setup lang="ts">const value = 1</script>\n<template><main>{{ value }}</main></template>\n');
92
+
93
+ const canonical = path.join(projectRoot, '.agents', 'skills', 'engineering-standards');
94
+ await mkdir(path.join(canonical, 'references', 'project'), { recursive: true });
95
+ await mkdir(path.join(canonical, 'references', 'rules'), { recursive: true });
96
+ await mkdir(path.join(canonical, 'references', 'typescript', 'frameworks'), { recursive: true });
97
+ await writeFile(path.join(canonical, 'SKILL.md'), `---\nname: engineering-standards\ndescription: Generated fixture standards.\n---\n\n# Engineering Standards\n\nRead [profile](references/project/00-project-profile.md), [modules](references/project/01-module-map.md), [decisions](references/project/02-decisions-and-exceptions.md), [review](references/project/review-checklist.md), [rules](references/rules/core.md), [TypeScript](references/typescript/language.md), and [Vue](references/typescript/frameworks/vue.md).\n`);
98
+ await writeFile(path.join(canonical, 'references', 'project', '00-project-profile.md'), '# Project Profile\n\nVue fixture.\n');
99
+ await writeFile(path.join(canonical, 'references', 'project', '01-module-map.md'), '# Module Map\n\n- module:. uses TypeScript and Vue.\n');
100
+ await writeFile(path.join(canonical, 'references', 'project', '02-decisions-and-exceptions.md'), '# Decisions and Exceptions\n\nNo exceptions.\n');
101
+ await writeFile(path.join(canonical, 'references', 'project', 'review-checklist.md'), '# Review Checklist\n\n- Verify scope.\n');
102
+ const rule = `# Core Rules\n\n### CORE-001 Validated boundary\nScope: repository\nLevel: MUST\nSource: repository-fact\nApplies when: external input enters the application.\nRule: Validate external input before domain use.\nRationale: Preserve trusted internal models.\nVerification: Review boundary tests.\nException: Recorded project exception only.\n`;
103
+ await writeFile(path.join(canonical, 'references', 'rules', 'core.md'), rule);
104
+ await writeFile(path.join(canonical, 'references', 'typescript', 'language.md'), rule.replace('CORE-001', 'TS-001'));
105
+ await writeFile(path.join(canonical, 'references', 'typescript', 'frameworks', 'vue.md'), rule.replace('CORE-001', 'VUE-001'));
106
+ return canonical;
107
+ }
108
+
109
+ async function main() {
110
+ let args;
111
+ try { args = parseArgs(process.argv.slice(2)); }
112
+ catch (error) {
113
+ process.stderr.write(`self-test: ${error.message}\n\n${usage()}`);
114
+ process.exitCode = 2; return;
115
+ }
116
+ if (args.help) { process.stdout.write(usage()); return; }
117
+
118
+ const scanner = path.join(args.root, 'scripts', 'discover-project.mjs');
119
+ const generatedValidator = path.join(args.root, 'scripts', 'validate-generated-skill.mjs');
120
+ const manifestScript = path.join(args.root, 'scripts', 'sync-manifest.mjs');
121
+ const fixtureRoot = path.join(args.root, 'examples');
122
+ const tempRoots = [];
123
+ try {
124
+ const expectedFiles = await findExpectedFiles(fixtureRoot);
125
+ assert(expectedFiles.length >= 6, `expected at least 6 fixtures, found ${expectedFiles.length}`);
126
+ for (const expectedPath of expectedFiles) {
127
+ const fixtureDir = path.dirname(expectedPath);
128
+ const fixtureName = path.relative(fixtureRoot, fixtureDir).split(path.sep).join('/');
129
+ const expected = JSON.parse(await readFile(expectedPath, 'utf8'));
130
+ const first = runNode(scanner, ['--root', fixtureDir]);
131
+ assert(first.status === 0, `${fixtureName}: scanner failed: ${(first.stderr || first.stdout).trim()}`);
132
+ const second = runNode(scanner, ['--root', fixtureDir]);
133
+ assert(second.status === 0, `${fixtureName}: second scanner run failed`);
134
+ assert(first.stdout === second.stdout, `${fixtureName}: scanner output is not deterministic`);
135
+ const inventory = JSON.parse(first.stdout);
136
+ assertFixture(inventory, expected, fixtureName);
137
+ process.stdout.write(`self-test: fixture OK ${fixtureName}\n`);
138
+ }
139
+
140
+ const missing = runNode(scanner, ['--root', path.join(args.root, 'examples', 'does-not-exist')]);
141
+ assert(missing.status !== 0, 'scanner must fail for a missing root');
142
+
143
+ const outputTemp = await mkdtemp(path.join(os.tmpdir(), 'standards-builder-output-'));
144
+ tempRoots.push(outputTemp);
145
+ const copiedFixture = path.join(outputTemp, 'fixture');
146
+ await cp(path.join(args.root, 'examples', 'typescript', 'vue-vite'), copiedFixture, { recursive: true });
147
+ const writeResult = runNode(scanner, ['--root', copiedFixture, '--output', '.inventory/project.json', '--pretty']);
148
+ assert(writeResult.status === 0, `scanner safe output failed: ${writeResult.stderr}`);
149
+ const outputInventory = JSON.parse(await readFile(path.join(copiedFixture, '.inventory', 'project.json'), 'utf8'));
150
+ assert(outputInventory.summary.frameworks.includes('vue'), 'safe output inventory lost Vue detection');
151
+ const escapeTarget = path.join(outputTemp, 'escape.json');
152
+ const escapeResult = runNode(scanner, ['--root', copiedFixture, '--output', '../escape.json']);
153
+ assert(escapeResult.status !== 0, 'scanner must reject output escaping root');
154
+ try { await readFile(escapeTarget, 'utf8'); throw new Error('scanner wrote an escaping output file'); }
155
+ catch (error) { if (error.message === 'scanner wrote an escaping output file') throw error; }
156
+ process.stdout.write('self-test: scanner safety OK\n');
157
+
158
+ const managedTemp = await mkdtemp(path.join(os.tmpdir(), 'standards-builder-managed-'));
159
+ tempRoots.push(managedTemp);
160
+ await cp(path.join(args.root, 'examples', 'typescript', 'vue-vite'), managedTemp, { recursive: true });
161
+ await mkdir(path.join(managedTemp, 'speculo', '.speculo'), { recursive: true });
162
+ await mkdir(path.join(managedTemp, 'speculo', 'skills', 'noise', 'examples'), { recursive: true });
163
+ await writeFile(path.join(managedTemp, 'speculo', '.speculo', 'workspace.json'), '{"schema_version":1}\n');
164
+ await writeFile(path.join(managedTemp, 'speculo', 'skills', 'noise', 'examples', 'go.mod'), 'module noise.invalid\n\ngo 1.24\n');
165
+ await writeFile(path.join(managedTemp, 'speculo', 'skills', 'noise', 'examples', 'Cargo.toml'), '[package]\nname = "noise"\nversion = "0.0.0"\n');
166
+ await mkdir(path.join(managedTemp, '.agents', 'skills', 'engineering-standards'), { recursive: true });
167
+ await writeFile(path.join(managedTemp, '.agents', 'skills', 'engineering-standards', 'SKILL.md'), '---\nname: engineering-standards\ndescription: existing\n---\n');
168
+ const managedResult = runNode(scanner, ['--root', managedTemp]);
169
+ assert(managedResult.status === 0, `managed-root exclusion scan failed: ${managedResult.stderr}`);
170
+ const managedInventory = JSON.parse(managedResult.stdout);
171
+ assert(!managedInventory.summary.languages.includes('go') && !managedInventory.summary.languages.includes('rust'), 'managed Speculo/Agent skill assets polluted project language detection');
172
+ assert(managedInventory.existingStandards.includes('.agents/skills/engineering-standards/SKILL.md'), 'managed standards entrypoint was not preserved as evidence');
173
+ process.stdout.write('self-test: managed asset exclusion OK\n');
174
+
175
+ const generatedTemp = await mkdtemp(path.join(os.tmpdir(), 'standards-builder-generated-'));
176
+ tempRoots.push(generatedTemp);
177
+ const canonical = await writeGeneratedFixture(generatedTemp);
178
+ const valid = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
179
+ assert(valid.status === 0, `valid generated Skill was rejected: ${(valid.stderr || valid.stdout).trim()}`);
180
+ await writeFile(path.join(canonical, 'references', 'typescript', 'frameworks', 'react.md'), '# React\n');
181
+ const wrongFramework = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
182
+ assert(wrongFramework.status !== 0, 'strict validator must reject an unselected React rule in a Vue-only project');
183
+ await rm(path.join(canonical, 'references', 'typescript', 'frameworks', 'react.md'));
184
+ const wrapper = path.join(generatedTemp, '.agents', 'skills', 'typescript-standards');
185
+ await mkdir(wrapper, { recursive: true });
186
+ await writeFile(path.join(wrapper, 'SKILL.md'), '---\nname: typescript-standards\ndescription: alias\n---\n\nRoute to engineering-standards.\nSecond body line.\n');
187
+ const badWrapper = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
188
+ assert(badWrapper.status !== 0, 'validator must reject a multi-line compatibility wrapper');
189
+ process.stdout.write('self-test: generated Skill validator OK\n');
190
+
191
+ const manifest = runNode(manifestScript, ['--root', args.root, '--check']);
192
+ assert(manifest.status === 0, `manifest check failed: ${(manifest.stderr || manifest.stdout).trim()}`);
193
+ process.stdout.write('self-test: manifest OK\n');
194
+ process.stdout.write(`self-test: OK (${expectedFiles.length} fixtures)\n`);
195
+ } catch (error) {
196
+ process.stderr.write(`self-test: ERROR: ${error.stack ?? error.message}\n`);
197
+ process.exitCode = 1;
198
+ } finally {
199
+ for (const tempRoot of tempRoots) await rm(tempRoot, { recursive: true, force: true });
200
+ }
201
+ }
202
+
203
+ await main();
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { lstat, readFile, readdir, realpath, stat, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ function usage() {
8
+ return `Usage: node scripts/sync-manifest.mjs --root <skill-root> [--check | --write]\n\n` +
9
+ `Validate or regenerate manifest.txt. The manifest lists every package file except itself.\n\n` +
10
+ `Options:\n` +
11
+ ` --root <path> Skill root (required)\n` +
12
+ ` --check Fail when manifest.txt is stale (default)\n` +
13
+ ` --write Rewrite manifest.txt deterministically\n` +
14
+ ` --help Show this help\n`;
15
+ }
16
+
17
+ function parseArgs(argv) {
18
+ const result = { root: null, mode: 'check' };
19
+ for (let i = 0; i < argv.length; i += 1) {
20
+ const arg = argv[i];
21
+ if (arg === '--help' || arg === '-h') return { help: true };
22
+ if (arg === '--check') { result.mode = 'check'; continue; }
23
+ if (arg === '--write') { result.mode = 'write'; continue; }
24
+ if (arg === '--root') {
25
+ const value = argv[i + 1];
26
+ if (!value || value.startsWith('--')) throw new Error('--root requires a value');
27
+ result.root = value;
28
+ i += 1;
29
+ continue;
30
+ }
31
+ throw new Error(`unknown argument: ${arg}`);
32
+ }
33
+ if (!result.root) throw new Error('--root is required');
34
+ return result;
35
+ }
36
+
37
+ function toPosix(value) { return value.split(path.sep).join('/'); }
38
+
39
+ async function collectFiles(rootReal) {
40
+ const result = [];
41
+ async function visit(directory) {
42
+ const entries = await readdir(directory, { withFileTypes: true });
43
+ entries.sort((a, b) => a.name.localeCompare(b.name, 'en'));
44
+ for (const entry of entries) {
45
+ if (entry.name === '.DS_Store') continue;
46
+ const absolute = path.join(directory, entry.name);
47
+ const relative = toPosix(path.relative(rootReal, absolute));
48
+ if (entry.isSymbolicLink()) throw new Error(`symlink is not allowed in the skill package: ${relative}`);
49
+ if (entry.isDirectory()) { await visit(absolute); continue; }
50
+ if (!entry.isFile()) continue;
51
+ if (relative === 'manifest.txt') continue;
52
+ result.push(relative);
53
+ }
54
+ }
55
+ await visit(rootReal);
56
+ return result.sort((a, b) => a.localeCompare(b, 'en'));
57
+ }
58
+
59
+ async function main() {
60
+ let args;
61
+ try { args = parseArgs(process.argv.slice(2)); }
62
+ catch (error) {
63
+ process.stderr.write(`sync-manifest: ${error.message}\n\n${usage()}`);
64
+ process.exitCode = 2;
65
+ return;
66
+ }
67
+ if (args.help) { process.stdout.write(usage()); return; }
68
+ try {
69
+ const rootReal = await realpath(path.resolve(args.root));
70
+ if (!(await stat(rootReal)).isDirectory()) throw new Error('--root is not a directory');
71
+ const manifestPath = path.join(rootReal, 'manifest.txt');
72
+ const expected = `${(await collectFiles(rootReal)).join('\n')}\n`;
73
+ if (args.mode === 'write') {
74
+ const info = await lstat(rootReal);
75
+ if (!info.isDirectory()) throw new Error('--root is not a directory');
76
+ await writeFile(manifestPath, expected, 'utf8');
77
+ process.stdout.write(`sync-manifest: wrote ${toPosix(path.relative(rootReal, manifestPath))}\n`);
78
+ return;
79
+ }
80
+ let actual;
81
+ try { actual = await readFile(manifestPath, 'utf8'); }
82
+ catch (error) {
83
+ if (error?.code === 'ENOENT') throw new Error('manifest.txt is missing; run with --write');
84
+ throw error;
85
+ }
86
+ if (actual !== expected) {
87
+ const actualSet = new Set(actual.split(/\r?\n/).filter(Boolean));
88
+ const expectedSet = new Set(expected.split(/\r?\n/).filter(Boolean));
89
+ const missing = [...expectedSet].filter((item) => !actualSet.has(item));
90
+ const stale = [...actualSet].filter((item) => !expectedSet.has(item));
91
+ const detail = [
92
+ missing.length ? `missing: ${missing.slice(0, 20).join(', ')}` : null,
93
+ stale.length ? `stale: ${stale.slice(0, 20).join(', ')}` : null,
94
+ !missing.length && !stale.length ? 'entries are not in deterministic order or newline format differs' : null,
95
+ ].filter(Boolean).join('; ');
96
+ throw new Error(`manifest.txt is stale (${detail}); run with --write`);
97
+ }
98
+ process.stdout.write(`sync-manifest: OK (${expected.split('\n').filter(Boolean).length} files)\n`);
99
+ } catch (error) {
100
+ process.stderr.write(`sync-manifest: ${error.message}\n`);
101
+ process.exitCode = 1;
102
+ }
103
+ }
104
+
105
+ await main();
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from 'node:fs';
4
+ import { lstat, readFile, readdir, realpath, stat } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { spawnSync } from 'node:child_process';
8
+
9
+ function usage() {
10
+ return `Usage: node scripts/validate-builder.mjs --root <skill-root>\n\n` +
11
+ `Validate the builder package: identity, links, reachability, manifest, fixtures, templates and script help.\n\n` +
12
+ `Options:\n --root <path> Skill root (required)\n --help Show this help\n`;
13
+ }
14
+
15
+ function parseArgs(argv) {
16
+ let root = null;
17
+ for (let i = 0; i < argv.length; i += 1) {
18
+ const arg = argv[i];
19
+ if (arg === '--help' || arg === '-h') return { help: true };
20
+ if (arg === '--root') {
21
+ const value = argv[i + 1];
22
+ if (!value || value.startsWith('--')) throw new Error('--root requires a value');
23
+ root = value; i += 1; continue;
24
+ }
25
+ throw new Error(`unknown argument: ${arg}`);
26
+ }
27
+ if (!root) throw new Error('--root is required');
28
+ return { root };
29
+ }
30
+
31
+ function toPosix(value) { return value.split(path.sep).join('/'); }
32
+
33
+ async function collect(rootReal) {
34
+ const files = [];
35
+ const directories = [];
36
+ async function visit(directory) {
37
+ const relDir = toPosix(path.relative(rootReal, directory)) || '.';
38
+ const entries = await readdir(directory, { withFileTypes: true });
39
+ entries.sort((a, b) => a.name.localeCompare(b.name, 'en'));
40
+ directories.push({ abs: directory, rel: relDir, entries });
41
+ for (const entry of entries) {
42
+ const abs = path.join(directory, entry.name);
43
+ const rel = toPosix(path.relative(rootReal, abs));
44
+ if (entry.isSymbolicLink()) throw new Error(`symlink is not allowed: ${rel}`);
45
+ if (entry.isDirectory()) { await visit(abs); continue; }
46
+ if (entry.isFile()) files.push({ abs, rel, name: entry.name });
47
+ }
48
+ }
49
+ await visit(rootReal);
50
+ return { files, directories };
51
+ }
52
+
53
+ function parseFrontmatter(text) {
54
+ if (!text.startsWith('---\n')) return null;
55
+ const end = text.indexOf('\n---\n', 4);
56
+ if (end < 0) return null;
57
+ const result = {};
58
+ for (const line of text.slice(4, end).split('\n')) {
59
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
60
+ if (match) result[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '');
61
+ }
62
+ return { values: result, body: text.slice(end + 5) };
63
+ }
64
+
65
+ function markdownLinks(text) {
66
+ const links = [];
67
+ const pattern = /\[[^\]]*\]\(([^)]+)\)/g;
68
+ let match;
69
+ while ((match = pattern.exec(text)) !== null) {
70
+ let target = match[1].trim();
71
+ if (target.startsWith('<') && target.endsWith('>')) target = target.slice(1, -1);
72
+ const titleIndex = target.search(/\s+["']/);
73
+ if (titleIndex >= 0) target = target.slice(0, titleIndex);
74
+ links.push(target);
75
+ }
76
+ return links;
77
+ }
78
+
79
+ function isExternalLink(link) {
80
+ return /^(?:[a-z]+:|#|\/\/)/i.test(link);
81
+ }
82
+
83
+ function normalizeLinkTarget(sourceAbs, link) {
84
+ const withoutAnchor = link.split('#')[0].split('?')[0];
85
+ if (!withoutAnchor) return null;
86
+ let decoded;
87
+ try { decoded = decodeURIComponent(withoutAnchor); } catch { decoded = withoutAnchor; }
88
+ return path.resolve(path.dirname(sourceAbs), decoded);
89
+ }
90
+
91
+ async function computeManifest(rootReal, files) {
92
+ return `${files.map((file) => file.rel).filter((rel) => rel !== 'manifest.txt' && !rel.endsWith('/.DS_Store')).sort((a, b) => a.localeCompare(b, 'en')).join('\n')}\n`;
93
+ }
94
+
95
+ function failLines(errors, warnings) {
96
+ if (warnings.length) {
97
+ process.stderr.write(warnings.map((item) => `validate-builder: WARN: ${item}\n`).join(''));
98
+ }
99
+ if (errors.length) {
100
+ process.stderr.write(errors.map((item) => `validate-builder: ERROR: ${item}\n`).join(''));
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ process.stdout.write(`validate-builder: OK${warnings.length ? ` (${warnings.length} warning(s))` : ''}\n`);
105
+ }
106
+
107
+ async function main() {
108
+ let args;
109
+ try { args = parseArgs(process.argv.slice(2)); }
110
+ catch (error) {
111
+ process.stderr.write(`validate-builder: ${error.message}\n\n${usage()}`);
112
+ process.exitCode = 2; return;
113
+ }
114
+ if (args.help) { process.stdout.write(usage()); return; }
115
+
116
+ const errors = [];
117
+ const warnings = [];
118
+ try {
119
+ const rootReal = await realpath(path.resolve(args.root));
120
+ if (!(await stat(rootReal)).isDirectory()) throw new Error('--root is not a directory');
121
+ const { files, directories } = await collect(rootReal);
122
+ const fileByAbs = new Map(files.map((file) => [file.abs, file]));
123
+ const fileByRel = new Map(files.map((file) => [file.rel, file]));
124
+
125
+ for (const directory of directories) {
126
+ if (directory.entries.length === 0) errors.push(`empty directory: ${directory.rel}`);
127
+ }
128
+
129
+ const skillFile = fileByRel.get('SKILL.md');
130
+ if (!skillFile) errors.push('SKILL.md is missing');
131
+ else {
132
+ const skillText = await readFile(skillFile.abs, 'utf8');
133
+ const frontmatter = parseFrontmatter(skillText);
134
+ if (!frontmatter) errors.push('SKILL.md frontmatter is missing or malformed');
135
+ else {
136
+ const expectedName = path.basename(rootReal);
137
+ if (frontmatter.values.name !== expectedName) errors.push(`frontmatter name must equal directory name (${expectedName}), found ${frontmatter.values.name ?? '<missing>'}`);
138
+ if (frontmatter.values.name !== 'engineering-standards-builder') errors.push('stable Skill ID must remain engineering-standards-builder for drop-in replacement');
139
+ if (!frontmatter.values.description) errors.push('frontmatter description is missing');
140
+ if (/[<>]/.test(frontmatter.values.description ?? '')) errors.push('frontmatter description must not contain angle brackets');
141
+ }
142
+ const completionCount = (skillText.match(/\*\*完成标准\*\*/g) ?? []).length;
143
+ if (completionCount < 7) errors.push(`SKILL.md must define completion criteria for all seven stages; found ${completionCount}`);
144
+ }
145
+
146
+ const localMarkdownFiles = files.filter((file) => /\.md(?:\.template)?$/.test(file.name) || file.name === 'SKILL.md.template');
147
+ const linkGraph = new Map();
148
+ for (const file of localMarkdownFiles) {
149
+ const text = await readFile(file.abs, 'utf8');
150
+ const targets = [];
151
+ for (const link of markdownLinks(text)) {
152
+ if (isExternalLink(link)) continue;
153
+ const targetAbs = normalizeLinkTarget(file.abs, link);
154
+ if (!targetAbs) continue;
155
+ if (!targetAbs.startsWith(`${rootReal}${path.sep}`) && targetAbs !== rootReal) {
156
+ errors.push(`${file.rel}: local link escapes skill root: ${link}`);
157
+ continue;
158
+ }
159
+ if (!existsSync(targetAbs)) {
160
+ const templateTarget = `${targetAbs}.template`;
161
+ if (!(file.rel.endsWith('.template') && existsSync(templateTarget))) {
162
+ errors.push(`${file.rel}: broken local link: ${link}`);
163
+ continue;
164
+ }
165
+ }
166
+ try {
167
+ const info = await lstat(targetAbs);
168
+ if (info.isSymbolicLink()) errors.push(`${file.rel}: link resolves through a symlink: ${link}`);
169
+ } catch { /* exists check already reported */ }
170
+ targets.push(targetAbs);
171
+ }
172
+ linkGraph.set(file.abs, targets);
173
+ if (!file.rel.startsWith('templates/') && /\{\{[A-Z0-9_]+\}\}/.test(text)) {
174
+ errors.push(`${file.rel}: unresolved template placeholder outside templates/`);
175
+ }
176
+ }
177
+
178
+ // All references must be reachable from the main entry through explicit Markdown links.
179
+ if (skillFile) {
180
+ const reachable = new Set();
181
+ const queue = [skillFile.abs];
182
+ while (queue.length) {
183
+ const current = queue.shift();
184
+ if (reachable.has(current)) continue;
185
+ reachable.add(current);
186
+ for (const target of linkGraph.get(current) ?? []) {
187
+ if (fileByAbs.has(target) && /\.md(?:\.template)?$/.test(path.basename(target))) queue.push(target);
188
+ }
189
+ }
190
+ for (const file of files.filter((item) => item.rel.startsWith('references/') && item.rel.endsWith('.md'))) {
191
+ if (!reachable.has(file.abs)) errors.push(`orphan reference not reachable from SKILL.md: ${file.rel}`);
192
+ }
193
+ }
194
+
195
+ const allowedReferenceRoots = new Set(['rules', 'typescript', 'java', 'go', 'rust']);
196
+ for (const file of files.filter((item) => item.rel.startsWith('references/'))) {
197
+ const first = file.rel.split('/')[1];
198
+ if (!allowedReferenceRoots.has(first)) errors.push(`unexpected top-level reference group: ${file.rel}`);
199
+ }
200
+ for (const required of ['references/rules', 'references/typescript', 'references/java', 'references/go', 'references/rust']) {
201
+ if (!directories.some((directory) => directory.rel === required)) errors.push(`required reference directory missing: ${required}`);
202
+ }
203
+
204
+ const allDocsText = (await Promise.all(files.filter((file) => file.rel.endsWith('.md')).map((file) => readFile(file.abs, 'utf8')))).join('\n');
205
+ for (const script of files.filter((file) => file.rel.startsWith('scripts/') && file.rel.endsWith('.mjs'))) {
206
+ if (!allDocsText.includes(script.name)) errors.push(`script is not referenced by Skill documentation: ${script.rel}`);
207
+ const result = spawnSync(process.execPath, [script.abs, '--help'], { encoding: 'utf8', timeout: 15000 });
208
+ if (result.status !== 0) errors.push(`${script.rel} --help failed: ${(result.stderr || result.stdout).trim()}`);
209
+ if (!/Usage:/.test(result.stdout)) errors.push(`${script.rel} --help does not print a Usage section`);
210
+ }
211
+
212
+ const templateFiles = files.filter((file) => file.rel.startsWith('templates/') && file.rel !== 'templates/README.md');
213
+ const templateIndex = fileByRel.get('templates/README.md');
214
+ if (!templateIndex) errors.push('templates/README.md is missing');
215
+ else {
216
+ const indexText = await readFile(templateIndex.abs, 'utf8');
217
+ for (const file of templateFiles) {
218
+ if (!indexText.includes(file.name)) errors.push(`template is not indexed in templates/README.md: ${file.rel}`);
219
+ }
220
+ }
221
+
222
+ const fixtureExpectations = files.filter((file) => file.name === 'expected.json' && file.rel.startsWith('examples/'));
223
+ if (fixtureExpectations.length < 6) errors.push(`at least six fixture contracts are required; found ${fixtureExpectations.length}`);
224
+ for (const requiredPrefix of ['examples/typescript/', 'examples/java/', 'examples/go/', 'examples/rust/', 'examples/polyglot/']) {
225
+ if (!fixtureExpectations.some((file) => file.rel.startsWith(requiredPrefix))) errors.push(`missing fixture family: ${requiredPrefix}`);
226
+ }
227
+ for (const fixture of fixtureExpectations) {
228
+ try { JSON.parse(await readFile(fixture.abs, 'utf8')); }
229
+ catch (error) { errors.push(`${fixture.rel}: invalid expected.json (${error.message})`); }
230
+ }
231
+ const selfTest = fileByRel.get('scripts/self-test.mjs');
232
+ if (selfTest) {
233
+ const text = await readFile(selfTest.abs, 'utf8');
234
+ if (!text.includes('expected.json')) errors.push('self-test.mjs must discover fixture expected.json files');
235
+ }
236
+
237
+ const manifestFile = fileByRel.get('manifest.txt');
238
+ if (!manifestFile) errors.push('manifest.txt is missing');
239
+ else {
240
+ const expected = await computeManifest(rootReal, files);
241
+ const actual = await readFile(manifestFile.abs, 'utf8');
242
+ if (actual !== expected) errors.push('manifest.txt is stale; run sync-manifest.mjs --write');
243
+ }
244
+
245
+ const flatReferenceFiles = files.filter((file) => /^references\/[^/]+\.md$/.test(file.rel));
246
+ if (flatReferenceFiles.length) errors.push(`references must be grouped by rules/language: ${flatReferenceFiles.map((file) => file.rel).join(', ')}`);
247
+
248
+ if (!warnings.length && files.length > 180) warnings.push(`package contains ${files.length} files; verify progressive disclosure remains focused`);
249
+ } catch (error) {
250
+ errors.push(error.stack ?? error.message);
251
+ }
252
+ failLines(errors, warnings);
253
+ }
254
+
255
+ await main();