create-sdd-project 0.17.1 → 0.17.3
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/lib/adapt-agents.js +4 -2
- package/lib/init-generator.js +59 -8
- package/lib/init-wizard.js +17 -2
- package/lib/meta.js +5 -4
- package/lib/scanner.js +184 -22
- package/package.json +1 -1
package/lib/adapt-agents.js
CHANGED
|
@@ -227,7 +227,8 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
|
|
|
227
227
|
// WORKFLOW_CORE_PROJECT_TYPE_RULES table above so upgrade-generator.js
|
|
228
228
|
// can apply the same rules in-memory (smart-diff fallback comparison).
|
|
229
229
|
// pr-template.md + AGENTS.md + base-standards.mdc remain inline because
|
|
230
|
-
// they're not workflow-core files (pr-template is v0.17.
|
|
230
|
+
// they're not workflow-core files (pr-template is out of scope for v0.17.1 —
|
|
231
|
+
// see dev/ROADMAP.md "Known follow-ups" item 2).
|
|
231
232
|
const wfRules = WORKFLOW_CORE_PROJECT_TYPE_RULES[config.projectType];
|
|
232
233
|
if (wfRules) {
|
|
233
234
|
for (const dir of toolDirs) {
|
|
@@ -243,7 +244,8 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
|
|
|
243
244
|
[', `ui-ux-designer`', ''],
|
|
244
245
|
]);
|
|
245
246
|
for (const dir of toolDirs) {
|
|
246
|
-
// pr-template: remove ui-components from checklist (
|
|
247
|
+
// pr-template: remove ui-components from checklist (out of scope for
|
|
248
|
+
// v0.17.1 — see dev/ROADMAP.md "Known follow-ups" item 2)
|
|
247
249
|
replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
|
|
248
250
|
[' / ui-components.md', ''],
|
|
249
251
|
]);
|
package/lib/init-generator.js
CHANGED
|
@@ -232,10 +232,32 @@ function generateInit(config) {
|
|
|
232
232
|
console.log(' These files were generated from project analysis. Adjust patterns');
|
|
233
233
|
console.log(' and conventions to match your team\'s actual practices.');
|
|
234
234
|
|
|
235
|
-
// Test coverage note
|
|
236
|
-
|
|
235
|
+
// Test coverage note — v0.17.3: broaden signal detection across root,
|
|
236
|
+
// backend, and frontend tests (covers E2E-only setups and monorepos
|
|
237
|
+
// with workspace-level tests). Only show the note when NO test signal
|
|
238
|
+
// exists anywhere.
|
|
239
|
+
const hasAnyTestSignal =
|
|
240
|
+
scan.tests.framework !== 'none' ||
|
|
241
|
+
scan.backendTests.framework !== 'none' ||
|
|
242
|
+
scan.frontendTests.framework !== 'none' ||
|
|
243
|
+
scan.tests.e2eFramework !== null ||
|
|
244
|
+
scan.tests.testFiles > 0 ||
|
|
245
|
+
scan.backendTests.testFiles > 0 ||
|
|
246
|
+
scan.frontendTests.testFiles > 0;
|
|
247
|
+
const maxCoverageRank = (c) =>
|
|
248
|
+
c === 'high' ? 3 : c === 'medium' ? 2 : c === 'low' ? 1 : 0;
|
|
249
|
+
const bestCoverage = Math.max(
|
|
250
|
+
maxCoverageRank(scan.tests.estimatedCoverage),
|
|
251
|
+
maxCoverageRank(scan.backendTests.estimatedCoverage),
|
|
252
|
+
maxCoverageRank(scan.frontendTests.estimatedCoverage),
|
|
253
|
+
);
|
|
254
|
+
if (!hasAnyTestSignal || bestCoverage <= 1 /* none or low */) {
|
|
237
255
|
console.log('');
|
|
238
|
-
const fileCount =
|
|
256
|
+
const fileCount = Math.max(
|
|
257
|
+
scan.tests.testFiles,
|
|
258
|
+
scan.backendTests.testFiles,
|
|
259
|
+
scan.frontendTests.testFiles,
|
|
260
|
+
);
|
|
239
261
|
if (fileCount === 0) {
|
|
240
262
|
console.log(' 📝 No test files detected.');
|
|
241
263
|
} else {
|
|
@@ -458,8 +480,11 @@ function adaptBackendStandards(template, scan) {
|
|
|
458
480
|
const db = scan.backend.db;
|
|
459
481
|
const lang = scan.language === 'typescript' ? 'TypeScript' : 'JavaScript';
|
|
460
482
|
|
|
461
|
-
|
|
462
|
-
|
|
483
|
+
// v0.17.3: consume backendTests instead of tests so that monorepos with
|
|
484
|
+
// workspace-only test frameworks (e.g., fx: vitest in packages/api) emit
|
|
485
|
+
// the correct Testing line. Single-package: backendTests === tests.
|
|
486
|
+
const testFramework = scan.backendTests.framework !== 'none'
|
|
487
|
+
? capitalizeFramework(scan.backendTests.framework)
|
|
463
488
|
: 'Not configured';
|
|
464
489
|
|
|
465
490
|
let stackLines = [
|
|
@@ -570,9 +595,14 @@ function adaptFrontendStandards(template, scan) {
|
|
|
570
595
|
const state = scan.frontend.state ? `, ${scan.frontend.state}` : '';
|
|
571
596
|
const lang = scan.language === 'typescript' ? 'TypeScript' : 'JavaScript';
|
|
572
597
|
|
|
598
|
+
// v0.17.3: consume frontendTests (see backend equivalent above).
|
|
599
|
+
const frontendTestFramework = scan.frontendTests.framework !== 'none'
|
|
600
|
+
? capitalizeFramework(scan.frontendTests.framework)
|
|
601
|
+
: 'Not configured';
|
|
602
|
+
|
|
573
603
|
content = content.replace(
|
|
574
604
|
/## Technology Stack\n\n[\s\S]*?(?=\n## Project Structure)/,
|
|
575
|
-
`## Technology Stack\n\n- **Framework**: ${framework}\n- **Language**: ${lang}\n- **Styling**: ${styling}${components ? `\n- **Components**: ${components.slice(2)}` : ''}${state ? `\n- **State Management**: ${state.slice(2)}` : ''}\n- **Testing**: ${
|
|
605
|
+
`## Technology Stack\n\n- **Framework**: ${framework}\n- **Language**: ${lang}\n- **Styling**: ${styling}${components ? `\n- **Components**: ${components.slice(2)}` : ''}${state ? `\n- **State Management**: ${state.slice(2)}` : ''}\n- **Testing**: ${frontendTestFramework}\n\n`
|
|
576
606
|
);
|
|
577
607
|
|
|
578
608
|
// Update Project Structure
|
|
@@ -848,8 +878,29 @@ function configureProductTracker(template, scan) {
|
|
|
848
878
|
content = content.replace('| backend | pending', `| ${featureType} | pending`);
|
|
849
879
|
}
|
|
850
880
|
|
|
851
|
-
// Add retrofit testing as first feature if coverage is low
|
|
852
|
-
|
|
881
|
+
// Add retrofit testing as first feature if coverage is low — v0.17.3
|
|
882
|
+
// broaden the gate: consider backend and frontend test coverage, not just
|
|
883
|
+
// root-level. Avoids false F001 recommendation on monorepos that have
|
|
884
|
+
// extensive tests in workspaces. Per Gemini round-3 CRITICAL: also gate
|
|
885
|
+
// on the broader "any test signal" disjunction so E2E-only setups
|
|
886
|
+
// (Playwright/Cypress at root, no unit tests anywhere) don't trigger
|
|
887
|
+
// F001 either — symmetric with the console-warning gate above.
|
|
888
|
+
const rankCoverage = (c) =>
|
|
889
|
+
c === 'high' ? 3 : c === 'medium' ? 2 : c === 'low' ? 1 : 0;
|
|
890
|
+
const bestCov = Math.max(
|
|
891
|
+
rankCoverage(scan.tests.estimatedCoverage),
|
|
892
|
+
rankCoverage(scan.backendTests.estimatedCoverage),
|
|
893
|
+
rankCoverage(scan.frontendTests.estimatedCoverage),
|
|
894
|
+
);
|
|
895
|
+
const hasAnyTestSignal =
|
|
896
|
+
scan.tests.framework !== 'none' ||
|
|
897
|
+
scan.backendTests.framework !== 'none' ||
|
|
898
|
+
scan.frontendTests.framework !== 'none' ||
|
|
899
|
+
scan.tests.e2eFramework !== null ||
|
|
900
|
+
scan.tests.testFiles > 0 ||
|
|
901
|
+
scan.backendTests.testFiles > 0 ||
|
|
902
|
+
scan.frontendTests.testFiles > 0;
|
|
903
|
+
if (!hasAnyTestSignal || bestCov <= 1 /* none or low */) {
|
|
853
904
|
// Use regex to match the F001 placeholder row resiliently (handles column changes)
|
|
854
905
|
content = content.replace(
|
|
855
906
|
/\| F001 \|[^\n]*\n/,
|
package/lib/init-wizard.js
CHANGED
|
@@ -41,8 +41,23 @@ function formatScanSummary(scanResult) {
|
|
|
41
41
|
};
|
|
42
42
|
lines.push(` Architecture: ${patternLabels[scanResult.srcStructure.pattern] || 'Unknown'}`);
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// v0.17.3: collect distinct non-'none' frameworks across root, backend,
|
|
45
|
+
// and frontend tests. For mixed monorepos (e.g., fx: vitest in api, jest
|
|
46
|
+
// in web), display joined (e.g., "vitest + jest") so the summary reflects
|
|
47
|
+
// what adapt-functions will actually write. Avoids the v1.1 UX issue
|
|
48
|
+
// where OR-precedence picked root-hoisted jest over workspace vitest.
|
|
49
|
+
const uniqueFrameworks = Array.from(new Set([
|
|
50
|
+
scanResult.tests.framework,
|
|
51
|
+
scanResult.backendTests.framework,
|
|
52
|
+
scanResult.frontendTests.framework,
|
|
53
|
+
].filter((f) => f !== 'none')));
|
|
54
|
+
if (uniqueFrameworks.length > 0) {
|
|
55
|
+
const totalFiles = Math.max(
|
|
56
|
+
scanResult.tests.testFiles,
|
|
57
|
+
scanResult.backendTests.testFiles,
|
|
58
|
+
scanResult.frontendTests.testFiles,
|
|
59
|
+
);
|
|
60
|
+
lines.push(` Tests: ${uniqueFrameworks.join(' + ')} (${totalFiles} test files)`);
|
|
46
61
|
} else {
|
|
47
62
|
lines.push(' Tests: None detected');
|
|
48
63
|
}
|
package/lib/meta.js
CHANGED
|
@@ -236,9 +236,10 @@ function writeMeta(dest, hashes) {
|
|
|
236
236
|
* - 6 workflow-core files (development-workflow SKILL.md + ticket-template.md
|
|
237
237
|
* + merge-checklist.md, × 2 tools) — filtered by aiTools
|
|
238
238
|
*
|
|
239
|
-
* Out of scope for v0.17.1 (deferred
|
|
240
|
-
*
|
|
241
|
-
* and all references/ files except the 3
|
|
239
|
+
* Out of scope for v0.17.1 (deferred — see dev/ROADMAP.md "Known follow-ups"
|
|
240
|
+
* item 2): bug-workflow/SKILL.md, health-check/SKILL.md, pm-orchestrator/SKILL.md,
|
|
241
|
+
* project-memory/SKILL.md, and all references/ files except the 3
|
|
242
|
+
* development-workflow ones above. Did not land in the v0.17.2 scanner hotfix.
|
|
242
243
|
*/
|
|
243
244
|
function expectedSmartDiffTrackedPaths(aiTools, projectType) {
|
|
244
245
|
const paths = new Set();
|
|
@@ -273,7 +274,7 @@ function expectedSmartDiffTrackedPaths(aiTools, projectType) {
|
|
|
273
274
|
|
|
274
275
|
// v0.17.1: development-workflow skill core files — filtered by aiTools.
|
|
275
276
|
// bug-workflow, health-check, pm-orchestrator, project-memory are OUT OF
|
|
276
|
-
// SCOPE for v0.17.1 (deferred
|
|
277
|
+
// SCOPE for v0.17.1 (deferred — see dev/ROADMAP.md "Known follow-ups" item 2).
|
|
277
278
|
for (const dir of toolDirs) {
|
|
278
279
|
paths.add(`${dir}/skills/development-workflow/SKILL.md`);
|
|
279
280
|
paths.add(`${dir}/skills/development-workflow/references/ticket-template.md`);
|
package/lib/scanner.js
CHANGED
|
@@ -8,18 +8,36 @@ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.n
|
|
|
8
8
|
/**
|
|
9
9
|
* Scan an existing project directory and return detected configuration.
|
|
10
10
|
*
|
|
11
|
-
* v0.17.1: monorepo-aware. If the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* declaration order (pattern outer, lexical inner, deduped by
|
|
15
|
-
* path) and run `detectBackend` / `detectFrontend` per workspace.
|
|
16
|
-
* FIRST workspace returning
|
|
11
|
+
* v0.17.1: monorepo-aware. If the project is a monorepo with
|
|
12
|
+
* `package.json#workspaces` and the root `package.json` does not yield a
|
|
13
|
+
* complete backend/frontend detection, enumerate workspace `package.json`
|
|
14
|
+
* files in declaration order (pattern outer, lexical inner, deduped by
|
|
15
|
+
* normalized path) and run `detectBackend` / `detectFrontend` per workspace.
|
|
16
|
+
* The FIRST workspace returning a framework wins and its result is merged
|
|
17
17
|
* into `result.backend` / `result.frontend` with a `workspaceSource` field
|
|
18
18
|
* recording the detected workspace's relative path (for diagnostics).
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* v0.17.2: the "complete detection" gate uses `framework` presence, not
|
|
21
|
+
* the older `detected` flag. This is because `detectBackend` has a
|
|
22
|
+
* partial-detection fallback (see `detectBackend` lines ~259-261) that
|
|
23
|
+
* sets `detected: true` when only `db` or `orm` is found — commonly
|
|
24
|
+
* triggered by a ROOT `.env.example` declaring `DATABASE_URL` + `PORT`
|
|
25
|
+
* in a monorepo whose real backend stack lives under `packages/api`.
|
|
26
|
+
* Under the v0.17.1 guard, that partial detection blocked the workspace
|
|
27
|
+
* enumeration and left `backend.framework` null → `adaptBackendStandards`
|
|
28
|
+
* produced generic placeholders → `adaptAgentsMd` fell back to the
|
|
29
|
+
* `(DDD, Express, Prisma)` template literal. Gating on `!framework`
|
|
30
|
+
* fires the enumeration in that case and correctly promotes
|
|
31
|
+
* workspace-level framework/orm info while preserving root-level
|
|
32
|
+
* env-derived fields (`db`, `port`) that the workspace didn't detect.
|
|
33
|
+
*
|
|
34
|
+
* Scanner additive invariant: for single-package projects, `isMonorepo`
|
|
35
|
+
* is false and the enumeration block is skipped entirely — same behavior
|
|
36
|
+
* as v0.17.0 and v0.17.1, byte-identical output. For monorepos, the
|
|
37
|
+
* v0.17.2 gate is strictly looser than v0.17.1's, so it runs the
|
|
38
|
+
* enumeration in a strict superset of cases. Enumeration only adds
|
|
39
|
+
* info (framework/orm from workspace), never removes it. Therefore
|
|
40
|
+
* v0.17.2 scan output ⊇ v0.17.1 scan output for the same input.
|
|
23
41
|
*/
|
|
24
42
|
function scan(projectDir) {
|
|
25
43
|
const pkg = readPackageJson(projectDir);
|
|
@@ -28,44 +46,187 @@ function scan(projectDir) {
|
|
|
28
46
|
const frontend = detectFrontend(projectDir, pkg);
|
|
29
47
|
const isMonorepo = detectMonorepo(projectDir, pkg);
|
|
30
48
|
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
let language = detectLanguage(projectDir);
|
|
50
|
+
let srcStructure = detectArchitecture(projectDir, pkg);
|
|
51
|
+
const rootTests = detectTests(projectDir, pkg);
|
|
52
|
+
let backendTests = rootTests;
|
|
53
|
+
let frontendTests = rootTests;
|
|
54
|
+
|
|
55
|
+
// v0.17.3: single-pass monorepo enumeration.
|
|
56
|
+
//
|
|
57
|
+
// Combines v0.17.2's framework promotion (runs only when root lacks a
|
|
58
|
+
// framework, gated by `!backend.framework` / `!frontend.framework`) with
|
|
59
|
+
// v0.17.3's auxiliary-field promotion (language, architecture, tests,
|
|
60
|
+
// frontend styling/components/state) into a single loop that always runs
|
|
61
|
+
// in monorepos.
|
|
62
|
+
//
|
|
63
|
+
// Rationale for always-enumerate: `workspaceSource` is only set when
|
|
64
|
+
// v0.17.2 actually promoted a framework from a workspace (root lacked it).
|
|
65
|
+
// If root has the framework hoisted (e.g., `"next": "^14.2.29"` at root in
|
|
66
|
+
// a Next.js monorepo), v0.17.2's loop never runs → `workspaceSource` is
|
|
67
|
+
// null → auxiliary detection has no workspace handle. v0.17.3 solves this
|
|
68
|
+
// by tracking `primaryBackendWs` / `primaryFrontendWs` independently:
|
|
69
|
+
// always populated for monorepos with a detectable backend/frontend
|
|
70
|
+
// workspace, regardless of where the framework came from. See
|
|
71
|
+
// dev/v0.17.3-plan.md D5 for full rationale + round-1/round-2 review trail.
|
|
72
|
+
let primaryBackendWs = null;
|
|
73
|
+
let primaryFrontendWs = null;
|
|
74
|
+
if (isMonorepo) {
|
|
33
75
|
const workspaces = enumerateWorkspaces(projectDir, pkg);
|
|
34
76
|
for (const wsRel of workspaces) {
|
|
35
77
|
const wsAbs = path.join(projectDir, ...wsRel.split('/'));
|
|
36
78
|
const wsPkg = readPackageJson(wsAbs);
|
|
37
|
-
|
|
79
|
+
|
|
80
|
+
if (!primaryBackendWs) {
|
|
38
81
|
const wsBackend = detectBackend(wsAbs, wsPkg);
|
|
39
|
-
if (wsBackend.
|
|
40
|
-
|
|
82
|
+
if (wsBackend.framework) {
|
|
83
|
+
primaryBackendWs = wsRel;
|
|
84
|
+
// v0.17.2 promotion — only when root lacked framework. Preserves
|
|
85
|
+
// v0.17.2 semantics byte-equivalent.
|
|
86
|
+
if (!backend.framework) {
|
|
87
|
+
for (const field of Object.keys(wsBackend)) {
|
|
88
|
+
if (wsBackend[field] !== null && wsBackend[field] !== undefined) {
|
|
89
|
+
backend[field] = wsBackend[field];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
backend.workspaceSource = wsRel;
|
|
93
|
+
}
|
|
41
94
|
}
|
|
42
95
|
}
|
|
43
|
-
|
|
96
|
+
|
|
97
|
+
if (!primaryFrontendWs) {
|
|
44
98
|
const wsFrontend = detectFrontend(wsAbs, wsPkg);
|
|
45
|
-
if (wsFrontend.
|
|
46
|
-
|
|
99
|
+
if (wsFrontend.framework) {
|
|
100
|
+
primaryFrontendWs = wsRel;
|
|
101
|
+
if (!frontend.framework) {
|
|
102
|
+
for (const field of Object.keys(wsFrontend)) {
|
|
103
|
+
if (wsFrontend[field] !== null && wsFrontend[field] !== undefined) {
|
|
104
|
+
frontend[field] = wsFrontend[field];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
frontend.workspaceSource = wsRel;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (primaryBackendWs && primaryFrontendWs) break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// v0.17.3 auxiliary detection — runs whenever a primary workspace was
|
|
116
|
+
// identified, independent of v0.17.2 promotion.
|
|
117
|
+
if (primaryBackendWs) {
|
|
118
|
+
const wsAbs = path.join(projectDir, ...primaryBackendWs.split('/'));
|
|
119
|
+
const wsPkg = readPackageJson(wsAbs);
|
|
120
|
+
// D1: scalar language merge — TypeScript wins over JavaScript, never
|
|
121
|
+
// demote. 'javascript' is detectLanguage's default; if the workspace
|
|
122
|
+
// returns 'typescript', promote.
|
|
123
|
+
if (detectLanguage(wsAbs) === 'typescript') language = 'typescript';
|
|
124
|
+
// D2: architecture per-field merge.
|
|
125
|
+
// - pattern: workspace wins if it detected ANY known pattern (any
|
|
126
|
+
// non-'unknown' value). Strict non-demotion: workspace 'unknown'
|
|
127
|
+
// never demotes a known root pattern. Workspace known-vs-known
|
|
128
|
+
// resolves to workspace because the workspace is the authoritative
|
|
129
|
+
// source for the primary backend's organization (root pattern in
|
|
130
|
+
// monorepos is often a false signal from the listing fallback when
|
|
131
|
+
// no src/ exists).
|
|
132
|
+
// - dirs: workspace wins when it has any dirs (root's empty []
|
|
133
|
+
// carries no info, and root in monorepos lists workspace dirs not
|
|
134
|
+
// source dirs which is misleading).
|
|
135
|
+
// - boolean hasX flags: OR-merge (true wins, never demoted from true).
|
|
136
|
+
const wsArch = detectArchitecture(wsAbs, wsPkg);
|
|
137
|
+
if (wsArch.pattern !== 'unknown') srcStructure.pattern = wsArch.pattern;
|
|
138
|
+
if (wsArch.dirs && wsArch.dirs.length > 0) srcStructure.dirs = wsArch.dirs;
|
|
139
|
+
for (const field of [
|
|
140
|
+
'hasControllers',
|
|
141
|
+
'hasRoutes',
|
|
142
|
+
'hasModels',
|
|
143
|
+
'hasServices',
|
|
144
|
+
'hasDomain',
|
|
145
|
+
'hasMiddleware',
|
|
146
|
+
'hasFeatures',
|
|
147
|
+
'hasHandlers',
|
|
148
|
+
]) {
|
|
149
|
+
if (wsArch[field] === true) srcStructure[field] = true;
|
|
150
|
+
}
|
|
151
|
+
// D3: backend tests per-field merge (preserves root-level E2E).
|
|
152
|
+
backendTests = mergeWorkspaceTests(rootTests, detectTests(wsAbs, wsPkg));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (primaryFrontendWs) {
|
|
156
|
+
const wsAbs = path.join(projectDir, ...primaryFrontendWs.split('/'));
|
|
157
|
+
const wsPkg = readPackageJson(wsAbs);
|
|
158
|
+
// D4 (revised): promote auxiliary frontend fields (styling,
|
|
159
|
+
// components, state) from the primary frontend workspace,
|
|
160
|
+
// independent of whether the framework was promoted at root.
|
|
161
|
+
const wsFrontendAux = detectFrontend(wsAbs, wsPkg);
|
|
162
|
+
for (const field of ['styling', 'components', 'state']) {
|
|
163
|
+
if (wsFrontendAux[field] !== null && wsFrontendAux[field] !== undefined) {
|
|
164
|
+
frontend[field] = wsFrontendAux[field];
|
|
47
165
|
}
|
|
48
166
|
}
|
|
49
|
-
|
|
167
|
+
frontendTests = mergeWorkspaceTests(rootTests, detectTests(wsAbs, wsPkg));
|
|
50
168
|
}
|
|
169
|
+
|
|
170
|
+
// D5 observability: export primary workspace identifiers for diagnostics
|
|
171
|
+
// and smoke-test assertions. `workspaceSource` (v0.17.2) remains set only
|
|
172
|
+
// when promotion happened; `primaryWorkspace` (v0.17.3) is set whenever a
|
|
173
|
+
// workspace was used for aux detection.
|
|
174
|
+
if (primaryBackendWs) backend.primaryWorkspace = primaryBackendWs;
|
|
175
|
+
if (primaryFrontendWs) frontend.primaryWorkspace = primaryFrontendWs;
|
|
51
176
|
}
|
|
52
177
|
|
|
53
178
|
return {
|
|
54
179
|
projectName: pkg.name || path.basename(projectDir),
|
|
55
180
|
description: pkg.description || '',
|
|
56
|
-
language
|
|
181
|
+
language,
|
|
57
182
|
backend,
|
|
58
183
|
frontend,
|
|
59
184
|
isMonorepo,
|
|
60
185
|
rootDirs: listRootDirs(projectDir),
|
|
61
|
-
srcStructure
|
|
62
|
-
tests:
|
|
186
|
+
srcStructure,
|
|
187
|
+
tests: rootTests,
|
|
188
|
+
backendTests,
|
|
189
|
+
frontendTests,
|
|
63
190
|
existingDocs: detectExistingDocs(projectDir),
|
|
64
191
|
gitBranch: detectGitBranch(projectDir),
|
|
65
192
|
hasGit: fs.existsSync(path.join(projectDir, '.git')),
|
|
66
193
|
};
|
|
67
194
|
}
|
|
68
195
|
|
|
196
|
+
/**
|
|
197
|
+
* v0.17.3: per-field merge of workspace test detection results into a
|
|
198
|
+
* new object based on root-level test detection.
|
|
199
|
+
*
|
|
200
|
+
* Preserves root-level signals the workspace doesn't see:
|
|
201
|
+
* - e2eFramework (Playwright/Cypress typically installed once at root)
|
|
202
|
+
* - framework (if workspace found no unit framework, keep root's default
|
|
203
|
+
* or hoisted value)
|
|
204
|
+
*
|
|
205
|
+
* Promotes workspace signals that override root:
|
|
206
|
+
* - framework: workspace wins if it detected a non-'none' unit framework
|
|
207
|
+
* - hasConfig: logical OR (either source)
|
|
208
|
+
* - testFiles / testDirs / estimatedCoverage: workspace authoritative when
|
|
209
|
+
* it saw any test files. (Note: `countFilesRecursive` walks from
|
|
210
|
+
* projectDir up to depth 6 traversing into packages/*, so rootTests
|
|
211
|
+
* already aggregates workspace counts. A comparison like
|
|
212
|
+
* `wsTests.testFiles > rootTests.testFiles` would be dead code — see
|
|
213
|
+
* dev/v0.17.3-plan.md D3 v1.2 revision.)
|
|
214
|
+
*/
|
|
215
|
+
function mergeWorkspaceTests(rootTests, wsTests) {
|
|
216
|
+
const merged = { ...rootTests };
|
|
217
|
+
if (wsTests.framework !== 'none') {
|
|
218
|
+
merged.framework = wsTests.framework;
|
|
219
|
+
merged.hasConfig = wsTests.hasConfig || rootTests.hasConfig;
|
|
220
|
+
}
|
|
221
|
+
if (wsTests.testFiles > 0) {
|
|
222
|
+
merged.testFiles = wsTests.testFiles;
|
|
223
|
+
merged.testDirs = wsTests.testDirs;
|
|
224
|
+
merged.estimatedCoverage = wsTests.estimatedCoverage;
|
|
225
|
+
}
|
|
226
|
+
if (wsTests.e2eFramework) merged.e2eFramework = wsTests.e2eFramework;
|
|
227
|
+
return merged;
|
|
228
|
+
}
|
|
229
|
+
|
|
69
230
|
/**
|
|
70
231
|
* v0.17.1: enumerate workspace paths declared in `pkg.workspaces`.
|
|
71
232
|
*
|
|
@@ -76,7 +237,8 @@ function scan(projectDir) {
|
|
|
76
237
|
* - Single-wildcard patterns: `"packages/*"` (expand immediate subdirs)
|
|
77
238
|
*
|
|
78
239
|
* Does NOT support: `**` recursive patterns, `!exclude` negation, or
|
|
79
|
-
* `pnpm-workspace.yaml` — all deferred
|
|
240
|
+
* `pnpm-workspace.yaml` — all deferred (see dev/ROADMAP.md "Known follow-ups"
|
|
241
|
+
* item 3). Did not land in the v0.17.2 scanner hotfix.
|
|
80
242
|
*
|
|
81
243
|
* Returns a deterministic, deduplicated array of POSIX-style relative
|
|
82
244
|
* workspace paths. Ordering: outer = declaration order of patterns; inner
|