code-as-plan 2.0.0
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.
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +1006 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-brainstormer.md +154 -0
- package/agents/cap-debugger.md +221 -0
- package/agents/cap-prototyper.md +170 -0
- package/agents/cap-reviewer.md +230 -0
- package/agents/cap-tester.md +193 -0
- package/bin/install.js +5002 -0
- package/cap/bin/gsd-tools.cjs +1141 -0
- package/cap/bin/lib/arc-scanner.cjs +341 -0
- package/cap/bin/lib/cap-feature-map.cjs +506 -0
- package/cap/bin/lib/cap-session.cjs +191 -0
- package/cap/bin/lib/cap-stack-docs.cjs +598 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
- package/cap/bin/lib/commands.cjs +959 -0
- package/cap/bin/lib/config.cjs +466 -0
- package/cap/bin/lib/convention-reader.cjs +180 -0
- package/cap/bin/lib/core.cjs +1230 -0
- package/cap/bin/lib/feature-aggregator.cjs +422 -0
- package/cap/bin/lib/frontmatter.cjs +336 -0
- package/cap/bin/lib/init.cjs +1442 -0
- package/cap/bin/lib/manifest-generator.cjs +381 -0
- package/cap/bin/lib/milestone.cjs +252 -0
- package/cap/bin/lib/model-profiles.cjs +68 -0
- package/cap/bin/lib/monorepo-context.cjs +224 -0
- package/cap/bin/lib/monorepo-migrator.cjs +507 -0
- package/cap/bin/lib/phase.cjs +888 -0
- package/cap/bin/lib/profile-output.cjs +952 -0
- package/cap/bin/lib/profile-pipeline.cjs +539 -0
- package/cap/bin/lib/roadmap.cjs +329 -0
- package/cap/bin/lib/security.cjs +382 -0
- package/cap/bin/lib/session-manager.cjs +290 -0
- package/cap/bin/lib/skeleton-generator.cjs +177 -0
- package/cap/bin/lib/state.cjs +1031 -0
- package/cap/bin/lib/template.cjs +222 -0
- package/cap/bin/lib/test-detector.cjs +61 -0
- package/cap/bin/lib/uat.cjs +282 -0
- package/cap/bin/lib/verify.cjs +888 -0
- package/cap/bin/lib/workspace-detector.cjs +369 -0
- package/cap/bin/lib/workstream.cjs +491 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +102 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/decimal-phase-calculation.md +64 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profile-resolution.md +36 -0
- package/cap/references/model-profiles.md +139 -0
- package/cap/references/phase-argument-parsing.md +61 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/questioning.md +162 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/ui-brand.md +160 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/references/workstream-flag.md +58 -0
- package/cap/templates/DEBUG.md +164 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/UI-SPEC.md +100 -0
- package/cap/templates/VALIDATION.md +76 -0
- package/cap/templates/claude-md.md +122 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/dev-preferences.md +21 -0
- package/cap/templates/discovery.md +146 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/retrospective.md +54 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary-complex.md +59 -0
- package/cap/templates/summary-minimal.md +41 -0
- package/cap/templates/summary-standard.md +48 -0
- package/cap/templates/summary.md +248 -0
- package/cap/templates/user-profile.md +146 -0
- package/cap/templates/user-setup.md +311 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +859 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +450 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +302 -0
- package/cap/workflows/ui-review.md +165 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +174 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +238 -0
- package/commands/cap/debug.md +297 -0
- package/commands/cap/init.md +262 -0
- package/commands/cap/iterate.md +234 -0
- package/commands/cap/prototype.md +281 -0
- package/commands/cap/refresh-docs.md +37 -0
- package/commands/cap/review.md +272 -0
- package/commands/cap/scan.md +249 -0
- package/commands/cap/start.md +234 -0
- package/commands/cap/status.md +189 -0
- package/commands/cap/test.md +250 -0
- package/hooks/dist/gsd-check-update.js +114 -0
- package/hooks/dist/gsd-context-monitor.js +156 -0
- package/hooks/dist/gsd-prompt-guard.js +96 -0
- package/hooks/dist/gsd-statusline.js +119 -0
- package/hooks/dist/gsd-workflow-guard.js +94 -0
- package/package.json +51 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +82 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +198 -0
- package/scripts/run-tests.cjs +29 -0
- package/scripts/secret-scan.sh +227 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// @gsd-context CAP v2.0 tag scanner -- extracts @cap-feature, @cap-todo, @cap-risk, and @cap-decision tags from source files.
|
|
2
|
+
// @gsd-decision Separate module from arc-scanner.cjs -- CAP tags use @cap- prefix (not @gsd-) and have different metadata semantics (feature: key instead of phase: key).
|
|
3
|
+
// @gsd-decision Regex-based extraction (not AST) -- language-agnostic, zero dependencies, proven sufficient in GSD arc-scanner.cjs.
|
|
4
|
+
// @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path).
|
|
5
|
+
// @gsd-pattern Same comment anchor rule as ARC: tag is only valid when first non-whitespace content on a line is a comment token.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
// @gsd-todo(ref:AC-20) Primary tags are @cap-feature and @cap-todo; risk and decision are optional standalone tags
|
|
13
|
+
// @gsd-decision CAP tag types: 2 primary (feature, todo) + 2 optional (risk, decision). Simplified from GSD's 8 types.
|
|
14
|
+
const CAP_TAG_TYPES = ['feature', 'todo', 'risk', 'decision'];
|
|
15
|
+
|
|
16
|
+
// @gsd-todo(ref:AC-25) Tag scanner uses native RegExp with dotAll flag for multiline extraction
|
|
17
|
+
// @gsd-pattern Tag regex anchors to comment tokens at line start -- identical approach to arc-scanner.cjs
|
|
18
|
+
const CAP_TAG_RE = /^[ \t]*(?:\/\/|\/\*|\*|#|--|"""|''')[ \t]*@cap-(feature|todo|risk|decision)(?:\(([^)]*)\))?[ \t]*(.*)/;
|
|
19
|
+
|
|
20
|
+
// @gsd-todo(ref:AC-26) Tag scanner is language-agnostic, operating on comment syntax patterns across JS, TS, Python, Ruby, Shell
|
|
21
|
+
const SUPPORTED_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.rb', '.sh', '.bash', '.sql', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.hpp'];
|
|
22
|
+
const DEFAULT_EXCLUDE = ['node_modules', '.git', '.cap', 'dist', 'build', 'coverage', '.planning'];
|
|
23
|
+
|
|
24
|
+
// @gsd-todo(ref:AC-22) @cap-todo supports structured subtypes: risk:..., decision:...
|
|
25
|
+
// @gsd-decision Subtype detection uses prefix matching on the description text (e.g., "risk: memory leak" -> subtype: "risk")
|
|
26
|
+
const SUBTYPE_RE = /^(risk|decision):\s*(.*)/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} CapTag
|
|
30
|
+
* @property {string} type - Tag type without @cap- prefix ('feature', 'todo', 'risk', 'decision')
|
|
31
|
+
* @property {string} file - Relative path from project root
|
|
32
|
+
* @property {number} line - 1-based line number
|
|
33
|
+
* @property {Object<string,string>} metadata - Parsed key-value pairs from parenthesized block
|
|
34
|
+
* @property {string} description - Text after metadata block
|
|
35
|
+
* @property {string} raw - Complete original line
|
|
36
|
+
* @property {string|null} subtype - For @cap-todo: 'risk' or 'decision' if prefixed, else null
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// @gsd-api parseMetadata(metadataStr) -- Parses parenthesized key:value pairs.
|
|
40
|
+
// Returns: Object<string,string> -- flat key-value object.
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} metadataStr - Raw metadata string without parens (e.g., "feature:auth, ac:AUTH/AC-1")
|
|
43
|
+
* @returns {Object<string,string>}
|
|
44
|
+
*/
|
|
45
|
+
function parseMetadata(metadataStr) {
|
|
46
|
+
if (!metadataStr || !metadataStr.trim()) return {};
|
|
47
|
+
const result = {};
|
|
48
|
+
const pairs = metadataStr.split(',');
|
|
49
|
+
for (const pair of pairs) {
|
|
50
|
+
const trimmed = pair.trim();
|
|
51
|
+
if (!trimmed) continue;
|
|
52
|
+
const colonIdx = trimmed.indexOf(':');
|
|
53
|
+
if (colonIdx === -1) {
|
|
54
|
+
// Key without value -- store as truthy flag
|
|
55
|
+
result[trimmed] = 'true';
|
|
56
|
+
} else {
|
|
57
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
58
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
59
|
+
if (key) result[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// @gsd-api extractTags(content, filePath) -- Regex extraction engine supporting //, #, /* */, """ """ comment styles.
|
|
66
|
+
// Returns: CapTag[] -- array of extracted tags.
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} content - File content to scan
|
|
69
|
+
* @param {string} filePath - Relative file path (for tag metadata)
|
|
70
|
+
* @returns {CapTag[]}
|
|
71
|
+
*/
|
|
72
|
+
function extractTags(content, filePath) {
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
const tags = [];
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
const match = line.match(CAP_TAG_RE);
|
|
78
|
+
if (!match) continue;
|
|
79
|
+
|
|
80
|
+
const type = match[1];
|
|
81
|
+
const metadataStr = match[2] || '';
|
|
82
|
+
const description = (match[3] || '').trim();
|
|
83
|
+
const metadata = parseMetadata(metadataStr);
|
|
84
|
+
|
|
85
|
+
// @gsd-todo(ref:AC-22) Detect subtypes in @cap-todo description (risk:..., decision:...)
|
|
86
|
+
let subtype = null;
|
|
87
|
+
if (type === 'todo') {
|
|
88
|
+
const subtypeMatch = description.match(SUBTYPE_RE);
|
|
89
|
+
if (subtypeMatch) {
|
|
90
|
+
subtype = subtypeMatch[1];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
tags.push({
|
|
95
|
+
type,
|
|
96
|
+
file: filePath,
|
|
97
|
+
line: i + 1,
|
|
98
|
+
metadata,
|
|
99
|
+
description,
|
|
100
|
+
raw: line,
|
|
101
|
+
subtype,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return tags;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// @gsd-api scanFile(filePath, projectRoot) -- Scans a single file for @cap-* tags.
|
|
108
|
+
// Returns: CapTag[] -- array of extracted tags with file, line, metadata, description.
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} filePath - Absolute path to file
|
|
111
|
+
* @param {string} projectRoot - Absolute path to project root (for relative path computation)
|
|
112
|
+
* @returns {CapTag[]}
|
|
113
|
+
*/
|
|
114
|
+
function scanFile(filePath, projectRoot) {
|
|
115
|
+
// @gsd-todo(ref:AC-25) Use native RegExp for tag extraction -- no AST parsing
|
|
116
|
+
let content;
|
|
117
|
+
try {
|
|
118
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
119
|
+
} catch (_e) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
123
|
+
return extractTags(content, relativePath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// @gsd-api scanDirectory(dirPath, options) -- Recursively scans a directory for @cap-* tags.
|
|
127
|
+
// Returns: CapTag[] -- aggregated tags from all matching files.
|
|
128
|
+
// Options: { extensions?: string[], exclude?: string[] }
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} dirPath - Absolute path to directory to scan
|
|
131
|
+
* @param {Object} [options]
|
|
132
|
+
* @param {string[]} [options.extensions] - File extensions to include (e.g., ['.js', '.ts', '.py'])
|
|
133
|
+
* @param {string[]} [options.exclude] - Directory names to exclude (e.g., ['node_modules', '.git'])
|
|
134
|
+
* @param {string} [options.projectRoot] - Project root for relative paths (defaults to dirPath)
|
|
135
|
+
* @returns {CapTag[]}
|
|
136
|
+
*/
|
|
137
|
+
function scanDirectory(dirPath, options = {}) {
|
|
138
|
+
const extensions = options.extensions || SUPPORTED_EXTENSIONS;
|
|
139
|
+
const exclude = options.exclude || DEFAULT_EXCLUDE;
|
|
140
|
+
const projectRoot = options.projectRoot || dirPath;
|
|
141
|
+
const tags = [];
|
|
142
|
+
|
|
143
|
+
// @gsd-constraint Uses readdirSync (not glob) per project zero-dep constraint
|
|
144
|
+
function walk(dir) {
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
148
|
+
} catch (_e) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const fullPath = path.join(dir, entry.name);
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
if (exclude.includes(entry.name)) continue;
|
|
155
|
+
walk(fullPath);
|
|
156
|
+
} else if (entry.isFile()) {
|
|
157
|
+
const ext = path.extname(entry.name);
|
|
158
|
+
if (!extensions.includes(ext)) continue;
|
|
159
|
+
const fileTags = scanFile(fullPath, projectRoot);
|
|
160
|
+
tags.push(...fileTags);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
walk(dirPath);
|
|
166
|
+
return tags;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// @gsd-api groupByFeature(tags) -- Groups tags by their feature: metadata value.
|
|
170
|
+
// Returns: Object<string, CapTag[]> -- map from feature name to tags.
|
|
171
|
+
/**
|
|
172
|
+
* @param {CapTag[]} tags - Array of extracted tags
|
|
173
|
+
* @returns {Object<string, CapTag[]>}
|
|
174
|
+
*/
|
|
175
|
+
function groupByFeature(tags) {
|
|
176
|
+
const groups = {};
|
|
177
|
+
for (const tag of tags) {
|
|
178
|
+
const featureId = tag.metadata.feature || '(unassigned)';
|
|
179
|
+
if (!groups[featureId]) groups[featureId] = [];
|
|
180
|
+
groups[featureId].push(tag);
|
|
181
|
+
}
|
|
182
|
+
return groups;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// @gsd-api detectOrphans(tags, featureIds) -- Compare tags against Feature Map entries, fuzzy-match hints for orphans.
|
|
186
|
+
// Returns: Array of { tag, hint } where hint is the closest matching feature ID.
|
|
187
|
+
// @gsd-todo(ref:AC-15) Orphan tags flagged with fuzzy-match hint suggesting closest existing feature ID
|
|
188
|
+
/**
|
|
189
|
+
* @param {CapTag[]} tags - Array of extracted tags
|
|
190
|
+
* @param {string[]} featureIds - Known feature IDs from Feature Map (e.g., ['F-001', 'F-002'])
|
|
191
|
+
* @returns {{ tag: CapTag, hint: string|null }[]}
|
|
192
|
+
*/
|
|
193
|
+
function detectOrphans(tags, featureIds) {
|
|
194
|
+
const orphans = [];
|
|
195
|
+
const featureSet = new Set(featureIds);
|
|
196
|
+
|
|
197
|
+
for (const tag of tags) {
|
|
198
|
+
const tagFeatureId = tag.metadata.feature;
|
|
199
|
+
if (!tagFeatureId) continue;
|
|
200
|
+
if (featureSet.has(tagFeatureId)) continue;
|
|
201
|
+
|
|
202
|
+
// Fuzzy match: find closest feature ID by Levenshtein-like similarity
|
|
203
|
+
const hint = findClosestMatch(tagFeatureId, featureIds);
|
|
204
|
+
orphans.push({ tag, hint });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return orphans;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// @gsd-decision Simple character-level distance for fuzzy matching -- no external library needed
|
|
211
|
+
/**
|
|
212
|
+
* Compute edit distance between two strings (Levenshtein).
|
|
213
|
+
* @param {string} a
|
|
214
|
+
* @param {string} b
|
|
215
|
+
* @returns {number}
|
|
216
|
+
*/
|
|
217
|
+
function editDistance(a, b) {
|
|
218
|
+
const la = a.length;
|
|
219
|
+
const lb = b.length;
|
|
220
|
+
const dp = Array.from({ length: la + 1 }, () => Array(lb + 1).fill(0));
|
|
221
|
+
for (let i = 0; i <= la; i++) dp[i][0] = i;
|
|
222
|
+
for (let j = 0; j <= lb; j++) dp[0][j] = j;
|
|
223
|
+
for (let i = 1; i <= la; i++) {
|
|
224
|
+
for (let j = 1; j <= lb; j++) {
|
|
225
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
226
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return dp[la][lb];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Find the closest matching string from candidates using edit distance.
|
|
234
|
+
* @param {string} target
|
|
235
|
+
* @param {string[]} candidates
|
|
236
|
+
* @returns {string|null}
|
|
237
|
+
*/
|
|
238
|
+
function findClosestMatch(target, candidates) {
|
|
239
|
+
if (candidates.length === 0) return null;
|
|
240
|
+
let bestDist = Infinity;
|
|
241
|
+
let bestMatch = null;
|
|
242
|
+
const lowerTarget = target.toLowerCase();
|
|
243
|
+
for (const candidate of candidates) {
|
|
244
|
+
const dist = editDistance(lowerTarget, candidate.toLowerCase());
|
|
245
|
+
if (dist < bestDist) {
|
|
246
|
+
bestDist = dist;
|
|
247
|
+
bestMatch = candidate;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Only suggest if distance is reasonable (less than half the target length)
|
|
251
|
+
if (bestDist <= Math.ceil(target.length / 2)) return bestMatch;
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// @gsd-todo(ref:AC-78) /cap:scan shall traverse all packages in a monorepo
|
|
256
|
+
// @gsd-todo(ref:AC-93) Zero runtime dependencies -- uses only Node.js built-ins
|
|
257
|
+
// @gsd-todo(ref:AC-94) Tag scanner uses native RegExp -- no comment-parser or AST parser
|
|
258
|
+
// @gsd-todo(ref:AC-95) File discovery uses fs.readdirSync with recursive walk -- no glob library
|
|
259
|
+
// @gsd-todo(ref:AC-96) CLI argument parsing uses existing parseNamedArgs() pattern
|
|
260
|
+
|
|
261
|
+
// @gsd-api detectWorkspaces(projectRoot) -- Detects monorepo workspaces from package.json and lerna.json.
|
|
262
|
+
// Returns: { isMonorepo: boolean, packages: string[] }
|
|
263
|
+
/**
|
|
264
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
265
|
+
* @returns {{ isMonorepo: boolean, packages: string[] }}
|
|
266
|
+
*/
|
|
267
|
+
function detectWorkspaces(projectRoot) {
|
|
268
|
+
const result = { isMonorepo: false, packages: [] };
|
|
269
|
+
|
|
270
|
+
// Check package.json workspaces (npm/yarn/pnpm)
|
|
271
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
272
|
+
if (fs.existsSync(pkgPath)) {
|
|
273
|
+
try {
|
|
274
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
275
|
+
if (pkg.workspaces) {
|
|
276
|
+
result.isMonorepo = true;
|
|
277
|
+
const patterns = Array.isArray(pkg.workspaces)
|
|
278
|
+
? pkg.workspaces
|
|
279
|
+
: (pkg.workspaces.packages || []);
|
|
280
|
+
result.packages = resolveWorkspaceGlobs(projectRoot, patterns);
|
|
281
|
+
}
|
|
282
|
+
} catch (_e) {
|
|
283
|
+
// Malformed package.json
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check lerna.json
|
|
288
|
+
if (!result.isMonorepo) {
|
|
289
|
+
const lernaPath = path.join(projectRoot, 'lerna.json');
|
|
290
|
+
if (fs.existsSync(lernaPath)) {
|
|
291
|
+
try {
|
|
292
|
+
const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf8'));
|
|
293
|
+
result.isMonorepo = true;
|
|
294
|
+
const patterns = lerna.packages || ['packages/*'];
|
|
295
|
+
result.packages = resolveWorkspaceGlobs(projectRoot, patterns);
|
|
296
|
+
} catch (_e) {
|
|
297
|
+
// Malformed lerna.json
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// @gsd-api resolveWorkspaceGlobs(projectRoot, patterns) -- Expands workspace glob patterns to actual directories.
|
|
306
|
+
// @gsd-decision Uses fs.readdirSync instead of glob library for workspace pattern expansion. Handles only simple patterns (dir/* and dir/**).
|
|
307
|
+
/**
|
|
308
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
309
|
+
* @param {string[]} patterns - Workspace glob patterns (e.g., ["packages/*", "apps/*"])
|
|
310
|
+
* @returns {string[]} - Array of relative package directory paths
|
|
311
|
+
*/
|
|
312
|
+
function resolveWorkspaceGlobs(projectRoot, patterns) {
|
|
313
|
+
const packages = [];
|
|
314
|
+
|
|
315
|
+
for (const pattern of patterns) {
|
|
316
|
+
// Strip trailing glob: "packages/*" -> "packages", "apps/**" -> "apps"
|
|
317
|
+
const baseDir = pattern.replace(/\/\*+$/, '');
|
|
318
|
+
const fullPath = path.join(projectRoot, baseDir);
|
|
319
|
+
|
|
320
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
321
|
+
|
|
322
|
+
const stat = fs.statSync(fullPath);
|
|
323
|
+
if (!stat.isDirectory()) continue;
|
|
324
|
+
|
|
325
|
+
// If pattern has no glob, it is a direct package reference
|
|
326
|
+
if (!pattern.includes('*')) {
|
|
327
|
+
packages.push(baseDir);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Enumerate subdirectories
|
|
332
|
+
try {
|
|
333
|
+
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
336
|
+
packages.push(path.join(baseDir, entry.name));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch (_e) {
|
|
340
|
+
// Skip unreadable directories
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return packages;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// @gsd-api scanMonorepo(projectRoot, options) -- Scans all workspace packages in a monorepo for @cap-* tags.
|
|
348
|
+
// @gsd-todo(ref:AC-79) Feature Map entries support cross-package file references (e.g., packages/core/src/auth.ts)
|
|
349
|
+
// @gsd-todo(ref:AC-80) Works seamlessly with single-repo projects -- returns regular scanDirectory results if not a monorepo
|
|
350
|
+
/**
|
|
351
|
+
* Scans a monorepo or single repo for @cap-* tags.
|
|
352
|
+
* In monorepo mode: scans root + each workspace package.
|
|
353
|
+
* In single-repo mode: delegates to scanDirectory.
|
|
354
|
+
* All file paths are relative to project root for cross-package references.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
357
|
+
* @param {Object} [options]
|
|
358
|
+
* @param {string[]} [options.extensions] - File extensions to include
|
|
359
|
+
* @param {string[]} [options.exclude] - Directory names to exclude
|
|
360
|
+
* @returns {{ tags: CapTag[], isMonorepo: boolean, packages: string[] }}
|
|
361
|
+
*/
|
|
362
|
+
function scanMonorepo(projectRoot, options = {}) {
|
|
363
|
+
const workspaces = detectWorkspaces(projectRoot);
|
|
364
|
+
|
|
365
|
+
if (!workspaces.isMonorepo) {
|
|
366
|
+
// Single repo -- delegate to base scanner
|
|
367
|
+
const tags = scanDirectory(projectRoot, {
|
|
368
|
+
...options,
|
|
369
|
+
projectRoot,
|
|
370
|
+
});
|
|
371
|
+
return { tags, isMonorepo: false, packages: [] };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Monorepo -- scan root and each package
|
|
375
|
+
const allTags = [];
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
|
|
378
|
+
// Scan root (excludes workspace dirs by default since they are scanned separately)
|
|
379
|
+
const rootTags = scanDirectory(projectRoot, {
|
|
380
|
+
...options,
|
|
381
|
+
projectRoot,
|
|
382
|
+
});
|
|
383
|
+
for (const tag of rootTags) {
|
|
384
|
+
const key = `${tag.file}:${tag.line}`;
|
|
385
|
+
if (!seen.has(key)) {
|
|
386
|
+
seen.add(key);
|
|
387
|
+
allTags.push(tag);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Scan each workspace package
|
|
392
|
+
for (const pkg of workspaces.packages) {
|
|
393
|
+
const pkgDir = path.join(projectRoot, pkg);
|
|
394
|
+
if (!fs.existsSync(pkgDir)) continue;
|
|
395
|
+
|
|
396
|
+
const pkgTags = scanDirectory(pkgDir, {
|
|
397
|
+
...options,
|
|
398
|
+
projectRoot, // Paths relative to monorepo root, not package root
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
for (const tag of pkgTags) {
|
|
402
|
+
const key = `${tag.file}:${tag.line}`;
|
|
403
|
+
if (!seen.has(key)) {
|
|
404
|
+
seen.add(key);
|
|
405
|
+
allTags.push(tag);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { tags: allTags, isMonorepo: true, packages: workspaces.packages };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// @gsd-api groupByPackage(tags) -- Groups tags by their workspace package based on file path prefix.
|
|
414
|
+
/**
|
|
415
|
+
* @param {CapTag[]} tags - Array of extracted tags
|
|
416
|
+
* @param {string[]} packages - Known workspace package paths
|
|
417
|
+
* @returns {Object<string, CapTag[]>}
|
|
418
|
+
*/
|
|
419
|
+
function groupByPackage(tags, packages) {
|
|
420
|
+
const groups = { '(root)': [] };
|
|
421
|
+
for (const pkg of packages) {
|
|
422
|
+
groups[pkg] = [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const tag of tags) {
|
|
426
|
+
let matched = false;
|
|
427
|
+
for (const pkg of packages) {
|
|
428
|
+
if (tag.file.startsWith(pkg + '/') || tag.file.startsWith(pkg + path.sep)) {
|
|
429
|
+
groups[pkg].push(tag);
|
|
430
|
+
matched = true;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!matched) {
|
|
435
|
+
groups['(root)'].push(tag);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return groups;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
module.exports = {
|
|
443
|
+
CAP_TAG_TYPES,
|
|
444
|
+
CAP_TAG_RE,
|
|
445
|
+
SUPPORTED_EXTENSIONS,
|
|
446
|
+
DEFAULT_EXCLUDE,
|
|
447
|
+
scanFile,
|
|
448
|
+
scanDirectory,
|
|
449
|
+
extractTags,
|
|
450
|
+
parseMetadata,
|
|
451
|
+
groupByFeature,
|
|
452
|
+
detectOrphans,
|
|
453
|
+
editDistance,
|
|
454
|
+
detectWorkspaces,
|
|
455
|
+
resolveWorkspaceGlobs,
|
|
456
|
+
scanMonorepo,
|
|
457
|
+
groupByPackage,
|
|
458
|
+
};
|