@ai-sdlc/orchestrator 0.6.0 → 0.9.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/dist/admission-composite.d.ts +67 -0
- package/dist/admission-composite.js +158 -0
- package/dist/admission-enrichment.d.ts +142 -0
- package/dist/admission-enrichment.js +331 -0
- package/dist/admission-hc.d.ts +62 -0
- package/dist/admission-hc.js +83 -0
- package/dist/admission-score.d.ts +95 -5
- package/dist/admission-score.js +84 -11
- package/dist/analysis/analyzer.js +3 -2
- package/dist/analysis/convention-detector.d.ts +85 -2
- package/dist/analysis/convention-detector.js +375 -70
- package/dist/analysis/diff-analyzer.d.ts +33 -0
- package/dist/analysis/diff-analyzer.js +122 -0
- package/dist/analysis/hotspot-analyzer.js +3 -1
- package/dist/analysis/index.d.ts +2 -1
- package/dist/analysis/index.js +2 -1
- package/dist/artifacts/index.d.ts +65 -0
- package/dist/artifacts/index.js +142 -0
- package/dist/backlog-adapter.d.ts +121 -0
- package/dist/backlog-adapter.js +438 -0
- package/dist/calibration.d.ts +43 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/agents.d.ts +24 -0
- package/dist/cli/commands/agents.js +66 -1
- package/dist/cli/commands/git-remote.d.ts +49 -0
- package/dist/cli/commands/git-remote.js +91 -0
- package/dist/cli/commands/health.d.ts +4 -0
- package/dist/cli/commands/health.js +13 -1
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +164 -22
- package/dist/cli/commands/mcp-setup.d.ts +31 -0
- package/dist/cli/commands/mcp-setup.js +78 -8
- package/dist/cli/formatters/table.js +15 -2
- package/dist/cli/index.d.ts +14 -1
- package/dist/cli/index.js +81 -20
- package/dist/cli/versions.d.ts +57 -0
- package/dist/cli/versions.js +128 -0
- package/dist/code-area-classifier.d.ts +21 -0
- package/dist/code-area-classifier.js +48 -0
- package/dist/config.d.ts +33 -1
- package/dist/config.js +78 -8
- package/dist/database/adapters/external.d.ts +24 -0
- package/dist/database/adapters/external.js +80 -0
- package/dist/database/adapters/neon.d.ts +41 -0
- package/dist/database/adapters/neon.js +98 -0
- package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
- package/dist/database/adapters/pg-snapshot-restore.js +68 -0
- package/dist/database/adapters/sqlite-copy.d.ts +32 -0
- package/dist/database/adapters/sqlite-copy.js +145 -0
- package/dist/database/connection-injection.d.ts +35 -0
- package/dist/database/connection-injection.js +93 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.js +23 -0
- package/dist/database/registry.d.ts +13 -0
- package/dist/database/registry.js +27 -0
- package/dist/database/topology.d.ts +52 -0
- package/dist/database/topology.js +44 -0
- package/dist/database/types.d.ts +89 -0
- package/dist/database/types.js +26 -0
- package/dist/design-authority.d.ts +40 -0
- package/dist/design-authority.js +71 -0
- package/dist/design-lookahead.d.ts +64 -0
- package/dist/design-lookahead.js +86 -0
- package/dist/design-quality-trend.d.ts +87 -0
- package/dist/design-quality-trend.js +190 -0
- package/dist/design-system-context.d.ts +46 -0
- package/dist/design-system-context.js +80 -0
- package/dist/design-system-correction-loop.d.ts +64 -0
- package/dist/design-system-correction-loop.js +128 -0
- package/dist/design-system-metrics.d.ts +61 -0
- package/dist/design-system-metrics.js +104 -0
- package/dist/design-system-stewardship.d.ts +22 -0
- package/dist/design-system-stewardship.js +85 -0
- package/dist/design-system-validation.d.ts +37 -0
- package/dist/design-system-validation.js +88 -0
- package/dist/dispatch/index.d.ts +4 -0
- package/dist/dispatch/index.js +4 -0
- package/dist/dispatch/merge-gate.d.ts +46 -0
- package/dist/dispatch/merge-gate.js +90 -0
- package/dist/dispatch/requeue.d.ts +57 -0
- package/dist/dispatch/requeue.js +131 -0
- package/dist/dispatch/worker-pool.d.ts +62 -0
- package/dist/dispatch/worker-pool.js +60 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.js +185 -15
- package/dist/fix-ci.js +8 -1
- package/dist/fix-review.js +8 -1
- package/dist/harness/adapters/claude-code.d.ts +29 -0
- package/dist/harness/adapters/claude-code.js +191 -0
- package/dist/harness/adapters/codex.d.ts +25 -0
- package/dist/harness/adapters/codex.js +61 -0
- package/dist/harness/independence.d.ts +51 -0
- package/dist/harness/independence.js +67 -0
- package/dist/harness/index.d.ts +14 -0
- package/dist/harness/index.js +20 -0
- package/dist/harness/registry.d.ts +17 -0
- package/dist/harness/registry.js +31 -0
- package/dist/harness/types.d.ts +123 -0
- package/dist/harness/types.js +8 -0
- package/dist/harness/version-probe.d.ts +14 -0
- package/dist/harness/version-probe.js +123 -0
- package/dist/index.d.ts +30 -4
- package/dist/index.js +35 -2
- package/dist/models/classifier.d.ts +76 -0
- package/dist/models/classifier.js +221 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/registry.d.ts +97 -0
- package/dist/models/registry.js +173 -0
- package/dist/pillar-breakdown.d.ts +63 -0
- package/dist/pillar-breakdown.js +153 -0
- package/dist/priority.d.ts +5 -0
- package/dist/priority.js +18 -5
- package/dist/review-meta.d.ts +65 -0
- package/dist/review-meta.js +149 -0
- package/dist/review.d.ts +4 -0
- package/dist/review.js +12 -2
- package/dist/runners/claude-code-sdk.d.ts +31 -0
- package/dist/runners/claude-code-sdk.js +238 -0
- package/dist/runners/claude-code.d.ts +3 -0
- package/dist/runners/claude-code.js +30 -66
- package/dist/runners/codex.js +4 -1
- package/dist/runners/copilot.js +4 -1
- package/dist/runners/cursor.js +4 -1
- package/dist/runners/git-utils.d.ts +81 -0
- package/dist/runners/git-utils.js +201 -0
- package/dist/runners/index.d.ts +3 -0
- package/dist/runners/index.js +3 -0
- package/dist/runners/review-agent.d.ts +20 -0
- package/dist/runners/review-agent.js +171 -41
- package/dist/runners/runner-registry.js +10 -0
- package/dist/runners/sdk-review-runner.d.ts +65 -0
- package/dist/runners/sdk-review-runner.js +185 -0
- package/dist/runners/security-triage.d.ts +20 -4
- package/dist/runners/security-triage.js +39 -15
- package/dist/runners/types.d.ts +6 -0
- package/dist/runtime/attestations.d.ts +274 -0
- package/dist/runtime/attestations.js +460 -0
- package/dist/runtime/git-env.d.ts +53 -0
- package/dist/runtime/git-env.js +60 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/parallelism-flag.d.ts +10 -0
- package/dist/runtime/parallelism-flag.js +18 -0
- package/dist/runtime/port-allocator.d.ts +21 -0
- package/dist/runtime/port-allocator.js +66 -0
- package/dist/runtime/worktree-pool.d.ts +86 -0
- package/dist/runtime/worktree-pool.js +204 -0
- package/dist/runtime/worktree.d.ts +25 -0
- package/dist/runtime/worktree.js +111 -0
- package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
- package/dist/sa-scoring/auto-calibrate.js +107 -0
- package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
- package/dist/sa-scoring/c1-sa2-computable.js +59 -0
- package/dist/sa-scoring/composite.d.ts +107 -0
- package/dist/sa-scoring/composite.js +139 -0
- package/dist/sa-scoring/depparse-client.d.ts +79 -0
- package/dist/sa-scoring/depparse-client.js +187 -0
- package/dist/sa-scoring/did-compiler.d.ts +122 -0
- package/dist/sa-scoring/did-compiler.js +286 -0
- package/dist/sa-scoring/drift-monitor.d.ts +84 -0
- package/dist/sa-scoring/drift-monitor.js +186 -0
- package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
- package/dist/sa-scoring/exemplar-bank.js +154 -0
- package/dist/sa-scoring/feedback-store.d.ts +100 -0
- package/dist/sa-scoring/feedback-store.js +156 -0
- package/dist/sa-scoring/index.d.ts +71 -0
- package/dist/sa-scoring/index.js +158 -0
- package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
- package/dist/sa-scoring/layer1-deterministic.js +298 -0
- package/dist/sa-scoring/layer2-structural.d.ts +71 -0
- package/dist/sa-scoring/layer2-structural.js +151 -0
- package/dist/sa-scoring/layer3-llm.d.ts +86 -0
- package/dist/sa-scoring/layer3-llm.js +282 -0
- package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
- package/dist/sa-scoring/rescore-orchestrator.js +47 -0
- package/dist/scheduling/burn-down.d.ts +27 -0
- package/dist/scheduling/burn-down.js +43 -0
- package/dist/scheduling/calibration.d.ts +42 -0
- package/dist/scheduling/calibration.js +150 -0
- package/dist/scheduling/index.d.ts +8 -0
- package/dist/scheduling/index.js +8 -0
- package/dist/scheduling/ledger.d.ts +59 -0
- package/dist/scheduling/ledger.js +216 -0
- package/dist/scheduling/off-peak.d.ts +27 -0
- package/dist/scheduling/off-peak.js +112 -0
- package/dist/scheduling/schedule-decision.d.ts +39 -0
- package/dist/scheduling/schedule-decision.js +89 -0
- package/dist/scheduling/tier-analysis.d.ts +47 -0
- package/dist/scheduling/tier-analysis.js +81 -0
- package/dist/scheduling/types.d.ts +140 -0
- package/dist/scheduling/types.js +11 -0
- package/dist/shared.d.ts +13 -0
- package/dist/shared.js +32 -0
- package/dist/state/schema.d.ts +5 -1
- package/dist/state/schema.js +223 -1
- package/dist/state/store.d.ts +55 -1
- package/dist/state/store.js +342 -7
- package/dist/state/types.d.ts +139 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate-agent-output.js +4 -1
- package/dist/watch.js +6 -0
- package/package.json +2 -2
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Detect coding conventions: naming style, test organization, import style.
|
|
3
|
+
*
|
|
4
|
+
* The detector accepts an optional `repoPath` so it can read project-level
|
|
5
|
+
* configuration (`package.json`, `vite.config.{js,ts}`, `tsconfig.json`,
|
|
6
|
+
* `jsconfig.json`, `webpack.config.*`) to interpret the file list more
|
|
7
|
+
* accurately. When `repoPath` is omitted the detector falls back to a pure
|
|
8
|
+
* file-name analysis (the original v1 behaviour).
|
|
9
|
+
*
|
|
10
|
+
* Three classes of false-positives motivated the project-config plumbing
|
|
11
|
+
* (AISDLC-80):
|
|
12
|
+
*
|
|
13
|
+
* 1. React projects mix PascalCase (components) with camelCase (hooks/stores
|
|
14
|
+
* and plain modules) — the v1 detector flagged this as `mixed`. With
|
|
15
|
+
* `package.json` `dependencies.react` visible we treat the dual-pattern
|
|
16
|
+
* case as the expected React convention.
|
|
17
|
+
* 2. Many repos host tests in multiple directories (`tests/`, `tests/e2e/`,
|
|
18
|
+
* `src/tests/`, `cypress/`, ...) plus collocated `*.test.*`. The v1
|
|
19
|
+
* detector picked one bucket and called it `mixed` when the proportions
|
|
20
|
+
* were close. We now enumerate the FULL set of locations.
|
|
21
|
+
* 3. Vite / TS / webpack path aliases (`@components`, `@engine`, ...) were
|
|
22
|
+
* classified as external imports because the v1 detector never read the
|
|
23
|
+
* alias maps. We now parse the alias map from each known config file and
|
|
24
|
+
* report alias usage as its own bucket.
|
|
3
25
|
*/
|
|
4
|
-
import {
|
|
26
|
+
import { readFile } from 'node:fs/promises';
|
|
27
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
5
28
|
// ── Naming style helpers ──────────────────────────────────────────
|
|
6
29
|
function isCamelCase(name) {
|
|
7
30
|
return /^[a-z][a-zA-Z0-9]*$/.test(name);
|
|
@@ -15,12 +38,80 @@ function isKebabCase(name) {
|
|
|
15
38
|
function isSnakeCase(name) {
|
|
16
39
|
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
|
|
17
40
|
}
|
|
18
|
-
|
|
41
|
+
const REACT_COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx']);
|
|
42
|
+
const REACT_MODULE_EXTENSIONS = new Set(['.js', '.ts']);
|
|
43
|
+
/**
|
|
44
|
+
* Recognised React project structure: PascalCase for `.jsx`/`.tsx` (components)
|
|
45
|
+
* AND camelCase for `.js`/`.ts` (hooks, stores, utilities). At least 60% of
|
|
46
|
+
* each extension family must follow the expected style for the heuristic to
|
|
47
|
+
* fire so an accidentally-mixed repo still gets flagged.
|
|
48
|
+
*/
|
|
49
|
+
function detectReactDualNamingPattern(files) {
|
|
50
|
+
let componentTotal = 0;
|
|
51
|
+
let componentPascal = 0;
|
|
52
|
+
let moduleTotal = 0;
|
|
53
|
+
let moduleCamel = 0;
|
|
54
|
+
const componentExamples = [];
|
|
55
|
+
const moduleExamples = [];
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const name = stripTestSuffix(basename(file.relativePath, extname(file.relativePath)));
|
|
58
|
+
if (!name || name === 'index')
|
|
59
|
+
continue;
|
|
60
|
+
if (REACT_COMPONENT_EXTENSIONS.has(file.extension)) {
|
|
61
|
+
componentTotal++;
|
|
62
|
+
if (isPascalCase(name)) {
|
|
63
|
+
componentPascal++;
|
|
64
|
+
if (componentExamples.length < 3)
|
|
65
|
+
componentExamples.push(file.relativePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (REACT_MODULE_EXTENSIONS.has(file.extension)) {
|
|
69
|
+
moduleTotal++;
|
|
70
|
+
// Hooks (`useFoo.js`) and store/util modules (`gameplayStore.js`) are
|
|
71
|
+
// both camelCase under React community convention.
|
|
72
|
+
if (isCamelCase(name)) {
|
|
73
|
+
moduleCamel++;
|
|
74
|
+
if (moduleExamples.length < 3)
|
|
75
|
+
moduleExamples.push(file.relativePath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Need meaningful evidence of BOTH families and at least 60% adherence in each.
|
|
80
|
+
if (componentTotal < 2 || moduleTotal < 2) {
|
|
81
|
+
return { matched: false, componentExamples, moduleExamples };
|
|
82
|
+
}
|
|
83
|
+
const componentRatio = componentPascal / componentTotal;
|
|
84
|
+
const moduleRatio = moduleCamel / moduleTotal;
|
|
85
|
+
return {
|
|
86
|
+
matched: componentRatio >= 0.6 && moduleRatio >= 0.6,
|
|
87
|
+
componentExamples,
|
|
88
|
+
moduleExamples,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function stripTestSuffix(name) {
|
|
92
|
+
return name.replace(/\.(test|spec|stories|d)$/, '');
|
|
93
|
+
}
|
|
94
|
+
function detectFileNamingStyle(files, reactMode) {
|
|
95
|
+
// React projects get a dual-pattern check first — if it matches we report
|
|
96
|
+
// the friendly "PascalCase + camelCase (React)" label and skip the legacy
|
|
97
|
+
// single-style heuristic that would otherwise (correctly but unhelpfully)
|
|
98
|
+
// call this `mixed`.
|
|
99
|
+
if (reactMode) {
|
|
100
|
+
const dual = detectReactDualNamingPattern(files);
|
|
101
|
+
if (dual.matched) {
|
|
102
|
+
return {
|
|
103
|
+
style: 'PascalCase + camelCase (React)',
|
|
104
|
+
confidence: 0.9,
|
|
105
|
+
examples: [...dual.componentExamples, ...dual.moduleExamples].slice(0, 3),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
19
109
|
const counts = {
|
|
20
110
|
camelCase: 0,
|
|
21
111
|
PascalCase: 0,
|
|
22
112
|
'kebab-case': 0,
|
|
23
113
|
snake_case: 0,
|
|
114
|
+
'PascalCase + camelCase (React)': 0,
|
|
24
115
|
mixed: 0,
|
|
25
116
|
};
|
|
26
117
|
const examples = {
|
|
@@ -28,10 +119,11 @@ function detectFileNamingStyle(files) {
|
|
|
28
119
|
PascalCase: [],
|
|
29
120
|
'kebab-case': [],
|
|
30
121
|
snake_case: [],
|
|
122
|
+
'PascalCase + camelCase (React)': [],
|
|
31
123
|
mixed: [],
|
|
32
124
|
};
|
|
33
125
|
for (const file of files) {
|
|
34
|
-
const name = basename(file.relativePath, extname(file.relativePath))
|
|
126
|
+
const name = stripTestSuffix(basename(file.relativePath, extname(file.relativePath)));
|
|
35
127
|
if (!name || name === 'index')
|
|
36
128
|
continue;
|
|
37
129
|
if (isKebabCase(name)) {
|
|
@@ -67,105 +159,318 @@ function detectFileNamingStyle(files) {
|
|
|
67
159
|
examples: examples[topStyle],
|
|
68
160
|
};
|
|
69
161
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
162
|
+
const KNOWN_TEST_DIRECTORY_PREFIXES = [
|
|
163
|
+
// Order matters: more specific matchers first so `tests/e2e/foo.test.ts`
|
|
164
|
+
// ends up in `tests/e2e/` rather than `tests/`.
|
|
165
|
+
{ label: 'tests/e2e/', matches: (d) => /(^|\/)tests\/e2e(\/|$)/.test(d) },
|
|
166
|
+
{ label: 'tests/integration/', matches: (d) => /(^|\/)tests\/integration(\/|$)/.test(d) },
|
|
167
|
+
{ label: 'tests/unit/', matches: (d) => /(^|\/)tests\/unit(\/|$)/.test(d) },
|
|
168
|
+
{ label: 'src/tests/', matches: (d) => /(^|\/)src\/tests(\/|$)/.test(d) },
|
|
169
|
+
{ label: '__tests__/', matches: (d) => /(^|\/)__tests__(\/|$)/.test(d) },
|
|
170
|
+
{ label: 'cypress/', matches: (d) => /(^|\/)cypress(\/|$)/.test(d) },
|
|
171
|
+
{ label: 'e2e/', matches: (d) => /(^|\/)e2e(\/|$)/.test(d) && !/tests\/e2e/.test(d) },
|
|
172
|
+
{ label: 'tests/', matches: (d) => /(^|\/)tests(\/|$)/.test(d) },
|
|
173
|
+
{ label: 'test/', matches: (d) => /(^|\/)test(\/|$)/.test(d) && !/__tests__/.test(d) },
|
|
174
|
+
];
|
|
175
|
+
/**
|
|
176
|
+
* Classify every test file found into ALL the locations it actually lives,
|
|
177
|
+
* preserving the multi-directory reality of real projects. Returns the
|
|
178
|
+
* complete set sorted by file count (descending) with examples for each.
|
|
179
|
+
*/
|
|
180
|
+
export function enumerateTestLocations(files) {
|
|
181
|
+
const buckets = new Map();
|
|
182
|
+
function bump(label, path) {
|
|
183
|
+
const existing = buckets.get(label) ?? { count: 0, examples: [] };
|
|
184
|
+
existing.count += 1;
|
|
185
|
+
if (existing.examples.length < 3)
|
|
186
|
+
existing.examples.push(path);
|
|
187
|
+
buckets.set(label, existing);
|
|
188
|
+
}
|
|
80
189
|
const testFiles = files.filter((f) => {
|
|
81
190
|
const name = basename(f.relativePath);
|
|
82
|
-
return (name.includes('.test.') ||
|
|
191
|
+
return (name.includes('.test.') ||
|
|
192
|
+
name.includes('.spec.') ||
|
|
193
|
+
f.relativePath.includes('__tests__') ||
|
|
194
|
+
// Cypress puts integration specs under `cypress/e2e/foo.cy.js` — pick
|
|
195
|
+
// those up too even if they don't carry a `.test.` / `.spec.` suffix.
|
|
196
|
+
/(^|\/)cypress\/.*\.(js|ts|jsx|tsx)$/.test(f.relativePath) ||
|
|
197
|
+
/(^|\/)tests?\/.*\.(js|ts|jsx|tsx|mjs|cjs)$/.test(f.relativePath));
|
|
83
198
|
});
|
|
84
199
|
for (const file of testFiles) {
|
|
85
200
|
const dir = dirname(file.relativePath);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
dir.startsWith('test/') ||
|
|
94
|
-
dir.startsWith('tests/')) {
|
|
95
|
-
testDir++;
|
|
96
|
-
if (examples['test-directory'].length < 3)
|
|
97
|
-
examples['test-directory'].push(file.relativePath);
|
|
201
|
+
let matched = false;
|
|
202
|
+
for (const matcher of KNOWN_TEST_DIRECTORY_PREFIXES) {
|
|
203
|
+
if (matcher.matches(dir)) {
|
|
204
|
+
bump(matcher.label, file.relativePath);
|
|
205
|
+
matched = true;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
98
208
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
examples['co-located'].push(file.relativePath);
|
|
209
|
+
if (!matched) {
|
|
210
|
+
// Anything else — collocated with source.
|
|
211
|
+
bump('co-located', file.relativePath);
|
|
103
212
|
}
|
|
104
213
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
214
|
+
return [...buckets.entries()]
|
|
215
|
+
.map(([label, info]) => ({ label, count: info.count, examples: info.examples }))
|
|
216
|
+
.sort((a, b) => b.count - a.count);
|
|
217
|
+
}
|
|
218
|
+
function summariseTestLocations(locations) {
|
|
219
|
+
if (locations.length === 0) {
|
|
220
|
+
return { pattern: 'No test files detected', examples: [], confidence: 0 };
|
|
221
|
+
}
|
|
222
|
+
const total = locations.reduce((sum, loc) => sum + loc.count, 0);
|
|
223
|
+
const labelList = locations.map((loc) => loc.label).join(', ');
|
|
224
|
+
// Confidence is the share of test files that fall into the largest bucket
|
|
225
|
+
// — but we still REPORT the full set in `pattern` so users see every place
|
|
226
|
+
// tests actually live.
|
|
227
|
+
const confidence = Math.round((locations[0].count / total) * 100) / 100;
|
|
112
228
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
229
|
+
pattern: `Test files in: ${labelList}`,
|
|
230
|
+
examples: locations.flatMap((loc) => loc.examples).slice(0, 3),
|
|
231
|
+
confidence,
|
|
116
232
|
};
|
|
117
233
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
234
|
+
async function readFileSafe(path) {
|
|
235
|
+
try {
|
|
236
|
+
return await readFile(path, 'utf-8');
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function stripJsonComments(text) {
|
|
243
|
+
// Tolerant pre-pass for `tsconfig.json` / `jsconfig.json` which permit
|
|
244
|
+
// line + block comments and trailing commas. The implementation is
|
|
245
|
+
// intentionally narrow: strings are scanned char-by-char so we don't
|
|
246
|
+
// accidentally strip "//" inside a string literal.
|
|
247
|
+
let out = '';
|
|
248
|
+
let i = 0;
|
|
249
|
+
let inString = null;
|
|
250
|
+
while (i < text.length) {
|
|
251
|
+
const ch = text[i];
|
|
252
|
+
const next = text[i + 1];
|
|
253
|
+
if (inString) {
|
|
254
|
+
out += ch;
|
|
255
|
+
if (ch === '\\' && i + 1 < text.length) {
|
|
256
|
+
out += next;
|
|
257
|
+
i += 2;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (ch === inString)
|
|
261
|
+
inString = null;
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (ch === '"' || ch === "'") {
|
|
266
|
+
inString = ch;
|
|
267
|
+
out += ch;
|
|
268
|
+
i++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (ch === '/' && next === '/') {
|
|
272
|
+
while (i < text.length && text[i] !== '\n')
|
|
273
|
+
i++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (ch === '/' && next === '*') {
|
|
277
|
+
i += 2;
|
|
278
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
279
|
+
i++;
|
|
280
|
+
i += 2;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
out += ch;
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
// Trailing comma cleanup.
|
|
287
|
+
return out.replace(/,(\s*[\]}])/g, '$1');
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Parse `compilerOptions.paths` from a `tsconfig.json` / `jsconfig.json`.
|
|
291
|
+
* The TS config format maps `"@components/*": ["src/components/*"]` — we
|
|
292
|
+
* normalise to `@components` → `src/components/*`.
|
|
293
|
+
*/
|
|
294
|
+
export function parseTsConfigAliases(content) {
|
|
295
|
+
const aliases = {};
|
|
296
|
+
let parsed;
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(stripJsonComments(content));
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return aliases;
|
|
302
|
+
}
|
|
303
|
+
const obj = parsed;
|
|
304
|
+
const paths = obj?.compilerOptions?.paths;
|
|
305
|
+
if (!paths || typeof paths !== 'object')
|
|
306
|
+
return aliases;
|
|
307
|
+
for (const [pattern, target] of Object.entries(paths)) {
|
|
308
|
+
if (!Array.isArray(target) || target.length === 0)
|
|
309
|
+
continue;
|
|
310
|
+
const firstTarget = target[0];
|
|
311
|
+
if (typeof firstTarget !== 'string')
|
|
312
|
+
continue;
|
|
313
|
+
// Drop wildcard suffix for the alias key — `"@/*"` → `@`,
|
|
314
|
+
// `"@components/*"` → `@components`.
|
|
315
|
+
const aliasKey = pattern.replace(/\/\*$/, '');
|
|
316
|
+
aliases[aliasKey] = firstTarget;
|
|
317
|
+
}
|
|
318
|
+
return aliases;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Extract `resolve.alias` (object literal form) from a Vite or webpack config
|
|
322
|
+
* file. We use a regex rather than a full JS parser because the contents are
|
|
323
|
+
* config files written in a small, well-known shape — the v1 detector's
|
|
324
|
+
* "regex over AST" tradeoff carries forward.
|
|
325
|
+
*
|
|
326
|
+
* Recognises both:
|
|
327
|
+
* resolve: { alias: { '@components': path.resolve(__dirname, 'src/components') } }
|
|
328
|
+
* alias: { '@engine': '/abs/path' }
|
|
329
|
+
*/
|
|
330
|
+
export function parseViteOrWebpackAliases(content) {
|
|
331
|
+
const aliases = {};
|
|
332
|
+
// Find an `alias:` or `alias =` followed by an object literal. Only the
|
|
333
|
+
// first occurrence is parsed — vite configs typically declare exactly one.
|
|
334
|
+
const aliasMatch = /alias\s*[:=]\s*\{([\s\S]*?)\}/m.exec(content);
|
|
335
|
+
if (!aliasMatch)
|
|
336
|
+
return aliases;
|
|
337
|
+
const body = aliasMatch[1];
|
|
338
|
+
// Each entry is `'key': <expr>` or `"key": <expr>`. We grab the key and
|
|
339
|
+
// ignore the expression because we only need the alias prefix to match
|
|
340
|
+
// user code.
|
|
341
|
+
const entryRe = /['"]([^'"]+)['"]\s*:\s*([^,\n}]+)/g;
|
|
342
|
+
let m;
|
|
343
|
+
while ((m = entryRe.exec(body)) !== null) {
|
|
344
|
+
const key = m[1];
|
|
345
|
+
const valueExpr = m[2].trim();
|
|
346
|
+
aliases[key] = valueExpr;
|
|
347
|
+
}
|
|
348
|
+
return aliases;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Read every alias source we know about and merge into a single map. Sources
|
|
352
|
+
* are read in parallel; later sources take precedence if there is overlap.
|
|
353
|
+
*/
|
|
354
|
+
export async function loadProjectAliases(repoPath) {
|
|
355
|
+
const candidates = [
|
|
356
|
+
'tsconfig.json',
|
|
357
|
+
'jsconfig.json',
|
|
358
|
+
'vite.config.js',
|
|
359
|
+
'vite.config.ts',
|
|
360
|
+
'vite.config.mjs',
|
|
361
|
+
'vite.config.cjs',
|
|
362
|
+
'webpack.config.js',
|
|
363
|
+
'webpack.config.ts',
|
|
364
|
+
'webpack.config.mjs',
|
|
365
|
+
'webpack.config.cjs',
|
|
366
|
+
];
|
|
367
|
+
const reads = await Promise.all(candidates.map(async (name) => ({ name, content: await readFileSafe(join(repoPath, name)) })));
|
|
368
|
+
const merged = {};
|
|
369
|
+
for (const { name, content } of reads) {
|
|
370
|
+
if (!content)
|
|
371
|
+
continue;
|
|
372
|
+
const isTsConfig = name === 'tsconfig.json' || name === 'jsconfig.json';
|
|
373
|
+
const parsed = isTsConfig ? parseTsConfigAliases(content) : parseViteOrWebpackAliases(content);
|
|
374
|
+
Object.assign(merged, parsed);
|
|
375
|
+
}
|
|
376
|
+
return merged;
|
|
377
|
+
}
|
|
378
|
+
function detectImportStyle(files, aliases) {
|
|
379
|
+
// Barrel detection is unchanged from v1 — it's a structural signal taken
|
|
380
|
+
// straight off the file list.
|
|
120
381
|
const indexFiles = files.filter((f) => basename(f.relativePath).startsWith('index.'));
|
|
121
382
|
const usesBarrels = indexFiles.length > 3;
|
|
122
|
-
|
|
123
|
-
|
|
383
|
+
const aliasKeys = Object.keys(aliases);
|
|
384
|
+
const hasAliases = aliasKeys.length > 0;
|
|
385
|
+
let style = 'relative';
|
|
386
|
+
if (hasAliases && usesBarrels) {
|
|
387
|
+
style = 'mixed-relative-and-aliases';
|
|
388
|
+
}
|
|
389
|
+
else if (hasAliases) {
|
|
390
|
+
style = 'path-alias';
|
|
391
|
+
}
|
|
392
|
+
const aliasExamples = aliasKeys.slice(0, 3);
|
|
393
|
+
const barrelExamples = indexFiles.slice(0, 3).map((f) => f.relativePath);
|
|
394
|
+
const examples = hasAliases ? aliasExamples : barrelExamples;
|
|
124
395
|
return {
|
|
125
|
-
style
|
|
396
|
+
style,
|
|
126
397
|
confidence: 0.8,
|
|
127
398
|
usesBarrels,
|
|
128
|
-
examples
|
|
399
|
+
examples,
|
|
400
|
+
aliases,
|
|
129
401
|
};
|
|
130
402
|
}
|
|
131
|
-
// ──
|
|
403
|
+
// ── React project detection ─────────────────────────────────────
|
|
132
404
|
/**
|
|
133
|
-
*
|
|
405
|
+
* Read `package.json` and decide whether this is a React project. Looks at
|
|
406
|
+
* `dependencies` AND `devDependencies` because Vite-driven SPAs sometimes
|
|
407
|
+
* declare React as a dev dep via templates.
|
|
134
408
|
*/
|
|
135
|
-
export function
|
|
409
|
+
export async function detectReactProject(repoPath) {
|
|
410
|
+
const content = await readFileSafe(join(repoPath, 'package.json'));
|
|
411
|
+
if (!content)
|
|
412
|
+
return false;
|
|
413
|
+
let pkg;
|
|
414
|
+
try {
|
|
415
|
+
pkg = JSON.parse(content);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
const obj = pkg;
|
|
421
|
+
return Boolean(obj.dependencies?.react ?? obj.devDependencies?.react);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Detect coding conventions from the file list. When `options.repoPath` is
|
|
425
|
+
* supplied the detector additionally reads project-level configuration to
|
|
426
|
+
* suppress the three classes of false-positives described at the top of this
|
|
427
|
+
* file (AISDLC-80).
|
|
428
|
+
*/
|
|
429
|
+
export async function detectConventions(files, options = {}) {
|
|
136
430
|
const conventions = [];
|
|
137
|
-
|
|
138
|
-
const
|
|
431
|
+
const repoPath = options.repoPath;
|
|
432
|
+
const reactMode = repoPath ? await detectReactProject(repoPath) : false;
|
|
433
|
+
const aliases = repoPath ? await loadProjectAliases(repoPath) : {};
|
|
434
|
+
// ── File naming ──
|
|
435
|
+
const naming = detectFileNamingStyle(files, reactMode);
|
|
139
436
|
if (naming.confidence > 0) {
|
|
437
|
+
const labelSuffix = naming.style.startsWith('PascalCase + camelCase')
|
|
438
|
+
? ' for React components/hooks'
|
|
439
|
+
: ' for file names';
|
|
140
440
|
conventions.push({
|
|
141
441
|
category: 'naming',
|
|
142
|
-
pattern: `${naming.style}
|
|
442
|
+
pattern: `${naming.style}${labelSuffix}`,
|
|
143
443
|
confidence: naming.confidence,
|
|
144
444
|
examples: naming.examples,
|
|
145
445
|
});
|
|
146
446
|
}
|
|
147
|
-
// Testing
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
? 'Co-located test files (*.test.ts)'
|
|
152
|
-
: testing.style === '__tests__'
|
|
153
|
-
? 'Test files in __tests__/ directories'
|
|
154
|
-
: testing.style === 'test-directory'
|
|
155
|
-
? 'Test files in test/ directory'
|
|
156
|
-
: 'Mixed test file placement';
|
|
447
|
+
// ── Testing ──
|
|
448
|
+
const locations = enumerateTestLocations(files);
|
|
449
|
+
const testingSummary = summariseTestLocations(locations);
|
|
450
|
+
if (testingSummary.confidence > 0) {
|
|
157
451
|
conventions.push({
|
|
158
452
|
category: 'testing',
|
|
159
|
-
pattern:
|
|
160
|
-
confidence:
|
|
161
|
-
examples:
|
|
453
|
+
pattern: testingSummary.pattern,
|
|
454
|
+
confidence: testingSummary.confidence,
|
|
455
|
+
examples: testingSummary.examples,
|
|
162
456
|
});
|
|
163
457
|
}
|
|
164
|
-
//
|
|
165
|
-
const imports = detectImportStyle(files);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
458
|
+
// ── Imports ──
|
|
459
|
+
const imports = detectImportStyle(files, aliases);
|
|
460
|
+
const aliasKeys = Object.keys(imports.aliases);
|
|
461
|
+
let importDesc;
|
|
462
|
+
if (imports.style === 'mixed-relative-and-aliases') {
|
|
463
|
+
importDesc = `Path aliases (${aliasKeys.join(', ')}) + relative imports + barrel re-exports via index.ts`;
|
|
464
|
+
}
|
|
465
|
+
else if (imports.style === 'path-alias') {
|
|
466
|
+
importDesc = `Path aliases (${aliasKeys.join(', ')}) + relative imports`;
|
|
467
|
+
}
|
|
468
|
+
else if (imports.usesBarrels) {
|
|
469
|
+
importDesc = 'Relative imports, barrel re-exports via index.ts';
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
importDesc = 'Relative imports';
|
|
473
|
+
}
|
|
169
474
|
conventions.push({
|
|
170
475
|
category: 'imports',
|
|
171
476
|
pattern: importDesc,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff analyzer — runs deterministic structural checks on changed files
|
|
3
|
+
* BEFORE LLM review agents see the diff.
|
|
4
|
+
*
|
|
5
|
+
* Reuses existing analysis infrastructure:
|
|
6
|
+
* - parseImports() for import counting
|
|
7
|
+
* - computeFileComplexity() for per-file complexity scoring
|
|
8
|
+
*/
|
|
9
|
+
export interface DiffFinding {
|
|
10
|
+
type: 'complexity' | 'imports' | 'file-length';
|
|
11
|
+
file: string;
|
|
12
|
+
severity: 'info' | 'warning';
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DiffAnalysisResult {
|
|
16
|
+
/** Files touched by the diff. */
|
|
17
|
+
changedFiles: string[];
|
|
18
|
+
/** Deterministic structural findings. */
|
|
19
|
+
findings: DiffFinding[];
|
|
20
|
+
/** Formatted summary for injection into review agent context. */
|
|
21
|
+
summary: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract changed file paths from a unified diff.
|
|
25
|
+
* Parses `--- a/path` and `+++ b/path` lines.
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractChangedFiles(diff: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Analyze a PR diff for structural issues.
|
|
30
|
+
* Returns deterministic findings that review agents can skip.
|
|
31
|
+
*/
|
|
32
|
+
export declare function analyzeDiff(diff: string, repoPath: string): Promise<DiffAnalysisResult>;
|
|
33
|
+
//# sourceMappingURL=diff-analyzer.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff analyzer — runs deterministic structural checks on changed files
|
|
3
|
+
* BEFORE LLM review agents see the diff.
|
|
4
|
+
*
|
|
5
|
+
* Reuses existing analysis infrastructure:
|
|
6
|
+
* - parseImports() for import counting
|
|
7
|
+
* - computeFileComplexity() for per-file complexity scoring
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { parseImports } from './dependency-parser.js';
|
|
13
|
+
import { computeFileComplexity } from './hotspot-analyzer.js';
|
|
14
|
+
// ── Thresholds ───────────────────────────────────────────────────────
|
|
15
|
+
const COMPLEXITY_THRESHOLD = 7;
|
|
16
|
+
const FILE_LENGTH_THRESHOLD = 300;
|
|
17
|
+
const IMPORT_COUNT_THRESHOLD = 15;
|
|
18
|
+
const CODE_EXTENSIONS = new Set([
|
|
19
|
+
'.ts',
|
|
20
|
+
'.tsx',
|
|
21
|
+
'.js',
|
|
22
|
+
'.jsx',
|
|
23
|
+
'.mjs',
|
|
24
|
+
'.cjs',
|
|
25
|
+
'.py',
|
|
26
|
+
'.go',
|
|
27
|
+
'.rs',
|
|
28
|
+
]);
|
|
29
|
+
// ── Diff parsing ─────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Extract changed file paths from a unified diff.
|
|
32
|
+
* Parses `--- a/path` and `+++ b/path` lines.
|
|
33
|
+
*/
|
|
34
|
+
export function extractChangedFiles(diff) {
|
|
35
|
+
const files = new Set();
|
|
36
|
+
for (const line of diff.split('\n')) {
|
|
37
|
+
const match = line.match(/^[+-]{3}\s[ab]\/(.+)$/);
|
|
38
|
+
if (match && match[1] !== '/dev/null') {
|
|
39
|
+
files.add(match[1]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...files];
|
|
43
|
+
}
|
|
44
|
+
// ── Analysis ─────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Analyze a PR diff for structural issues.
|
|
47
|
+
* Returns deterministic findings that review agents can skip.
|
|
48
|
+
*/
|
|
49
|
+
export async function analyzeDiff(diff, repoPath) {
|
|
50
|
+
const changedFiles = extractChangedFiles(diff);
|
|
51
|
+
const findings = [];
|
|
52
|
+
// Only analyze code files that exist on disk (skip deleted files)
|
|
53
|
+
const codeFiles = changedFiles.filter((f) => {
|
|
54
|
+
const ext = '.' + f.split('.').pop();
|
|
55
|
+
return CODE_EXTENSIONS.has(ext) && existsSync(join(repoPath, f));
|
|
56
|
+
});
|
|
57
|
+
for (const file of codeFiles) {
|
|
58
|
+
const fullPath = join(repoPath, file);
|
|
59
|
+
// Read file for line count
|
|
60
|
+
let content;
|
|
61
|
+
try {
|
|
62
|
+
content = await readFile(fullPath, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const lineCount = content.split('\n').length;
|
|
68
|
+
// Parse imports
|
|
69
|
+
const imports = await parseImports(fullPath);
|
|
70
|
+
const importCount = imports.length;
|
|
71
|
+
// Check 1: File complexity
|
|
72
|
+
const complexity = computeFileComplexity(lineCount, importCount);
|
|
73
|
+
if (complexity >= COMPLEXITY_THRESHOLD) {
|
|
74
|
+
findings.push({
|
|
75
|
+
type: 'complexity',
|
|
76
|
+
file,
|
|
77
|
+
severity: 'warning',
|
|
78
|
+
message: `High complexity (${complexity}/10) — ${lineCount} lines, ${importCount} imports. Review with extra care.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Check 2: Large file
|
|
82
|
+
if (lineCount > FILE_LENGTH_THRESHOLD) {
|
|
83
|
+
findings.push({
|
|
84
|
+
type: 'file-length',
|
|
85
|
+
file,
|
|
86
|
+
severity: lineCount > 500 ? 'warning' : 'info',
|
|
87
|
+
message: `Large file (${lineCount} lines). Consider splitting if it has multiple responsibilities.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Check 3: High import count (coupling)
|
|
91
|
+
if (importCount >= IMPORT_COUNT_THRESHOLD) {
|
|
92
|
+
findings.push({
|
|
93
|
+
type: 'imports',
|
|
94
|
+
file,
|
|
95
|
+
severity: 'warning',
|
|
96
|
+
message: `High coupling (${importCount} imports). May indicate the file has too many responsibilities.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const summary = formatSummary(changedFiles, codeFiles, findings);
|
|
101
|
+
return { changedFiles, findings, summary };
|
|
102
|
+
}
|
|
103
|
+
// ── Formatting ───────────────────────────────────────────────────────
|
|
104
|
+
function formatSummary(allFiles, codeFiles, findings) {
|
|
105
|
+
if (findings.length === 0) {
|
|
106
|
+
return `## Pre-Verified Structural Analysis\n\nAnalyzed ${codeFiles.length} code files (${allFiles.length} total changed). No structural issues found.`;
|
|
107
|
+
}
|
|
108
|
+
const lines = [
|
|
109
|
+
`## Pre-Verified Structural Analysis`,
|
|
110
|
+
'',
|
|
111
|
+
`Analyzed ${codeFiles.length} code files (${allFiles.length} total changed). ${findings.length} structural finding(s):`,
|
|
112
|
+
'',
|
|
113
|
+
];
|
|
114
|
+
for (const f of findings) {
|
|
115
|
+
const icon = f.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
116
|
+
lines.push(`- ${icon} **${f.file}**: ${f.message}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push('*These findings are deterministic. Do NOT re-analyze these files for the issues above.*');
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=diff-analyzer.js.map
|