@rigour-labs/core 6.2.4 → 6.3.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/README.md +2 -0
- package/dist/deep/code-context.d.ts +28 -0
- package/dist/deep/code-context.js +93 -0
- package/dist/deep/code-context.test.d.ts +1 -0
- package/dist/deep/code-context.test.js +48 -0
- package/dist/deep/code-pass.d.ts +15 -0
- package/dist/deep/code-pass.js +44 -0
- package/dist/deep/code-review-prompt.d.ts +17 -0
- package/dist/deep/code-review-prompt.js +30 -0
- package/dist/deep/code-review-prompt.test.d.ts +1 -0
- package/dist/deep/code-review-prompt.test.js +20 -0
- package/dist/deep/code-verifier.d.ts +19 -0
- package/dist/deep/code-verifier.js +48 -0
- package/dist/deep/code-verifier.test.d.ts +1 -0
- package/dist/deep/code-verifier.test.js +46 -0
- package/dist/deep/fact-extractor.d.ts +7 -1
- package/dist/deep/fact-extractor.js +16 -8
- package/dist/deep/fact-extractor.test.js +34 -1
- package/dist/deep/facts-pass.d.ts +24 -0
- package/dist/deep/facts-pass.js +69 -0
- package/dist/deep/index.d.ts +6 -1
- package/dist/deep/index.js +5 -1
- package/dist/deep/parse-findings.d.ts +12 -0
- package/dist/deep/parse-findings.js +73 -0
- package/dist/deep/parse-findings.test.d.ts +1 -0
- package/dist/deep/parse-findings.test.js +25 -0
- package/dist/deep/prompts.d.ts +1 -1
- package/dist/deep/prompts.js +7 -8
- package/dist/deep/prompts.test.js +15 -0
- package/dist/discovery-signals.d.ts +27 -0
- package/dist/discovery-signals.js +96 -0
- package/dist/discovery.d.ts +1 -2
- package/dist/discovery.js +36 -49
- package/dist/discovery.test.js +84 -71
- package/dist/gates/context.js +7 -0
- package/dist/gates/deep-analysis.d.ts +33 -8
- package/dist/gates/deep-analysis.js +169 -306
- package/dist/gates/deep-analysis.test.d.ts +1 -0
- package/dist/gates/deep-analysis.test.js +86 -0
- package/dist/gates/deep-intent.d.ts +3 -0
- package/dist/gates/deep-intent.js +57 -0
- package/dist/gates/deep-intent.test.d.ts +1 -0
- package/dist/gates/deep-intent.test.js +51 -0
- package/dist/gates/deep-runner.d.ts +15 -0
- package/dist/gates/deep-runner.js +66 -0
- package/dist/gates/dependency.js +4 -0
- package/dist/gates/deprecated-apis-ast.d.ts +8 -0
- package/dist/gates/deprecated-apis-ast.js +25 -0
- package/dist/gates/deprecated-apis-rules-node.js +2 -8
- package/dist/gates/deprecated-apis.js +5 -1
- package/dist/gates/deprecated-apis.test.js +30 -0
- package/dist/gates/frontend-secret-exposure.js +5 -1
- package/dist/gates/frontend-secret-exposure.test.js +20 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
- package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
- package/dist/gates/hallucinated-imports/index.js +14 -6
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
- package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
- package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
- package/dist/gates/logic-drift.js +7 -1
- package/dist/gates/runner.js +10 -40
- package/dist/gates/runner.test.js +37 -25
- package/dist/gates/scoped-gates.test.d.ts +1 -0
- package/dist/gates/scoped-gates.test.js +52 -0
- package/dist/gates/semantic-bugs.d.ts +23 -0
- package/dist/gates/semantic-bugs.js +48 -0
- package/dist/gates/semantic-bugs.test.d.ts +1 -0
- package/dist/gates/semantic-bugs.test.js +70 -0
- package/dist/gates/side-effect-analysis/index.js +11 -1
- package/dist/gates/side-effect-analysis.test.d.ts +1 -0
- package/dist/gates/side-effect-analysis.test.js +162 -0
- package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
- package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
- package/dist/gates/style-drift.js +3 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -2
- package/dist/inference/http-download.d.ts +22 -0
- package/dist/inference/http-download.js +132 -0
- package/dist/inference/http-download.test.d.ts +1 -0
- package/dist/inference/http-download.test.js +109 -0
- package/dist/inference/index.d.ts +5 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/llama-engine.d.ts +27 -0
- package/dist/inference/llama-engine.js +152 -0
- package/dist/inference/llama-engine.test.d.ts +1 -0
- package/dist/inference/llama-engine.test.js +51 -0
- package/dist/inference/llama-process.d.ts +72 -0
- package/dist/inference/llama-process.js +126 -0
- package/dist/inference/llama-process.test.d.ts +1 -0
- package/dist/inference/llama-process.test.js +61 -0
- package/dist/inference/model-download.test.d.ts +1 -0
- package/dist/inference/model-download.test.js +73 -0
- package/dist/inference/model-manager.d.ts +14 -1
- package/dist/inference/model-manager.js +65 -80
- package/dist/inference/sidecar-provider.d.ts +19 -10
- package/dist/inference/sidecar-provider.js +125 -342
- package/dist/inference/types.d.ts +2 -0
- package/dist/semantic/ast.d.ts +29 -0
- package/dist/semantic/ast.js +122 -0
- package/dist/semantic/benchmark.d.ts +16 -0
- package/dist/semantic/benchmark.js +31 -0
- package/dist/semantic/benchmark.test.d.ts +1 -0
- package/dist/semantic/benchmark.test.js +18 -0
- package/dist/semantic/engine.d.ts +8 -0
- package/dist/semantic/engine.js +45 -0
- package/dist/semantic/intent/benchmark.d.ts +31 -0
- package/dist/semantic/intent/benchmark.js +74 -0
- package/dist/semantic/intent/intent-check.d.ts +43 -0
- package/dist/semantic/intent/intent-check.js +72 -0
- package/dist/semantic/intent/intent.test.d.ts +1 -0
- package/dist/semantic/intent/intent.test.js +46 -0
- package/dist/semantic/intent/parallel-reads.d.ts +23 -0
- package/dist/semantic/intent/parallel-reads.js +59 -0
- package/dist/semantic/learn/benchmark.d.ts +20 -0
- package/dist/semantic/learn/benchmark.js +71 -0
- package/dist/semantic/learn/compile.d.ts +4 -0
- package/dist/semantic/learn/compile.js +56 -0
- package/dist/semantic/learn/conditions.d.ts +22 -0
- package/dist/semantic/learn/conditions.js +93 -0
- package/dist/semantic/learn/extract.d.ts +30 -0
- package/dist/semantic/learn/extract.js +122 -0
- package/dist/semantic/learn/git-trees.d.ts +8 -0
- package/dist/semantic/learn/git-trees.js +32 -0
- package/dist/semantic/learn/identity.d.ts +18 -0
- package/dist/semantic/learn/identity.js +97 -0
- package/dist/semantic/learn/learn.d.ts +25 -0
- package/dist/semantic/learn/learn.js +109 -0
- package/dist/semantic/learn/learn.test.d.ts +1 -0
- package/dist/semantic/learn/learn.test.js +67 -0
- package/dist/semantic/learn/store.d.ts +4 -0
- package/dist/semantic/learn/store.js +39 -0
- package/dist/semantic/learn/types.d.ts +468 -0
- package/dist/semantic/learn/types.js +55 -0
- package/dist/semantic/origins.d.ts +42 -0
- package/dist/semantic/origins.js +149 -0
- package/dist/semantic/origins.test.d.ts +1 -0
- package/dist/semantic/origins.test.js +77 -0
- package/dist/semantic/program.d.ts +13 -0
- package/dist/semantic/program.js +62 -0
- package/dist/semantic/rules/credential-redirect.d.ts +2 -0
- package/dist/semantic/rules/credential-redirect.js +209 -0
- package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
- package/dist/semantic/rules/degraded-response-cached.js +271 -0
- package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
- package/dist/semantic/rules/in-memory-aggregation.js +179 -0
- package/dist/semantic/summaries.d.ts +18 -0
- package/dist/semantic/summaries.js +33 -0
- package/dist/semantic/types.d.ts +32 -0
- package/dist/semantic/types.js +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/team-diagnostics.d.ts +22 -0
- package/dist/storage/team-diagnostics.js +58 -0
- package/dist/storage/team-diagnostics.test.d.ts +1 -0
- package/dist/storage/team-diagnostics.test.js +51 -0
- package/dist/storage/team-schema.d.ts +6 -0
- package/dist/storage/team-schema.js +82 -0
- package/dist/storage/team-store.js +25 -72
- package/dist/templates/universal-config.js +4 -2
- package/dist/types/index.d.ts +114 -14
- package/dist/types/index.js +16 -2
- package/dist/utils/deep-status.d.ts +7 -0
- package/dist/utils/deep-status.js +11 -0
- package/dist/utils/diff.d.ts +3 -0
- package/dist/utils/diff.js +52 -0
- package/dist/utils/diff.test.d.ts +1 -0
- package/dist/utils/diff.test.js +38 -0
- package/dist/utils/scanner.js +13 -2
- package/dist/utils/scanner.test.js +22 -8
- package/dist/utils/scope.d.ts +15 -0
- package/dist/utils/scope.js +51 -0
- package/dist/utils/scope.test.d.ts +1 -0
- package/dist/utils/scope.test.js +38 -0
- package/package.json +6 -6
package/dist/deep/index.d.ts
CHANGED
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Neither AST nor LLM works alone. Together they're accurate.
|
|
9
9
|
*/
|
|
10
|
-
export { extractFacts, factsToPromptString } from './fact-extractor.js';
|
|
10
|
+
export { extractFacts, factsToPromptString, isTestFile } from './fact-extractor.js';
|
|
11
11
|
export type { FileFacts, ClassFact, FunctionFact, ErrorHandlingFact, StructFact, InterfaceFact } from './fact-extractor.js';
|
|
12
12
|
export { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
13
13
|
export { verifyFindings } from './verifier.js';
|
|
14
14
|
export type { VerifiedFinding } from './verifier.js';
|
|
15
|
+
export { parseFindings } from './parse-findings.js';
|
|
16
|
+
export { buildCodeContext } from './code-context.js';
|
|
17
|
+
export type { CodeContext, LineRange } from './code-context.js';
|
|
18
|
+
export { buildCodeReviewPrompt, REVIEW_CATEGORIES } from './code-review-prompt.js';
|
|
19
|
+
export { verifyCodeFindings } from './code-verifier.js';
|
package/dist/deep/index.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Neither AST nor LLM works alone. Together they're accurate.
|
|
9
9
|
*/
|
|
10
|
-
export { extractFacts, factsToPromptString } from './fact-extractor.js';
|
|
10
|
+
export { extractFacts, factsToPromptString, isTestFile } from './fact-extractor.js';
|
|
11
11
|
export { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
12
12
|
export { verifyFindings } from './verifier.js';
|
|
13
|
+
export { parseFindings } from './parse-findings.js';
|
|
14
|
+
export { buildCodeContext } from './code-context.js';
|
|
15
|
+
export { buildCodeReviewPrompt, REVIEW_CATEGORIES } from './code-review-prompt.js';
|
|
16
|
+
export { verifyCodeFindings } from './code-verifier.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an LLM response into structured findings.
|
|
3
|
+
*
|
|
4
|
+
* Handles raw JSON, markdown-wrapped JSON, a findings object embedded in
|
|
5
|
+
* prose, and truncated responses (the model hit its token budget mid-array).
|
|
6
|
+
*/
|
|
7
|
+
import type { DeepFinding } from '../inference/types.js';
|
|
8
|
+
export declare function parseFindings(response: string): DeepFinding[];
|
|
9
|
+
/** Extract the complete finding objects from a response cut off mid-array. */
|
|
10
|
+
export declare function recoverTruncatedFindings(response: string): DeepFinding[];
|
|
11
|
+
/** Drop malformed entries and normalise confidence and severity. */
|
|
12
|
+
export declare function validateFindings(raw: unknown[]): DeepFinding[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
const VALID_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
3
|
+
const FINDING_OBJECT = /\{\s*"category"\s*:\s*"[^"]+"\s*,[\s\S]*?"description"\s*:\s*"[^"]*"[^}]*\}/g;
|
|
4
|
+
export function parseFindings(response) {
|
|
5
|
+
if (!response || response.trim().length === 0) {
|
|
6
|
+
Logger.warn('Empty LLM response received');
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const candidates = [
|
|
10
|
+
response,
|
|
11
|
+
response.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1],
|
|
12
|
+
response.match(/\{[\s\S]*"findings"[\s\S]*\}/)?.[0],
|
|
13
|
+
];
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
const parsed = candidate === undefined ? null : tryParseFindings(candidate);
|
|
16
|
+
if (parsed)
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
const recovered = recoverTruncatedFindings(response);
|
|
20
|
+
if (recovered.length > 0) {
|
|
21
|
+
Logger.info(`Recovered ${recovered.length} findings from truncated response`);
|
|
22
|
+
return recovered;
|
|
23
|
+
}
|
|
24
|
+
Logger.warn(`Could not parse LLM response as findings JSON. First 200 chars: ${response.substring(0, 200)}`);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
function tryParseFindings(text) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(text);
|
|
30
|
+
if (Array.isArray(parsed?.findings))
|
|
31
|
+
return validateFindings(parsed.findings);
|
|
32
|
+
if (Array.isArray(parsed))
|
|
33
|
+
return validateFindings(parsed);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Extract the complete finding objects from a response cut off mid-array. */
|
|
41
|
+
export function recoverTruncatedFindings(response) {
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const match of response.matchAll(FINDING_OBJECT)) {
|
|
44
|
+
try {
|
|
45
|
+
findings.push(JSON.parse(match[0]));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// The object itself was truncated; skip it.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return validateFindings(findings);
|
|
52
|
+
}
|
|
53
|
+
/** Drop malformed entries and normalise confidence and severity. */
|
|
54
|
+
export function validateFindings(raw) {
|
|
55
|
+
return raw.filter(hasRequiredFields).map(normalizeFinding);
|
|
56
|
+
}
|
|
57
|
+
function hasRequiredFields(f) {
|
|
58
|
+
return !!f && typeof f === 'object'
|
|
59
|
+
&& isNonEmptyString(f.category)
|
|
60
|
+
&& isNonEmptyString(f.file)
|
|
61
|
+
&& isNonEmptyString(f.description);
|
|
62
|
+
}
|
|
63
|
+
function isNonEmptyString(value) {
|
|
64
|
+
return typeof value === 'string' && value.length > 0;
|
|
65
|
+
}
|
|
66
|
+
function normalizeFinding(f) {
|
|
67
|
+
const confidenceOk = typeof f.confidence === 'number' && f.confidence >= 0 && f.confidence <= 1;
|
|
68
|
+
return {
|
|
69
|
+
...f,
|
|
70
|
+
confidence: confidenceOk ? f.confidence : 0.5,
|
|
71
|
+
severity: VALID_SEVERITIES.includes(f.severity) ? f.severity : 'medium',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseFindings } from './parse-findings.js';
|
|
3
|
+
const finding = { category: 'correctness', severity: 'high', file: 'a.ts', line: 3, description: 'bug', suggestion: 'fix', confidence: 0.8 };
|
|
4
|
+
describe('parseFindings', () => {
|
|
5
|
+
it('parses a findings object, a bare array and fenced JSON', () => {
|
|
6
|
+
expect(parseFindings(JSON.stringify({ findings: [finding] }))).toHaveLength(1);
|
|
7
|
+
expect(parseFindings(JSON.stringify([finding]))).toHaveLength(1);
|
|
8
|
+
expect(parseFindings('Here you go:\n```json\n' + JSON.stringify({ findings: [finding] }) + '\n```')).toHaveLength(1);
|
|
9
|
+
});
|
|
10
|
+
it('recovers complete findings from a response cut off by the token budget', () => {
|
|
11
|
+
const cut = `{"findings": [${JSON.stringify(finding)}, {"category": "security", "severity": "hi`;
|
|
12
|
+
expect(parseFindings(cut)).toEqual([finding]);
|
|
13
|
+
});
|
|
14
|
+
it('drops malformed entries and normalises confidence and severity', () => {
|
|
15
|
+
const parsed = parseFindings(JSON.stringify({ findings: [
|
|
16
|
+
{ ...finding, severity: 'urgent', confidence: 7 },
|
|
17
|
+
{ category: 'x', description: 'no file' },
|
|
18
|
+
] }));
|
|
19
|
+
expect(parsed).toEqual([{ ...finding, severity: 'medium', confidence: 0.5 }]);
|
|
20
|
+
});
|
|
21
|
+
it('returns nothing for empty or non-JSON output', () => {
|
|
22
|
+
expect(parseFindings('')).toEqual([]);
|
|
23
|
+
expect(parseFindings('I could not find any issues.')).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
});
|
package/dist/deep/prompts.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Prompt Engineering — Step 2 of the three-step pipeline.
|
|
3
3
|
* Constructs structured prompts that ask the LLM to interpret AST-extracted facts.
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
5
|
+
import { type FileFacts } from './fact-extractor.js';
|
|
6
6
|
/**
|
|
7
7
|
* System prompt that defines the LLM's role and output format.
|
|
8
8
|
*/
|
package/dist/deep/prompts.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Engineering — Step 2 of the three-step pipeline.
|
|
3
|
+
* Constructs structured prompts that ask the LLM to interpret AST-extracted facts.
|
|
4
|
+
*/
|
|
5
|
+
import { factsToPromptString } from './fact-extractor.js';
|
|
1
6
|
/**
|
|
2
7
|
* System prompt that defines the LLM's role and output format.
|
|
3
8
|
*/
|
|
@@ -362,13 +367,7 @@ export function chunkFacts(facts, maxCharsPerChunk = 6000) {
|
|
|
362
367
|
}
|
|
363
368
|
return chunks;
|
|
364
369
|
}
|
|
370
|
+
/** Serialized size of one file's facts, so chunks never overrun the prompt budget. */
|
|
365
371
|
function estimateFactSize(f) {
|
|
366
|
-
|
|
367
|
-
size += f.classes.reduce((a, c) => a + c.name.length + c.methods.length * 20 + 50, 0);
|
|
368
|
-
size += (f.structs || []).reduce((a, s) => a + s.name.length + s.methods.length * 20 + s.embeds.length * 15 + 60, 0);
|
|
369
|
-
size += (f.interfaces || []).reduce((a, i) => a + i.name.length + i.methods.length * 15 + 40, 0);
|
|
370
|
-
size += f.functions.reduce((a, fn) => a + fn.name.length + fn.params.length * 15 + 50, 0);
|
|
371
|
-
size += f.imports.length * 30;
|
|
372
|
-
size += f.errorHandling.length * 30;
|
|
373
|
-
return size;
|
|
372
|
+
return factsToPromptString([f], Number.POSITIVE_INFINITY).length + 2;
|
|
374
373
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
3
|
+
import { factsToPromptString } from './fact-extractor.js';
|
|
3
4
|
// ── Test helpers ──
|
|
4
5
|
function makeGoFacts(overrides = {}) {
|
|
5
6
|
return {
|
|
@@ -195,6 +196,20 @@ describe('Deep Analysis Prompts', () => {
|
|
|
195
196
|
});
|
|
196
197
|
// ── chunkFacts ──
|
|
197
198
|
describe('chunkFacts', () => {
|
|
199
|
+
it('never lets the serialized chunk overrun the prompt budget, so no file is dropped', () => {
|
|
200
|
+
const facts = Array.from({ length: 40 }, (_, i) => makeTsFacts({
|
|
201
|
+
path: `src/deeply/nested/module-${i}/implementation-file-with-a-long-name-${i}.ts`,
|
|
202
|
+
lineCount: 300,
|
|
203
|
+
imports: Array.from({ length: 8 }, (_, j) => `@scope/package-with-long-name-${j}/sub/path`),
|
|
204
|
+
}));
|
|
205
|
+
const chunks = chunkFacts(facts, 6000);
|
|
206
|
+
expect(chunks.flat()).toHaveLength(facts.length);
|
|
207
|
+
for (const chunk of chunks) {
|
|
208
|
+
const text = factsToPromptString(chunk);
|
|
209
|
+
for (const f of chunk)
|
|
210
|
+
expect(text).toContain(`FILE: ${f.path}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
198
213
|
it('should split facts into token-limited chunks', () => {
|
|
199
214
|
const manyFacts = Array.from({ length: 50 }, (_, i) => makeTsFacts({
|
|
200
215
|
path: `src/file${i}.ts`,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework and library markers. These are decided by the project's declared
|
|
3
|
+
* dependencies only: as words they are too common in code and prose
|
|
4
|
+
* ("next()" in Express middleware, "reactivity" in a Svelte comment).
|
|
5
|
+
*/
|
|
6
|
+
export declare const PACKAGE_MARKERS: Set<string>;
|
|
7
|
+
/** Declared dependency names: package.json sections, requirements.txt and pyproject.toml. */
|
|
8
|
+
export declare function dependencyNames(cwd: string): Promise<Set<string>>;
|
|
9
|
+
/** `marker` is declared as a dependency, directly or as its scope (`@nestjs/core`). */
|
|
10
|
+
export declare function declaresPackage(dependencies: Set<string>, marker: string): boolean;
|
|
11
|
+
/** Up to MAX_SAMPLE project source files (not configs or tests), spread evenly so one package cannot fill the sample. */
|
|
12
|
+
export declare function sampleSourceFiles(cwd: string): Promise<string[]>;
|
|
13
|
+
/** Markers naming a file extension (`ipynb`): proven by one such file existing, not by the word. */
|
|
14
|
+
export declare const EXTENSION_MARKERS: Set<string>;
|
|
15
|
+
export declare function hasFileWithExtension(cwd: string, extension: string): Promise<boolean>;
|
|
16
|
+
/** A marker naming a file or folder (`next.config.js`, `k8s/`, `Dockerfile`), which only its existence proves. */
|
|
17
|
+
export declare function isPathMarker(marker: string): boolean;
|
|
18
|
+
/** `marker` as a whole token, not inside a longer word or package name. */
|
|
19
|
+
export declare function containsToken(text: string, marker: string): boolean;
|
|
20
|
+
/** Drop line and block comments so prose is not read as code. */
|
|
21
|
+
export declare function stripComments(text: string): string;
|
|
22
|
+
export interface ParadigmSignal {
|
|
23
|
+
name: 'oop' | 'functional';
|
|
24
|
+
marker: string;
|
|
25
|
+
}
|
|
26
|
+
/** OOP or functional by declaration counts in comment-free sources; null when neither clearly dominates. */
|
|
27
|
+
export declare function detectParadigm(contents: string[]): ParadigmSignal | null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signals for `rigour init` discovery.
|
|
3
|
+
*
|
|
4
|
+
* Substring search misread projects: "reactivity" in an ESLint comment made
|
|
5
|
+
* a Svelte repo "react", and "the public internet" in a Playwright comment
|
|
6
|
+
* made it "OOP". Framework markers now match package.json dependency names,
|
|
7
|
+
* other markers match whole tokens, and the paradigm comes from counting
|
|
8
|
+
* declarations in real source files, with no guess when neither side leads.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { globby } from 'globby';
|
|
13
|
+
const SOURCE_EXT = '{ts,tsx,js,jsx,mjs,py,go,java,kt,rb}';
|
|
14
|
+
const SOURCE_GLOBS = [
|
|
15
|
+
`{src,lib,app,pkg,api,internal,cmd}/**/*.${SOURCE_EXT}`,
|
|
16
|
+
`{packages,apps,services}/*/{src,lib,app}/**/*.${SOURCE_EXT}`,
|
|
17
|
+
'*.{py,go}',
|
|
18
|
+
];
|
|
19
|
+
const SOURCE_IGNORE = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.d.ts', '**/*.config.*', '**/*.{test,spec}.*', '**/__tests__/**'];
|
|
20
|
+
const MAX_SAMPLE = 40;
|
|
21
|
+
/**
|
|
22
|
+
* Framework and library markers. These are decided by the project's declared
|
|
23
|
+
* dependencies only: as words they are too common in code and prose
|
|
24
|
+
* ("next()" in Express middleware, "reactivity" in a Svelte comment).
|
|
25
|
+
*/
|
|
26
|
+
export const PACKAGE_MARKERS = new Set(['react', 'next', 'vue', 'svelte', 'express', 'fastify', 'nestjs', 'spark', 'pandas']);
|
|
27
|
+
/** Declared dependency names: package.json sections, requirements.txt and pyproject.toml. */
|
|
28
|
+
export async function dependencyNames(cwd) {
|
|
29
|
+
const names = new Set();
|
|
30
|
+
const pkg = await fs.readJson(path.join(cwd, 'package.json')).catch(() => null);
|
|
31
|
+
for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
32
|
+
for (const name of Object.keys(pkg?.[section] ?? {}))
|
|
33
|
+
names.add(name);
|
|
34
|
+
}
|
|
35
|
+
const requirements = await fs.readFile(path.join(cwd, 'requirements.txt'), 'utf-8').catch(() => '');
|
|
36
|
+
for (const match of requirements.matchAll(/^\s*([A-Za-z0-9][\w.-]*)/gm))
|
|
37
|
+
names.add(match[1].toLowerCase());
|
|
38
|
+
const pyproject = await fs.readFile(path.join(cwd, 'pyproject.toml'), 'utf-8').catch(() => '');
|
|
39
|
+
for (const match of pyproject.matchAll(/["']([A-Za-z0-9][\w.-]*)\s*(?:\[[^\]]*\])?\s*(?:[<>=~!;]|["'])/g))
|
|
40
|
+
names.add(match[1].toLowerCase());
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
/** `marker` is declared as a dependency, directly or as its scope (`@nestjs/core`). */
|
|
44
|
+
export function declaresPackage(dependencies, marker) {
|
|
45
|
+
if (dependencies.has(marker))
|
|
46
|
+
return true;
|
|
47
|
+
for (const name of dependencies)
|
|
48
|
+
if (name.startsWith(`@${marker}/`))
|
|
49
|
+
return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/** Up to MAX_SAMPLE project source files (not configs or tests), spread evenly so one package cannot fill the sample. */
|
|
53
|
+
export async function sampleSourceFiles(cwd) {
|
|
54
|
+
const files = (await globby(SOURCE_GLOBS, { cwd, ignore: SOURCE_IGNORE, gitignore: false, followSymbolicLinks: false })).sort();
|
|
55
|
+
const stride = Math.max(1, files.length / MAX_SAMPLE);
|
|
56
|
+
const sample = [];
|
|
57
|
+
for (let i = 0; i < files.length && sample.length < MAX_SAMPLE; i += stride)
|
|
58
|
+
sample.push(files[Math.floor(i)]);
|
|
59
|
+
return sample.map(f => path.join(cwd, f));
|
|
60
|
+
}
|
|
61
|
+
/** Markers naming a file extension (`ipynb`): proven by one such file existing, not by the word. */
|
|
62
|
+
export const EXTENSION_MARKERS = new Set(['ipynb']);
|
|
63
|
+
export async function hasFileWithExtension(cwd, extension) {
|
|
64
|
+
const found = await globby(`**/*.${extension}`, { cwd, ignore: ['**/node_modules/**', '**/.git/**'], gitignore: false, followSymbolicLinks: false, deep: 4 });
|
|
65
|
+
return found.length > 0;
|
|
66
|
+
}
|
|
67
|
+
/** A marker naming a file or folder (`next.config.js`, `k8s/`, `Dockerfile`), which only its existence proves. */
|
|
68
|
+
export function isPathMarker(marker) {
|
|
69
|
+
return /[./]/.test(marker) || /^[A-Z]/.test(marker);
|
|
70
|
+
}
|
|
71
|
+
/** `marker` as a whole token, not inside a longer word or package name. */
|
|
72
|
+
export function containsToken(text, marker) {
|
|
73
|
+
const escaped = marker.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
return new RegExp(`(?<![\\w@/.-])${escaped}(?![\\w-])`, 'i').test(text);
|
|
75
|
+
}
|
|
76
|
+
/** Drop line and block comments so prose is not read as code. */
|
|
77
|
+
export function stripComments(text) {
|
|
78
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:"'`])\/\/.*$/gm, '$1').replace(/^\s*#.*$/gm, '');
|
|
79
|
+
}
|
|
80
|
+
const CLASS_DECLARATION = /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(?:public\s+)?class\s+[A-Z]\w*/gm;
|
|
81
|
+
const FUNCTION_DECLARATION = /^\s*(?:export\s+(?:default\s+)?(?:async\s+)?function\b|export\s+const\s+\w+\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|\w+)\s*(?::[^=]+)?=>|def\s+\w+\s*\(|func\s+\w+\s*\()/gm;
|
|
82
|
+
/** OOP or functional by declaration counts in comment-free sources; null when neither clearly dominates. */
|
|
83
|
+
export function detectParadigm(contents) {
|
|
84
|
+
let classes = 0;
|
|
85
|
+
let functions = 0;
|
|
86
|
+
for (const text of contents) {
|
|
87
|
+
classes += text.match(CLASS_DECLARATION)?.length ?? 0;
|
|
88
|
+
functions += text.match(FUNCTION_DECLARATION)?.length ?? 0;
|
|
89
|
+
}
|
|
90
|
+
const marker = `declarations: ${classes} classes, ${functions} functions`;
|
|
91
|
+
if (classes > 0 && classes >= 2 * functions)
|
|
92
|
+
return { name: 'oop', marker };
|
|
93
|
+
if (functions > 0 && functions >= 2 * classes)
|
|
94
|
+
return { name: 'functional', marker };
|
|
95
|
+
return null;
|
|
96
|
+
}
|
package/dist/discovery.d.ts
CHANGED
package/dist/discovery.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { TEMPLATES, PARADIGM_TEMPLATES, UNIVERSAL_CONFIG } from './templates/index.js';
|
|
4
|
+
import { EXTENSION_MARKERS, PACKAGE_MARKERS, containsToken, hasFileWithExtension, declaresPackage, dependencyNames, detectParadigm, isPathMarker, sampleSourceFiles, stripComments, } from './discovery-signals.js';
|
|
5
|
+
/**
|
|
6
|
+
* Domain keywords are ambiguous alone ("health" score, "transaction" block), and a
|
|
7
|
+
* domain preset adds compliance docs and stricter gates. It needs several keywords,
|
|
8
|
+
* each recurring across files rather than listed once.
|
|
9
|
+
*/
|
|
10
|
+
const MIN_KEYWORDS = 2;
|
|
11
|
+
const MIN_KEYWORD_FILES = 2;
|
|
4
12
|
export class DiscoveryService {
|
|
5
13
|
async discover(cwd) {
|
|
6
14
|
let config = { ...UNIVERSAL_CONFIG };
|
|
7
15
|
const matches = {};
|
|
8
|
-
|
|
16
|
+
const dependencies = await dependencyNames(cwd);
|
|
17
|
+
const files = await sampleSourceFiles(cwd);
|
|
18
|
+
const contents = await Promise.all(files.map(f => fs.readFile(f, 'utf-8').then(stripComments, () => '')));
|
|
19
|
+
// 1. Role (ui, api, infra, data, domain presets): the first template with a marker wins.
|
|
9
20
|
for (const template of TEMPLATES) {
|
|
10
|
-
const marker = await this.findFirstMarker(cwd, template.markers,
|
|
21
|
+
const marker = await this.findFirstMarker(cwd, template.markers, dependencies, contents);
|
|
11
22
|
if (marker) {
|
|
12
23
|
config = this.mergeConfig(config, template.config);
|
|
13
24
|
matches.preset = { name: template.name, marker };
|
|
14
|
-
break; // Only one role for now
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
// 2. Detect Paradigm (oop, functional)
|
|
18
|
-
for (const template of PARADIGM_TEMPLATES) {
|
|
19
|
-
const marker = await this.findFirstMarker(cwd, template.markers, true); // Search content
|
|
20
|
-
if (marker) {
|
|
21
|
-
config = this.mergeConfig(config, template.config);
|
|
22
|
-
matches.paradigm = { name: template.name, marker };
|
|
23
25
|
break;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
// 2. Paradigm (oop, functional): only when the declarations clearly lean one way.
|
|
29
|
+
const paradigm = detectParadigm(contents);
|
|
30
|
+
const template = paradigm && PARADIGM_TEMPLATES.find(t => t.name === paradigm.name);
|
|
31
|
+
if (paradigm && template) {
|
|
32
|
+
config = this.mergeConfig(config, template.config);
|
|
33
|
+
matches.paradigm = paradigm;
|
|
34
|
+
}
|
|
26
35
|
return { config, matches };
|
|
27
36
|
}
|
|
28
37
|
mergeConfig(base, extension) {
|
|
@@ -47,49 +56,27 @@ export class DiscoveryService {
|
|
|
47
56
|
ignore: [...new Set([...(base.ignore || []), ...(extension.ignore || [])])],
|
|
48
57
|
};
|
|
49
58
|
}
|
|
50
|
-
async findFirstMarker(cwd, markers,
|
|
59
|
+
async findFirstMarker(cwd, markers, dependencies, contents) {
|
|
60
|
+
const keywords = [];
|
|
51
61
|
for (const marker of markers) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
if (PACKAGE_MARKERS.has(marker)) {
|
|
63
|
+
if (declaresPackage(dependencies, marker))
|
|
64
|
+
return `dependency:${marker}`;
|
|
65
|
+
}
|
|
66
|
+
else if (EXTENSION_MARKERS.has(marker)) {
|
|
67
|
+
if (await hasFileWithExtension(cwd, marker))
|
|
68
|
+
return `file:*.${marker}`;
|
|
69
|
+
}
|
|
70
|
+
else if (await fs.pathExists(path.join(cwd, marker))) {
|
|
55
71
|
return marker;
|
|
56
72
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const match = await this.existsInContent(cwd, marker);
|
|
60
|
-
if (match)
|
|
61
|
-
return `content:${marker}`;
|
|
73
|
+
else if (!isPathMarker(marker) && this.recurs(contents, marker)) {
|
|
74
|
+
keywords.push(marker);
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
async existsInContent(cwd, pattern) {
|
|
67
|
-
// Simple heuristic: search in top 5 source files
|
|
68
|
-
const files = await this.findSourceFiles(cwd);
|
|
69
|
-
for (const file of files) {
|
|
70
|
-
const content = await fs.readFile(file, 'utf-8');
|
|
71
|
-
if (content.includes(pattern))
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
77
|
+
return keywords.length >= MIN_KEYWORDS ? `content:${keywords.join(',')}` : null;
|
|
75
78
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const samples = [];
|
|
79
|
-
const commonDirs = ['.', 'src', 'app', 'lib', 'api', 'pkg'];
|
|
80
|
-
for (const dir of commonDirs) {
|
|
81
|
-
const fullDir = path.join(cwd, dir);
|
|
82
|
-
if (!(await fs.pathExists(fullDir)))
|
|
83
|
-
continue;
|
|
84
|
-
const files = await fs.readdir(fullDir);
|
|
85
|
-
for (const file of files) {
|
|
86
|
-
if (extensions.some(ext => file.endsWith(ext))) {
|
|
87
|
-
samples.push(path.join(fullDir, file));
|
|
88
|
-
if (samples.length >= 5)
|
|
89
|
-
return samples;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return samples;
|
|
79
|
+
recurs(contents, keyword) {
|
|
80
|
+
return contents.filter(text => containsToken(text, keyword)).length >= MIN_KEYWORD_FILES;
|
|
94
81
|
}
|
|
95
82
|
}
|
package/dist/discovery.test.js
CHANGED
|
@@ -1,80 +1,93 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
5
|
import { DiscoveryService } from './discovery.js';
|
|
3
|
-
import
|
|
4
|
-
vi.mock('fs-extra');
|
|
6
|
+
import { containsToken, detectParadigm, stripComments } from './discovery-signals.js';
|
|
5
7
|
describe('DiscoveryService', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
let cwd;
|
|
9
|
+
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'discovery-')); });
|
|
10
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
11
|
+
function write(rel, body) {
|
|
12
|
+
fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true });
|
|
13
|
+
fs.writeFileSync(path.join(cwd, rel), body);
|
|
14
|
+
}
|
|
15
|
+
const discover = () => new DiscoveryService().discover(cwd);
|
|
16
|
+
it('detects an API from a declared dependency', async () => {
|
|
17
|
+
write('package.json', JSON.stringify({ dependencies: { express: '^4.0.0' } }));
|
|
18
|
+
const result = await discover();
|
|
19
|
+
expect(result.matches.preset).toEqual({ name: 'api', marker: 'dependency:express' });
|
|
8
20
|
});
|
|
9
|
-
it('
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
vi.mocked(fs.readdir).mockResolvedValue(['package.json']);
|
|
13
|
-
vi.mocked(fs.readFile).mockResolvedValue('{}');
|
|
14
|
-
const result = await service.discover('/test');
|
|
15
|
-
// If package.json doesn't match a specific role marker, it stays Universal.
|
|
16
|
-
// Let's mock a specific one like 'express'
|
|
17
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p) => p.includes('express'));
|
|
18
|
-
const result2 = await service.discover('/test');
|
|
19
|
-
expect(result2.matches.preset?.name).toBe('api');
|
|
21
|
+
it('detects a scoped framework package such as @nestjs/core', async () => {
|
|
22
|
+
write('package.json', JSON.stringify({ dependencies: { '@nestjs/core': '^10.0.0' } }));
|
|
23
|
+
expect((await discover()).matches.preset?.marker).toBe('dependency:nestjs');
|
|
20
24
|
});
|
|
21
|
-
it('
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (p.endsWith('src'))
|
|
25
|
-
return true;
|
|
26
|
-
if (p.includes('src/index.ts'))
|
|
27
|
-
return true;
|
|
28
|
-
return false;
|
|
29
|
-
}));
|
|
30
|
-
vi.mocked(fs.readdir).mockImplementation((async (p) => {
|
|
31
|
-
if (p.toString().endsWith('/test'))
|
|
32
|
-
return ['src'];
|
|
33
|
-
if (p.toString().endsWith('src'))
|
|
34
|
-
return ['index.ts'];
|
|
35
|
-
return [];
|
|
36
|
-
}));
|
|
37
|
-
vi.mocked(fs.readFile).mockResolvedValue('export const x = 1;');
|
|
38
|
-
const result = await service.discover('/test');
|
|
39
|
-
// Since UNIVERSAL_CONFIG has a default, we check if it found something extra or matches expectation
|
|
40
|
-
// Default is universal, but detecting .ts should tilt it towards node or similar if configured
|
|
41
|
-
// In our current templates, package.json is the node marker.
|
|
42
|
-
// Let's check for paradigm detection which uses content
|
|
43
|
-
expect(result.config).toBeDefined();
|
|
44
|
-
});
|
|
45
|
-
it('should identify OOP paradigm from content in subfolder', async () => {
|
|
46
|
-
const service = new DiscoveryService();
|
|
47
|
-
vi.mocked(fs.pathExists).mockImplementation((async (p) => p.endsWith('src') || p.endsWith('src/Service.ts')));
|
|
48
|
-
vi.mocked(fs.readdir).mockImplementation((async (p) => {
|
|
49
|
-
if (p.toString().endsWith('src'))
|
|
50
|
-
return ['Service.ts'];
|
|
51
|
-
return ['src'];
|
|
52
|
-
}));
|
|
53
|
-
vi.mocked(fs.readFile).mockResolvedValue('class MyService {}');
|
|
54
|
-
const result = await service.discover('/test');
|
|
55
|
-
expect(result.matches.paradigm?.name).toBe('oop');
|
|
56
|
-
});
|
|
57
|
-
it('should include project-type-aware ignore patterns for API preset', async () => {
|
|
58
|
-
const service = new DiscoveryService();
|
|
59
|
-
// Mock finding requirements.txt (Python API marker)
|
|
60
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p) => p.includes('requirements.txt'));
|
|
61
|
-
vi.mocked(fs.readdir).mockResolvedValue(['requirements.txt']);
|
|
62
|
-
vi.mocked(fs.readFile).mockResolvedValue('flask==2.0.0');
|
|
63
|
-
const result = await service.discover('/test');
|
|
25
|
+
it('detects a Python API with its ignore patterns', async () => {
|
|
26
|
+
write('requirements.txt', 'flask==2.0.0\n');
|
|
27
|
+
const result = await discover();
|
|
64
28
|
expect(result.matches.preset?.name).toBe('api');
|
|
65
|
-
expect(result.config.ignore).
|
|
66
|
-
expect(result.config.ignore).toContain('__pycache__/**');
|
|
67
|
-
expect(result.config.ignore).toContain('*.pyc');
|
|
29
|
+
expect(result.config.ignore).toEqual(expect.arrayContaining(['venv/**', '__pycache__/**', '*.pyc']));
|
|
68
30
|
});
|
|
69
|
-
it('
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p) => p.includes('next.config.js'));
|
|
73
|
-
vi.mocked(fs.readdir).mockResolvedValue(['next.config.js']);
|
|
74
|
-
vi.mocked(fs.readFile).mockResolvedValue('module.exports = {}');
|
|
75
|
-
const result = await service.discover('/test');
|
|
31
|
+
it('detects a UI project from a config file with its ignore patterns', async () => {
|
|
32
|
+
write('next.config.js', 'module.exports = {};\n');
|
|
33
|
+
const result = await discover();
|
|
76
34
|
expect(result.matches.preset?.name).toBe('ui');
|
|
77
|
-
expect(result.config.ignore).
|
|
78
|
-
|
|
35
|
+
expect(result.config.ignore).toEqual(expect.arrayContaining(['node_modules/**', '.next/**']));
|
|
36
|
+
});
|
|
37
|
+
it('detects Svelte from its dependencies, not "reactivity" or "public" in comments', async () => {
|
|
38
|
+
write('package.json', JSON.stringify({ devDependencies: { '@sveltejs/kit': '^2.0.0', svelte: '^5.0.0' } }));
|
|
39
|
+
write('eslint.config.js', '// Svelte 5 rules (state_referenced_locally, a11y, reactivity)\nexport default [];\n');
|
|
40
|
+
write('playwright.config.ts', '// rides the public internet\nexport default {};\n');
|
|
41
|
+
write('src/lib/format.ts', 'export function format(v: number) { return v.toFixed(2); }\nexport const pad = (s: string) => s.padStart(2);\n');
|
|
42
|
+
const result = await discover();
|
|
43
|
+
expect(result.matches.preset).toEqual({ name: 'ui', marker: 'dependency:svelte' });
|
|
44
|
+
expect(result.matches.paradigm?.name).toBe('functional');
|
|
45
|
+
});
|
|
46
|
+
it('does not read a framework name in source text as a dependency', async () => {
|
|
47
|
+
write('src/middleware.ts', 'export function run(next: () => void) { next(); }\n');
|
|
48
|
+
expect((await discover()).matches.preset).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
it('identifies OOP from class declarations in a subfolder', async () => {
|
|
51
|
+
write('src/Service.ts', 'export class MyService {}\nexport class OtherService {}\n');
|
|
52
|
+
const result = await discover();
|
|
53
|
+
expect(result.matches.paradigm).toEqual({ name: 'oop', marker: 'declarations: 2 classes, 0 functions' });
|
|
54
|
+
});
|
|
55
|
+
it('needs several recurring domain keywords before choosing a domain preset', async () => {
|
|
56
|
+
write('src/billing.ts', 'export const ledger = new Map<string, number>();\n');
|
|
57
|
+
write('src/report.ts', "import { ledger } from './billing';\nexport const total = () => ledger.size;\n");
|
|
58
|
+
expect((await discover()).matches.preset).toBeUndefined();
|
|
59
|
+
write('src/pay.ts', 'export function payment(ledger: Map<string, number>) { return ledger; }\n');
|
|
60
|
+
write('src/refund.ts', 'export function refund(payment: number) { return -payment; }\n');
|
|
61
|
+
expect((await discover()).matches.preset).toEqual({ name: 'fintech', marker: 'content:payment,ledger' });
|
|
62
|
+
});
|
|
63
|
+
it('needs a file marker to exist, not to be mentioned', async () => {
|
|
64
|
+
write('src/serve.ts', "export const page = () => 'index.html';\n");
|
|
65
|
+
expect((await discover()).matches.preset).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
it('detects notebooks by file, not by the word ipynb in code', async () => {
|
|
68
|
+
write('src/scan.ts', "export const exts = ['.ipynb', '.py'];\n");
|
|
69
|
+
expect((await discover()).matches.preset).toBeUndefined();
|
|
70
|
+
write('notebooks/explore.ipynb', '{}');
|
|
71
|
+
expect((await discover()).matches.preset).toEqual({ name: 'data', marker: 'file:*.ipynb' });
|
|
72
|
+
});
|
|
73
|
+
it('leaves the paradigm unset for an empty project', async () => {
|
|
74
|
+
expect((await discover()).matches.paradigm).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('discovery signals', () => {
|
|
78
|
+
it('matches whole tokens only', () => {
|
|
79
|
+
expect(containsToken('import x from "react"', 'react')).toBe(true);
|
|
80
|
+
expect(containsToken('reactivity', 'react')).toBe(false);
|
|
81
|
+
expect(containsToken('@vue/compiler', 'vue')).toBe(false);
|
|
82
|
+
expect(containsToken('GET /health', 'health')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it('drops comments but keeps URLs in strings', () => {
|
|
85
|
+
const text = stripComments('/* public */\nconst u = "https://x.io"; // public\n# public\n');
|
|
86
|
+
expect(text).not.toContain('public');
|
|
87
|
+
expect(text).toContain('https://x.io');
|
|
88
|
+
});
|
|
89
|
+
it('declines to guess a paradigm when neither side clearly leads', () => {
|
|
90
|
+
expect(detectParadigm(['export class A {}\nexport function f() {}\n'])).toBeNull();
|
|
91
|
+
expect(detectParadigm(['export class A {}\nexport function f() {}\nexport function g() {}\n'])?.name).toBe('functional');
|
|
79
92
|
});
|
|
80
93
|
});
|
package/dist/gates/context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Gate } from './base.js';
|
|
2
2
|
import { FileScanner } from '../utils/scanner.js';
|
|
3
|
+
import { isScoped, resolveScopedFiles, touchesScope } from '../utils/scope.js';
|
|
3
4
|
import fs from 'fs-extra';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
export class ContextGate extends Gate {
|
|
@@ -57,6 +58,12 @@ export class ContextGate extends Gate {
|
|
|
57
58
|
if (this.extendedConfig.import_relationships) {
|
|
58
59
|
this.analyzeImportPatterns(importPatterns, failures);
|
|
59
60
|
}
|
|
61
|
+
// Cross-file patterns need the whole repo, but a scoped run reports
|
|
62
|
+
// only findings that cite a file in scope.
|
|
63
|
+
if (isScoped(context.patterns)) {
|
|
64
|
+
const scoped = await resolveScopedFiles(context.cwd, context.patterns, context.ignore);
|
|
65
|
+
return failures.filter(f => touchesScope(f.files, scoped));
|
|
66
|
+
}
|
|
60
67
|
return failures;
|
|
61
68
|
}
|
|
62
69
|
checkEnvDrift(content, file, anchors, failures) {
|