create-quiver 0.16.0 → 0.17.1

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 (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +7 -1
  3. package/README_FOR_AI.md +4 -1
  4. package/docs/CLI_UX_GUIDE.md +11 -0
  5. package/docs/INDEX.md +5 -1
  6. package/docs/reference/commands.md +34 -0
  7. package/package.json +1 -1
  8. package/specs/quiver-v43-cli-i18n-audit-release-readiness/command-language-mode-matrix.json +31 -1
  9. package/specs/quiver-v46-deep-project-analysis/EVIDENCE_REPORT.md +94 -0
  10. package/specs/quiver-v46-deep-project-analysis/EXECUTION_PLAN.md +26 -0
  11. package/specs/quiver-v46-deep-project-analysis/SPEC.md +157 -0
  12. package/specs/quiver-v46-deep-project-analysis/STATUS.md +26 -0
  13. package/specs/quiver-v46-deep-project-analysis/pr.md +131 -0
  14. package/specs/quiver-v46-deep-project-analysis/slices/slice-00-analysis-contract-foundation/CLOSURE_BRIEF.md +14 -0
  15. package/specs/quiver-v46-deep-project-analysis/slices/slice-00-analysis-contract-foundation/EXECUTION_BRIEF.md +41 -0
  16. package/specs/quiver-v46-deep-project-analysis/slices/slice-00-analysis-contract-foundation/slice.json +61 -0
  17. package/specs/quiver-v46-deep-project-analysis/slices/slice-01-stack-agnostic-discovery-sampling/CLOSURE_BRIEF.md +22 -0
  18. package/specs/quiver-v46-deep-project-analysis/slices/slice-01-stack-agnostic-discovery-sampling/EXECUTION_BRIEF.md +33 -0
  19. package/specs/quiver-v46-deep-project-analysis/slices/slice-01-stack-agnostic-discovery-sampling/slice.json +80 -0
  20. package/specs/quiver-v46-deep-project-analysis/slices/slice-02-provider-analysis-json-contract/CLOSURE_BRIEF.md +21 -0
  21. package/specs/quiver-v46-deep-project-analysis/slices/slice-02-provider-analysis-json-contract/EXECUTION_BRIEF.md +34 -0
  22. package/specs/quiver-v46-deep-project-analysis/slices/slice-02-provider-analysis-json-contract/slice.json +79 -0
  23. package/specs/quiver-v46-deep-project-analysis/slices/slice-03-doc-proposal-review-safe-writes/CLOSURE_BRIEF.md +19 -0
  24. package/specs/quiver-v46-deep-project-analysis/slices/slice-03-doc-proposal-review-safe-writes/EXECUTION_BRIEF.md +33 -0
  25. package/specs/quiver-v46-deep-project-analysis/slices/slice-03-doc-proposal-review-safe-writes/slice.json +79 -0
  26. package/specs/quiver-v46-deep-project-analysis/slices/slice-04-validation-docs-release-readiness/CLOSURE_BRIEF.md +20 -0
  27. package/specs/quiver-v46-deep-project-analysis/slices/slice-04-validation-docs-release-readiness/EXECUTION_BRIEF.md +32 -0
  28. package/specs/quiver-v46-deep-project-analysis/slices/slice-04-validation-docs-release-readiness/slice.json +85 -0
  29. package/src/create-quiver/commands/ai.js +587 -0
  30. package/src/create-quiver/index.js +113 -3
  31. package/src/create-quiver/lib/ai/analyze-project-discovery.js +387 -0
  32. package/src/create-quiver/lib/ai/analyze-project-docs.js +364 -0
  33. package/src/create-quiver/lib/ai/analyze-project-parser.js +277 -0
  34. package/src/create-quiver/lib/ai/analyze-project-prompts.js +214 -0
  35. package/src/create-quiver/lib/ai/analyze-project-review.js +99 -0
  36. package/src/create-quiver/lib/ai/analyze-project-sampling.js +402 -0
  37. package/src/create-quiver/lib/ai/analyze-project-schema.js +92 -0
  38. package/src/create-quiver/lib/ai/analyze-project-validation.js +309 -0
  39. package/src/create-quiver/lib/ai/artifacts.js +4 -1
  40. package/src/create-quiver/lib/cli/command-registry.js +1 -0
  41. package/src/create-quiver/lib/i18n/messages/en.js +1 -0
  42. package/src/create-quiver/lib/i18n/messages/es.js +1 -0
  43. package/.quiver/runs/run-2026-06-09t19-14-39z/approvals.json +0 -5
  44. package/.quiver/runs/run-2026-06-09t19-14-39z/decisions.md +0 -2
  45. package/.quiver/runs/run-2026-06-09t19-14-39z/requirement.md +0 -0
  46. package/.quiver/runs/run-2026-06-09t19-14-39z/snapshots/20260609T191439Z/docs/INDEX.md +0 -97
  47. package/.quiver/runs/run-2026-06-09t19-14-39z/snapshots/20260609T191439Z/manifest.json +0 -61
  48. package/.quiver/runs/run-2026-06-09t19-14-39z/state.json +0 -28
  49. package/ACTIVE_SLICES.md +0 -43
  50. package/auditoria-ux-ui-performance-documentacion.md +0 -705
  51. package/copys-landing-page.md +0 -244
  52. package/docs/ai/ACTIVE_SLICE.md +0 -61
  53. package/pr.md +0 -154
@@ -12,6 +12,7 @@ const {
12
12
  const {
13
13
  runActiveSlice: runAiActiveSlice,
14
14
  runAgent: runAiAgent,
15
+ runAnalyzeProject: runAiAnalyzeProject,
15
16
  runApprovalStatus: runAiApprovalStatus,
16
17
  runApprove: runAiApprove,
17
18
  runDoctor: runAiDoctor,
@@ -229,6 +230,7 @@ const COMMAND_HELP_GROUPS = [
229
230
  ['ai active-slice status|reconcile', 'Inspect or dry-run reconcile local active-slice state from every supported source.'],
230
231
  ['ai status', 'Show current AI lifecycle phase, approved versions, blockers, and next command.'],
231
232
  ['ai resume', 'Resume guidance from the last valid lifecycle phase without chat memory.'],
233
+ ['ai analyze-project', 'Read and explain a bounded project sample without provider execution or writes.'],
232
234
  ['ai onboard', 'Run or print the planner onboarding prompt with a token-aware context pack.'],
233
235
  ['ai prepare-context', 'Preview or write docs-only AI context updates with assumptions and risks.'],
234
236
  ['ai agent set|list|show|doctor|repair', 'Manage, diagnose, and dry-run repair planner, executor, reviewer, and doctor provider profiles without secrets.'],
@@ -327,6 +329,7 @@ function printUsage(language = DEFAULT_LANGUAGE) {
327
329
  npx create-quiver ai active-slice reconcile --dry-run
328
330
  npx create-quiver ai status [options]
329
331
  npx create-quiver ai resume [options]
332
+ npx create-quiver ai analyze-project [--deep] [--dry-run] [--json]
330
333
  npx create-quiver ai inspect [options]
331
334
  npx create-quiver ai export [--format json|markdown]
332
335
  npx create-quiver ai specs list [--json]
@@ -386,7 +389,14 @@ ${helpText(help, 'headings', 'options', 'Options:')}
386
389
  --full ${optionDescription(help, 'Plan or run the full compatibility init profile')}
387
390
  --legacy-scripts ${optionDescription(help, 'Include legacy Bash wrappers in init profile')}
388
391
  --include-templates ${optionDescription(help, 'Export packaged templates in init profile')}
389
- --dry-run ${optionDescription(help, 'Preview init, analyze, migrate, prepare, spec create/start/close, demo, ai agent set, or AI work without executing writes/providers')}
392
+ --dry-run ${optionDescription(help, 'Preview init, analyze, migrate, prepare, spec create/start/close, demo, ai agent set, ai analyze-project, or AI work without executing writes/providers')}
393
+ --deep ${optionDescription(help, 'For ai analyze-project, include source and DB files in the read-only sample')}
394
+ --max-files <n> ${optionDescription(help, 'For ai analyze-project, maximum files in the semantic sample')}
395
+ --max-bytes <n> ${optionDescription(help, 'For ai analyze-project, maximum selected bytes in the semantic sample')}
396
+ --include-source ${optionDescription(help, 'For ai analyze-project, include source files without requiring --deep')}
397
+ --include-tests ${optionDescription(help, 'For ai analyze-project, include test files in the sample')}
398
+ --include-db ${optionDescription(help, 'For ai analyze-project, include schema, migration, and DB files in the sample')}
399
+ --scope <path|name> ${optionDescription(help, 'For ai analyze-project, restrict discovery to a repo path or workspace name')}
390
400
  --print-prompt ${optionDescription(help, 'Print the exact AI prompt and exit without executing provider CLIs')}
391
401
  --with-planner ${optionDescription(help, 'Enable planner-assisted behavior on commands that explicitly support it')}
392
402
  --interactive ${optionDescription(help, 'Enable prompts on commands that explicitly support interactive choices')}
@@ -435,6 +445,8 @@ ${helpText(help, 'headings', 'examples', 'Examples:')}
435
445
  cd ./my-project && npx create-quiver plan --json
436
446
  cd ./my-project && npx create-quiver ai onboard --dry-run
437
447
  cd ./my-project && npx create-quiver ai onboard --print-prompt
448
+ cd ./my-project && npx create-quiver ai analyze-project --deep --dry-run
449
+ cd ./my-project && npx create-quiver ai analyze-project --deep --json
438
450
  cd ./my-project && npx create-quiver ai prepare-context --dry-run
439
451
  cd ./my-project && npx create-quiver ai run create --input requirements.md
440
452
  cd ./my-project && npx create-quiver ai active-slice reconcile --dry-run
@@ -563,6 +575,14 @@ function parseArgs(argv, options = {}) {
563
575
  aiInput: '',
564
576
  aiSlice: '',
565
577
  aiTimeout: null,
578
+ aiAnalyzeDeep: false,
579
+ aiAnalyzeIncludeDb: false,
580
+ aiAnalyzeIncludeSource: false,
581
+ aiAnalyzeIncludeTests: false,
582
+ aiAnalyzeMaxBytes: null,
583
+ aiAnalyzeMaxFiles: null,
584
+ aiAnalyzeScope: '',
585
+ aiAnalyzeOptionUsed: false,
566
586
  aiCommit: false,
567
587
  aiAllowDirty: false,
568
588
  aiExecute: false,
@@ -719,6 +739,68 @@ function parseArgs(argv, options = {}) {
719
739
  continue;
720
740
  }
721
741
 
742
+ if (arg === '--deep') {
743
+ result.aiAnalyzeDeep = true;
744
+ result.aiAnalyzeOptionUsed = true;
745
+ continue;
746
+ }
747
+
748
+ if (arg === '--include-source') {
749
+ result.aiAnalyzeIncludeSource = true;
750
+ result.aiAnalyzeOptionUsed = true;
751
+ continue;
752
+ }
753
+
754
+ if (arg === '--include-tests') {
755
+ result.aiAnalyzeIncludeTests = true;
756
+ result.aiAnalyzeOptionUsed = true;
757
+ continue;
758
+ }
759
+
760
+ if (arg === '--include-db') {
761
+ result.aiAnalyzeIncludeDb = true;
762
+ result.aiAnalyzeOptionUsed = true;
763
+ continue;
764
+ }
765
+
766
+ if (arg === '--max-files') {
767
+ const value = args[++index];
768
+ if (typeof value === 'undefined') {
769
+ throw new Error(formatError('missing value for --max-files'));
770
+ }
771
+ const parsed = Number.parseInt(value, 10);
772
+ if (!Number.isInteger(parsed) || parsed <= 0) {
773
+ throw new Error(formatError('invalid value for --max-files'));
774
+ }
775
+ result.aiAnalyzeMaxFiles = parsed;
776
+ result.aiAnalyzeOptionUsed = true;
777
+ continue;
778
+ }
779
+
780
+ if (arg === '--max-bytes') {
781
+ const value = args[++index];
782
+ if (typeof value === 'undefined') {
783
+ throw new Error(formatError('missing value for --max-bytes'));
784
+ }
785
+ const parsed = Number.parseInt(value, 10);
786
+ if (!Number.isInteger(parsed) || parsed <= 0) {
787
+ throw new Error(formatError('invalid value for --max-bytes'));
788
+ }
789
+ result.aiAnalyzeMaxBytes = parsed;
790
+ result.aiAnalyzeOptionUsed = true;
791
+ continue;
792
+ }
793
+
794
+ if (arg === '--scope') {
795
+ const value = args[++index];
796
+ if (!value || String(value).startsWith('--')) {
797
+ throw new Error(formatError('missing value for --scope'));
798
+ }
799
+ result.aiAnalyzeScope = value;
800
+ result.aiAnalyzeOptionUsed = true;
801
+ continue;
802
+ }
803
+
722
804
  if (arg === '--print-prompt') {
723
805
  result.aiPrintPrompt = true;
724
806
  continue;
@@ -1178,6 +1260,10 @@ function parseArgs(argv, options = {}) {
1178
1260
  positional.push(arg);
1179
1261
  }
1180
1262
 
1263
+ if (result.aiAnalyzeOptionUsed && result.mode !== 'ai') {
1264
+ throw new Error(formatError('analysis flags are only supported by ai analyze-project'));
1265
+ }
1266
+
1181
1267
  if (result.mode === 'init') {
1182
1268
  if (!result.projectName && positional.length > 0) {
1183
1269
  result.projectName = positional.shift();
@@ -1232,6 +1318,9 @@ function parseArgs(argv, options = {}) {
1232
1318
  if (!result.aiCommand && positional.length > 0) {
1233
1319
  result.aiCommand = positional.shift();
1234
1320
  }
1321
+ if (result.aiAnalyzeOptionUsed && result.aiCommand !== 'analyze-project') {
1322
+ throw new Error(formatError('analysis flags are only supported by ai analyze-project'));
1323
+ }
1235
1324
  if (result.aiCommand === 'agent') {
1236
1325
  if (!result.aiAgentCommand && positional.length > 0) {
1237
1326
  result.aiAgentCommand = positional.shift();
@@ -3420,7 +3509,28 @@ async function run(argv) {
3420
3509
 
3421
3510
  if (args.mode === 'ai') {
3422
3511
  if (!args.aiCommand) {
3423
- throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | models | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
3512
+ throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai analyze-project | onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | models | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
3513
+ }
3514
+
3515
+ if (args.aiCommand === 'analyze-project') {
3516
+ await runAiAnalyzeProject(process.cwd(), {
3517
+ deep: args.aiAnalyzeDeep,
3518
+ dryRun: args.dryRun,
3519
+ force: args.force,
3520
+ includeDb: args.aiAnalyzeIncludeDb,
3521
+ includeSource: args.aiAnalyzeIncludeSource,
3522
+ includeTests: args.aiAnalyzeIncludeTests,
3523
+ interactive: args.interactive,
3524
+ json: args.json,
3525
+ language: args.language,
3526
+ maxBytes: args.aiAnalyzeMaxBytes || undefined,
3527
+ maxFiles: args.aiAnalyzeMaxFiles || undefined,
3528
+ printPrompt: args.aiPrintPrompt,
3529
+ review: args.review,
3530
+ scope: args.aiAnalyzeScope || undefined,
3531
+ strict: args.strict,
3532
+ });
3533
+ return;
3424
3534
  }
3425
3535
 
3426
3536
  if (args.aiCommand === 'run') {
@@ -3738,7 +3848,7 @@ async function run(argv) {
3738
3848
  return;
3739
3849
  }
3740
3850
 
3741
- throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, models, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
3851
+ throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: analyze-project, onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, models, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
3742
3852
  }
3743
3853
 
3744
3854
  if (args.mode === 'graph') {
@@ -0,0 +1,387 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { getContextPathExclusionReason } = require('./safety');
5
+ const { classifyProjectFile, summarizeByReason } = require('./analyze-project-sampling');
6
+
7
+ const BINARY_EXTENSIONS = new Set([
8
+ '.7z',
9
+ '.avif',
10
+ '.bmp',
11
+ '.class',
12
+ '.dll',
13
+ '.dmg',
14
+ '.doc',
15
+ '.docx',
16
+ '.eot',
17
+ '.exe',
18
+ '.gif',
19
+ '.gz',
20
+ '.ico',
21
+ '.jar',
22
+ '.jpeg',
23
+ '.jpg',
24
+ '.lockb',
25
+ '.mov',
26
+ '.mp3',
27
+ '.mp4',
28
+ '.otf',
29
+ '.pdf',
30
+ '.png',
31
+ '.so',
32
+ '.tar',
33
+ '.ttf',
34
+ '.wasm',
35
+ '.webm',
36
+ '.webp',
37
+ '.woff',
38
+ '.woff2',
39
+ '.zip',
40
+ ]);
41
+
42
+ function toPosix(filePath) {
43
+ return String(filePath || '').replace(/\\/g, '/');
44
+ }
45
+
46
+ function relativePath(from, to) {
47
+ const relative = toPosix(path.relative(from, to));
48
+ return relative || '.';
49
+ }
50
+
51
+ function isInside(parent, child) {
52
+ const relative = path.relative(parent, child);
53
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
54
+ }
55
+
56
+ function safeReadJson(filePath) {
57
+ try {
58
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
59
+ } catch (_) {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function detectPackageManager(repoRoot) {
65
+ const checks = [
66
+ ['pnpm', 'pnpm-lock.yaml'],
67
+ ['yarn', 'yarn.lock'],
68
+ ['npm', 'package-lock.json'],
69
+ ['bun', 'bun.lockb'],
70
+ ];
71
+ for (const [manager, fileName] of checks) {
72
+ if (fs.existsSync(path.join(repoRoot, fileName))) {
73
+ return manager;
74
+ }
75
+ }
76
+ const pkg = safeReadJson(path.join(repoRoot, 'package.json'));
77
+ if (pkg && typeof pkg.packageManager === 'string') {
78
+ return pkg.packageManager.split('@')[0] || 'unknown';
79
+ }
80
+ return 'unknown';
81
+ }
82
+
83
+ function readPackageJson(repoRoot, relativeDir = '.') {
84
+ const packagePath = path.join(repoRoot, relativeDir, 'package.json');
85
+ const pkg = fs.existsSync(packagePath) ? safeReadJson(packagePath) : null;
86
+ return pkg && typeof pkg === 'object' ? pkg : null;
87
+ }
88
+
89
+ function expandWorkspacePattern(repoRoot, pattern) {
90
+ const normalized = toPosix(pattern).trim();
91
+ if (!normalized || normalized.startsWith('!')) {
92
+ return [];
93
+ }
94
+
95
+ const withoutRecursive = normalized.replace(/\/\*\*.*$/, '');
96
+ if (!withoutRecursive.includes('*')) {
97
+ const absolute = path.resolve(repoRoot, withoutRecursive);
98
+ return fs.existsSync(absolute) && fs.statSync(absolute).isDirectory() ? [relativePath(repoRoot, absolute)] : [];
99
+ }
100
+
101
+ const parts = withoutRecursive.split('/');
102
+ const starIndex = parts.indexOf('*');
103
+ if (starIndex === -1) {
104
+ return [];
105
+ }
106
+
107
+ const baseRelative = parts.slice(0, starIndex).join('/') || '.';
108
+ const suffix = parts.slice(starIndex + 1);
109
+ const baseAbsolute = path.resolve(repoRoot, baseRelative);
110
+ if (!fs.existsSync(baseAbsolute) || !fs.statSync(baseAbsolute).isDirectory()) {
111
+ return [];
112
+ }
113
+
114
+ const results = [];
115
+ for (const entry of fs.readdirSync(baseAbsolute, { withFileTypes: true })) {
116
+ if (!entry.isDirectory()) {
117
+ continue;
118
+ }
119
+ const workspaceAbsolute = path.join(baseAbsolute, entry.name, ...suffix);
120
+ if (fs.existsSync(workspaceAbsolute) && fs.statSync(workspaceAbsolute).isDirectory()) {
121
+ results.push(relativePath(repoRoot, workspaceAbsolute));
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+
127
+ function readPnpmWorkspacePatterns(repoRoot) {
128
+ const filePath = path.join(repoRoot, 'pnpm-workspace.yaml');
129
+ if (!fs.existsSync(filePath)) {
130
+ return [];
131
+ }
132
+ const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
133
+ return lines
134
+ .map((line) => line.trim().match(/^-\s+['"]?([^'"]+)['"]?$/))
135
+ .filter(Boolean)
136
+ .map((match) => match[1]);
137
+ }
138
+
139
+ function discoverWorkspaceRoots(repoRoot) {
140
+ const rootPackage = readPackageJson(repoRoot);
141
+ const patterns = [];
142
+ const sources = new Map();
143
+
144
+ if (rootPackage && Array.isArray(rootPackage.workspaces)) {
145
+ for (const pattern of rootPackage.workspaces) {
146
+ patterns.push(pattern);
147
+ sources.set(pattern, 'package.json#workspaces');
148
+ }
149
+ } else if (rootPackage && rootPackage.workspaces && Array.isArray(rootPackage.workspaces.packages)) {
150
+ for (const pattern of rootPackage.workspaces.packages) {
151
+ patterns.push(pattern);
152
+ sources.set(pattern, 'package.json#workspaces.packages');
153
+ }
154
+ }
155
+
156
+ for (const pattern of readPnpmWorkspacePatterns(repoRoot)) {
157
+ patterns.push(pattern);
158
+ sources.set(pattern, 'pnpm-workspace.yaml');
159
+ }
160
+
161
+ for (const conventional of ['apps/*', 'packages/*']) {
162
+ if (fs.existsSync(path.join(repoRoot, conventional.split('/')[0]))) {
163
+ patterns.push(conventional);
164
+ sources.set(conventional, 'convention');
165
+ }
166
+ }
167
+
168
+ const rootsByPath = new Map();
169
+ rootsByPath.set('.', {
170
+ name: rootPackage?.name || path.basename(repoRoot),
171
+ path: '.',
172
+ source: 'repo-root',
173
+ });
174
+
175
+ for (const pattern of patterns) {
176
+ for (const relative of expandWorkspacePattern(repoRoot, pattern)) {
177
+ const pkg = readPackageJson(repoRoot, relative);
178
+ rootsByPath.set(relative, {
179
+ name: pkg?.name || path.basename(relative),
180
+ path: relative,
181
+ source: sources.get(pattern) || 'workspace-pattern',
182
+ });
183
+ }
184
+ }
185
+
186
+ return [...rootsByPath.values()].sort((a, b) => a.path.localeCompare(b.path));
187
+ }
188
+
189
+ function resolveAnalysisRoot(repoRoot, scope, workspaceRoots) {
190
+ if (!scope) {
191
+ return {
192
+ absolutePath: repoRoot,
193
+ relativePath: '.',
194
+ source: 'repo-root',
195
+ };
196
+ }
197
+
198
+ const matchingWorkspace = workspaceRoots.find((workspace) => workspace.name === scope || workspace.path === scope);
199
+ if (matchingWorkspace) {
200
+ return {
201
+ absolutePath: path.resolve(repoRoot, matchingWorkspace.path),
202
+ relativePath: matchingWorkspace.path,
203
+ source: 'workspace-scope',
204
+ };
205
+ }
206
+
207
+ const absolutePath = path.resolve(repoRoot, scope);
208
+ if (!isInside(repoRoot, absolutePath)) {
209
+ throw new Error(`scope is outside the repository: ${scope}`);
210
+ }
211
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) {
212
+ throw new Error(`scope directory does not exist: ${scope}`);
213
+ }
214
+
215
+ return {
216
+ absolutePath,
217
+ relativePath: relativePath(repoRoot, absolutePath),
218
+ source: 'path-scope',
219
+ };
220
+ }
221
+
222
+ function isBinaryFile(filePath) {
223
+ const extension = path.extname(filePath).toLowerCase();
224
+ if (BINARY_EXTENSIONS.has(extension)) {
225
+ return true;
226
+ }
227
+
228
+ let descriptor;
229
+ try {
230
+ descriptor = fs.openSync(filePath, 'r');
231
+ const buffer = Buffer.alloc(1024);
232
+ const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
233
+ for (let index = 0; index < bytesRead; index += 1) {
234
+ if (buffer[index] === 0) {
235
+ return true;
236
+ }
237
+ }
238
+ } catch (_) {
239
+ return true;
240
+ } finally {
241
+ if (typeof descriptor === 'number') {
242
+ fs.closeSync(descriptor);
243
+ }
244
+ }
245
+
246
+ return false;
247
+ }
248
+
249
+ function walkFiles(repoRoot, analysisRoot) {
250
+ const files = [];
251
+ const skipped = [];
252
+ const safetyExclusions = [];
253
+
254
+ function visit(currentPath) {
255
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
256
+ for (const entry of entries) {
257
+ const absolutePath = path.join(currentPath, entry.name);
258
+ const relative = relativePath(repoRoot, absolutePath);
259
+ const normalizedRelative = relative === '.' ? entry.name : relative;
260
+
261
+ if (entry.isSymbolicLink()) {
262
+ skipped.push({ path: toPosix(normalizedRelative), reason: 'symlink' });
263
+ continue;
264
+ }
265
+
266
+ const exclusionReason = getContextPathExclusionReason(normalizedRelative);
267
+ if (exclusionReason) {
268
+ safetyExclusions.push({ path: toPosix(normalizedRelative), reason: exclusionReason });
269
+ continue;
270
+ }
271
+
272
+ if (entry.isDirectory()) {
273
+ visit(absolutePath);
274
+ continue;
275
+ }
276
+
277
+ if (!entry.isFile()) {
278
+ skipped.push({ path: toPosix(normalizedRelative), reason: 'not-regular-file' });
279
+ continue;
280
+ }
281
+
282
+ const stat = fs.statSync(absolutePath);
283
+ if (isBinaryFile(absolutePath)) {
284
+ skipped.push({ path: toPosix(normalizedRelative), reason: 'binary-file' });
285
+ continue;
286
+ }
287
+
288
+ files.push({
289
+ path: toPosix(normalizedRelative),
290
+ bytes: stat.size,
291
+ });
292
+ }
293
+ }
294
+
295
+ visit(analysisRoot);
296
+ return { files, safetyExclusions, skipped };
297
+ }
298
+
299
+ function detectStack(repoRoot, files) {
300
+ const stack = new Set();
301
+ const configs = [];
302
+ const entrypoints = [];
303
+ const sourceRoots = new Set();
304
+ const packageJson = readPackageJson(repoRoot);
305
+ const dependencies = {
306
+ ...(packageJson?.dependencies || {}),
307
+ ...(packageJson?.devDependencies || {}),
308
+ };
309
+
310
+ for (const dependency of Object.keys(dependencies)) {
311
+ if (dependency === 'next') stack.add('nextjs');
312
+ if (dependency === 'react') stack.add('react');
313
+ if (dependency === 'vue') stack.add('vue');
314
+ if (dependency === 'svelte') stack.add('svelte');
315
+ if (dependency === '@nestjs/core') stack.add('nestjs');
316
+ if (dependency === 'express') stack.add('express');
317
+ if (dependency === 'fastify') stack.add('fastify');
318
+ if (dependency === 'prisma' || dependency === '@prisma/client') stack.add('prisma');
319
+ if (dependency === 'drizzle-orm') stack.add('drizzle');
320
+ if (dependency.includes('supabase')) stack.add('supabase');
321
+ if (dependency === 'typescript') stack.add('typescript');
322
+ }
323
+
324
+ for (const file of files) {
325
+ const classification = classifyProjectFile(file.path);
326
+ const firstSegment = file.path.split('/')[0];
327
+ if (classification.signals.includes('config')) {
328
+ configs.push(file.path);
329
+ }
330
+ if (classification.signals.includes('entrypoint')) {
331
+ entrypoints.push(file.path);
332
+ }
333
+ if (classification.signals.some((signal) => ['source', 'frontend', 'backend'].includes(signal))) {
334
+ sourceRoots.add(firstSegment);
335
+ }
336
+
337
+ const base = path.posix.basename(file.path);
338
+ if (base === 'pyproject.toml') stack.add('python');
339
+ if (base === 'pom.xml') stack.add('java');
340
+ if (base === 'Gemfile') stack.add('ruby');
341
+ if (base === 'go.mod') stack.add('go');
342
+ if (base === 'composer.json') stack.add('php');
343
+ if (base === 'pubspec.yaml') stack.add('dart');
344
+ }
345
+
346
+ return {
347
+ stack: [...stack].sort(),
348
+ configs: configs.sort(),
349
+ entrypoints: entrypoints.sort(),
350
+ source_roots: [...sourceRoots].sort(),
351
+ scripts: packageJson?.scripts || {},
352
+ };
353
+ }
354
+
355
+ function discoverProjectFiles(repoRoot, options = {}) {
356
+ const resolvedRepoRoot = path.resolve(repoRoot);
357
+ const workspaceRoots = discoverWorkspaceRoots(resolvedRepoRoot);
358
+ const analysisRoot = resolveAnalysisRoot(resolvedRepoRoot, options.scope, workspaceRoots);
359
+ const walked = walkFiles(resolvedRepoRoot, analysisRoot.absolutePath);
360
+ const packageJson = readPackageJson(resolvedRepoRoot);
361
+
362
+ return {
363
+ project: {
364
+ name: packageJson?.name || path.basename(resolvedRepoRoot),
365
+ root: resolvedRepoRoot,
366
+ package_manager: detectPackageManager(resolvedRepoRoot),
367
+ },
368
+ roots: {
369
+ repo_root: resolvedRepoRoot,
370
+ analysis_root: analysisRoot.relativePath,
371
+ analysis_root_source: analysisRoot.source,
372
+ workspaces: workspaceRoots,
373
+ },
374
+ files: walked.files,
375
+ skippedFiles: walked.skipped,
376
+ skippedSummary: summarizeByReason(walked.skipped),
377
+ safetyExclusions: walked.safetyExclusions,
378
+ safetySummary: summarizeByReason(walked.safetyExclusions),
379
+ detected: detectStack(resolvedRepoRoot, walked.files),
380
+ };
381
+ }
382
+
383
+ module.exports = {
384
+ discoverProjectFiles,
385
+ discoverWorkspaceRoots,
386
+ resolveAnalysisRoot,
387
+ };