@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.
- package/README.md +1 -1
- package/adapters/laravel/registries/codegen.registry.json +9 -9
- package/bin/flowgrid.mjs +326 -115
- package/bin/lib/agent-mcp.mjs +4 -0
- package/bin/lib/agent-profiles.mjs +30 -6
- package/bin/lib/audit-run.mjs +1 -1
- package/bin/lib/cli-update.mjs +48 -8
- package/bin/lib/doctor.mjs +90 -3
- package/bin/lib/harness-overlay.mjs +12 -5
- package/bin/lib/harness-sync.mjs +17 -3
- package/bin/lib/init-adapters.mjs +75 -0
- package/bin/lib/init-scaffold.mjs +9 -0
- package/bin/lib/inject-consumer-scripts.mjs +115 -0
- package/bin/lib/merge-stack-config.mjs +89 -0
- package/bin/lib/project-gitignore.mjs +1 -0
- package/bin/lib/repo-maps-align.mjs +203 -0
- package/dist/graph/config/load-config.js +7 -2
- package/dist/graph/config/load-config.js.map +1 -1
- package/dist/graph/mcp/tools.js +1 -1
- package/dist/graph/mcp/tools.js.map +1 -1
- package/dist/graph/registry/load-registries.d.ts +1 -0
- package/dist/graph/registry/load-registries.js +14 -1
- package/dist/graph/registry/load-registries.js.map +1 -1
- package/engines/cases/render-cases.mjs +67 -5
- package/engines/docs/lib/qa-item.mjs +91 -0
- package/engines/docs/lib/render-api-summary-markdown.mjs +28 -0
- package/engines/docs/lib/render-bundle-markdown.mjs +77 -2
- package/engines/docs/lib/render-data-model-markdown.mjs +171 -0
- package/engines/docs/lib/render-design-tables.mjs +89 -7
- package/engines/docs/lib/render-qa-list.mjs +123 -25
- package/engines/docs/lib/render-template.mjs +6 -0
- package/engines/docs/render-docs.mjs +6 -2
- package/engines/docs/vitepress/config.ts +7 -7
- package/engines/docs/vitepress/surfaces-nav.mjs +45 -14
- package/engines/openapi/check-backend-spec.mjs +2 -2
- package/engines/openapi/lib/markdown-table.mjs +8 -0
- package/engines/openapi/lib/render-backend-spec-markdown.mjs +78 -0
- package/engines/registry-sync/be-capabilities-sync.mjs +118 -0
- package/engines/registry-sync/fe-design-sync.mjs +258 -0
- package/engines/registry-sync/run-registry-sync.mjs +107 -0
- package/engines/shared/e2e-output-layout.mjs +68 -0
- package/engines/shared/flowgrid-e2e-root.mjs +19 -0
- package/engines/shared/resolve-flowgrid-context.mjs +176 -0
- package/engines/spec/lib/audit-api-gaps.mjs +1 -1
- package/engines/spec/lib/audit-bundle-gaps.mjs +92 -3
- package/engines/spec/lib/audit-db-tables.mjs +529 -0
- package/engines/spec/lib/audit-e2e-coverage.mjs +88 -30
- package/engines/spec/lib/bundle-schema.mjs +4 -1
- package/engines/spec/lib/open-qa.mjs +71 -21
- package/engines/spec/split-bundle.mjs +11 -1
- package/engines/testcase/runners/generate-api.mjs +23 -23
- package/engines/testcase/runners/generate.mjs +19 -17
- package/engines/testcase/runners/lib/bootstrap-context.mjs +44 -0
- package/engines/testcase/runners/lib/write-files.mjs +37 -9
- package/harness/agents/antigravity/rules/antigravity-mcp.mdc +12 -0
- package/harness/agents/gemini/rules/gemini-mcp.mdc +11 -0
- package/harness/agents/gemini_antigravity/rules/antigravity-mcp.mdc +8 -6
- package/harness/be/skills/{grill-api → audit-api}/SKILL.md +8 -7
- package/harness/common/extracts/artifact-graph.md +2 -2
- package/harness/common/extracts/artifactgraph-phase-hooks.md +2 -2
- package/harness/common/extracts/docs-mark-detect.md +2 -2
- package/harness/common/extracts/entity-relationship.md +23 -0
- package/harness/common/rules/flowgrid-ux-common.mdc +3 -3
- package/harness/common/skills/configure-repo-maps/SKILL.md +4 -2
- package/harness/docs/extracts/agent-execution-protocol.md +3 -3
- package/harness/docs/extracts/api-codegen-readiness.md +34 -0
- package/harness/docs/extracts/api-codegen-tags.md +30 -0
- package/harness/docs/extracts/api-contract.md +43 -0
- package/harness/docs/extracts/api-spec-sync.md +35 -0
- package/harness/docs/extracts/artifactgraph-hooks-docs.md +1 -1
- package/harness/docs/extracts/call-external.md +16 -0
- package/harness/docs/extracts/common-scope.md +9 -10
- package/harness/docs/extracts/db-audit-wizard.md +45 -0
- package/harness/docs/extracts/derived-data.md +18 -0
- package/harness/docs/extracts/design-leaf-signoff.md +16 -0
- package/harness/docs/extracts/extract-registry.docs.json +11 -2
- package/harness/docs/extracts/qa-inbox.md +19 -10
- package/harness/docs/extracts/qa-team.md +32 -0
- package/harness/docs/extracts/spec-core.md +7 -3
- package/harness/docs/extracts/spec-evolution.md +21 -0
- package/harness/docs/extracts/spec-prd-lite.md +19 -0
- package/harness/docs/extracts/spec-requirement.md +6 -2
- package/harness/docs/extracts/spec-ssot-prep.md +25 -0
- package/harness/docs/extracts/tpl-module.md +12 -0
- package/harness/docs/extracts/verify-gate.md +33 -0
- package/harness/docs/extracts/wire-spec-feedback.md +31 -0
- package/harness/docs/rules/agent-compliance.mdc +1 -1
- package/harness/docs/rules/team-flow-spec.mdc +3 -4
- package/harness/docs/schemas/flowgrid-docs/qa-item.schema.json +65 -0
- package/harness/docs/skills/adopt/SKILL.md +2 -0
- package/harness/docs/skills/api/SKILL.md +4 -5
- package/harness/docs/skills/api-spec/SKILL.md +19 -6
- package/harness/docs/skills/api-update/SKILL.md +4 -4
- package/harness/docs/skills/architecture/SKILL.md +1 -1
- package/harness/docs/skills/business-process/SKILL.md +2 -0
- package/harness/docs/skills/common/SKILL.md +2 -2
- package/harness/docs/skills/common-spec/SKILL.md +10 -47
- package/harness/docs/skills/db-erd/SKILL.md +26 -0
- package/harness/docs/skills/grill/SKILL.md +28 -22
- package/harness/docs/skills/grill-api/SKILL.md +4 -6
- package/harness/docs/skills/grill-api-spec/SKILL.md +44 -24
- package/harness/docs/skills/grill-bqa/SKILL.md +28 -13
- package/harness/docs/skills/grill-common-spec/SKILL.md +10 -35
- package/harness/docs/skills/grill-dev/SKILL.md +8 -7
- package/harness/docs/skills/grill-docs/SKILL.md +11 -4
- package/harness/docs/skills/module/SKILL.md +3 -1
- package/harness/docs/skills/openapi/SKILL.md +2 -1
- package/harness/docs/skills/overview/SKILL.md +7 -1
- package/harness/docs/skills/qa-resolve/SKILL.md +13 -12
- package/harness/docs/skills/qa-review/SKILL.md +45 -0
- package/harness/docs/skills/spec/SKILL.md +43 -10
- package/harness/docs/skills/update-spec/SKILL.md +5 -2
- package/harness/fe/extracts/wire-audit-loop.md +72 -0
- package/harness/fe/extracts/wire-phase.md +45 -0
- package/harness/fe/rules/platform-design-vocabulary.mdc +1 -1
- package/harness/fe/rules/team-flow-prototype.mdc +8 -4
- package/harness/fe/skills/gen-common/SKILL.md +11 -84
- package/harness/fe/skills/grill-prototype/SKILL.md +51 -25
- package/harness/fe/skills/grill-test/SKILL.md +78 -20
- package/harness/fe/skills/grill-wire/SKILL.md +81 -0
- package/harness/fe/skills/prototype/SKILL.md +3 -2
- package/harness/fe/skills/wire/SKILL.md +8 -3
- package/harness/shared/AGENTS.md +3 -3
- package/harness/shared/SSOT_AGENT_PROTOCOL.md +3 -3
- package/harness/tests/extracts/grill-api-hook.md +69 -0
- package/harness/tests/extracts/grill-scenario-flow.md +39 -0
- package/harness/tests/extracts/grill-screen-tc.md +40 -0
- package/harness/tests/extracts/testcase-gen-cli.md +57 -0
- package/harness/tests/extracts/testcase-plan.md +29 -0
- package/harness/tests/extracts/tests-verify-gate.md +29 -0
- package/harness/tests/extracts/wire-test-handoff.md +37 -0
- package/harness/tests/skills/grill-testcase/SKILL.md +1 -0
- package/harness/tests/skills/test-api/SKILL.md +14 -6
- package/harness/tests/skills/testcase/SKILL.md +3 -1
- package/harness/tests/templates/TC.example-api.yaml +7 -1
- package/harness/tests/templates/TC.example.yaml +4 -1
- package/harness/tests/templates/tpl-testcase-plan.md +75 -0
- package/package.json +1 -1
- package/stacks/fastapi.json +1 -0
- package/stacks/laravel.json +1 -0
- package/stacks/nestjs.json +72 -0
- package/stacks/nextjs-nest.json +1 -0
- package/stacks/nuxt4-nest.json +1 -0
- package/templates/project-skeleton/architecture/03-business-process/FLOW-template.md +2 -0
- package/templates/project-skeleton/overview/index.md +68 -2
- package/templates/project-skeleton/overview/operational-areas/_template.md +37 -0
- package/templates/project-skeleton/qa/README.md +4 -8
- package/templates/project-skeleton/surfaces/common/data-model/index.md +10 -2
- package/templates/schemas/qa-item.schema.json +98 -0
- package/templates/shared/api-03-mock.stub.yaml +14 -0
- package/templates/shared/backend-api.bundle.yaml +3 -0
- package/templates/shared/backend-api.yaml +4 -0
- package/templates/shared/be-capabilities.registry.base.json +8 -0
- package/templates/shared/bundle-authoring.md +44 -7
- package/templates/shared/default-layout.ejs +131 -18
- package/templates/shared/design-spec.yaml +2 -3
- package/templates/shared/design.registry.base.json +38 -0
- package/templates/shared/feature.bundle.yaml +16 -9
- package/templates/shared/ir/generated/spec.md +281 -0
- package/templates/shared/ir-spec.yaml +1 -1
- package/templates/shared/qa-authoring.md +78 -0
- package/templates/shared/qa-item.yaml +35 -14
- package/templates/shared/tpl-api-contract.md +133 -0
- package/templates/shared/tpl-screen-data-model.md +76 -0
- package/templates/tests-skeleton/cases/README.md +4 -0
- package/templates/tests-skeleton/catalog/locale.yaml +9 -0
- package/templates/tests-skeleton/tpl-testcase-plan.md +9 -0
- package/harness/docs/skills/api-integration/SKILL.md +0 -110
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
272
|
-
let
|
|
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 === '--
|
|
281
|
-
|
|
282
|
-
} else if (a === '--
|
|
283
|
-
|
|
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(
|
|
332
|
+
tcFiles.push(path.resolve(ctx.projectRoot, a));
|
|
295
333
|
}
|
|
296
334
|
}
|
|
297
335
|
|
|
298
|
-
return {
|
|
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:
|
|
305
|
-
' flowgrid audit e2e
|
|
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
|
-
' --
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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, '
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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 <
|
|
25
|
-
const arg =
|
|
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 =
|
|
30
|
-
else if (arg === '--feature') options.feature =
|
|
31
|
-
else if (arg === '--id') options.id =
|
|
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(
|
|
82
|
+
const options = parseArgs(boot.argv)
|
|
80
83
|
const paths = []
|
|
81
84
|
|
|
82
85
|
if (options.all) {
|
|
83
|
-
const testsRoot =
|
|
84
|
-
? path.resolve(process.env[ENV_TESTS_DOC])
|
|
85
|
-
|
|
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 =
|
|
95
|
-
if (resolved.
|
|
96
|
-
paths.push(resolved.
|
|
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 {
|
|
12
|
+
import {
|
|
13
|
+
bootstrapTestcaseRun,
|
|
14
|
+
applyUiOutputLayout,
|
|
15
|
+
} from './lib/bootstrap-context.mjs'
|
|
13
16
|
|
|
14
|
-
const
|
|
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 <
|
|
40
|
-
const arg =
|
|
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 =
|
|
45
|
-
else if (arg === '--feature') options.feature =
|
|
46
|
-
else if (arg === '--id') options.id =
|
|
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(
|
|
109
|
+
const options = parseArgs(boot.argv)
|
|
108
110
|
|
|
109
111
|
if (options.all) {
|
|
110
|
-
const testsRoot =
|
|
111
|
-
? path.resolve(process.env.FLOWGRID_TESTS_DOC)
|
|
112
|
-
|
|
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
|
+
}
|