@shanyucoder/flowgrid 0.1.5 → 0.1.9

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.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/adapters/laravel/registries/codegen.registry.json +9 -9
  3. package/bin/flowgrid.mjs +326 -115
  4. package/bin/lib/agent-mcp.mjs +4 -0
  5. package/bin/lib/agent-profiles.mjs +30 -6
  6. package/bin/lib/audit-run.mjs +1 -1
  7. package/bin/lib/cli-update.mjs +48 -8
  8. package/bin/lib/doctor.mjs +90 -3
  9. package/bin/lib/harness-overlay.mjs +12 -5
  10. package/bin/lib/harness-sync.mjs +17 -3
  11. package/bin/lib/init-adapters.mjs +75 -0
  12. package/bin/lib/init-scaffold.mjs +9 -0
  13. package/bin/lib/inject-consumer-scripts.mjs +115 -0
  14. package/bin/lib/merge-stack-config.mjs +89 -0
  15. package/bin/lib/project-gitignore.mjs +1 -0
  16. package/bin/lib/repo-maps-align.mjs +203 -0
  17. package/dist/graph/config/load-config.js +7 -2
  18. package/dist/graph/config/load-config.js.map +1 -1
  19. package/dist/graph/mcp/tools.js +1 -1
  20. package/dist/graph/mcp/tools.js.map +1 -1
  21. package/dist/graph/registry/load-registries.d.ts +1 -0
  22. package/dist/graph/registry/load-registries.js +14 -1
  23. package/dist/graph/registry/load-registries.js.map +1 -1
  24. package/engines/cases/render-cases.mjs +67 -5
  25. package/engines/docs/lib/qa-item.mjs +91 -0
  26. package/engines/docs/lib/render-api-summary-markdown.mjs +28 -0
  27. package/engines/docs/lib/render-bundle-markdown.mjs +77 -2
  28. package/engines/docs/lib/render-data-model-markdown.mjs +171 -0
  29. package/engines/docs/lib/render-design-tables.mjs +89 -7
  30. package/engines/docs/lib/render-qa-list.mjs +123 -25
  31. package/engines/docs/lib/render-template.mjs +6 -0
  32. package/engines/docs/render-docs.mjs +6 -2
  33. package/engines/docs/vitepress/config.ts +7 -7
  34. package/engines/docs/vitepress/surfaces-nav.mjs +45 -14
  35. package/engines/openapi/check-backend-spec.mjs +2 -2
  36. package/engines/openapi/lib/markdown-table.mjs +8 -0
  37. package/engines/openapi/lib/render-backend-spec-markdown.mjs +78 -0
  38. package/engines/registry-sync/be-capabilities-sync.mjs +118 -0
  39. package/engines/registry-sync/fe-design-sync.mjs +258 -0
  40. package/engines/registry-sync/run-registry-sync.mjs +107 -0
  41. package/engines/shared/e2e-output-layout.mjs +68 -0
  42. package/engines/shared/flowgrid-e2e-root.mjs +19 -0
  43. package/engines/shared/resolve-flowgrid-context.mjs +176 -0
  44. package/engines/spec/lib/audit-api-gaps.mjs +1 -1
  45. package/engines/spec/lib/audit-bundle-gaps.mjs +92 -3
  46. package/engines/spec/lib/audit-db-tables.mjs +529 -0
  47. package/engines/spec/lib/audit-e2e-coverage.mjs +88 -30
  48. package/engines/spec/lib/bundle-schema.mjs +4 -1
  49. package/engines/spec/lib/open-qa.mjs +71 -21
  50. package/engines/spec/split-bundle.mjs +11 -1
  51. package/engines/testcase/runners/generate-api.mjs +23 -23
  52. package/engines/testcase/runners/generate.mjs +19 -17
  53. package/engines/testcase/runners/lib/bootstrap-context.mjs +44 -0
  54. package/engines/testcase/runners/lib/write-files.mjs +37 -9
  55. package/harness/agents/antigravity/rules/antigravity-mcp.mdc +12 -0
  56. package/harness/agents/gemini/rules/gemini-mcp.mdc +11 -0
  57. package/harness/agents/gemini_antigravity/rules/antigravity-mcp.mdc +8 -6
  58. package/harness/be/skills/{grill-api → audit-api}/SKILL.md +8 -7
  59. package/harness/common/extracts/artifact-graph.md +2 -2
  60. package/harness/common/extracts/artifactgraph-phase-hooks.md +2 -2
  61. package/harness/common/extracts/docs-mark-detect.md +2 -2
  62. package/harness/common/extracts/entity-relationship.md +23 -0
  63. package/harness/common/rules/flowgrid-ux-common.mdc +3 -3
  64. package/harness/common/skills/configure-repo-maps/SKILL.md +4 -2
  65. package/harness/docs/extracts/agent-execution-protocol.md +3 -3
  66. package/harness/docs/extracts/api-codegen-readiness.md +34 -0
  67. package/harness/docs/extracts/api-codegen-tags.md +30 -0
  68. package/harness/docs/extracts/api-contract.md +43 -0
  69. package/harness/docs/extracts/api-spec-sync.md +35 -0
  70. package/harness/docs/extracts/artifactgraph-hooks-docs.md +1 -1
  71. package/harness/docs/extracts/call-external.md +16 -0
  72. package/harness/docs/extracts/common-scope.md +9 -10
  73. package/harness/docs/extracts/db-audit-wizard.md +45 -0
  74. package/harness/docs/extracts/derived-data.md +18 -0
  75. package/harness/docs/extracts/design-leaf-signoff.md +16 -0
  76. package/harness/docs/extracts/extract-registry.docs.json +11 -2
  77. package/harness/docs/extracts/qa-inbox.md +19 -10
  78. package/harness/docs/extracts/qa-team.md +32 -0
  79. package/harness/docs/extracts/spec-core.md +7 -3
  80. package/harness/docs/extracts/spec-evolution.md +21 -0
  81. package/harness/docs/extracts/spec-prd-lite.md +19 -0
  82. package/harness/docs/extracts/spec-requirement.md +6 -2
  83. package/harness/docs/extracts/spec-ssot-prep.md +25 -0
  84. package/harness/docs/extracts/tpl-module.md +12 -0
  85. package/harness/docs/extracts/verify-gate.md +33 -0
  86. package/harness/docs/extracts/wire-spec-feedback.md +31 -0
  87. package/harness/docs/rules/agent-compliance.mdc +1 -1
  88. package/harness/docs/rules/team-flow-spec.mdc +3 -4
  89. package/harness/docs/schemas/flowgrid-docs/qa-item.schema.json +65 -0
  90. package/harness/docs/skills/adopt/SKILL.md +2 -0
  91. package/harness/docs/skills/api/SKILL.md +4 -5
  92. package/harness/docs/skills/api-spec/SKILL.md +19 -6
  93. package/harness/docs/skills/api-update/SKILL.md +4 -4
  94. package/harness/docs/skills/architecture/SKILL.md +1 -1
  95. package/harness/docs/skills/business-process/SKILL.md +2 -0
  96. package/harness/docs/skills/common/SKILL.md +2 -2
  97. package/harness/docs/skills/common-spec/SKILL.md +10 -47
  98. package/harness/docs/skills/db-erd/SKILL.md +26 -0
  99. package/harness/docs/skills/grill/SKILL.md +28 -22
  100. package/harness/docs/skills/grill-api/SKILL.md +4 -6
  101. package/harness/docs/skills/grill-api-spec/SKILL.md +44 -24
  102. package/harness/docs/skills/grill-bqa/SKILL.md +28 -13
  103. package/harness/docs/skills/grill-common-spec/SKILL.md +10 -35
  104. package/harness/docs/skills/grill-dev/SKILL.md +8 -7
  105. package/harness/docs/skills/grill-docs/SKILL.md +11 -4
  106. package/harness/docs/skills/module/SKILL.md +3 -1
  107. package/harness/docs/skills/openapi/SKILL.md +2 -1
  108. package/harness/docs/skills/overview/SKILL.md +7 -1
  109. package/harness/docs/skills/qa-resolve/SKILL.md +13 -12
  110. package/harness/docs/skills/qa-review/SKILL.md +45 -0
  111. package/harness/docs/skills/spec/SKILL.md +43 -10
  112. package/harness/docs/skills/update-spec/SKILL.md +5 -2
  113. package/harness/fe/extracts/wire-audit-loop.md +72 -0
  114. package/harness/fe/extracts/wire-phase.md +45 -0
  115. package/harness/fe/rules/platform-design-vocabulary.mdc +1 -1
  116. package/harness/fe/rules/team-flow-prototype.mdc +8 -4
  117. package/harness/fe/skills/gen-common/SKILL.md +11 -84
  118. package/harness/fe/skills/grill-prototype/SKILL.md +51 -25
  119. package/harness/fe/skills/grill-test/SKILL.md +78 -20
  120. package/harness/fe/skills/grill-wire/SKILL.md +81 -0
  121. package/harness/fe/skills/prototype/SKILL.md +3 -2
  122. package/harness/fe/skills/wire/SKILL.md +8 -3
  123. package/harness/shared/AGENTS.md +3 -3
  124. package/harness/shared/SSOT_AGENT_PROTOCOL.md +3 -3
  125. package/harness/tests/extracts/grill-api-hook.md +69 -0
  126. package/harness/tests/extracts/grill-scenario-flow.md +39 -0
  127. package/harness/tests/extracts/grill-screen-tc.md +40 -0
  128. package/harness/tests/extracts/testcase-gen-cli.md +57 -0
  129. package/harness/tests/extracts/testcase-plan.md +29 -0
  130. package/harness/tests/extracts/tests-verify-gate.md +29 -0
  131. package/harness/tests/extracts/wire-test-handoff.md +37 -0
  132. package/harness/tests/skills/grill-testcase/SKILL.md +1 -0
  133. package/harness/tests/skills/test-api/SKILL.md +14 -6
  134. package/harness/tests/skills/testcase/SKILL.md +3 -1
  135. package/harness/tests/templates/TC.example-api.yaml +7 -1
  136. package/harness/tests/templates/TC.example.yaml +4 -1
  137. package/harness/tests/templates/tpl-testcase-plan.md +75 -0
  138. package/package.json +1 -1
  139. package/stacks/fastapi.json +1 -0
  140. package/stacks/laravel.json +1 -0
  141. package/stacks/nestjs.json +72 -0
  142. package/stacks/nextjs-nest.json +1 -0
  143. package/stacks/nuxt4-nest.json +1 -0
  144. package/templates/project-skeleton/architecture/03-business-process/FLOW-template.md +2 -0
  145. package/templates/project-skeleton/overview/index.md +68 -2
  146. package/templates/project-skeleton/overview/operational-areas/_template.md +37 -0
  147. package/templates/project-skeleton/qa/README.md +4 -8
  148. package/templates/project-skeleton/surfaces/common/data-model/index.md +10 -2
  149. package/templates/schemas/qa-item.schema.json +98 -0
  150. package/templates/shared/api-03-mock.stub.yaml +14 -0
  151. package/templates/shared/backend-api.bundle.yaml +3 -0
  152. package/templates/shared/backend-api.yaml +4 -0
  153. package/templates/shared/be-capabilities.registry.base.json +8 -0
  154. package/templates/shared/bundle-authoring.md +44 -7
  155. package/templates/shared/default-layout.ejs +131 -18
  156. package/templates/shared/design-spec.yaml +2 -3
  157. package/templates/shared/design.registry.base.json +38 -0
  158. package/templates/shared/feature.bundle.yaml +16 -9
  159. package/templates/shared/ir/generated/spec.md +281 -0
  160. package/templates/shared/ir-spec.yaml +1 -1
  161. package/templates/shared/qa-authoring.md +78 -0
  162. package/templates/shared/qa-item.yaml +35 -14
  163. package/templates/shared/tpl-api-contract.md +133 -0
  164. package/templates/shared/tpl-screen-data-model.md +76 -0
  165. package/templates/tests-skeleton/cases/README.md +4 -0
  166. package/templates/tests-skeleton/catalog/locale.yaml +9 -0
  167. package/templates/tests-skeleton/tpl-testcase-plan.md +9 -0
  168. package/harness/docs/skills/api-integration/SKILL.md +0 -110
  169. package/harness/docs/skills/grill-integration-spec/SKILL.md +0 -51
@@ -9,6 +9,9 @@ import fs from 'fs';
9
9
  import path from 'path';
10
10
  import { parse } from 'yaml';
11
11
  import { resolveTestsDocsRoot } from '../../shared/flowgrid-tests-docs.mjs';
12
+ import { resolveFlowgridContext } from '../../shared/resolve-flowgrid-context.mjs';
13
+ import { playwrightScanRoots } from '../../shared/e2e-output-layout.mjs';
14
+ import { resolveHubId } from '../../testcase/runners/lib/resolve-hub-id.mjs';
12
15
 
13
16
  const TC_ID_PATTERN = /^TC-[A-Z0-9][A-Z0-9-]*$/;
14
17
  const DESCRIBE_TC_RE = /test\.describe\s*\(\s*['"](TC-[^'"]+)['"]/g;
@@ -42,7 +45,21 @@ function walkFiles(root, onFile) {
42
45
  /**
43
46
  * @param {string} testsRoot
44
47
  */
45
- export function scanPlaywrightTests(testsRoot) {
48
+ function mergeScanState(into, from) {
49
+ for (const rec of from.specFiles) {
50
+ if (!into.specFiles.includes(rec)) into.specFiles.push(rec);
51
+ }
52
+ for (const [tcId, files] of from.tcIdToFiles.entries()) {
53
+ const list = into.tcIdToFiles.get(tcId) ?? [];
54
+ for (const f of files) {
55
+ if (!list.includes(f)) list.push(f);
56
+ }
57
+ into.tcIdToFiles.set(tcId, list);
58
+ }
59
+ for (const id of from.referencedTcIds) into.referencedTcIds.add(id);
60
+ }
61
+
62
+ function scanPlaywrightTestsSingle(testsRoot) {
46
63
  const specFiles = [];
47
64
  const tcIdToFiles = new Map();
48
65
 
@@ -77,6 +94,10 @@ export function scanPlaywrightTests(testsRoot) {
77
94
 
78
95
  const referencedTcIds = new Set(tcIdToFiles.keys());
79
96
 
97
+ return buildScanResult(specFiles, tcIdToFiles, referencedTcIds);
98
+ }
99
+
100
+ function buildScanResult(specFiles, tcIdToFiles, referencedTcIds) {
80
101
  return {
81
102
  specFiles,
82
103
  specFileCount: specFiles.length,
@@ -92,6 +113,22 @@ export function scanPlaywrightTests(testsRoot) {
92
113
  };
93
114
  }
94
115
 
116
+ /**
117
+ * @param {string | string[]} testsRootOrRoots
118
+ */
119
+ export function scanPlaywrightTests(testsRootOrRoots) {
120
+ const roots = Array.isArray(testsRootOrRoots) ? testsRootOrRoots : [testsRootOrRoots];
121
+ const existing = roots.filter((r) => r && fs.existsSync(r));
122
+ if (!existing.length) {
123
+ return buildScanResult([], new Map(), new Set());
124
+ }
125
+ let merged = scanPlaywrightTestsSingle(existing[0]);
126
+ for (let i = 1; i < existing.length; i++) {
127
+ mergeScanState(merged, scanPlaywrightTestsSingle(existing[i]));
128
+ }
129
+ return merged;
130
+ }
131
+
95
132
  /**
96
133
  * @param {string} casesRoot
97
134
  * @param {{ screen?: string }} [filter]
@@ -150,11 +187,12 @@ function addGap(gaps, code, severity, fieldPath, message, suggestedFix) {
150
187
  }
151
188
 
152
189
  /**
153
- * @param {{ tcDocuments: Array<{ path: string; id: string; screen: string | null; matrix: unknown[] }>; e2eRoot: string; strict?: boolean; strictMatrix?: boolean }} opts
190
+ * @param {{ tcDocuments: Array<{ path: string; id: string; screen: string | null; matrix: unknown[] }>; e2eRoot?: string; e2eScanRoots?: string[]; strict?: boolean; strictMatrix?: boolean }} opts
154
191
  */
155
192
  export function auditE2eCoverage(opts) {
156
- const { tcDocuments, e2eRoot, strict = false, strictMatrix = false } = opts;
157
- const pw = scanPlaywrightTests(e2eRoot);
193
+ const { tcDocuments, e2eRoot, e2eScanRoots, strict = false, strictMatrix = false } = opts;
194
+ const scanRoots = e2eScanRoots ?? (e2eRoot ? [e2eRoot] : []);
195
+ const pw = scanPlaywrightTests(scanRoots);
158
196
  const gaps = [];
159
197
  const missingInPlaywright = [];
160
198
  const matrixRowsUncovered = [];
@@ -268,21 +306,21 @@ export function auditE2eCoverage(opts) {
268
306
  }
269
307
 
270
308
  function parseCliArgs(argv) {
271
- let e2eRoot = null;
272
- let testsDocs = null;
309
+ const ctx = resolveFlowgridContext({ argv });
310
+ let e2eRoot = ctx.e2eRoot;
311
+ let testsDocs = ctx.testsDocs;
273
312
  let screen = null;
274
313
  let strict = false;
275
314
  let strictMatrix = false;
315
+ let hubId = null;
276
316
  const tcFiles = [];
277
317
 
278
- for (let i = 0; i < argv.length; i++) {
279
- const a = argv[i];
280
- if (a === '--e2e-root' && argv[i + 1]) {
281
- e2eRoot = path.resolve(process.cwd(), argv[++i]);
282
- } else if (a === '--tests-docs' && argv[i + 1]) {
283
- testsDocs = path.resolve(process.cwd(), argv[++i]);
284
- } else if (a === '--screen' && argv[i + 1]) {
285
- screen = argv[++i];
318
+ for (let i = 0; i < ctx.argv.length; i++) {
319
+ const a = ctx.argv[i];
320
+ if (a === '--screen' && ctx.argv[i + 1]) {
321
+ screen = ctx.argv[++i];
322
+ } else if (a === '--id' && ctx.argv[i + 1]) {
323
+ hubId = ctx.argv[++i];
286
324
  } else if (a === '--strict') {
287
325
  strict = true;
288
326
  strictMatrix = true;
@@ -291,22 +329,32 @@ function parseCliArgs(argv) {
291
329
  } else if (a === '--help' || a === '-h') {
292
330
  return { help: true };
293
331
  } else if (!a.startsWith('-')) {
294
- tcFiles.push(path.resolve(process.cwd(), a));
332
+ tcFiles.push(path.resolve(ctx.projectRoot, a));
295
333
  }
296
334
  }
297
335
 
298
- return { e2eRoot, testsDocs, screen, strict, strictMatrix, tcFiles };
336
+ return {
337
+ e2eRoot,
338
+ testsDocs,
339
+ screen,
340
+ strict,
341
+ strictMatrix,
342
+ tcFiles,
343
+ hubId,
344
+ projectRoot: ctx.projectRoot,
345
+ };
299
346
  }
300
347
 
301
348
  function printUsage() {
302
349
  console.log(
303
350
  [
304
- 'Usage: node engines/spec/lib/audit-e2e-coverage.mjs --e2e-root <playwright-dir> [options]',
305
- ' flowgrid audit e2e --e2e-root <dir> --tests-docs <tests-hub> [--screen W-*]',
306
- ' flowgrid audit e2e --e2e-root <dir> <TC-*.yaml> [more…]',
351
+ 'Usage: flowgrid audit e2e [--id TC-*|W-*|suite] [options]',
352
+ ' flowgrid audit e2e <TC-*.yaml> [more…]',
307
353
  '',
308
354
  'Options:',
309
- ' --tests-docs Tests hub (TC YAML); or set FLOWGRID_TESTS_DOC',
355
+ ' --e2e-root Playwright scan root (default: config/MCP FLOWGRID_E2E_ROOT)',
356
+ ' --tests-docs Tests hub (TC YAML); or FLOWGRID_TESTS_DOC / config',
357
+ ' --id Resolve TC YAML via tests hub index (TC-*, screen W-*, suite)',
310
358
  ' --screen W-* Filter TC docs by refs.screen',
311
359
  ' --strict Treat missing TC spec + matrix gaps as critical',
312
360
  ' --strict-matrix Require each testMatrix row signal in Playwright text',
@@ -327,19 +375,27 @@ if (parsed.help) {
327
375
  process.exit(0);
328
376
  }
329
377
 
330
- if (!parsed.e2eRoot) {
331
- console.error(JSON.stringify({ error: '--e2e-root is required (Playwright specs directory)' }));
332
- process.exit(1);
333
- }
334
-
335
- if (!fs.existsSync(parsed.e2eRoot)) {
336
- console.error(JSON.stringify({ error: `e2e-root not found: ${parsed.e2eRoot}` }));
378
+ const e2eScanRoots = playwrightScanRoots(parsed.projectRoot, parsed.e2eRoot).filter((r) =>
379
+ fs.existsSync(r),
380
+ );
381
+ if (!e2eScanRoots.length) {
382
+ console.error(
383
+ JSON.stringify({
384
+ error:
385
+ 'No e2e scan directory found — set frontend/backend.e2eRoot, FLOWGRID_E2E_ROOT, or pass --e2e-root',
386
+ }),
387
+ );
337
388
  process.exit(1);
338
389
  }
339
390
 
340
391
  let tcDocuments = [];
341
392
  try {
342
- if (parsed.tcFiles.length) {
393
+ if (parsed.hubId) {
394
+ const resolved = resolveHubId(parsed.projectRoot, parsed.hubId, 'testcase');
395
+ for (const f of resolved.paths) {
396
+ tcDocuments.push(loadTcDocument(f));
397
+ }
398
+ } else if (parsed.tcFiles.length) {
343
399
  for (const f of parsed.tcFiles) {
344
400
  if (!fs.existsSync(f)) {
345
401
  console.error(JSON.stringify({ error: `TC file not found: ${f}` }));
@@ -352,7 +408,7 @@ try {
352
408
  if (!testsDocs) {
353
409
  console.error(
354
410
  JSON.stringify({
355
- error: 'Provide --tests-docs, FLOWGRID_TESTS_DOC, or explicit TC-*.yaml paths',
411
+ error: 'Provide --id, --tests-docs, FLOWGRID_TESTS_DOC, or explicit TC-*.yaml paths',
356
412
  }),
357
413
  );
358
414
  process.exit(1);
@@ -370,10 +426,12 @@ try {
370
426
 
371
427
  const report = auditE2eCoverage({
372
428
  tcDocuments,
373
- e2eRoot: parsed.e2eRoot,
429
+ e2eScanRoots,
430
+ e2eRoot: e2eScanRoots[0],
374
431
  strict: parsed.strict,
375
432
  strictMatrix: parsed.strictMatrix,
376
433
  });
434
+ report.e2eScanRoots = e2eScanRoots;
377
435
  if (parsed.testsDocs ?? resolveTestsDocsRoot()) {
378
436
  report.testsDocs = parsed.testsDocs ?? resolveTestsDocsRoot();
379
437
  }
@@ -12,6 +12,8 @@ export const BUNDLE_META_KEYS = [
12
12
  'status',
13
13
  'owner',
14
14
  'summary',
15
+ 'successMetrics',
16
+ 'nonGoals',
15
17
  'userStories',
16
18
  'specOrigin',
17
19
  'grillStatus'
@@ -36,7 +38,8 @@ export const DESIGN_LAYOUT_KEYS = [
36
38
  'behavior',
37
39
  'actions',
38
40
  'deviations',
39
- 'i18n'
41
+ 'i18n',
42
+ 'dataModel'
40
43
  ]
41
44
 
42
45
  /**
@@ -2,11 +2,20 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { parse } from 'yaml'
4
4
 
5
- /** Walk up from a bundle dir to the docs hub (`qa/open` or `architecture/`). */
5
+ function qaDirHasFiles(qaDir) {
6
+ if (!existsSync(qaDir)) return false
7
+ for (const name of readdirSync(qaDir)) {
8
+ if (/\.ya?ml$/i.test(name)) return true
9
+ if (name === 'open' && existsSync(path.join(qaDir, 'open'))) return true
10
+ }
11
+ return existsSync(path.join(qaDir, 'index.md'))
12
+ }
13
+
14
+ /** Walk up from a bundle dir to the docs hub (`qa/` or `architecture/`). */
6
15
  export function findDocsHubRoot(fromDir) {
7
16
  let dir = path.resolve(fromDir)
8
17
  for (let i = 0; i < 40; i++) {
9
- if (existsSync(path.join(dir, 'qa', 'open')) || existsSync(path.join(dir, 'architecture'))) {
18
+ if (existsSync(path.join(dir, 'architecture')) || qaDirHasFiles(path.join(dir, 'qa'))) {
10
19
  return dir
11
20
  }
12
21
  const parent = path.dirname(dir)
@@ -16,32 +25,73 @@ export function findDocsHubRoot(fromDir) {
16
25
  return null
17
26
  }
18
27
 
19
- export function openQaCsv(hubRoot, bundleId) {
20
- return getOpenQaDetails(hubRoot, bundleId).csv
28
+ function listQaYamlPaths(hubRoot) {
29
+ const qaDir = path.join(hubRoot, 'qa')
30
+ const paths = []
31
+ if (!existsSync(qaDir)) return paths
32
+ for (const name of readdirSync(qaDir)) {
33
+ if (/\.ya?ml$/i.test(name)) paths.push(path.join(qaDir, name))
34
+ }
35
+ const openDir = path.join(qaDir, 'open')
36
+ if (existsSync(openDir)) {
37
+ for (const name of readdirSync(openDir)) {
38
+ if (/\.ya?ml$/i.test(name)) paths.push(path.join(openDir, name))
39
+ }
40
+ }
41
+ return paths
42
+ }
43
+
44
+ function qaAppliesToBundle(doc, stem, bundleId, bundlePath) {
45
+ if (stem.startsWith(`QA-${bundleId}-`)) return true
46
+ if (doc?.screen && String(doc.screen) === bundleId) return true
47
+ const tp = doc?.target?.path
48
+ if (tp && bundlePath) {
49
+ const base = path.basename(bundlePath)
50
+ if (String(tp).includes(base)) return true
51
+ }
52
+ return false
53
+ }
54
+
55
+ function isOpenQa(doc) {
56
+ if (doc?.status === 'closed') return false
57
+ if (doc?.status === 'open') return true
58
+ if (doc?.resolution) return false
59
+ return true
60
+ }
61
+
62
+ function questionLine(doc) {
63
+ if (typeof doc?.question === 'string' && doc.question.trim()) {
64
+ return doc.question.trim().split('\n')[0]
65
+ }
66
+ const updates = Array.isArray(doc?.updates) ? doc.updates : []
67
+ const q = updates.find((u) => u?.kind === 'question')
68
+ if (q?.text) return String(q.text).trim().split('\n')[0]
69
+ const last = updates[updates.length - 1]
70
+ if (last?.text) return String(last.text).trim().split('\n')[0]
71
+ return ''
72
+ }
73
+
74
+ export function openQaCsv(hubRoot, bundleId, bundlePath = '') {
75
+ return getOpenQaDetails(hubRoot, bundleId, bundlePath).csv
21
76
  }
22
77
 
23
- export function getOpenQaDetails(hubRoot, bundleId) {
78
+ export function getOpenQaDetails(hubRoot, bundleId, bundlePath = '') {
24
79
  if (!hubRoot || !bundleId) return { csv: '', techDebt: [] }
25
- const openDir = path.join(hubRoot, 'qa', 'open')
26
- if (!existsSync(openDir)) return { csv: '', techDebt: [] }
27
- const prefix = `QA-${bundleId}-`
28
80
  const ids = []
29
81
  const techDebt = []
30
- for (const name of readdirSync(openDir)) {
31
- if (!/\.ya?ml$/i.test(name)) continue
32
- const stem = name.replace(/\.ya?ml$/i, '')
33
- if (!stem.startsWith(prefix)) continue
34
- let id = stem
35
- let kind = ''
36
- let question = ''
82
+ for (const full of listQaYamlPaths(hubRoot)) {
83
+ const stem = path.basename(full).replace(/\.ya?ml$/i, '')
84
+ let doc = {}
37
85
  try {
38
- const parsed = parse(readFileSync(path.join(openDir, name), 'utf8'))
39
- if (parsed && typeof parsed.id === 'string' && parsed.id.trim()) id = parsed.id.trim()
40
- if (parsed && typeof parsed.kind === 'string') kind = parsed.kind
41
- if (parsed && typeof parsed.question === 'string') question = parsed.question.trim().split('\n')[0]
86
+ doc = parse(readFileSync(full, 'utf8')) ?? {}
42
87
  } catch {
43
- /* filename */
88
+ continue
44
89
  }
90
+ if (!isOpenQa(doc)) continue
91
+ if (!qaAppliesToBundle(doc, stem, bundleId, bundlePath)) continue
92
+ const id = typeof doc.id === 'string' && doc.id.trim() ? doc.id.trim() : stem
93
+ const kind = typeof doc.kind === 'string' ? doc.kind : ''
94
+ const question = questionLine(doc)
45
95
  ids.push(id)
46
96
  if (kind === 'tech-debt') {
47
97
  techDebt.push({ id, status: 'open', summary: question })
@@ -57,7 +107,7 @@ const QA_FIELD = 'Q&A'
57
107
  /** Set or remove `Q&A` on a business spec object (comma-separated ids). */
58
108
  export function applyOpenQaField(spec, bundlePath, bundleId) {
59
109
  const hub = findDocsHubRoot(path.dirname(path.resolve(bundlePath)))
60
- const qa = getOpenQaDetails(hub, bundleId)
110
+ const qa = getOpenQaDetails(hub, bundleId, bundlePath)
61
111
  if (qa.csv) spec[QA_FIELD] = qa.csv
62
112
  else delete spec[QA_FIELD]
63
113
  if (qa.techDebt && qa.techDebt.length > 0) spec.pendingTechDebt = qa.techDebt
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import { checkSplitBundle, splitBundleFile } from './lib/bundle-ir.mjs'
4
5
  import { writeBundleMarkdownFile } from '../docs/lib/render-bundle-markdown.mjs'
@@ -30,7 +31,16 @@ async function main() {
30
31
  } else {
31
32
  const { irDir } = await splitBundleFile(resolved)
32
33
  const mdOut = await writeBundleMarkdownFile(resolved)
33
- const mdNote = mdOut ? ` + ${path.relative(process.cwd(), mdOut)}` : ' (no MD; missing ir/spec.yaml)'
34
+ const mdRel = mdOut ? path.relative(process.cwd(), mdOut) : null
35
+ const genDir = mdOut ? path.dirname(mdOut) : null
36
+ const extra = genDir
37
+ ? ['data-model.md', 'api.md']
38
+ .filter((name) => fs.existsSync(path.join(genDir, name)))
39
+ .map((name) => path.relative(process.cwd(), path.join(genDir, name)))
40
+ : []
41
+ const mdNote = mdRel
42
+ ? ` + ${mdRel}${extra.length ? ` + ${extra.join(' + ')}` : ''}`
43
+ : ' (no MD; missing ir/spec.yaml)'
34
44
  console.log(`spec:split: ${path.relative(process.cwd(), resolved)} → ${path.relative(process.cwd(), irDir)}/${mdNote}`)
35
45
  }
36
46
  } catch (error) {
@@ -2,33 +2,34 @@ import path from 'node:path'
2
2
 
3
3
  import Handlebars from 'handlebars'
4
4
 
5
- import { listFeatureTestcases, readTestcaseFile } from './lib/read-testcase.mjs'
5
+ import { readTestcaseFile } from './lib/read-testcase.mjs'
6
6
  import { buildApiTestcaseContext, isApiE2eTestcase } from './lib/api-plan.mjs'
7
7
  import { renderTemplate } from './lib/render.mjs'
8
8
  import { writeOutputs } from './lib/write-files.mjs'
9
9
  import { resolveHubId, resolveProjectRoot, loadTestsIndex } from './lib/resolve-hub-id.mjs'
10
- import { extractTestsDocsFlag, ENV_TESTS_DOC } from '../../shared/flowgrid-tests-docs.mjs'
10
+ import { ENV_TESTS_DOC } from '../../shared/flowgrid-tests-docs.mjs'
11
+ import {
12
+ bootstrapTestcaseRun,
13
+ applyApiOutputLayout,
14
+ } from './lib/bootstrap-context.mjs'
11
15
 
12
- const root = path.resolve(process.env.FLOWGRID_PROJECT_ROOT || process.cwd())
16
+ const boot = bootstrapTestcaseRun(process.argv.slice(2))
17
+ const root = boot.ctx.projectRoot
18
+ const codegenLayout = boot.layout
13
19
 
14
20
  Handlebars.registerHelper('json', (value) => JSON.stringify(value, null, 2))
15
21
 
16
22
  function parseArgs(argv) {
17
- const { argv: stripped, testsDocs } = extractTestsDocsFlag(argv)
18
- if (testsDocs) {
19
- process.env[ENV_TESTS_DOC] = testsDocs
20
- }
21
-
22
23
  const options = { dryRun: false, force: false, testcase: null, feature: null, id: null, all: false }
23
24
 
24
- for (let i = 0; i < stripped.length; i++) {
25
- const arg = stripped[i]
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const arg = argv[i]
26
27
  if (arg === '--dry-run' || arg === '--dry') options.dryRun = true
27
28
  else if (arg === '--force') options.force = true
28
29
  else if (arg === '--all') options.all = true
29
- else if (arg === '--testcase') options.testcase = stripped[++i]
30
- else if (arg === '--feature') options.feature = stripped[++i]
31
- else if (arg === '--id') options.id = stripped[++i]
30
+ else if (arg === '--testcase') options.testcase = argv[++i]
31
+ else if (arg === '--feature') options.feature = argv[++i]
32
+ else if (arg === '--id') options.id = argv[++i]
32
33
  else if (!arg.startsWith('-') && !options.testcase && !options.feature && !options.id && !options.all) {
33
34
  options.testcase = arg
34
35
  }
@@ -55,12 +56,14 @@ async function generateOne(testcasePath, options) {
55
56
  }
56
57
 
57
58
  const ctx = buildApiTestcaseContext(testcase, testcaseFile)
59
+ applyApiOutputLayout(ctx, codegenLayout)
58
60
  const specContent = await renderTemplate('api.spec.ts.hbs', ctx)
59
61
  const outputs = [{ relativePath: ctx.outputs.spec, content: specContent }]
60
62
 
61
63
  const { written, skipped } = await writeOutputs(root, outputs, {
62
64
  dryRun: options.dryRun,
63
65
  force: options.force,
66
+ allowedRelRoots: codegenLayout.allowedRelRoots,
64
67
  })
65
68
 
66
69
  console.log(`testcase-gen:api id=${ctx.testcaseId} module=${ctx.module}`)
@@ -76,13 +79,13 @@ async function generateOne(testcasePath, options) {
76
79
  }
77
80
 
78
81
  async function main() {
79
- const options = parseArgs(process.argv.slice(2))
82
+ const options = parseArgs(boot.argv)
80
83
  const paths = []
81
84
 
82
85
  if (options.all) {
83
- const testsRoot = process.env[ENV_TESTS_DOC]
84
- ? path.resolve(process.env[ENV_TESTS_DOC])
85
- : resolveProjectRoot(root, 'tests')
86
+ const testsRoot = boot.ctx.testsDocs
87
+ ?? (process.env[ENV_TESTS_DOC] ? path.resolve(process.env[ENV_TESTS_DOC]) : null)
88
+ ?? resolveProjectRoot(root, 'tests')
86
89
  const index = loadTestsIndex(testsRoot)
87
90
  for (const [id, rel] of Object.entries(index.codeIds || {})) {
88
91
  if (id.startsWith('TC-')) paths.push(path.resolve(testsRoot, rel))
@@ -91,12 +94,9 @@ async function main() {
91
94
  paths.push(path.resolve(root, options.testcase))
92
95
  } else if (options.feature || options.id) {
93
96
  const hubId = options.id ?? options.feature
94
- const resolved = await resolveHubId(root, hubId)
95
- if (resolved.kind === 'testcase') {
96
- paths.push(resolved.absPath)
97
- } else if (resolved.kind === 'feature' || resolved.kind === 'screen') {
98
- const list = await listFeatureTestcases(resolved.absPath, { repoRoot: root })
99
- paths.push(...list)
97
+ const resolved = resolveHubId(root, hubId, 'testcase')
98
+ if (resolved.paths?.length) {
99
+ paths.push(...resolved.paths)
100
100
  } else {
101
101
  throw new Error(`Could not resolve --id ${hubId} to testcase(s)`)
102
102
  }
@@ -9,9 +9,14 @@ import { renderTemplate } from './lib/render.mjs'
9
9
  import { resolveSemanticPlan } from './lib/semantic-plan.mjs'
10
10
  import { writeOutputs } from './lib/write-files.mjs'
11
11
  import { resolveHubId, resolveProjectRoot, loadTestsIndex } from './lib/resolve-hub-id.mjs'
12
- import { extractTestsDocsFlag, ENV_TESTS_DOC } from '../../shared/flowgrid-tests-docs.mjs'
12
+ import {
13
+ bootstrapTestcaseRun,
14
+ applyUiOutputLayout,
15
+ } from './lib/bootstrap-context.mjs'
13
16
 
14
- const root = path.resolve(process.env.FLOWGRID_PROJECT_ROOT || process.env.FLOWGRID_PROJECT_ROOT || process.cwd())
17
+ const boot = bootstrapTestcaseRun(process.argv.slice(2))
18
+ const root = boot.ctx.projectRoot
19
+ const codegenLayout = boot.layout
15
20
 
16
21
  Handlebars.registerHelper('eq', (a, b) => a === b)
17
22
  Handlebars.registerHelper('and', (a, b) => a && b)
@@ -29,21 +34,16 @@ Handlebars.registerHelper('pascalCase', (value) => {
29
34
  })
30
35
 
31
36
  function parseArgs(argv) {
32
- const { argv: stripped, testsDocs } = extractTestsDocsFlag(argv)
33
- if (testsDocs) {
34
- process.env[ENV_TESTS_DOC] = testsDocs
35
- }
36
-
37
37
  const options = { dryRun: false, force: false, testcase: null, feature: null, id: null, all: false }
38
38
 
39
- for (let i = 0; i < stripped.length; i++) {
40
- const arg = stripped[i]
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const arg = argv[i]
41
41
  if (arg === '--dry-run' || arg === '--dry') options.dryRun = true
42
42
  else if (arg === '--force') options.force = true
43
43
  else if (arg === '--all') options.all = true
44
- else if (arg === '--testcase') options.testcase = stripped[++i]
45
- else if (arg === '--feature') options.feature = stripped[++i]
46
- else if (arg === '--id') options.id = stripped[++i]
44
+ else if (arg === '--testcase') options.testcase = argv[++i]
45
+ else if (arg === '--feature') options.feature = argv[++i]
46
+ else if (arg === '--id') options.id = argv[++i]
47
47
  else if (!arg.startsWith('-') && !options.testcase && !options.feature && !options.id && !options.all) {
48
48
  options.testcase = arg
49
49
  }
@@ -65,6 +65,7 @@ async function generateOne(testcasePath, options) {
65
65
  const { testcase, spec, testcaseFile, specFile } = await readTestcaseFile(testcasePath, { repoRoot: root })
66
66
  const ctx = buildTestcaseContext(testcase, spec, testcaseFile)
67
67
  ctx.specFile = specFile
68
+ applyUiOutputLayout(ctx, codegenLayout)
68
69
 
69
70
  const semantic = await resolveSemanticPlan(root, testcase)
70
71
  ctx.useSemanticFixture = semantic.useSemanticFixture
@@ -84,7 +85,8 @@ async function generateOne(testcasePath, options) {
84
85
 
85
86
  const { written, skipped } = await writeOutputs(root, outputs, {
86
87
  dryRun: options.dryRun,
87
- force: options.force
88
+ force: options.force,
89
+ allowedRelRoots: codegenLayout.allowedRelRoots,
88
90
  })
89
91
 
90
92
  console.log(`testcase-gen: id=${ctx.testcaseId} module=${ctx.module}`)
@@ -104,12 +106,12 @@ async function generateOne(testcasePath, options) {
104
106
  }
105
107
 
106
108
  async function main() {
107
- const options = parseArgs(process.argv.slice(2))
109
+ const options = parseArgs(boot.argv)
108
110
 
109
111
  if (options.all) {
110
- const testsRoot = process.env.FLOWGRID_TESTS_DOC
111
- ? path.resolve(process.env.FLOWGRID_TESTS_DOC)
112
- : resolveProjectRoot(root, 'tests')
112
+ const testsRoot = boot.ctx.testsDocs
113
+ ?? (process.env.FLOWGRID_TESTS_DOC ? path.resolve(process.env.FLOWGRID_TESTS_DOC) : null)
114
+ ?? resolveProjectRoot(root, 'tests')
113
115
  const index = loadTestsIndex(testsRoot)
114
116
  const paths = []
115
117
  for (const [id, rel] of Object.entries(index.codeIds || {})) {
@@ -0,0 +1,44 @@
1
+ import { resolveFlowgridContext, flowgridContextToEnv } from '../../../shared/resolve-flowgrid-context.mjs';
2
+ import { codegenOutputLayout } from '../../../shared/e2e-output-layout.mjs';
3
+ import { extractTestsDocsFlag, ENV_TESTS_DOC } from '../../../shared/flowgrid-tests-docs.mjs';
4
+
5
+ /**
6
+ * Resolve project/tests/e2e pointers and apply FLOWGRID_* env before testcase engines run.
7
+ * @param {string[]} argv raw process argv slice (after command name)
8
+ */
9
+ export function bootstrapTestcaseRun(argv) {
10
+ const { argv: afterTestsFlag, testsDocs: testsFromFlag } = extractTestsDocsFlag(argv);
11
+ const ctx = resolveFlowgridContext({ argv: afterTestsFlag });
12
+ if (testsFromFlag) {
13
+ ctx.testsDocs = testsFromFlag;
14
+ }
15
+ Object.assign(process.env, flowgridContextToEnv(ctx));
16
+ if (ctx.testsDocs) {
17
+ process.env[ENV_TESTS_DOC] = ctx.testsDocs;
18
+ }
19
+ const layout = codegenOutputLayout(ctx.projectRoot, ctx.e2eRoot, {
20
+ projectType: ctx.config?.type,
21
+ });
22
+ return { ctx, layout, argv: ctx.argv };
23
+ }
24
+
25
+ /**
26
+ * @param {import('../../../shared/e2e-output-layout.mjs').codegenOutputLayout} layout
27
+ * @param {{ outputs: { pageObject: string, spec: string }, module: string, pageClassName: string, testcaseId: string }} ctx
28
+ */
29
+ export function applyUiOutputLayout(ctx, layout) {
30
+ ctx.outputs = {
31
+ pageObject: `${layout.uiPrefix}/pages/${ctx.module}/${ctx.pageClassName}.ts`,
32
+ spec: `${layout.uiPrefix}/${ctx.module}/${ctx.testcaseId}.spec.ts`,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * @param {{ outputs: { spec: string }, module: string, testcaseId: string }} ctx
38
+ * @param layout
39
+ */
40
+ export function applyApiOutputLayout(ctx, layout) {
41
+ ctx.outputs = {
42
+ spec: `${layout.apiPrefix}/${ctx.module}/${ctx.testcaseId}.api.spec.ts`,
43
+ };
44
+ }