@planu/cli 4.1.3 → 4.2.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/CHANGELOG.md +23 -1
- package/dist/engine/code-scanner/layer-scanner.js +29 -116
- package/dist/engine/core-bridge.d.ts +6 -6
- package/dist/engine/crash-shield/index.js +26 -63
- package/dist/engine/detect-duplication.js +63 -53
- package/dist/engine/drift-monitor.js +1 -1
- package/dist/engine/onboarding/new-project-resolver.d.ts +7 -0
- package/dist/engine/onboarding/new-project-resolver.js +265 -0
- package/dist/engine/reviewer-tokens/signer.js +4 -22
- package/dist/engine/scan-project/module-discoverer.js +9 -11
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +2 -1
- package/dist/engine/validator/analyzer.js +14 -11
- package/dist/engine/validator/deep-code-checker.js +4 -8
- package/dist/storage/base-store.js +2 -6
- package/dist/storage/gaps-log.js +38 -32
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/technology-selection-store.d.ts +5 -0
- package/dist/storage/technology-selection-store.js +42 -0
- package/dist/tools/create-spec.js +23 -3
- package/dist/tools/facilitate.js +39 -29
- package/dist/tools/init-project/handler.js +59 -12
- package/dist/tools/register-spec-tools/core-spec-tools.js +33 -1
- package/dist/tools/tool-registry/core-tools.js +35 -1
- package/dist/tools/update-status/batch.js +4 -1
- package/dist/types/facilitator.d.ts +13 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/project/inputs.d.ts +12 -0
- package/dist/types/technology-selection.d.ts +77 -0
- package/dist/types/technology-selection.js +3 -0
- package/package.json +22 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [4.2.0] - 2026-05-21
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `d036f3d5f73279fc0a12935995f899950fdf06afe454bba49da748183e07f477`
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- feat(onboarding): add new project technology contract
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
- fix(release): sync native package versions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [4.1.4] - 2026-05-21
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
- Centralize Rust-first hot-path fallback behavior in `core-bridge` so callers use native acceleration when available and TypeScript fallbacks consistently when unavailable.
|
|
16
|
+
- Keep crash scanning, broad validation reads, gaps log verification, HMAC signing, duplicate detection, and layer scanning resilient when native reads return partial results or test doubles expose older nullable bridge behavior.
|
|
17
|
+
- Lock published native optional dependencies across macOS and Linux architectures in `pnpm` so release dependency checks remain cross-platform stable.
|
|
18
|
+
|
|
19
|
+
### Tests
|
|
20
|
+
- Re-run the full suite with 31,275 passing tests and 5 skipped tests.
|
|
21
|
+
|
|
22
|
+
|
|
1
23
|
## [4.1.3] - 2026-05-21
|
|
2
24
|
|
|
3
25
|
### Bug Fixes
|
|
@@ -3834,4 +3856,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
3834
3856
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
3835
3857
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
3836
3858
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
3837
|
-
- 10,857 tests with ≥95% coverage
|
|
3859
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/engine/code-scanner/layer-scanner.ts — Scans the 4 implementation layers (SPEC-441)
|
|
2
|
-
import {
|
|
3
|
-
import { join, relative,
|
|
4
|
-
import {
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { join, relative, basename } from 'node:path';
|
|
4
|
+
import { fastFindFilesByExt, fastReadFiles } from '../core-bridge.js';
|
|
5
5
|
/**
|
|
6
6
|
* Scan a project directory for evidence of spec implementation across 4 layers.
|
|
7
7
|
*/
|
|
@@ -11,29 +11,18 @@ export async function scanLayers(projectPath, keywords, symbolVariants) {
|
|
|
11
11
|
const [srcExists, testsExists] = await Promise.all([dirExists(srcDir), dirExists(testsDir)]);
|
|
12
12
|
let srcFiles = [];
|
|
13
13
|
let testFiles = [];
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
srcFiles = fastFindFilesByExt(srcDir, ['ts', 'js', 'tsx', 'jsx']) ?? [];
|
|
17
|
-
// Filter out .d.ts natively mapped
|
|
18
|
-
srcFiles = srcFiles.filter((f) => !f.endsWith('.d.ts'));
|
|
19
|
-
}
|
|
20
|
-
if (testsExists) {
|
|
21
|
-
testFiles = fastFindFilesByExt(testsDir, ['ts', 'js', 'tsx', 'jsx']) ?? [];
|
|
22
|
-
testFiles = testFiles.filter((f) => !f.endsWith('.d.ts'));
|
|
23
|
-
}
|
|
14
|
+
if (srcExists) {
|
|
15
|
+
srcFiles = fastFindFilesByExt(srcDir, ['ts', 'js', 'tsx', 'jsx']).filter((f) => !f.endsWith('.d.ts'));
|
|
24
16
|
}
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
srcExists ? collectTsFiles(srcDir) : Promise.resolve([]),
|
|
28
|
-
testsExists ? collectTsFiles(testsDir) : Promise.resolve([]),
|
|
29
|
-
]);
|
|
17
|
+
if (testsExists) {
|
|
18
|
+
testFiles = fastFindFilesByExt(testsDir, ['ts', 'js', 'tsx', 'jsx']).filter((f) => !f.endsWith('.d.ts'));
|
|
30
19
|
}
|
|
31
20
|
// Layer 1: files whose names match keywords
|
|
32
21
|
const matchedFiles = srcFiles.filter((f) => keywords.some((kw) => basename(f).toLowerCase().includes(kw)));
|
|
33
22
|
// Layer 2: exported symbols in matched files (and register/index files)
|
|
34
23
|
const registerFiles = srcFiles.filter((f) => /register.*\.ts$|index\.ts$/.test(f));
|
|
35
24
|
const filesToScanForSymbols = [...new Set([...matchedFiles, ...registerFiles])];
|
|
36
|
-
const symbols =
|
|
25
|
+
const symbols = findSymbols(filesToScanForSymbols, symbolVariants);
|
|
37
26
|
// Layer 3: test files matching keywords
|
|
38
27
|
const matchedTests = testFiles.filter((f) => keywords.some((kw) => basename(f).toLowerCase().includes(kw)));
|
|
39
28
|
// Layer 4: registrations in license-plans.json and register-*.ts files
|
|
@@ -48,78 +37,22 @@ export async function scanLayers(projectPath, keywords, symbolVariants) {
|
|
|
48
37
|
// ---------------------------------------------------------------------------
|
|
49
38
|
// Internal helpers
|
|
50
39
|
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
const results = [];
|
|
53
|
-
try {
|
|
54
|
-
await walkDir(dir, results);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// Directory not accessible — return empty
|
|
58
|
-
}
|
|
59
|
-
return results;
|
|
60
|
-
}
|
|
61
|
-
async function walkDir(dir, out) {
|
|
62
|
-
let entries;
|
|
63
|
-
try {
|
|
64
|
-
entries = await readdir(dir);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
await Promise.all(entries.map(async (entry) => {
|
|
70
|
-
const full = join(dir, entry);
|
|
71
|
-
try {
|
|
72
|
-
const s = await stat(full);
|
|
73
|
-
if (s.isDirectory()) {
|
|
74
|
-
await walkDir(full, out);
|
|
75
|
-
}
|
|
76
|
-
else if (extname(entry) === '.ts' && !entry.endsWith('.d.ts')) {
|
|
77
|
-
out.push(full);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// skip
|
|
82
|
-
}
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
async function findSymbols(files, variants, projectPath) {
|
|
40
|
+
function findSymbols(files, variants) {
|
|
86
41
|
const found = new Set();
|
|
87
|
-
if (
|
|
88
|
-
const EXPORT_RE =
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
// Extract the group 1 (symbol name)
|
|
96
|
-
const execMatch = new RegExp(EXPORT_RE).exec(m.content);
|
|
97
|
-
const name = execMatch?.[1] ?? '';
|
|
98
|
-
if (variants.some((v) => name.toLowerCase().includes(v.toLowerCase()))) {
|
|
99
|
-
found.add(name);
|
|
100
|
-
}
|
|
42
|
+
if (files.length > 0) {
|
|
43
|
+
const EXPORT_RE = /^export\s+(?:async\s+)?(?:function|class|const|type|interface)\s+(\w+)/gm;
|
|
44
|
+
for (const file of fastReadFiles(files)) {
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = EXPORT_RE.exec(file.content)) !== null) {
|
|
47
|
+
const name = match[1] ?? '';
|
|
48
|
+
if (variants.some((v) => name.toLowerCase().includes(v.toLowerCase()))) {
|
|
49
|
+
found.add(name);
|
|
101
50
|
}
|
|
102
51
|
}
|
|
103
|
-
|
|
52
|
+
EXPORT_RE.lastIndex = 0;
|
|
104
53
|
}
|
|
54
|
+
return [...found];
|
|
105
55
|
}
|
|
106
|
-
const EXPORT_RE_NODE = /^export\s+(?:async\s+)?(?:function|class|const|type|interface)\s+(\w+)/gm;
|
|
107
|
-
await Promise.all(files.map(async (f) => {
|
|
108
|
-
let content;
|
|
109
|
-
try {
|
|
110
|
-
content = await readFile(f, 'utf8');
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
let match;
|
|
116
|
-
while ((match = EXPORT_RE_NODE.exec(content)) !== null) {
|
|
117
|
-
const name = match[1] ?? '';
|
|
118
|
-
if (variants.some((v) => name.toLowerCase().includes(v.toLowerCase()))) {
|
|
119
|
-
found.add(name);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}));
|
|
123
56
|
return [...found];
|
|
124
57
|
}
|
|
125
58
|
async function findRegistrations(projectPath, srcFiles, keywords) {
|
|
@@ -141,40 +74,20 @@ async function findRegistrations(projectPath, srcFiles, keywords) {
|
|
|
141
74
|
}
|
|
142
75
|
// Check register-*.ts files for registerTool calls matching keywords
|
|
143
76
|
const registerFiles = srcFiles.filter((f) => /register.*\.ts$/.test(f));
|
|
144
|
-
if (
|
|
145
|
-
const REGISTER_RE =
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
const toolName = execMatch?.[1] ?? '';
|
|
153
|
-
if (keywords.some((kw) => toolName.includes(kw))) {
|
|
154
|
-
found.add(`registered:${toolName}`);
|
|
155
|
-
}
|
|
77
|
+
if (registerFiles.length > 0) {
|
|
78
|
+
const REGISTER_RE = /registerTool\s*\(\s*['"]([^'"]+)['"]/g;
|
|
79
|
+
for (const file of fastReadFiles(registerFiles)) {
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = REGISTER_RE.exec(file.content)) !== null) {
|
|
82
|
+
const toolName = match[1] ?? '';
|
|
83
|
+
if (keywords.some((kw) => toolName.includes(kw))) {
|
|
84
|
+
found.add(`registered:${toolName}`);
|
|
156
85
|
}
|
|
157
86
|
}
|
|
158
|
-
|
|
87
|
+
REGISTER_RE.lastIndex = 0;
|
|
159
88
|
}
|
|
89
|
+
return [...found];
|
|
160
90
|
}
|
|
161
|
-
const REGISTER_RE_NODE = /registerTool\s*\(\s*['"]([^'"]+)['"]/g;
|
|
162
|
-
await Promise.all(registerFiles.map(async (f) => {
|
|
163
|
-
let content;
|
|
164
|
-
try {
|
|
165
|
-
content = await readFile(f, 'utf8');
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
let match;
|
|
171
|
-
while ((match = REGISTER_RE_NODE.exec(content)) !== null) {
|
|
172
|
-
const toolName = match[1] ?? '';
|
|
173
|
-
if (keywords.some((kw) => toolName.includes(kw))) {
|
|
174
|
-
found.add(`registered:${toolName}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}));
|
|
178
91
|
return [...found];
|
|
179
92
|
}
|
|
180
93
|
async function dirExists(p) {
|
|
@@ -12,12 +12,12 @@ export declare function fastScanAndHashFiles(rootPath: string): FileHash[];
|
|
|
12
12
|
export declare function fastScanProjectMetadata(rootPath: string): FileMetadata[];
|
|
13
13
|
export declare function fastScanSpecAnnotations(rootPath: string): SpecAnnotation[];
|
|
14
14
|
export declare function fastCheckAcCoverage(filePaths: string[], keywords: string[]): string[];
|
|
15
|
-
export declare function fastDetectDriftParallel(rootPath: string, keywords: string[]): DriftResult
|
|
16
|
-
export declare function fastFindDuplicateBlocks(paths: string[], minLines: number): DuplicateRawBlockRs[]
|
|
17
|
-
export declare function fastFindPatterns(rootPath: string, patterns: string[], extensions?: string[], exclude?: string[]): FastGrepMatch[]
|
|
18
|
-
export declare function fastFindFilesByName(rootPath: string, names: string[]): string[]
|
|
19
|
-
export declare function fastFindFilesByExt(rootPath: string, extensions: string[]): string[]
|
|
20
|
-
export declare function fastReadFiles(paths: string[]): FileContent[]
|
|
15
|
+
export declare function fastDetectDriftParallel(rootPath: string, keywords: string[]): DriftResult;
|
|
16
|
+
export declare function fastFindDuplicateBlocks(paths: string[], minLines: number): DuplicateRawBlockRs[];
|
|
17
|
+
export declare function fastFindPatterns(rootPath: string, patterns: string[], extensions?: string[], exclude?: string[]): FastGrepMatch[];
|
|
18
|
+
export declare function fastFindFilesByName(rootPath: string, names: string[]): string[];
|
|
19
|
+
export declare function fastFindFilesByExt(rootPath: string, extensions: string[]): string[];
|
|
20
|
+
export declare function fastReadFiles(paths: string[]): FileContent[];
|
|
21
21
|
export declare function fastScanSpecs(rootPath: string): SpecBriefRs[] | null;
|
|
22
22
|
export declare function fastVerifyGapsChain(filePath: string): GapsLogChainResultRs;
|
|
23
23
|
export declare function fastAppendGapEntry(filePath: string, id: string, timestamp: string, specId: string, description: string, severity: string, affectedSpecs: string[]): string;
|
|
@@ -7,7 +7,7 @@ import { goDetector } from './detectors/go-detector.js';
|
|
|
7
7
|
import { rustDetector } from './detectors/rust-detector.js';
|
|
8
8
|
import { javaDetector } from './detectors/java-detector.js';
|
|
9
9
|
import { frameworkDetector } from './detectors/framework-detector.js';
|
|
10
|
-
import {
|
|
10
|
+
import { fastReadFiles } from '../core-bridge.js';
|
|
11
11
|
export { fixCrashRisks } from './auto-fixer/index.js';
|
|
12
12
|
const ALL_DETECTORS = [
|
|
13
13
|
typescriptDetector,
|
|
@@ -17,7 +17,6 @@ const ALL_DETECTORS = [
|
|
|
17
17
|
javaDetector,
|
|
18
18
|
frameworkDetector,
|
|
19
19
|
];
|
|
20
|
-
const FILE_SCAN_TIMEOUT_MS = 1000;
|
|
21
20
|
/**
|
|
22
21
|
* Scans a project for crash risks and returns a SafetyReport.
|
|
23
22
|
* @param projectPath - absolute path to the project root
|
|
@@ -39,80 +38,44 @@ export async function scanCrashRisks(projectPath, stackHint) {
|
|
|
39
38
|
}
|
|
40
39
|
async function scanFiles(files, detectedStack) {
|
|
41
40
|
const applicableDetectors = selectDetectors(detectedStack);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const found = detector.detect(path, content);
|
|
51
|
-
risks.push(...found);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Detector error: skip silently
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return risks;
|
|
58
|
-
});
|
|
59
|
-
return results.flat();
|
|
41
|
+
const fileContents = fastReadFiles(files);
|
|
42
|
+
const seen = new Set(fileContents.map((file) => file.path));
|
|
43
|
+
const results = fileContents.map(({ path, content }) => detectRisksInContent(path, content, applicableDetectors));
|
|
44
|
+
const unreadByBridge = files.filter((file) => !seen.has(file));
|
|
45
|
+
const fallbackResults = await Promise.all(unreadByBridge.map(async (file) => {
|
|
46
|
+
try {
|
|
47
|
+
const content = await readFile(file, 'utf-8');
|
|
48
|
+
return detectRisksInContent(file, content, applicableDetectors);
|
|
60
49
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return results.flat();
|
|
64
|
-
}
|
|
65
|
-
function selectDetectors(detectedStack) {
|
|
66
|
-
if (detectedStack.length === 0) {
|
|
67
|
-
return ALL_DETECTORS;
|
|
68
|
-
}
|
|
69
|
-
return ALL_DETECTORS.filter((detector) => {
|
|
70
|
-
if (detector.language === 'multi') {
|
|
71
|
-
return true;
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
72
52
|
}
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
}));
|
|
54
|
+
return [...results, ...fallbackResults].flat();
|
|
75
55
|
}
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
if (applicableDetectors.length === 0) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
let content;
|
|
82
|
-
try {
|
|
83
|
-
content = await withTimeout(readFile(file, 'utf-8'), FILE_SCAN_TIMEOUT_MS, file);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return [];
|
|
87
|
-
}
|
|
56
|
+
function detectRisksInContent(path, content, applicableDetectors) {
|
|
57
|
+
const fileDetectors = applicableDetectors.filter((d) => d.fileExtensions.some((ext) => path.endsWith(ext)));
|
|
88
58
|
const risks = [];
|
|
89
|
-
for (const detector of
|
|
59
|
+
for (const detector of fileDetectors) {
|
|
90
60
|
try {
|
|
91
|
-
const found = detector.detect(
|
|
61
|
+
const found = detector.detect(path, content);
|
|
92
62
|
risks.push(...found);
|
|
93
63
|
}
|
|
94
64
|
catch {
|
|
95
|
-
/* c8 ignore next */
|
|
96
65
|
// Detector error: skip silently
|
|
97
66
|
}
|
|
98
67
|
}
|
|
99
68
|
return risks;
|
|
100
69
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
/* c8 ignore next 4 */
|
|
112
|
-
(err) => {
|
|
113
|
-
clearTimeout(timer);
|
|
114
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
115
|
-
});
|
|
70
|
+
function selectDetectors(detectedStack) {
|
|
71
|
+
if (detectedStack.length === 0) {
|
|
72
|
+
return ALL_DETECTORS;
|
|
73
|
+
}
|
|
74
|
+
return ALL_DETECTORS.filter((detector) => {
|
|
75
|
+
if (detector.language === 'multi') {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return detectedStack.includes(detector.language);
|
|
116
79
|
});
|
|
117
80
|
}
|
|
118
81
|
//# sourceMappingURL=index.js.map
|
|
@@ -10,24 +10,6 @@ const LANGUAGE_EXTENSIONS = {
|
|
|
10
10
|
typescript: ['.ts', '.tsx'],
|
|
11
11
|
javascript: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
12
12
|
};
|
|
13
|
-
/**
|
|
14
|
-
* Normalize a single line: remove inline comments, trim, lowercase.
|
|
15
|
-
*/
|
|
16
|
-
function normalizeLine(line) {
|
|
17
|
-
let trimmed = line;
|
|
18
|
-
// Remove single-line comments (but not URLs like http://)
|
|
19
|
-
const commentIdx = trimmed.indexOf('//');
|
|
20
|
-
if (commentIdx >= 0) {
|
|
21
|
-
trimmed = trimmed.slice(0, commentIdx);
|
|
22
|
-
}
|
|
23
|
-
return trimmed.trim().toLowerCase();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Remove multi-line comment blocks from source text.
|
|
27
|
-
*/
|
|
28
|
-
function stripMultilineComments(text) {
|
|
29
|
-
return text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
30
|
-
}
|
|
31
13
|
/**
|
|
32
14
|
* Build glob patterns for the requested languages.
|
|
33
15
|
*/
|
|
@@ -46,6 +28,24 @@ function buildIgnorePatterns(userExclude) {
|
|
|
46
28
|
const user = userExclude ?? [];
|
|
47
29
|
return [...DEFAULT_EXCLUDE, ...user];
|
|
48
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a single line: remove inline comments, trim, lowercase.
|
|
33
|
+
*/
|
|
34
|
+
function normalizeLine(line) {
|
|
35
|
+
let trimmed = line;
|
|
36
|
+
// Remove single-line comments (but not URLs like http://)
|
|
37
|
+
const commentIdx = trimmed.indexOf('//');
|
|
38
|
+
if (commentIdx >= 0) {
|
|
39
|
+
trimmed = trimmed.slice(0, commentIdx);
|
|
40
|
+
}
|
|
41
|
+
return trimmed.trim().toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Remove multi-line comment blocks from source text.
|
|
45
|
+
*/
|
|
46
|
+
function stripMultilineComments(text) {
|
|
47
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
48
|
+
}
|
|
49
49
|
/**
|
|
50
50
|
* Hash an array of normalized lines using SHA-256 (first 16 chars for speed).
|
|
51
51
|
*/
|
|
@@ -153,7 +153,47 @@ function buildSuggestion(cluster) {
|
|
|
153
153
|
}
|
|
154
154
|
return `Extract ${String(lineCount)} lines from ${first.path}:${String(first.startLine)} and ${second.path}:${String(second.startLine)} into shared utility`;
|
|
155
155
|
}
|
|
156
|
-
import {
|
|
156
|
+
import { fastFindDuplicateBlocks } from './core-bridge.js';
|
|
157
|
+
async function detectDuplicateBlocksFallback(uniqueFiles, minLines) {
|
|
158
|
+
const hashMap = new Map();
|
|
159
|
+
for (const filePath of uniqueFiles) {
|
|
160
|
+
let content;
|
|
161
|
+
try {
|
|
162
|
+
content = await readFile(filePath, 'utf-8');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
content = stripMultilineComments(content);
|
|
168
|
+
const rawLines = content.split('\n');
|
|
169
|
+
const blocks = extractBlocks(filePath, rawLines, minLines);
|
|
170
|
+
for (const block of blocks) {
|
|
171
|
+
const h = hashLines(block.lines);
|
|
172
|
+
const existing = hashMap.get(h) ?? [];
|
|
173
|
+
existing.push(block);
|
|
174
|
+
hashMap.set(h, existing);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const rawClusters = [];
|
|
178
|
+
for (const [h, locations] of hashMap.entries()) {
|
|
179
|
+
if (locations.length >= 2) {
|
|
180
|
+
rawClusters.push({ hash: h, locations });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return rawClusters;
|
|
184
|
+
}
|
|
185
|
+
function isDuplicateRawBlockArray(value) {
|
|
186
|
+
if (!Array.isArray(value)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return value.every((item) => {
|
|
190
|
+
if (typeof item !== 'object' || item === null) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const candidate = item;
|
|
194
|
+
return typeof candidate.hash === 'string' && Array.isArray(candidate.locations);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
157
197
|
/**
|
|
158
198
|
* Detect code duplication across project files.
|
|
159
199
|
*/
|
|
@@ -173,40 +213,10 @@ export async function detectDuplication(input) {
|
|
|
173
213
|
}
|
|
174
214
|
// Deduplicate file list
|
|
175
215
|
const uniqueFiles = [...new Set(allFiles)];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
rawClusters = nativeBlocks;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
const hashMap = new Map();
|
|
185
|
-
for (const filePath of uniqueFiles) {
|
|
186
|
-
let content;
|
|
187
|
-
try {
|
|
188
|
-
content = await readFile(filePath, 'utf-8');
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
content = stripMultilineComments(content);
|
|
194
|
-
const rawLines = content.split('\n');
|
|
195
|
-
const blocks = extractBlocks(filePath, rawLines, minLines);
|
|
196
|
-
for (const block of blocks) {
|
|
197
|
-
const h = hashLines(block.lines);
|
|
198
|
-
const existing = hashMap.get(h) ?? [];
|
|
199
|
-
existing.push(block);
|
|
200
|
-
hashMap.set(h, existing);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Find collisions (same hash, different files or different locations)
|
|
204
|
-
for (const [h, locations] of hashMap.entries()) {
|
|
205
|
-
if (locations.length >= 2) {
|
|
206
|
-
rawClusters.push({ hash: h, locations });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
216
|
+
const bridgeResult = fastFindDuplicateBlocks(uniqueFiles, minLines);
|
|
217
|
+
const rawClusters = isDuplicateRawBlockArray(bridgeResult)
|
|
218
|
+
? bridgeResult
|
|
219
|
+
: await detectDuplicateBlocksFallback(uniqueFiles, minLines);
|
|
210
220
|
// Group locations into clusters, merging per-file overlaps
|
|
211
221
|
const clusters = [];
|
|
212
222
|
let clusterId = 0;
|
|
@@ -281,7 +281,7 @@ async function computeAcCoverageScore(criteria, filePaths) {
|
|
|
281
281
|
}
|
|
282
282
|
const rootPath = path.dirname(firstFile); // Use first file's dir as heuristic
|
|
283
283
|
const result = fastDetectDriftParallel(rootPath, [...allTerms]);
|
|
284
|
-
foundTerms = new Set(result
|
|
284
|
+
foundTerms = new Set(result.foundKeywords);
|
|
285
285
|
}
|
|
286
286
|
else {
|
|
287
287
|
// Fallback: Read all implementation files once
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InteractiveQuestion, NewProjectOnboardingInput, NewProjectQuestionState, NewProjectResolution } from '../../types/index.js';
|
|
2
|
+
export declare function slugifyAppName(input: string): string;
|
|
3
|
+
export declare function validateAppSlug(slug: string): string | null;
|
|
4
|
+
export declare function resolveNewProjectPath(parentWorkspacePath: string, appSlug: string): string;
|
|
5
|
+
export declare function buildNewProjectQuestions(input: NewProjectOnboardingInput, resolved: NewProjectQuestionState): InteractiveQuestion[];
|
|
6
|
+
export declare function resolveNewProjectOnboarding(input: NewProjectOnboardingInput): Promise<NewProjectResolution>;
|
|
7
|
+
//# sourceMappingURL=new-project-resolver.d.ts.map
|