@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,957 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { lstat, mkdir, readFile, readdir, realpath, stat, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const SCHEMA_VERSION = '1.0.0';
9
+ const DEFAULT_IGNORED_DIRECTORIES = new Set([
10
+ '.git', '.hg', '.svn', '.idea', '.vscode',
11
+ 'node_modules', 'bower_components', 'vendor',
12
+ 'dist', 'build', 'out', 'target', 'coverage',
13
+ '.next', '.nuxt', '.output', '.turbo', '.gradle',
14
+ '.cache', '.parcel-cache', '.vite', '.svelte-kit',
15
+ 'bin', 'obj', '.venv', 'venv', '__pycache__',
16
+ '.pytest_cache', '.mypy_cache', '.ruff_cache',
17
+ ]);
18
+
19
+ const SOURCE_EXTENSIONS = new Set([
20
+ '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.vue',
21
+ '.java', '.go', '.rs', '.kt', '.kts', '.py', '.cs', '.c', '.cc', '.cpp',
22
+ '.h', '.hpp', '.swift', '.rb', '.php', '.scala', '.ex', '.exs',
23
+ ]);
24
+
25
+ function usage() {
26
+ return `Usage: node scripts/discover-project.mjs --root <path> [options]\n\n` +
27
+ `Build a deterministic, read-only Project Inventory. No project command is executed.\n\n` +
28
+ `Options:\n` +
29
+ ` --root <path> Repository/workspace root (required)\n` +
30
+ ` --output <path> Write JSON below --root; stdout when omitted\n` +
31
+ ` --pretty Pretty-print JSON\n` +
32
+ ` --max-depth <n> Maximum directory depth (default: 14)\n` +
33
+ ` --max-files <n> Maximum visited files (default: 30000)\n` +
34
+ ` --max-bytes <n> Maximum bytes read from one file (default: 2097152)\n` +
35
+ ` --help Show this help\n\n` +
36
+ `Examples:\n` +
37
+ ` node scripts/discover-project.mjs --root . --pretty\n` +
38
+ ` node scripts/discover-project.mjs --root . --output .engineering/project-inventory.json --pretty\n`;
39
+ }
40
+
41
+ function fail(message, code = 1) {
42
+ process.stderr.write(`discover-project: ${message}\n`);
43
+ process.exitCode = code;
44
+ }
45
+
46
+ function parsePositiveInt(value, flag) {
47
+ const parsed = Number(value);
48
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
49
+ throw new Error(`${flag} must be a positive integer`);
50
+ }
51
+ return parsed;
52
+ }
53
+
54
+ function parseArgs(argv) {
55
+ const args = {
56
+ root: null,
57
+ output: null,
58
+ pretty: false,
59
+ maxDepth: 14,
60
+ maxFiles: 30000,
61
+ maxBytes: 2 * 1024 * 1024,
62
+ };
63
+ for (let i = 0; i < argv.length; i += 1) {
64
+ const arg = argv[i];
65
+ if (arg === '--help' || arg === '-h') return { help: true };
66
+ if (arg === '--pretty') { args.pretty = true; continue; }
67
+ if (['--root', '--output', '--max-depth', '--max-files', '--max-bytes'].includes(arg)) {
68
+ const value = argv[i + 1];
69
+ if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
70
+ i += 1;
71
+ if (arg === '--root') args.root = value;
72
+ if (arg === '--output') args.output = value;
73
+ if (arg === '--max-depth') args.maxDepth = parsePositiveInt(value, arg);
74
+ if (arg === '--max-files') args.maxFiles = parsePositiveInt(value, arg);
75
+ if (arg === '--max-bytes') args.maxBytes = parsePositiveInt(value, arg);
76
+ continue;
77
+ }
78
+ throw new Error(`unknown argument: ${arg}`);
79
+ }
80
+ if (!args.root) throw new Error('--root is required');
81
+ return args;
82
+ }
83
+
84
+ function toPosix(value) {
85
+ return value.split(path.sep).join('/');
86
+ }
87
+
88
+ function normalizeRel(value) {
89
+ if (!value || value === '') return '.';
90
+ return toPosix(value);
91
+ }
92
+
93
+ function isPathInside(root, candidate) {
94
+ const relative = path.relative(root, candidate);
95
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
96
+ }
97
+
98
+ async function pathIsDirectory(candidate) {
99
+ try { return (await stat(candidate)).isDirectory(); }
100
+ catch { return false; }
101
+ }
102
+
103
+ function isWithinAny(candidate, roots) {
104
+ return roots.some((root) => isPathInside(root, candidate));
105
+ }
106
+
107
+ async function nearestExistingAncestor(candidate) {
108
+ let current = candidate;
109
+ while (true) {
110
+ try {
111
+ await lstat(current);
112
+ return current;
113
+ } catch (error) {
114
+ if (error?.code !== 'ENOENT') throw error;
115
+ const parent = path.dirname(current);
116
+ if (parent === current) throw error;
117
+ current = parent;
118
+ }
119
+ }
120
+ }
121
+
122
+ async function resolveSafeOutput(rootReal, outputArg) {
123
+ const outputAbs = path.resolve(rootReal, outputArg);
124
+ if (!isPathInside(rootReal, outputAbs)) {
125
+ throw new Error(`--output escapes --root: ${outputArg}`);
126
+ }
127
+ const existingAncestor = await nearestExistingAncestor(path.dirname(outputAbs));
128
+ const ancestorReal = await realpath(existingAncestor);
129
+ if (!isPathInside(rootReal, ancestorReal)) {
130
+ throw new Error(`--output traverses a symlink outside --root: ${outputArg}`);
131
+ }
132
+ try {
133
+ const outputInfo = await lstat(outputAbs);
134
+ if (outputInfo.isSymbolicLink()) {
135
+ const outputReal = await realpath(outputAbs);
136
+ if (!isPathInside(rootReal, outputReal)) {
137
+ throw new Error(`--output symlink escapes --root: ${outputArg}`);
138
+ }
139
+ }
140
+ } catch (error) {
141
+ if (error?.code !== 'ENOENT') throw error;
142
+ }
143
+ return outputAbs;
144
+ }
145
+
146
+ function addSortedUnique(target, values) {
147
+ for (const value of values) {
148
+ if (value) target.add(value);
149
+ }
150
+ }
151
+
152
+ function sorted(values) {
153
+ return [...values].sort((a, b) => a.localeCompare(b, 'en'));
154
+ }
155
+
156
+ function stableSortObjects(values, keys) {
157
+ return values.sort((left, right) => {
158
+ for (const key of keys) {
159
+ const cmp = String(left[key] ?? '').localeCompare(String(right[key] ?? ''), 'en');
160
+ if (cmp !== 0) return cmp;
161
+ }
162
+ return 0;
163
+ });
164
+ }
165
+
166
+ function relativeToModule(fileRel, modulePath) {
167
+ if (modulePath === '.') return fileRel;
168
+ return fileRel === modulePath ? '.' : fileRel.slice(modulePath.length + 1);
169
+ }
170
+
171
+ function relIsWithin(fileRel, directoryRel) {
172
+ return directoryRel === '.' || fileRel === directoryRel || fileRel.startsWith(`${directoryRel}/`);
173
+ }
174
+
175
+ async function walkFiles(rootReal, options, outputAbs, excludedRoots = []) {
176
+ const files = [];
177
+ const limitations = [];
178
+ const skippedSymlinks = [];
179
+ let visited = 0;
180
+ let truncated = false;
181
+ const outputRel = outputAbs ? normalizeRel(path.relative(rootReal, outputAbs)) : null;
182
+
183
+ async function visit(directoryAbs, depth) {
184
+ if (truncated) return;
185
+ if (depth > options.maxDepth) {
186
+ limitations.push({ type: 'max-depth', path: normalizeRel(path.relative(rootReal, directoryAbs)), limit: options.maxDepth });
187
+ return;
188
+ }
189
+ let entries;
190
+ try {
191
+ entries = await readdir(directoryAbs, { withFileTypes: true });
192
+ } catch (error) {
193
+ limitations.push({ type: 'read-directory-failed', path: normalizeRel(path.relative(rootReal, directoryAbs)), error: error.message });
194
+ return;
195
+ }
196
+ entries.sort((a, b) => a.name.localeCompare(b.name, 'en'));
197
+ for (const entry of entries) {
198
+ if (truncated) break;
199
+ const abs = path.join(directoryAbs, entry.name);
200
+ const rel = normalizeRel(path.relative(rootReal, abs));
201
+ if (entry.isSymbolicLink()) {
202
+ let target = null;
203
+ try { target = await realpath(abs); } catch { /* broken link */ }
204
+ skippedSymlinks.push({ path: rel, target: target && isPathInside(rootReal, target) ? normalizeRel(path.relative(rootReal, target)) : target ? '<outside-root>' : '<unresolved>' });
205
+ continue;
206
+ }
207
+ if (entry.isDirectory()) {
208
+ if (DEFAULT_IGNORED_DIRECTORIES.has(entry.name)) continue;
209
+ if (isWithinAny(abs, excludedRoots)) continue;
210
+ await visit(abs, depth + 1);
211
+ continue;
212
+ }
213
+ if (!entry.isFile()) continue;
214
+ if (outputRel && rel === outputRel) continue;
215
+ visited += 1;
216
+ if (visited > options.maxFiles) {
217
+ limitations.push({ type: 'max-files', path: rel, limit: options.maxFiles });
218
+ truncated = true;
219
+ break;
220
+ }
221
+ let info;
222
+ try { info = await stat(abs); } catch { continue; }
223
+ files.push({ abs, rel, size: info.size, ext: path.extname(entry.name).toLowerCase(), name: entry.name });
224
+ }
225
+ }
226
+
227
+ await visit(rootReal, 0);
228
+ return { files, limitations, skippedSymlinks, truncated };
229
+ }
230
+
231
+ async function createReader(maxBytes, limitations) {
232
+ const cache = new Map();
233
+ return async function readText(file) {
234
+ if (!file) return null;
235
+ if (cache.has(file.abs)) return cache.get(file.abs);
236
+ if (file.size > maxBytes) {
237
+ limitations.push({ type: 'max-bytes', path: file.rel, size: file.size, limit: maxBytes });
238
+ cache.set(file.abs, null);
239
+ return null;
240
+ }
241
+ try {
242
+ const value = await readFile(file.abs, 'utf8');
243
+ cache.set(file.abs, value);
244
+ return value;
245
+ } catch (error) {
246
+ limitations.push({ type: 'read-file-failed', path: file.rel, error: error.message });
247
+ cache.set(file.abs, null);
248
+ return null;
249
+ }
250
+ };
251
+ }
252
+
253
+ async function collectManagedSkillEntrypoints(rootReal, managedRoots) {
254
+ const entries = [];
255
+ let visited = 0;
256
+ async function visit(directory, depth) {
257
+ if (depth > 5 || visited > 5000 || !(await pathIsDirectory(directory))) return;
258
+ const children = await readdir(directory, { withFileTypes: true });
259
+ children.sort((a, b) => a.name.localeCompare(b.name, 'en'));
260
+ for (const child of children) {
261
+ visited += 1;
262
+ if (visited > 5000) break;
263
+ const abs = path.join(directory, child.name);
264
+ if (child.isSymbolicLink()) continue;
265
+ if (child.isDirectory()) { await visit(abs, depth + 1); continue; }
266
+ if (child.isFile() && child.name === 'SKILL.md') entries.push(normalizeRel(path.relative(rootReal, abs)));
267
+ }
268
+ }
269
+ for (const managedRoot of managedRoots) await visit(managedRoot, 0);
270
+ return sorted(new Set(entries));
271
+ }
272
+
273
+ function findCandidateModulePaths(files) {
274
+ const moduleFiles = new Map();
275
+ const manifestNames = new Set(['package.json', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'go.mod', 'go.work', 'Cargo.toml']);
276
+ for (const file of files) {
277
+ if (!manifestNames.has(file.name)) continue;
278
+ const directory = normalizeRel(path.posix.dirname(file.rel));
279
+ if (!moduleFiles.has(directory)) moduleFiles.set(directory, []);
280
+ moduleFiles.get(directory).push(file);
281
+ }
282
+ if (moduleFiles.size === 0 && files.some((file) => SOURCE_EXTENSIONS.has(file.ext))) {
283
+ moduleFiles.set('.', []);
284
+ }
285
+ return moduleFiles;
286
+ }
287
+
288
+ function assignFilesToModules(files, modulePaths) {
289
+ const ordered = [...modulePaths].sort((a, b) => b.length - a.length || a.localeCompare(b, 'en'));
290
+ const assigned = new Map(modulePaths.map((modulePath) => [modulePath, []]));
291
+ const unscoped = [];
292
+ for (const file of files) {
293
+ const owner = ordered.find((modulePath) => relIsWithin(file.rel, modulePath));
294
+ if (owner) assigned.get(owner).push(file);
295
+ else unscoped.push(file);
296
+ }
297
+ return { assigned, unscoped };
298
+ }
299
+
300
+ function dependencyMap(packageJson) {
301
+ return {
302
+ ...(packageJson?.dependencies ?? {}),
303
+ ...(packageJson?.devDependencies ?? {}),
304
+ ...(packageJson?.peerDependencies ?? {}),
305
+ ...(packageJson?.optionalDependencies ?? {}),
306
+ };
307
+ }
308
+
309
+ function hasDependency(dependencies, names) {
310
+ return names.some((name) => Object.prototype.hasOwnProperty.call(dependencies, name));
311
+ }
312
+
313
+ function firstMatchingRoot(paths, patterns) {
314
+ const roots = new Set();
315
+ for (const rel of paths) {
316
+ for (const pattern of patterns) {
317
+ if (rel === pattern || rel.startsWith(`${pattern}/`)) roots.add(pattern);
318
+ }
319
+ }
320
+ return roots;
321
+ }
322
+
323
+ function detectSourceRoots(localFiles, modulePath, language) {
324
+ const rels = localFiles.map((file) => relativeToModule(file.rel, modulePath));
325
+ const roots = new Set();
326
+ if (language === 'java') {
327
+ addSortedUnique(roots, firstMatchingRoot(rels, ['src/main/java', 'src/main/resources', 'src/test/java', 'src/test/resources']));
328
+ } else if (language === 'go') {
329
+ addSortedUnique(roots, firstMatchingRoot(rels, ['cmd', 'internal', 'pkg', 'api']));
330
+ if (rels.some((rel) => rel.endsWith('.go') && !rel.includes('/'))) roots.add('.');
331
+ } else if (language === 'rust') {
332
+ addSortedUnique(roots, firstMatchingRoot(rels, ['src', 'tests', 'examples', 'benches']));
333
+ } else {
334
+ addSortedUnique(roots, firstMatchingRoot(rels, ['src', 'app', 'pages', 'components', 'lib', 'test', 'tests', 'e2e']));
335
+ if (rels.some((rel) => /\.(?:ts|tsx|js|jsx|vue)$/.test(rel) && !rel.includes('/'))) roots.add('.');
336
+ }
337
+ return roots;
338
+ }
339
+
340
+ function detectTestRoots(localFiles, modulePath) {
341
+ const roots = new Set();
342
+ for (const file of localFiles) {
343
+ const rel = relativeToModule(file.rel, modulePath);
344
+ if (rel.startsWith('src/test/')) roots.add(rel.startsWith('src/test/java') ? 'src/test/java' : 'src/test');
345
+ if (/^(test|tests|e2e|__tests__)(\/|$)/.test(rel)) roots.add(rel.split('/')[0]);
346
+ if (/(^|\/)(?:[^/]+\.)?(?:test|spec)\.[^.]+$/.test(rel)) {
347
+ const first = rel.split('/')[0];
348
+ roots.add(first === rel ? '.' : first);
349
+ }
350
+ if (rel.endsWith('_test.go')) {
351
+ const dir = path.posix.dirname(rel);
352
+ roots.add(dir === '.' ? '.' : dir);
353
+ }
354
+ }
355
+ return roots;
356
+ }
357
+
358
+ function publicEntrypointsForPackage(pkg, localFiles, modulePath) {
359
+ const entries = new Set();
360
+ if (pkg) {
361
+ for (const key of ['main', 'module', 'types', 'typings', 'browser']) {
362
+ if (typeof pkg[key] === 'string') entries.add(pkg[key]);
363
+ }
364
+ if (typeof pkg.bin === 'string') entries.add(pkg.bin);
365
+ if (pkg.bin && typeof pkg.bin === 'object') {
366
+ for (const value of Object.values(pkg.bin)) if (typeof value === 'string') entries.add(value);
367
+ }
368
+ if (typeof pkg.exports === 'string') entries.add(pkg.exports);
369
+ if (pkg.exports && typeof pkg.exports === 'object') {
370
+ for (const key of Object.keys(pkg.exports)) entries.add(`package-export:${key}`);
371
+ }
372
+ }
373
+ const common = new Set(['src/main.ts', 'src/main.tsx', 'src/main.js', 'src/main.jsx', 'src/index.ts', 'src/index.tsx', 'src/index.js', 'src/index.jsx', 'src/lib.rs', 'src/main.rs']);
374
+ for (const file of localFiles) {
375
+ const rel = relativeToModule(file.rel, modulePath);
376
+ if (common.has(rel)) entries.add(rel);
377
+ if (rel === 'module-info.java') entries.add(rel);
378
+ if (rel.startsWith('cmd/') && rel.endsWith('/main.go')) entries.add(rel);
379
+ }
380
+ return entries;
381
+ }
382
+
383
+ function extractPackageManager(pkg, localFiles) {
384
+ const managers = new Set();
385
+ if (typeof pkg?.packageManager === 'string') managers.add(pkg.packageManager.split('@')[0]);
386
+ for (const file of localFiles) {
387
+ if (file.name === 'pnpm-lock.yaml') managers.add('pnpm');
388
+ if (file.name === 'yarn.lock') managers.add('yarn');
389
+ if (file.name === 'package-lock.json' || file.name === 'npm-shrinkwrap.json') managers.add('npm');
390
+ if (file.name === 'bun.lock' || file.name === 'bun.lockb') managers.add('bun');
391
+ }
392
+ return managers;
393
+ }
394
+
395
+ function evidence(pathValue, signal, confidence = 'high') {
396
+ return { path: pathValue, signal, confidence };
397
+ }
398
+
399
+ function dependencyVersion(dependencies, name) {
400
+ const value = dependencies[name];
401
+ return typeof value === 'string' ? value : null;
402
+ }
403
+
404
+ function parseTomlValue(text, key) {
405
+ if (!text) return null;
406
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
407
+ const match = text.match(new RegExp(`^\\s*${escaped}\\s*=\\s*["']([^"']+)["']`, 'm'));
408
+ return match?.[1] ?? null;
409
+ }
410
+
411
+ function parseXmlTag(text, tag) {
412
+ if (!text) return null;
413
+ const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
414
+ const match = text.match(new RegExp(`<${escaped}>([^<]+)</${escaped}>`));
415
+ return match?.[1]?.trim() ?? null;
416
+ }
417
+
418
+ function parseProjectArtifactId(pomText) {
419
+ if (!pomText) return null;
420
+ const withoutParent = pomText.replace(/<parent>[\s\S]*?<\/parent>/, '');
421
+ return parseXmlTag(withoutParent, 'artifactId');
422
+ }
423
+
424
+ function parseGradleJavaVersion(text) {
425
+ if (!text) return null;
426
+ const patterns = [
427
+ /JavaLanguageVersion\.of\((\d+)\)/,
428
+ /sourceCompatibility\s*=\s*(?:JavaVersion\.VERSION_)?([0-9_]+)/,
429
+ /jvmToolchain\((\d+)\)/,
430
+ ];
431
+ for (const pattern of patterns) {
432
+ const match = text.match(pattern);
433
+ if (match) return match[1].replaceAll('_', '.');
434
+ }
435
+ return null;
436
+ }
437
+
438
+ function sourceContains(localTexts, pattern) {
439
+ return localTexts.some(({ text }) => text && pattern.test(text));
440
+ }
441
+
442
+ async function analyzeModule(modulePath, moduleFiles, localFiles, readText, rootFilesByRel) {
443
+ const byName = new Map(moduleFiles.map((file) => [file.name, file]));
444
+ const packageFile = byName.get('package.json');
445
+ let pkg = null;
446
+ if (packageFile) {
447
+ const text = await readText(packageFile);
448
+ try { pkg = text ? JSON.parse(text) : null; } catch { pkg = null; }
449
+ }
450
+ const dependencies = dependencyMap(pkg);
451
+ const localSourceFiles = localFiles.filter((file) => SOURCE_EXTENSIONS.has(file.ext));
452
+ const representativeSource = localSourceFiles.slice(0, 150);
453
+ const localTexts = [];
454
+ for (const file of representativeSource) {
455
+ const text = await readText(file);
456
+ if (text !== null) localTexts.push({ file, text });
457
+ }
458
+
459
+ const languages = new Set();
460
+ const adapters = new Set();
461
+ const frameworks = new Set();
462
+ const runtimes = new Set();
463
+ const buildSystems = new Set();
464
+ const packageManagers = extractPackageManager(pkg, localFiles);
465
+ const testSystems = new Set();
466
+ const applicationTypes = new Set();
467
+ const configuredTools = new Set();
468
+ const versions = {};
469
+ const moduleEvidence = [];
470
+ const conflicts = [];
471
+ const unknowns = [];
472
+
473
+ const hasVueSource = localSourceFiles.some((file) => file.ext === '.vue');
474
+ const vueUsesTypeScript = localTexts.some(({ file, text }) => file.ext === '.vue' && /<script\b[^>]*\blang=["']ts["']/i.test(text));
475
+ const hasTsSource = localSourceFiles.some((file) => ['.ts', '.tsx', '.mts', '.cts'].includes(file.ext)) || vueUsesTypeScript;
476
+ const hasJsSource = localSourceFiles.some((file) => ['.js', '.jsx', '.mjs', '.cjs'].includes(file.ext));
477
+ const hasTsConfig = localFiles.some((file) => /^tsconfig(?:\..+)?\.json$/.test(file.name));
478
+ const packageHasCodeSignal = hasTsSource || hasJsSource || hasVueSource || hasTsConfig || hasDependency(dependencies, ['typescript', 'tsx', 'ts-node']);
479
+ if (packageFile && packageHasCodeSignal) {
480
+ if (hasTsSource || hasTsConfig || hasDependency(dependencies, ['typescript'])) languages.add('typescript');
481
+ else languages.add('javascript');
482
+ adapters.add('typescript');
483
+ moduleEvidence.push(evidence(packageFile.rel, 'JavaScript/TypeScript package manifest'));
484
+ } else if (hasTsSource || hasTsConfig) {
485
+ languages.add('typescript'); adapters.add('typescript');
486
+ moduleEvidence.push(evidence(localSourceFiles.find((file) => ['.ts', '.tsx', '.mts', '.cts'].includes(file.ext))?.rel ?? localSourceFiles.find((file) => file.ext === '.vue')?.rel ?? modulePath, 'TypeScript-family source', 'medium'));
487
+ } else if (hasJsSource || hasVueSource) {
488
+ languages.add('javascript'); adapters.add('typescript');
489
+ moduleEvidence.push(evidence(localSourceFiles.find((file) => ['.js', '.jsx', '.mjs', '.cjs', '.vue'].includes(file.ext))?.rel ?? modulePath, 'JavaScript or Vue source', 'medium'));
490
+ }
491
+
492
+ if (adapters.has('typescript')) {
493
+ if (hasDependency(dependencies, ['typescript'])) versions.typescript = dependencyVersion(dependencies, 'typescript');
494
+ if (typeof pkg?.engines?.node === 'string') versions.node = pkg.engines.node;
495
+ if (typeof pkg?.packageManager === 'string') versions.packageManager = pkg.packageManager;
496
+ if (pkg?.type === 'module') versions.moduleType = 'esm';
497
+ else if (pkg?.type === 'commonjs') versions.moduleType = 'commonjs';
498
+
499
+ const vueSource = localSourceFiles.find((file) => file.ext === '.vue');
500
+ const reactSourceSignal = localSourceFiles.find((file) => ['.tsx', '.jsx'].includes(file.ext));
501
+ if (hasDependency(dependencies, ['vue', '@vue/runtime-core', '@vue/runtime-dom']) || vueSource) {
502
+ frameworks.add('vue'); runtimes.add('browser'); applicationTypes.add('web-app');
503
+ moduleEvidence.push(evidence(vueSource?.rel ?? packageFile?.rel ?? modulePath, vueSource ? 'Vue single-file component' : 'Vue dependency'));
504
+ versions.vue = dependencyVersion(dependencies, 'vue');
505
+ }
506
+ if (hasDependency(dependencies, ['pinia'])) { frameworks.add('pinia'); versions.pinia = dependencyVersion(dependencies, 'pinia'); }
507
+ if (hasDependency(dependencies, ['vuex'])) { frameworks.add('vuex'); versions.vuex = dependencyVersion(dependencies, 'vuex'); }
508
+ if (hasDependency(dependencies, ['react', 'react-dom', 'next']) || (reactSourceSignal && sourceContains(localTexts, /(?:from\s+['"]react|react-dom\/client|jsx-runtime)/))) {
509
+ frameworks.add('react'); runtimes.add('browser'); applicationTypes.add('web-app');
510
+ moduleEvidence.push(evidence(packageFile?.rel ?? reactSourceSignal?.rel ?? modulePath, 'React dependency or source import'));
511
+ versions.react = dependencyVersion(dependencies, 'react');
512
+ }
513
+ if (hasDependency(dependencies, ['next'])) { frameworks.add('next'); runtimes.add('node'); versions.next = dependencyVersion(dependencies, 'next'); }
514
+ if (hasDependency(dependencies, ['nuxt'])) { frameworks.add('nuxt'); frameworks.add('vue'); runtimes.add('node'); runtimes.add('browser'); versions.nuxt = dependencyVersion(dependencies, 'nuxt'); }
515
+ if (hasDependency(dependencies, ['@nestjs/core'])) { frameworks.add('nestjs'); runtimes.add('node'); applicationTypes.add('service'); }
516
+ if (hasDependency(dependencies, ['express', 'fastify', 'koa', 'h3'])) { frameworks.add('node-web'); runtimes.add('node'); applicationTypes.add('service'); }
517
+ if (hasDependency(dependencies, ['electron'])) { frameworks.add('electron'); runtimes.add('electron'); runtimes.add('node'); runtimes.add('browser'); applicationTypes.add('desktop-app'); versions.electron = dependencyVersion(dependencies, 'electron'); }
518
+ if (hasDependency(dependencies, ['vite']) || localFiles.some((file) => /^vite\.config\./.test(file.name))) { buildSystems.add('vite'); versions.vite = dependencyVersion(dependencies, 'vite'); }
519
+ if (hasDependency(dependencies, ['webpack']) || localFiles.some((file) => /^webpack\.config\./.test(file.name))) buildSystems.add('webpack');
520
+ if (hasTsConfig || hasDependency(dependencies, ['typescript'])) buildSystems.add('tsc');
521
+ if (hasDependency(dependencies, ['rollup'])) buildSystems.add('rollup');
522
+ if (hasDependency(dependencies, ['esbuild'])) buildSystems.add('esbuild');
523
+
524
+ if (hasDependency(dependencies, ['vitest']) || Object.values(pkg?.scripts ?? {}).some((script) => /\bvitest\b/.test(String(script)))) testSystems.add('vitest');
525
+ if (hasDependency(dependencies, ['jest', '@jest/core']) || Object.values(pkg?.scripts ?? {}).some((script) => /\bjest\b/.test(String(script)))) testSystems.add('jest');
526
+ if (hasDependency(dependencies, ['@playwright/test', 'playwright'])) testSystems.add('playwright');
527
+ if (hasDependency(dependencies, ['cypress'])) testSystems.add('cypress');
528
+ if (hasDependency(dependencies, ['@vue/test-utils'])) testSystems.add('vue-test-utils');
529
+ if (hasDependency(dependencies, ['@testing-library/react', '@testing-library/vue'])) testSystems.add('testing-library');
530
+ if (localSourceFiles.some((file) => /\.(?:test|spec)\.(?:ts|tsx|js|jsx)$/.test(file.name)) && testSystems.size === 0) testSystems.add('javascript-test-runner:unknown');
531
+
532
+ if (hasDependency(dependencies, ['eslint', 'typescript-eslint', '@typescript-eslint/parser'])) configuredTools.add('eslint');
533
+ if (hasDependency(dependencies, ['eslint-plugin-vue'])) configuredTools.add('eslint-plugin-vue');
534
+ if (hasDependency(dependencies, ['prettier'])) configuredTools.add('prettier');
535
+ if (hasDependency(dependencies, ['@biomejs/biome'])) configuredTools.add('biome');
536
+ if (hasDependency(dependencies, ['oxlint'])) configuredTools.add('oxlint');
537
+ if (hasDependency(dependencies, ['vue-tsc'])) configuredTools.add('vue-tsc');
538
+
539
+ if (pkg?.bin) { applicationTypes.add('cli'); runtimes.add('node'); }
540
+ if (pkg?.exports || pkg?.main || pkg?.module || pkg?.types || pkg?.typings) applicationTypes.add('library');
541
+ if (sourceContains(localTexts, /(?:from\s+['"]node:|require\(['"](?:fs|path|http|https|stream|child_process)['"]\)|\bprocess\.(?:env|argv|cwd))/)) runtimes.add('node');
542
+ if ((frameworks.has('react') || frameworks.has('vue')) && !runtimes.has('browser')) runtimes.add('browser');
543
+ if (frameworks.has('react') && frameworks.has('vue')) conflicts.push({ type: 'multiple-ui-frameworks', detail: 'React and Vue signals exist in the same module scope.' });
544
+ }
545
+
546
+ const pomFile = byName.get('pom.xml');
547
+ const gradleFiles = [byName.get('build.gradle'), byName.get('build.gradle.kts')].filter(Boolean);
548
+ const javaFiles = localSourceFiles.filter((file) => file.ext === '.java');
549
+ const kotlinFiles = localSourceFiles.filter((file) => ['.kt', '.kts'].includes(file.ext));
550
+ const pomText = pomFile ? await readText(pomFile) : null;
551
+ const gradleTexts = [];
552
+ for (const file of gradleFiles) gradleTexts.push((await readText(file)) ?? '');
553
+ const javaText = localTexts.filter(({ file }) => file.ext === '.java').map(({ text }) => text).join('\n');
554
+ const kotlinText = localTexts.filter(({ file }) => ['.kt', '.kts'].includes(file.ext)).map(({ text }) => text).join('\n');
555
+ const combinedJavaBuild = `${pomText ?? ''}\n${gradleTexts.join('\n')}`;
556
+ const kotlinBuildSignal = /kotlin-maven-plugin|org\.jetbrains\.kotlin|kotlin\(["']jvm["']\)/.test(combinedJavaBuild);
557
+ const explicitJavaBuildSignal = /maven-compiler-plugin|<maven\.compiler\.|id\s*["']java["']|java-library|sourceCompatibility|JavaLanguageVersion/.test(combinedJavaBuild);
558
+ const hasJvmBuild = Boolean(pomFile || gradleFiles.length > 0);
559
+ if (pomFile) { buildSystems.add('maven'); moduleEvidence.push(evidence(pomFile.rel, 'Maven project')); }
560
+ if (gradleFiles.length > 0) { buildSystems.add('gradle'); moduleEvidence.push(evidence(gradleFiles[0].rel, 'Gradle project')); }
561
+ if (pomFile && gradleFiles.length > 0) conflicts.push({ type: 'multiple-java-build-systems', detail: 'Maven and Gradle build files exist in the same module scope.' });
562
+ if (hasJvmBuild || javaFiles.length > 0 || kotlinFiles.length > 0) runtimes.add('jvm');
563
+ const javaLanguageSignal = javaFiles.length > 0 || (hasJvmBuild && !kotlinBuildSignal) || explicitJavaBuildSignal;
564
+ if (javaLanguageSignal) {
565
+ languages.add('java'); adapters.add('java');
566
+ }
567
+ if (kotlinFiles.length > 0 || kotlinBuildSignal) languages.add('kotlin');
568
+ if (hasJvmBuild || javaFiles.length > 0 || kotlinFiles.length > 0) {
569
+ if (/spring-boot|org\.springframework\.boot/.test(combinedJavaBuild) || /@SpringBootApplication|SpringApplication\.run/.test(`${javaText}\n${kotlinText}`)) {
570
+ frameworks.add('spring-boot'); applicationTypes.add('service');
571
+ moduleEvidence.push(evidence(pomFile?.rel ?? gradleFiles[0]?.rel ?? javaFiles[0]?.rel ?? kotlinFiles[0]?.rel ?? modulePath, 'Spring Boot build or application signal'));
572
+ const bootVersion = combinedJavaBuild.match(/spring-boot[^<\n]*<\/artifactId>\s*<version>([^<]+)/)?.[1]
573
+ ?? combinedJavaBuild.match(/org\.springframework\.boot[^\n]*version\s*["']([^"']+)/)?.[1];
574
+ if (bootVersion) versions.springBoot = bootVersion.trim();
575
+ }
576
+ if (/junit-jupiter|org\.junit|spring-boot-starter-test/.test(combinedJavaBuild) || localFiles.some((file) => file.rel.includes('/src/test/'))) testSystems.add('junit');
577
+ if (/testcontainers/.test(combinedJavaBuild) || /org\.testcontainers/.test(`${javaText}\n${kotlinText}`)) testSystems.add('testcontainers');
578
+ if (/mockito/.test(combinedJavaBuild)) testSystems.add('mockito');
579
+ for (const [signal, tool] of [['spotless', 'spotless'], ['checkstyle', 'checkstyle'], ['pmd', 'pmd'], ['spotbugs', 'spotbugs'], ['error_prone', 'error-prone'], ['jacoco', 'jacoco'], ['archunit', 'archunit']]) {
580
+ if (combinedJavaBuild.toLowerCase().includes(signal)) configuredTools.add(tool);
581
+ }
582
+ versions.java = parseXmlTag(pomText, 'java.version') ?? parseXmlTag(pomText, 'maven.compiler.release') ?? gradleTexts.map(parseGradleJavaVersion).find(Boolean) ?? null;
583
+ }
584
+
585
+ const goModFile = byName.get('go.mod');
586
+ const goWorkFile = byName.get('go.work');
587
+ const goFiles = localSourceFiles.filter((file) => file.ext === '.go');
588
+ if (goModFile || goWorkFile || goFiles.length > 0) {
589
+ languages.add('go'); adapters.add('go'); runtimes.add('native'); buildSystems.add('go-modules');
590
+ moduleEvidence.push(evidence(goModFile?.rel ?? goWorkFile?.rel ?? goFiles[0]?.rel ?? modulePath, goWorkFile ? 'Go workspace/module' : 'Go module/source'));
591
+ const goModText = goModFile ? await readText(goModFile) : null;
592
+ const goWorkText = goWorkFile ? await readText(goWorkFile) : null;
593
+ versions.go = (goModText ?? goWorkText)?.match(/^go\s+([^\s]+)$/m)?.[1] ?? null;
594
+ versions.goToolchain = (goModText ?? goWorkText)?.match(/^toolchain\s+([^\s]+)$/m)?.[1] ?? null;
595
+ const goTexts = localTexts.filter(({ file }) => file.ext === '.go');
596
+ const mainFiles = goTexts.filter(({ text }) => /^package\s+main\b/m.test(text));
597
+ if (mainFiles.length > 0) applicationTypes.add('cli');
598
+ if (mainFiles.some(({ file, text }) => /(^|\/)(?:cmd\/)?(?:api|server|service|worker)(\/|$)/.test(relativeToModule(file.rel, modulePath)) || /net\/http|grpc|ListenAndServe/.test(text))) applicationTypes.add('service');
599
+ if (goFiles.some((file) => file.name.endsWith('_test.go'))) testSystems.add('go-test');
600
+ if (localFiles.some((file) => /^\.golangci\.(?:ya?ml|toml|json)$/.test(file.name))) configuredTools.add('golangci-lint');
601
+ if (localFiles.some((file) => /staticcheck/.test(file.name))) configuredTools.add('staticcheck');
602
+ if (goTexts.some(({ text }) => /go:generate/.test(text))) configuredTools.add('go-generate');
603
+ }
604
+
605
+ const cargoFile = byName.get('Cargo.toml');
606
+ const rustFiles = localSourceFiles.filter((file) => file.ext === '.rs');
607
+ if (cargoFile || rustFiles.length > 0) {
608
+ languages.add('rust'); adapters.add('rust'); runtimes.add('native'); buildSystems.add('cargo'); testSystems.add('cargo-test');
609
+ moduleEvidence.push(evidence(cargoFile?.rel ?? rustFiles[0]?.rel ?? modulePath, 'Cargo package/workspace or Rust source'));
610
+ const cargoText = cargoFile ? await readText(cargoFile) : null;
611
+ versions.rustEdition = parseTomlValue(cargoText, 'edition');
612
+ versions.rustVersion = parseTomlValue(cargoText, 'rust-version');
613
+ if (cargoText && /^\s*\[workspace\]/m.test(cargoText)) applicationTypes.add('workspace-root');
614
+ if (rustFiles.some((file) => relativeToModule(file.rel, modulePath) === 'src/lib.rs')) applicationTypes.add('library');
615
+ if (rustFiles.some((file) => relativeToModule(file.rel, modulePath) === 'src/main.rs' || relativeToModule(file.rel, modulePath).startsWith('src/bin/'))) applicationTypes.add('cli');
616
+ if (sourceContains(localTexts.filter(({ file }) => file.ext === '.rs'), /\bunsafe\b/)) configuredTools.add('unsafe-rust-present');
617
+ if (cargoText && /tokio|async-std|smol/.test(cargoText)) applicationTypes.add('async-application');
618
+ if (rootFilesByRel.has(modulePath === '.' ? 'rust-toolchain.toml' : `${modulePath}/rust-toolchain.toml`)) configuredTools.add('rust-toolchain');
619
+ configuredTools.add('rustfmt'); configuredTools.add('clippy');
620
+ }
621
+
622
+ // Pending adapters are evidence only; no language-specific rules are fabricated.
623
+ const otherLanguageMap = new Map([
624
+ ['.py', 'python'], ['.kt', 'kotlin'], ['.kts', 'kotlin'], ['.cs', 'csharp'],
625
+ ['.c', 'c'], ['.cc', 'cpp'], ['.cpp', 'cpp'], ['.swift', 'swift'], ['.rb', 'ruby'],
626
+ ['.php', 'php'], ['.scala', 'scala'], ['.ex', 'elixir'], ['.exs', 'elixir'],
627
+ ]);
628
+ for (const file of localSourceFiles) {
629
+ const language = otherLanguageMap.get(file.ext);
630
+ if (language) languages.add(language);
631
+ }
632
+
633
+ const sourceRoots = new Set();
634
+ for (const language of languages) addSortedUnique(sourceRoots, detectSourceRoots(localFiles, modulePath, language));
635
+ const testRoots = detectTestRoots(localFiles, modulePath);
636
+ const publicEntrypoints = publicEntrypointsForPackage(pkg, localFiles, modulePath);
637
+ const generatedPaths = new Set();
638
+ for (const file of localFiles) {
639
+ const rel = relativeToModule(file.rel, modulePath);
640
+ const pathSignal = /(?:^|\/)(?:generated|gen)(?:\/|$)/i.test(rel);
641
+ const markerSignal = SOURCE_EXTENSIONS.has(file.ext)
642
+ ? /(?:^|\n)\/\/ Code generated .* DO NOT EDIT\.(?:\n|$)/.test((await readText(file)) ?? '')
643
+ : false;
644
+ if (pathSignal || markerSignal) {
645
+ const dir = path.posix.dirname(rel);
646
+ generatedPaths.add(dir === '.' ? rel : dir);
647
+ }
648
+ }
649
+
650
+ const qualityGates = [];
651
+ if (pkg?.scripts && typeof pkg.scripts === 'object') {
652
+ for (const [name, command] of Object.entries(pkg.scripts)) {
653
+ if (!['string'].includes(typeof command)) continue;
654
+ const responsibility = /format/.test(name) ? 'format'
655
+ : /lint/.test(name) ? 'lint'
656
+ : /type|check/.test(name) ? 'compile-or-check'
657
+ : /test/.test(name) ? 'test'
658
+ : /build|package/.test(name) ? 'build-or-package'
659
+ : null;
660
+ if (responsibility) qualityGates.push({ name, command, responsibility, source: packageFile?.rel ?? 'package.json', status: 'configured' });
661
+ }
662
+ }
663
+
664
+ const lockNames = localFiles.filter((file) => ['pnpm-lock.yaml', 'yarn.lock', 'package-lock.json', 'npm-shrinkwrap.json', 'bun.lock', 'bun.lockb'].includes(file.name)).map((file) => file.name);
665
+ if (new Set(lockNames).size > 1) conflicts.push({ type: 'multiple-package-manager-lockfiles', detail: sorted(new Set(lockNames)).join(', ') });
666
+ if (languages.size === 0 && moduleFiles.length > 0) unknowns.push({ type: 'aggregator-or-unknown-language', detail: 'Build/manifest exists without local editable source signals.' });
667
+ if (sourceRoots.size === 0 && localSourceFiles.length > 0) unknowns.push({ type: 'source-root-unclassified', detail: 'Source files exist outside recognized roots; inspect representative files.' });
668
+
669
+ const id = pkg?.name
670
+ ?? parseProjectArtifactId(pomFile ? await readText(pomFile) : null, 'artifactId')
671
+ ?? (goModFile ? (await readText(goModFile))?.match(/^module\s+(.+)$/m)?.[1] : null)
672
+ ?? (cargoFile ? parseTomlValue(await readText(cargoFile), 'name') : null)
673
+ ?? (modulePath === '.' ? 'root' : path.posix.basename(modulePath));
674
+
675
+ const extensionCounts = {};
676
+ for (const file of localSourceFiles) extensionCounts[file.ext] = (extensionCounts[file.ext] ?? 0) + 1;
677
+
678
+ const confidence = moduleEvidence.some((item) => item.confidence === 'high') ? 'high' : moduleEvidence.length > 0 ? 'medium' : 'low';
679
+ return {
680
+ id,
681
+ path: modulePath,
682
+ role: languages.size === 0 ? 'aggregator-or-configuration' : 'editable-module',
683
+ languages: sorted(languages),
684
+ adapters: sorted(adapters),
685
+ frameworks: sorted(frameworks),
686
+ runtimes: sorted(runtimes),
687
+ applicationTypes: sorted(applicationTypes),
688
+ buildSystems: sorted(buildSystems),
689
+ packageManagers: sorted(packageManagers),
690
+ testSystems: sorted(testSystems),
691
+ configuredTools: sorted(configuredTools),
692
+ versions: Object.fromEntries(Object.entries(versions).filter(([, value]) => value !== null && value !== undefined).sort(([a], [b]) => a.localeCompare(b, 'en'))),
693
+ sourceRoots: sorted(sourceRoots),
694
+ testRoots: sorted(testRoots),
695
+ publicEntrypoints: sorted(publicEntrypoints),
696
+ generatedPaths: sorted(generatedPaths),
697
+ qualityGates: stableSortObjects(qualityGates, ['name', 'command']),
698
+ sourceFileCounts: Object.fromEntries(Object.entries(extensionCounts).sort(([a], [b]) => a.localeCompare(b, 'en'))),
699
+ representativeFiles: localSourceFiles.map((file) => relativeToModule(file.rel, modulePath)).sort((a, b) => a.localeCompare(b, 'en')).slice(0, 20),
700
+ evidence: stableSortObjects(moduleEvidence, ['path', 'signal']),
701
+ confidence,
702
+ conflicts: stableSortObjects(conflicts, ['type', 'detail']),
703
+ unknowns: stableSortObjects(unknowns, ['type', 'detail']),
704
+ };
705
+ }
706
+
707
+ function findWorkspaceSignals(files, textsByRel) {
708
+ const signals = [];
709
+ const add = (pathValue, signal) => signals.push({ path: pathValue, signal, confidence: 'high' });
710
+ if (files.some((file) => file.rel === 'pnpm-workspace.yaml')) add('pnpm-workspace.yaml', 'pnpm workspace');
711
+ if (files.some((file) => file.rel === 'lerna.json')) add('lerna.json', 'Lerna workspace');
712
+ if (files.some((file) => file.rel === 'nx.json')) add('nx.json', 'Nx workspace');
713
+ if (files.some((file) => file.rel === 'turbo.json')) add('turbo.json', 'Turborepo workspace');
714
+ if (files.some((file) => file.rel === 'go.work')) add('go.work', 'Go workspace');
715
+ const rootPackage = textsByRel.get('package.json');
716
+ if (rootPackage) {
717
+ try {
718
+ const pkg = JSON.parse(rootPackage);
719
+ if (pkg.workspaces) add('package.json', 'package workspaces');
720
+ } catch { /* parse error recorded elsewhere */ }
721
+ }
722
+ const rootCargo = textsByRel.get('Cargo.toml');
723
+ if (rootCargo && /^\s*\[workspace\]/m.test(rootCargo)) add('Cargo.toml', 'Cargo workspace');
724
+ const settings = textsByRel.get('settings.gradle') ?? textsByRel.get('settings.gradle.kts');
725
+ if (settings && /\binclude\s*\(?/.test(settings)) add(textsByRel.has('settings.gradle') ? 'settings.gradle' : 'settings.gradle.kts', 'Gradle multi-project');
726
+ const pom = textsByRel.get('pom.xml');
727
+ if (pom && /<modules>/.test(pom)) add('pom.xml', 'Maven multi-module aggregator');
728
+ return stableSortObjects(signals, ['path', 'signal']);
729
+ }
730
+
731
+ function classifyGateCommand(command) {
732
+ const value = command.toLowerCase();
733
+ if (/\b(?:prettier|biome|gofmt|rustfmt|cargo\s+fmt|spotless|google-java-format)\b/.test(value)) return 'format';
734
+ if (/\b(?:eslint|oxlint|golangci-lint|staticcheck|go\s+vet|cargo\s+clippy|checkstyle|pmd|spotbugs|errorprone|error-prone)\b/.test(value)) return 'lint-or-static-analysis';
735
+ if (/\b(?:tsc|vue-tsc|cargo\s+check|javac)\b/.test(value)) return 'compile-or-typecheck';
736
+ if (/\b(?:vitest|jest|playwright|cypress|go\s+test|cargo\s+test|mvnw?.*\btest\b|gradlew?.*\btest\b|pytest|junit)\b/.test(value)) return 'test';
737
+ if (/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:build|package)\b|\b(?:cargo|go)\s+build\b|\b(?:mvnw?|gradlew?)\b/.test(value)) return 'build-or-package';
738
+ if (/\b(?:govulncheck|cargo\s+(?:audit|deny|vet)|npm\s+audit|pnpm\s+audit|dependency-check)\b/.test(value)) return 'security-or-dependency-audit';
739
+ return null;
740
+ }
741
+
742
+ function cleanCiCommand(raw) {
743
+ return raw.trim()
744
+ .replace(/^[-]\s+/, '')
745
+ .replace(/^(?:run|command|script):\s*/i, '')
746
+ .replace(/^['"]|['"]$/g, '')
747
+ .trim();
748
+ }
749
+
750
+ async function extractCiCommands(ciFileObjects, readText) {
751
+ const commands = [];
752
+ const seen = new Set();
753
+ const push = (file, line, raw) => {
754
+ const command = cleanCiCommand(raw);
755
+ const responsibility = classifyGateCommand(command);
756
+ if (!command || !responsibility) return;
757
+ const key = `${file.rel}\0${command}`;
758
+ if (seen.has(key)) return;
759
+ seen.add(key);
760
+ commands.push({
761
+ command,
762
+ responsibility,
763
+ source: file.rel,
764
+ line,
765
+ status: 'active-in-ci',
766
+ confidence: 'high',
767
+ });
768
+ };
769
+ for (const file of ciFileObjects) {
770
+ const text = await readText(file);
771
+ if (!text) continue;
772
+ const lines = text.split(/\r?\n/);
773
+ for (let index = 0; index < lines.length; index += 1) {
774
+ const line = lines[index];
775
+ const run = line.match(/^(\s*)(?:-\s*)?run:\s*(.*)$/);
776
+ if (run) {
777
+ if (['|', '>', '|-', '>-', ''].includes(run[2].trim())) {
778
+ const baseIndent = run[1].length;
779
+ const block = [];
780
+ for (let next = index + 1; next < lines.length; next += 1) {
781
+ const candidate = lines[next];
782
+ if (!candidate.trim()) { block.push(''); continue; }
783
+ const indent = candidate.match(/^\s*/)?.[0].length ?? 0;
784
+ if (indent <= baseIndent) break;
785
+ block.push(candidate.trim());
786
+ index = next;
787
+ }
788
+ for (const command of block.join('\n').split(/\n|&&|;/)) push(file, index + 1, command);
789
+ } else push(file, index + 1, run[2]);
790
+ continue;
791
+ }
792
+ const yamlList = line.match(/^\s*-\s+(.+)$/);
793
+ if (yamlList) push(file, index + 1, yamlList[1]);
794
+ for (const match of line.matchAll(/\bsh\s*\(?\s*['"]([^'"]+)['"]/g)) push(file, index + 1, match[1]);
795
+ const command = line.match(/^\s*command:\s*(.+)$/);
796
+ if (command) push(file, index + 1, command[1]);
797
+ }
798
+ }
799
+ return stableSortObjects(commands, ['source', 'line', 'command']);
800
+ }
801
+
802
+ function classifyTopology(modules, workspaceSignals) {
803
+ if (modules.length <= 1) return 'single-project';
804
+ const adapters = new Set(modules.flatMap((module) => module.adapters));
805
+ if (adapters.size > 1) return 'polyglot-monorepo';
806
+ if (workspaceSignals.length > 0) return 'workspace';
807
+ if (modules.every((module) => module.languages.includes('java'))) return 'multi-module';
808
+ return 'monorepo';
809
+ }
810
+
811
+ async function discover(args) {
812
+ const rootAbs = path.resolve(args.root);
813
+ let rootReal;
814
+ try {
815
+ rootReal = await realpath(rootAbs);
816
+ } catch (error) {
817
+ throw new Error(`cannot resolve --root ${args.root}: ${error.message}`);
818
+ }
819
+ const rootInfo = await stat(rootReal);
820
+ if (!rootInfo.isDirectory()) throw new Error(`--root is not a directory: ${args.root}`);
821
+ const outputAbs = args.output ? await resolveSafeOutput(rootReal, args.output) : null;
822
+ const managedRoots = [
823
+ path.join(rootReal, '.agents', 'skills'),
824
+ path.join(rootReal, '.claude', 'skills'),
825
+ path.join(rootReal, '.codex', 'skills'),
826
+ ];
827
+ if (await pathIsDirectory(path.join(rootReal, 'speculo', '.speculo'))) {
828
+ managedRoots.push(path.join(rootReal, 'speculo', 'skills'));
829
+ managedRoots.push(path.join(rootReal, 'speculo', 'commands'));
830
+ managedRoots.push(path.join(rootReal, 'speculo', 'workflows'));
831
+ }
832
+ const currentSkillRoot = await realpath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'));
833
+ if (isPathInside(rootReal, currentSkillRoot)) managedRoots.push(currentSkillRoot);
834
+ const excludedRoots = [];
835
+ for (const candidate of managedRoots) {
836
+ if (await pathIsDirectory(candidate) && !excludedRoots.some((root) => root === candidate)) excludedRoots.push(candidate);
837
+ }
838
+ const managedSkillEntrypoints = await collectManagedSkillEntrypoints(rootReal, excludedRoots);
839
+ const walk = await walkFiles(rootReal, args, outputAbs, excludedRoots);
840
+ const readText = await createReader(args.maxBytes, walk.limitations);
841
+ const filesByRel = new Map(walk.files.map((file) => [file.rel, file]));
842
+
843
+ const rootTextNames = new Set(['package.json', 'pom.xml', 'settings.gradle', 'settings.gradle.kts', 'Cargo.toml', 'go.work']);
844
+ const textsByRel = new Map();
845
+ for (const name of rootTextNames) {
846
+ const file = filesByRel.get(name);
847
+ if (file) textsByRel.set(name, await readText(file));
848
+ }
849
+
850
+ const moduleFiles = findCandidateModulePaths(walk.files);
851
+ const { assigned, unscoped } = assignFilesToModules(walk.files, [...moduleFiles.keys()]);
852
+ const modules = [];
853
+ for (const modulePath of [...moduleFiles.keys()].sort((a, b) => a.localeCompare(b, 'en'))) {
854
+ modules.push(await analyzeModule(modulePath, moduleFiles.get(modulePath), assigned.get(modulePath), readText, filesByRel));
855
+ }
856
+
857
+ const workspaceSignals = findWorkspaceSignals(walk.files, textsByRel);
858
+ const rootModule = modules.find((module) => module.path === '.');
859
+ if (rootModule && rootModule.packageManagers.length > 0 && workspaceSignals.some((item) => /package|pnpm|Lerna|Nx|Turborepo/i.test(item.signal))) {
860
+ for (const module of modules) {
861
+ if (module.path === '.' || !module.adapters.includes('typescript') || module.packageManagers.length > 0) continue;
862
+ module.packageManagers = [...rootModule.packageManagers];
863
+ module.evidence.push(evidence('package.json', `Inherited workspace package manager: ${rootModule.packageManagers.join(', ')}`, 'high'));
864
+ stableSortObjects(module.evidence, ['path', 'signal']);
865
+ }
866
+ }
867
+ const allLanguages = new Set(modules.flatMap((module) => module.languages));
868
+ const allAdapters = new Set(modules.flatMap((module) => module.adapters));
869
+ const allFrameworks = new Set(modules.flatMap((module) => module.frameworks));
870
+ const allRuntimes = new Set(modules.flatMap((module) => module.runtimes));
871
+ const allBuildSystems = new Set(modules.flatMap((module) => module.buildSystems));
872
+ const allTestSystems = new Set(modules.flatMap((module) => module.testSystems));
873
+
874
+ const existingStandards = sorted(new Set([
875
+ ...managedSkillEntrypoints,
876
+ ...walk.files
877
+ .filter((file) => /(^|\/)(?:AGENTS|CLAUDE|CONTRIBUTING)\.md$/i.test(file.rel)
878
+ || /(^|\/)(?:docs\/)?(?:adr|architecture|standards?)(\/|\.|$)/i.test(file.rel))
879
+ .map((file) => file.rel),
880
+ ]));
881
+
882
+ const ciFileObjects = walk.files
883
+ .filter((file) => /(^|\/)(?:\.github\/workflows\/.*\.ya?ml|\.gitlab-ci\.yml|Jenkinsfile|azure-pipelines\.ya?ml|\.circleci\/config\.yml)$/.test(file.rel));
884
+ const ciFiles = ciFileObjects.map((file) => file.rel).sort((a, b) => a.localeCompare(b, 'en'));
885
+ const ciCommands = await extractCiCommands(ciFileObjects, readText);
886
+
887
+ const conflicts = stableSortObjects(modules.flatMap((module) => module.conflicts.map((item) => ({ ...item, scope: `module:${module.path}` }))), ['scope', 'type', 'detail']);
888
+ const unknowns = stableSortObjects(modules.flatMap((module) => module.unknowns.map((item) => ({ ...item, scope: `module:${module.path}` }))), ['scope', 'type', 'detail']);
889
+ if (ciFiles.length === 0) unknowns.push({ scope: 'repository', type: 'ci-not-detected', detail: 'No supported CI configuration was detected; inspect custom automation and documentation.' });
890
+ if (unscoped.some((file) => SOURCE_EXTENSIONS.has(file.ext))) unknowns.push({ scope: 'repository', type: 'unscoped-source', detail: 'Editable source exists outside detected module roots.' });
891
+
892
+ const inventory = {
893
+ schemaVersion: SCHEMA_VERSION,
894
+ root: rootReal,
895
+ topology: classifyTopology(modules, workspaceSignals),
896
+ summary: {
897
+ modules: modules.length,
898
+ languages: sorted(allLanguages),
899
+ adapters: sorted(allAdapters),
900
+ frameworks: sorted(allFrameworks),
901
+ runtimes: sorted(allRuntimes),
902
+ buildSystems: sorted(allBuildSystems),
903
+ testSystems: sorted(allTestSystems),
904
+ },
905
+ workspaceSignals,
906
+ modules,
907
+ repositoryQualityGates: stableSortObjects([
908
+ ...modules.flatMap((module) => module.qualityGates.map((gate) => ({ ...gate, scope: `module:${module.path}` }))),
909
+ ...ciCommands.map((gate) => ({ ...gate, name: gate.responsibility, scope: 'repository' })),
910
+ ], ['scope', 'name', 'command']),
911
+ ciFiles,
912
+ ciCommands,
913
+ existingStandards,
914
+ conflicts,
915
+ unknowns: stableSortObjects(unknowns, ['scope', 'type', 'detail']),
916
+ scan: {
917
+ visitedFiles: walk.files.length,
918
+ sourceFiles: walk.files.filter((file) => SOURCE_EXTENSIONS.has(file.ext)).length,
919
+ skippedSymlinks: walk.skippedSymlinks,
920
+ limitations: stableSortObjects(walk.limitations, ['type', 'path']),
921
+ truncated: walk.truncated,
922
+ limits: { maxDepth: args.maxDepth, maxFiles: args.maxFiles, maxBytes: args.maxBytes },
923
+ ignoredDirectoryNames: sorted(DEFAULT_IGNORED_DIRECTORIES),
924
+ excludedManagedRoots: excludedRoots.map((item) => normalizeRel(path.relative(rootReal, item))).sort((a, b) => a.localeCompare(b, 'en')),
925
+ },
926
+ };
927
+ return { inventory, outputAbs };
928
+ }
929
+
930
+ async function main() {
931
+ let args;
932
+ try {
933
+ args = parseArgs(process.argv.slice(2));
934
+ } catch (error) {
935
+ fail(`${error.message}\n\n${usage()}`, 2);
936
+ return;
937
+ }
938
+ if (args.help) {
939
+ process.stdout.write(usage());
940
+ return;
941
+ }
942
+ try {
943
+ const { inventory, outputAbs } = await discover(args);
944
+ const json = `${JSON.stringify(inventory, null, args.pretty ? 2 : 0)}\n`;
945
+ if (outputAbs) {
946
+ await mkdir(path.dirname(outputAbs), { recursive: true });
947
+ await writeFile(outputAbs, json, { encoding: 'utf8', flag: 'w' });
948
+ process.stderr.write(`discover-project: wrote ${normalizeRel(path.relative(inventory.root, outputAbs))}\n`);
949
+ } else {
950
+ process.stdout.write(json);
951
+ }
952
+ } catch (error) {
953
+ fail(error.stack ?? error.message, 1);
954
+ }
955
+ }
956
+
957
+ await main();