@shanyucoder/flowgrid 0.1.5 → 0.1.8

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 (157) 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/render-api-summary-markdown.mjs +28 -0
  26. package/engines/docs/lib/render-bundle-markdown.mjs +77 -2
  27. package/engines/docs/lib/render-data-model-markdown.mjs +171 -0
  28. package/engines/docs/lib/render-design-tables.mjs +89 -7
  29. package/engines/docs/lib/render-template.mjs +6 -0
  30. package/engines/docs/render-docs.mjs +6 -2
  31. package/engines/docs/vitepress/config.ts +7 -7
  32. package/engines/docs/vitepress/surfaces-nav.mjs +45 -14
  33. package/engines/openapi/check-backend-spec.mjs +2 -2
  34. package/engines/openapi/lib/markdown-table.mjs +8 -0
  35. package/engines/openapi/lib/render-backend-spec-markdown.mjs +78 -0
  36. package/engines/registry-sync/be-capabilities-sync.mjs +118 -0
  37. package/engines/registry-sync/fe-design-sync.mjs +258 -0
  38. package/engines/registry-sync/run-registry-sync.mjs +107 -0
  39. package/engines/shared/e2e-output-layout.mjs +68 -0
  40. package/engines/shared/flowgrid-e2e-root.mjs +19 -0
  41. package/engines/shared/resolve-flowgrid-context.mjs +176 -0
  42. package/engines/spec/lib/audit-api-gaps.mjs +1 -1
  43. package/engines/spec/lib/audit-bundle-gaps.mjs +92 -3
  44. package/engines/spec/lib/audit-db-tables.mjs +529 -0
  45. package/engines/spec/lib/audit-e2e-coverage.mjs +88 -30
  46. package/engines/spec/lib/bundle-schema.mjs +4 -1
  47. package/engines/spec/split-bundle.mjs +11 -1
  48. package/engines/testcase/runners/generate-api.mjs +23 -23
  49. package/engines/testcase/runners/generate.mjs +19 -17
  50. package/engines/testcase/runners/lib/bootstrap-context.mjs +44 -0
  51. package/engines/testcase/runners/lib/write-files.mjs +37 -9
  52. package/harness/agents/antigravity/rules/antigravity-mcp.mdc +12 -0
  53. package/harness/agents/gemini/rules/gemini-mcp.mdc +11 -0
  54. package/harness/agents/gemini_antigravity/rules/antigravity-mcp.mdc +8 -6
  55. package/harness/be/skills/{grill-api → audit-api}/SKILL.md +8 -7
  56. package/harness/common/extracts/artifact-graph.md +2 -2
  57. package/harness/common/extracts/artifactgraph-phase-hooks.md +2 -2
  58. package/harness/common/extracts/docs-mark-detect.md +2 -2
  59. package/harness/common/extracts/entity-relationship.md +23 -0
  60. package/harness/common/rules/flowgrid-ux-common.mdc +3 -3
  61. package/harness/common/skills/configure-repo-maps/SKILL.md +4 -2
  62. package/harness/docs/extracts/agent-execution-protocol.md +1 -1
  63. package/harness/docs/extracts/api-codegen-readiness.md +34 -0
  64. package/harness/docs/extracts/api-codegen-tags.md +30 -0
  65. package/harness/docs/extracts/api-contract.md +43 -0
  66. package/harness/docs/extracts/api-spec-sync.md +35 -0
  67. package/harness/docs/extracts/artifactgraph-hooks-docs.md +1 -1
  68. package/harness/docs/extracts/call-external.md +16 -0
  69. package/harness/docs/extracts/common-scope.md +9 -10
  70. package/harness/docs/extracts/db-audit-wizard.md +45 -0
  71. package/harness/docs/extracts/derived-data.md +18 -0
  72. package/harness/docs/extracts/design-leaf-signoff.md +16 -0
  73. package/harness/docs/extracts/extract-registry.docs.json +9 -1
  74. package/harness/docs/extracts/spec-core.md +7 -3
  75. package/harness/docs/extracts/spec-evolution.md +21 -0
  76. package/harness/docs/extracts/spec-prd-lite.md +19 -0
  77. package/harness/docs/extracts/spec-requirement.md +6 -2
  78. package/harness/docs/extracts/spec-ssot-prep.md +25 -0
  79. package/harness/docs/extracts/tpl-module.md +12 -0
  80. package/harness/docs/extracts/verify-gate.md +33 -0
  81. package/harness/docs/extracts/wire-spec-feedback.md +31 -0
  82. package/harness/docs/rules/agent-compliance.mdc +1 -1
  83. package/harness/docs/rules/team-flow-spec.mdc +3 -4
  84. package/harness/docs/skills/adopt/SKILL.md +2 -0
  85. package/harness/docs/skills/api/SKILL.md +4 -5
  86. package/harness/docs/skills/api-spec/SKILL.md +19 -6
  87. package/harness/docs/skills/api-update/SKILL.md +3 -3
  88. package/harness/docs/skills/architecture/SKILL.md +1 -1
  89. package/harness/docs/skills/business-process/SKILL.md +2 -0
  90. package/harness/docs/skills/common/SKILL.md +2 -2
  91. package/harness/docs/skills/common-spec/SKILL.md +10 -47
  92. package/harness/docs/skills/db-erd/SKILL.md +26 -0
  93. package/harness/docs/skills/grill/SKILL.md +28 -22
  94. package/harness/docs/skills/grill-api/SKILL.md +4 -6
  95. package/harness/docs/skills/grill-api-spec/SKILL.md +44 -24
  96. package/harness/docs/skills/grill-bqa/SKILL.md +28 -13
  97. package/harness/docs/skills/grill-common-spec/SKILL.md +10 -35
  98. package/harness/docs/skills/grill-dev/SKILL.md +7 -6
  99. package/harness/docs/skills/grill-docs/SKILL.md +10 -3
  100. package/harness/docs/skills/module/SKILL.md +3 -1
  101. package/harness/docs/skills/openapi/SKILL.md +2 -1
  102. package/harness/docs/skills/overview/SKILL.md +7 -1
  103. package/harness/docs/skills/spec/SKILL.md +42 -9
  104. package/harness/docs/skills/update-spec/SKILL.md +4 -1
  105. package/harness/fe/extracts/wire-audit-loop.md +72 -0
  106. package/harness/fe/extracts/wire-phase.md +45 -0
  107. package/harness/fe/rules/platform-design-vocabulary.mdc +1 -1
  108. package/harness/fe/rules/team-flow-prototype.mdc +8 -4
  109. package/harness/fe/skills/gen-common/SKILL.md +11 -84
  110. package/harness/fe/skills/grill-prototype/SKILL.md +51 -25
  111. package/harness/fe/skills/grill-test/SKILL.md +78 -20
  112. package/harness/fe/skills/grill-wire/SKILL.md +81 -0
  113. package/harness/fe/skills/prototype/SKILL.md +3 -2
  114. package/harness/fe/skills/wire/SKILL.md +7 -2
  115. package/harness/shared/AGENTS.md +3 -3
  116. package/harness/shared/SSOT_AGENT_PROTOCOL.md +3 -3
  117. package/harness/tests/extracts/grill-api-hook.md +69 -0
  118. package/harness/tests/extracts/grill-scenario-flow.md +39 -0
  119. package/harness/tests/extracts/grill-screen-tc.md +40 -0
  120. package/harness/tests/extracts/testcase-gen-cli.md +57 -0
  121. package/harness/tests/extracts/testcase-plan.md +29 -0
  122. package/harness/tests/extracts/tests-verify-gate.md +29 -0
  123. package/harness/tests/extracts/wire-test-handoff.md +37 -0
  124. package/harness/tests/skills/grill-testcase/SKILL.md +1 -0
  125. package/harness/tests/skills/test-api/SKILL.md +14 -6
  126. package/harness/tests/skills/testcase/SKILL.md +3 -1
  127. package/harness/tests/templates/TC.example-api.yaml +7 -1
  128. package/harness/tests/templates/TC.example.yaml +4 -1
  129. package/harness/tests/templates/tpl-testcase-plan.md +75 -0
  130. package/package.json +1 -1
  131. package/stacks/fastapi.json +1 -0
  132. package/stacks/laravel.json +1 -0
  133. package/stacks/nestjs.json +72 -0
  134. package/stacks/nextjs-nest.json +1 -0
  135. package/stacks/nuxt4-nest.json +1 -0
  136. package/templates/project-skeleton/architecture/03-business-process/FLOW-template.md +2 -0
  137. package/templates/project-skeleton/overview/index.md +68 -2
  138. package/templates/project-skeleton/overview/operational-areas/_template.md +37 -0
  139. package/templates/project-skeleton/surfaces/common/data-model/index.md +10 -2
  140. package/templates/shared/api-03-mock.stub.yaml +14 -0
  141. package/templates/shared/backend-api.bundle.yaml +3 -0
  142. package/templates/shared/backend-api.yaml +4 -0
  143. package/templates/shared/be-capabilities.registry.base.json +8 -0
  144. package/templates/shared/bundle-authoring.md +39 -3
  145. package/templates/shared/default-layout.ejs +131 -18
  146. package/templates/shared/design-spec.yaml +2 -3
  147. package/templates/shared/design.registry.base.json +38 -0
  148. package/templates/shared/feature.bundle.yaml +16 -9
  149. package/templates/shared/ir/generated/spec.md +281 -0
  150. package/templates/shared/qa-item.yaml +1 -1
  151. package/templates/shared/tpl-api-contract.md +133 -0
  152. package/templates/shared/tpl-screen-data-model.md +76 -0
  153. package/templates/tests-skeleton/cases/README.md +4 -0
  154. package/templates/tests-skeleton/catalog/locale.yaml +9 -0
  155. package/templates/tests-skeleton/tpl-testcase-plan.md +9 -0
  156. package/harness/docs/skills/api-integration/SKILL.md +0 -110
  157. 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
  /**
@@ -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
+ }
@@ -1,13 +1,20 @@
1
1
  import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
+ import {
4
+ DEFAULT_E2E_ROOT_REL,
5
+ E2E_ROOT_DIR_PARTS,
6
+ API_E2E_ROOT_DIR_PARTS,
7
+ } from '../../../shared/flowgrid-e2e-root.mjs'
3
8
 
4
- export const E2E_OUTPUT_ROOT = path.join('tests', 'e2e')
9
+ export const E2E_OUTPUT_ROOT = path.join(...E2E_ROOT_DIR_PARTS)
10
+ export const API_E2E_OUTPUT_ROOT = path.join(...API_E2E_ROOT_DIR_PARTS)
11
+ export { DEFAULT_E2E_ROOT_REL }
5
12
 
6
13
  /**
7
14
  * Preflight every output path before any write or dry-run result is produced.
8
15
  *
9
16
  * A target is accepted only when, lexically (without following symlinks), it
10
- * resolves strictly beneath `<root>/tests/e2e`. Rejected: absolute paths,
17
+ * resolves strictly beneath `<root>/tests/e2e` or `<root>/tests/api-e2e`. Rejected: absolute paths,
11
18
  * `..` traversal, targets outside `tests/e2e` (including sibling prefixes
12
19
  * such as `tests/e2e-evil`), existing symlink ancestors that could redirect
13
20
  * the write, and existing targets that are not regular files.
@@ -17,9 +24,28 @@ export const E2E_OUTPUT_ROOT = path.join('tests', 'e2e')
17
24
  * @param {string} root project root
18
25
  * @param {{ relativePath: string }[]} outputs
19
26
  */
20
- export async function preflightOutputPaths(root, outputs) {
27
+ function allowedAbsRoots(resolvedRoot, allowedRelRoots) {
28
+ const rels =
29
+ allowedRelRoots?.length
30
+ ? allowedRelRoots
31
+ : [E2E_OUTPUT_ROOT, API_E2E_OUTPUT_ROOT]
32
+ return rels.map((rel) => path.join(resolvedRoot, rel.replace(/\//g, path.sep)))
33
+ }
34
+
35
+ function isUnderCodegenTree(resolvedRoot, absolutePath, allowedRelRoots) {
36
+ return allowedAbsRoots(resolvedRoot, allowedRelRoots).some(
37
+ (base) => absolutePath === base || absolutePath.startsWith(base + path.sep),
38
+ )
39
+ }
40
+
41
+ /**
42
+ * @param {string} root
43
+ * @param {{ relativePath: string }[]} outputs
44
+ * @param {{ allowedRelRoots?: string[] }} [opts]
45
+ */
46
+ export async function preflightOutputPaths(root, outputs, opts = {}) {
21
47
  const resolvedRoot = path.resolve(root)
22
- const e2eRoot = path.join(resolvedRoot, E2E_OUTPUT_ROOT)
48
+ const allowedLabel = (opts.allowedRelRoots ?? [E2E_OUTPUT_ROOT, API_E2E_OUTPUT_ROOT]).join(', ')
23
49
  const errors = []
24
50
 
25
51
  for (const { relativePath } of outputs) {
@@ -31,22 +57,22 @@ export async function preflightOutputPaths(root, outputs) {
31
57
  }
32
58
  if (path.isAbsolute(relativePath)) {
33
59
  errors.push(
34
- `output ${label}: absolute paths are not allowed — use a path relative to the project root, under ${E2E_OUTPUT_ROOT}/`,
60
+ `output ${label}: absolute paths are not allowed — use a path relative to the project root, under: ${allowedLabel}`,
35
61
  )
36
62
  continue
37
63
  }
38
64
 
39
65
  if (relativePath.split(/[\\/]+/).includes('..')) {
40
66
  errors.push(
41
- `output ${label}: ".." traversal is not allowed — outputs must stay under ${E2E_OUTPUT_ROOT}/`,
67
+ `output ${label}: ".." traversal is not allowed — outputs must stay under: ${allowedLabel}`,
42
68
  )
43
69
  continue
44
70
  }
45
71
 
46
72
  const absolutePath = path.resolve(resolvedRoot, relativePath)
47
- if (!absolutePath.startsWith(e2eRoot + path.sep)) {
73
+ if (!isUnderCodegenTree(resolvedRoot, absolutePath, opts.allowedRelRoots)) {
48
74
  errors.push(
49
- `output ${label}: resolves outside ${E2E_OUTPUT_ROOT}/ — generated files must live beneath <projectRoot>/${E2E_OUTPUT_ROOT}` +
75
+ `output ${label}: resolves outside allowed codegen trees (${allowedLabel})` +
50
76
  ' (check module/testcase ids used to build output paths)',
51
77
  )
52
78
  continue
@@ -108,7 +134,9 @@ async function findUnsafePathComponent(resolvedRoot, absolutePath) {
108
134
  * @param {{ dryRun?: boolean, force?: boolean }} options
109
135
  */
110
136
  export async function writeOutputs(root, outputs, options = {}) {
111
- await preflightOutputPaths(root, outputs)
137
+ await preflightOutputPaths(root, outputs, {
138
+ allowedRelRoots: options.allowedRelRoots,
139
+ })
112
140
 
113
141
  const written = []
114
142
  const skipped = []
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Google Antigravity IDE — FlowGrid MCP (mcp_config.json under .antigravity)
3
+ alwaysApply: false
4
+ ---
5
+ # Antigravity IDE + FlowGrid
6
+
7
+ - Workspace harness: **`.antigravity/`** (Antigravity IDE hardcodes this path; it does **not** fall back to `.agents/`).
8
+ - MCP config: **`.antigravity/mcp_config.json`** (not `mcp.json`).
9
+ - Server name: `flowgrid` → `bin/flowgrid-mcp.mjs` from the FlowGrid package used at `flowgrid init`.
10
+ - Skills: **`.antigravity/skills/<name>/SKILL.md`**; rules: **`.antigravity/rules/`.
11
+ - Re-sync: `flowgrid harness sync --agent=antigravity`.
12
+ - **Gemini CLI / VS Code** use **`.agents/`** — select **Gemini (CLI / extension)** in init (`--agent=gemini`).
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Gemini CLI / extension — FlowGrid MCP (mcp.json under .agents)
3
+ alwaysApply: false
4
+ ---
5
+ # Gemini + FlowGrid
6
+
7
+ - Workspace harness: **`.agents/`** (open Agent Skills standard; Gemini CLI, VS Code Gemini extension, tools compatible with `.agents` / `.agent`).
8
+ - MCP config: **`.agents/mcp.json`** (server `flowgrid` → `bin/flowgrid-mcp.mjs` from the FlowGrid package used at `flowgrid init`).
9
+ - Skills: **`.agents/skills/<name>/SKILL.md`**; rules: **`.agents/rules/`.
10
+ - Re-sync: `flowgrid harness sync --agent=gemini`.
11
+ - **Antigravity IDE** uses a separate folder — select **Google Antigravity IDE** in init (`--agent=antigravity` → `.antigravity/`), not this profile.
@@ -1,10 +1,12 @@
1
1
  ---
2
- description: Antigravity — FlowGrid MCP wiring (mcp_config.json)
2
+ description: DEPRECATED — use gemini or antigravity agent profiles
3
3
  alwaysApply: false
4
4
  ---
5
- # Antigravity + FlowGrid
5
+ # Deprecated combined profile
6
6
 
7
- - MCP config file: `.agents/mcp_config.json` (not `mcp.json`).
8
- - Server name: `flowgrid` → `bin/flowgrid-mcp.mjs` from the FlowGrid package used at `flowgrid init`.
9
- - Skills live under `.agents/skills/`; rules under `.agents/rules/`.
10
- - Re-sync after config changes: `flowgrid harness sync --agent=gemini_antigravity`.
7
+ `gemini_antigravity` was split:
8
+
9
+ - **Gemini CLI / VS Code** → `flowgrid harness sync --agent=gemini` → `.agents/`
10
+ - **Antigravity IDE** → `flowgrid harness sync --agent=antigravity` → `.antigravity/`
11
+
12
+ Update `.flowgrid/config.json` `agents` and re-run init or harness sync.
@@ -1,10 +1,10 @@
1
1
  ---
2
- name: grill-api
3
- description: /grill-api — audit generated FastAPI/Laravel/Nest API before integration.
2
+ name: audit-api
3
+ description: /audit-api — audit generated BE routes/code against 01-backend-spec.yaml before wire.
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
7
- # /grill-api
7
+ # /audit-api — BE implementation vs contract
8
8
 
9
9
  Run after `npm run codegen:api:dry` / `flowgrid api-gen:dry` and after implementation.
10
10
 
@@ -13,7 +13,7 @@ Run after `npm run codegen:api:dry` / `flowgrid api-gen:dry` and after implement
13
13
  ## Target / ID Resolution Rule
14
14
 
15
15
  - User prompt MAY specify a function ID, API slug, or short name (e.g. `API-AUTH-001`, `login`, `CMP-ADM-009`).
16
- - Agent MUST resolve **`…/api/<seq>/01-backend-spec.yaml`** via `FLOWGRID_DOCS_ROOT` or `flowgrid_docs_route` (screen leaf `CMP-*/<NN…>/api/<seq>/`, or `common/yaml/<slug>/`, or `integrations/…/api/<seq>/`).
16
+ - Agent MUST resolve **`…/api/<seq>/01-backend-spec.yaml`** via `FLOWGRID_DOCS_ROOT` or `flowgrid_docs_route` (screen leaf `CMP-*/<NN…>/api/<seq>/`, or `common/yaml/<slug>/`, or external-channel surface `…/api/<seq>/`).
17
17
  - **Read the entire `01-backend-spec.yaml`**. Do **not** use `ir/design.yaml` or `ir/spec.yaml` as BE contract.
18
18
  - Compare generated routes/code against that 01 file. Missing 01 → STOP, hand off to docs `/grill-api-spec`.
19
19
  - Do NOT demand full filesystem paths from the user if an ID is given.
@@ -36,6 +36,8 @@ Check:
36
36
  - Writes are transaction-safe; async retries are idempotent.
37
37
  - Generated placeholders are replaced before ship.
38
38
 
39
+ **CLI (lượng):** `flowgrid audit api` on `01`; when portal bundle exists → `flowgrid audit fe-be` on sibling `*.bundle.yaml`.
40
+
39
41
  ## Accelerators (optional)
40
42
 
41
43
  ```text
@@ -48,9 +50,8 @@ else: targeted repository search
48
50
  architecture IDs / C4 → bộ docs (FLOWGRID_DOCS_ROOT), never CodeGraph
49
51
  ```
50
52
 
51
- Missing accelerators never block the grill. Complete each scoped model or
52
- targeted-local fallback first, then follow
53
- the remainder of this skill.
53
+ Missing accelerators never block the audit. Complete each scoped model or
54
+ targeted-local fallback first, then follow the remainder of this skill.
54
55
 
55
56
  ## Translation Rule (i18n)
56
57
  Always wrap static text with native framework i18n helpers (e.g. `__('key')`, `_localizer["key"]`, `i18n.t('key')`).