@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
@@ -0,0 +1,107 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { syncFeDesignRegistry } from './fe-design-sync.mjs';
5
+ import { syncBeCapabilitiesRegistry } from './be-capabilities-sync.mjs';
6
+
7
+ function packageRootFromMeta(metaUrl) {
8
+ return path.resolve(path.dirname(metaUrl), '../..');
9
+ }
10
+
11
+ async function rebuildArtifactGraphIndex(projectRoot) {
12
+ try {
13
+ const { loadEffectiveRepoConfig } = await import('../../dist/graph/config/load-config.js');
14
+ const { IndexStore } = await import('../../dist/graph/db/index-store.js');
15
+ const { loadRegistries, indexRegistries } = await import('../../dist/graph/registry/load-registries.js');
16
+ const { indexLexicons } = await import('../../dist/graph/lexicon/load-lexicon.js');
17
+ const cfg = loadEffectiveRepoConfig(projectRoot);
18
+ const store = new IndexStore(projectRoot);
19
+ store.transaction(() => {
20
+ const loaded = loadRegistries(projectRoot, cfg);
21
+ indexRegistries(store, loaded, projectRoot, cfg);
22
+ indexLexicons(store, projectRoot, cfg);
23
+ });
24
+ store.close();
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {string[]} argv
33
+ * @param {{ packageRoot?: string, projectRoot?: string, config?: object }} ctx
34
+ */
35
+ export async function runRegistrySync(argv = [], ctx = {}) {
36
+ const projectRoot = ctx.projectRoot ?? process.cwd();
37
+ const packageRoot = ctx.packageRoot ?? packageRootFromMeta(import.meta.url);
38
+ const dryRun = argv.includes('--dry-run');
39
+ const feOnly = argv.includes('--fe');
40
+ const beOnly = argv.includes('--be');
41
+
42
+ const configPath = path.join(projectRoot, '.flowgrid/config.json');
43
+ let config = ctx.config ?? {};
44
+ if (!ctx.config && fs.existsSync(configPath)) {
45
+ try {
46
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
47
+ } catch {
48
+ config = {};
49
+ }
50
+ }
51
+
52
+ const feAdapter = config.frontend?.adapter;
53
+ const beAdapter = config.backend?.adapter;
54
+ const type = config.type ?? '';
55
+ const baseProfile = config.baseProfile ?? 'standard';
56
+
57
+ const runFe =
58
+ !beOnly &&
59
+ feAdapter &&
60
+ ['nuxt4', 'nextjs'].includes(feAdapter) &&
61
+ (feOnly || ['Frontend', 'Fullstack'].includes(type) || argv.includes('--fe'));
62
+ const runBe =
63
+ !feOnly &&
64
+ beAdapter &&
65
+ (beOnly || ['Backend', 'Fullstack'].includes(type) || argv.includes('--be'));
66
+
67
+ if (baseProfile === 'custom' && !argv.includes('--force')) {
68
+ console.log(
69
+ pc.yellow(
70
+ '[registry:sync] baseProfile=custom — skip FE scan (use build-template-code). Pass --force to scan anyway.',
71
+ ),
72
+ );
73
+ if (!runBe) return { code: 0 };
74
+ }
75
+
76
+ console.log(pc.cyan('\n=== flowgrid registry:sync ==='));
77
+ console.log(` root: ${projectRoot}`);
78
+ if (dryRun) console.log(pc.magenta(' mode: dry-run'));
79
+
80
+ if (runFe) {
81
+ const fe = syncFeDesignRegistry(projectRoot, feAdapter, packageRoot, { dryRun });
82
+ console.log(pc.green('✔ FE design registry'));
83
+ console.log(
84
+ ` ${fe.report.registryPath}: ui=${fe.report.uiComponents} composables=${fe.report.composables} helpers=${fe.report.helpers} widgets=${fe.report.fieldWidgets} shells=${fe.report.shellsImplemented}`,
85
+ );
86
+ } else if (!beOnly) {
87
+ console.log(pc.gray(' (skip FE — no nuxt4/nextjs frontend in config)'));
88
+ }
89
+
90
+ if (runBe) {
91
+ const be = syncBeCapabilitiesRegistry(projectRoot, beAdapter, packageRoot, { dryRun });
92
+ console.log(pc.green('✔ BE capabilities registry'));
93
+ console.log(
94
+ ` ${be.report.registryPath}: entries=${be.report.capabilityKeys} middleware=${be.report.middleware}`,
95
+ );
96
+ } else if (!feOnly) {
97
+ console.log(pc.gray(' (skip BE — no backend adapter in config)'));
98
+ }
99
+
100
+ if (!dryRun && (runFe || runBe)) {
101
+ const ok = await rebuildArtifactGraphIndex(projectRoot);
102
+ if (ok) console.log(pc.gray(' + ArtifactGraph index rebuilt (.flowgrid/index.db)'));
103
+ }
104
+
105
+ console.log('');
106
+ return { code: 0 };
107
+ }
@@ -0,0 +1,68 @@
1
+ import path from 'node:path';
2
+
3
+ import {
4
+ API_E2E_ROOT_DIR_PARTS,
5
+ DEFAULT_E2E_ROOT_BACKEND_REL,
6
+ DEFAULT_E2E_ROOT_REL,
7
+ E2E_ROOT_DIR_PARTS,
8
+ } from './flowgrid-e2e-root.mjs';
9
+
10
+ /**
11
+ * Relative codegen + preflight trees from project root, given resolved automation root.
12
+ * @param {string} projectRoot
13
+ * @param {string | null | undefined} e2eRootAbs
14
+ * @param {{ projectType?: string }} [opts]
15
+ */
16
+ export function codegenOutputLayout(projectRoot, e2eRootAbs, opts = {}) {
17
+ const resolvedProject = path.resolve(projectRoot);
18
+ let e2eAbs = e2eRootAbs ? path.resolve(e2eRootAbs) : null;
19
+ if (!e2eAbs) {
20
+ const rel =
21
+ opts.projectType === 'Backend' ? DEFAULT_E2E_ROOT_BACKEND_REL : DEFAULT_E2E_ROOT_REL;
22
+ e2eAbs = path.join(resolvedProject, rel);
23
+ }
24
+
25
+ const uiPrefix = path.relative(resolvedProject, e2eAbs).split(path.sep).join('/');
26
+ const defaultApi = path.join(...API_E2E_ROOT_DIR_PARTS);
27
+ let apiPrefix = defaultApi;
28
+
29
+ const uiNorm = uiPrefix.replace(/\\/g, '/');
30
+ if (uiNorm === 'tests') {
31
+ apiPrefix = defaultApi;
32
+ } else if (uiNorm === 'tests/e2e' || uiNorm.endsWith('/e2e')) {
33
+ const parent = path.dirname(uiNorm);
34
+ apiPrefix = parent === '.' ? 'api-e2e' : `${parent}/api-e2e`;
35
+ }
36
+
37
+ const allowedRelRoots = [
38
+ path.join(...E2E_ROOT_DIR_PARTS),
39
+ path.join(...API_E2E_ROOT_DIR_PARTS),
40
+ uiPrefix,
41
+ apiPrefix,
42
+ ]
43
+ .map((p) => p.replace(/\\/g, '/'))
44
+ .filter((p, i, arr) => p && arr.indexOf(p) === i);
45
+
46
+ return {
47
+ projectRoot: resolvedProject,
48
+ e2eRootAbs: e2eAbs,
49
+ uiPrefix,
50
+ apiPrefix,
51
+ allowedRelRoots,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Directories to scan for Playwright specs (UI + api-e2e).
57
+ * @param {string} projectRoot
58
+ * @param {string | null | undefined} e2eRootAbs
59
+ */
60
+ export function playwrightScanRoots(projectRoot, e2eRootAbs) {
61
+ const layout = codegenOutputLayout(projectRoot, e2eRootAbs);
62
+ const roots = [layout.e2eRootAbs];
63
+ const apiAbs = path.join(layout.projectRoot, layout.apiPrefix);
64
+ if (apiAbs !== layout.e2eRootAbs) {
65
+ roots.push(apiAbs);
66
+ }
67
+ return roots;
68
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Playwright automation roots (relative to code repo).
3
+ * testcase:gen (UI) → tests/e2e; testcase:gen:api → tests/api-e2e.
4
+ * audit e2e --e2e-root scans recursively for *.spec.ts / *.api.spec.ts.
5
+ * Distinct from tests-docs hub (YAML plan) and playwright.config (often repo root).
6
+ */
7
+ export const E2E_ROOT_DIR_PARTS = ['tests', 'e2e'];
8
+ export const API_E2E_ROOT_DIR_PARTS = ['tests', 'api-e2e'];
9
+
10
+ export const DEFAULT_E2E_ROOT_REL = 'tests/e2e';
11
+ /** Backend / API-hook lane: scan parent so api-e2e + optional UI specs both count. */
12
+ export const DEFAULT_E2E_ROOT_BACKEND_REL = 'tests';
13
+
14
+ /**
15
+ * @param {'Frontend' | 'Backend' | 'Fullstack' | string} projectType
16
+ */
17
+ export function defaultE2eRootRel(projectType) {
18
+ return projectType === 'Backend' ? DEFAULT_E2E_ROOT_BACKEND_REL : DEFAULT_E2E_ROOT_REL;
19
+ }
@@ -0,0 +1,176 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { ENV_TESTS_DOC, resolveTestsDocsRoot } from './flowgrid-tests-docs.mjs';
5
+
6
+ export const ENV_DOCS_ROOT = 'FLOWGRID_DOCS_ROOT';
7
+ export const ENV_PROJECT_ROOT = 'FLOWGRID_PROJECT_ROOT';
8
+ export const ENV_E2E_ROOT = 'FLOWGRID_E2E_ROOT';
9
+
10
+ /**
11
+ * @param {string} [cwd]
12
+ * @returns {object | null}
13
+ */
14
+ export function loadFlowgridConfig(cwd = process.cwd()) {
15
+ const configPath = path.join(path.resolve(cwd), '.flowgrid', 'config.json');
16
+ if (!fs.existsSync(configPath)) return null;
17
+ try {
18
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * @param {string | null | undefined} relOrAbs
26
+ * @param {string} baseDir
27
+ * @returns {string | null}
28
+ */
29
+ function resolveHubPath(relOrAbs, baseDir) {
30
+ if (!relOrAbs || typeof relOrAbs !== 'string' || !relOrAbs.trim()) return null;
31
+ const trimmed = relOrAbs.trim();
32
+ if (path.isAbsolute(trimmed)) return path.resolve(trimmed);
33
+ return path.resolve(baseDir, trimmed);
34
+ }
35
+
36
+ /**
37
+ * Docs hub pointer from config (FE lane first, then BE).
38
+ * @param {object | null | undefined} config
39
+ */
40
+ export function docsRootFromConfig(config) {
41
+ if (!config || config.type === 'Document') return null;
42
+ const rel = config.frontend?.docsRoot ?? config.backend?.docsRoot ?? null;
43
+ return rel || null;
44
+ }
45
+
46
+ /**
47
+ * Tests-docs hub pointer from config.
48
+ * @param {object | null | undefined} config
49
+ */
50
+ export function testsRootFromConfig(config) {
51
+ if (!config || config.type === 'Test') return null;
52
+ const rel = config.frontend?.testsRoot ?? config.backend?.testsRoot ?? null;
53
+ return rel || null;
54
+ }
55
+
56
+ /**
57
+ * E2e automation root from config.
58
+ * @param {object | null | undefined} config
59
+ */
60
+ export function e2eRootFromConfig(config) {
61
+ if (!config) return null;
62
+ const rel = config.frontend?.e2eRoot ?? config.backend?.e2eRoot ?? null;
63
+ return rel || null;
64
+ }
65
+
66
+ /**
67
+ * Strip hub path flags from argv for downstream engines.
68
+ * @param {string[]} argv
69
+ * @returns {{ argv: string[]; docsRoot: string | null; testsDocs: string | null; e2eRoot: string | null; projectRoot: string | null }}
70
+ */
71
+ export function extractFlowgridPathFlags(argv, baseDir = process.cwd()) {
72
+ const out = [];
73
+ let docsRoot = null;
74
+ let testsDocs = null;
75
+ let e2eRoot = null;
76
+ let projectRoot = null;
77
+ const cwd = path.resolve(baseDir);
78
+
79
+ for (let i = 0; i < argv.length; i++) {
80
+ const arg = argv[i];
81
+ if (arg === '--docs-root' && argv[i + 1]) {
82
+ docsRoot = path.resolve(cwd, argv[++i]);
83
+ continue;
84
+ }
85
+ if (arg === '--tests-docs' && argv[i + 1]) {
86
+ testsDocs = path.resolve(cwd, argv[++i]);
87
+ continue;
88
+ }
89
+ if (arg === '--e2e-root' && argv[i + 1]) {
90
+ e2eRoot = path.resolve(cwd, argv[++i]);
91
+ continue;
92
+ }
93
+ if (arg === '--project-root' && argv[i + 1]) {
94
+ projectRoot = path.resolve(cwd, argv[++i]);
95
+ continue;
96
+ }
97
+ out.push(arg);
98
+ }
99
+
100
+ return { argv: out, docsRoot, testsDocs, e2eRoot, projectRoot };
101
+ }
102
+
103
+ /**
104
+ * Resolve docs hub absolute path.
105
+ * Priority: CLI --docs-root → FLOWGRID_DOCS_ROOT → config frontend|backend.docsRoot → Document type = cwd.
106
+ *
107
+ * @param {{ cwd?: string; config?: object | null; argv?: string[]; env?: NodeJS.ProcessEnv }} [opts]
108
+ * @returns {{ docsRoot: string | null; argv: string[]; projectRoot: string }}
109
+ */
110
+ export function resolveFlowgridContext(opts = {}) {
111
+ const cwd = path.resolve(opts.cwd ?? process.cwd());
112
+ const env = opts.env ?? process.env;
113
+ const config = opts.config !== undefined ? opts.config : loadFlowgridConfig(cwd);
114
+ const extracted = extractFlowgridPathFlags(opts.argv ?? [], cwd);
115
+
116
+ const projectRoot = extracted.projectRoot
117
+ ?? (env[ENV_PROJECT_ROOT] ? path.resolve(env[ENV_PROJECT_ROOT]) : cwd);
118
+
119
+ let docsRoot = extracted.docsRoot;
120
+ if (!docsRoot && env[ENV_DOCS_ROOT]) {
121
+ docsRoot = path.resolve(env[ENV_DOCS_ROOT]);
122
+ }
123
+ if (!docsRoot && config?.type === 'Document') {
124
+ docsRoot = projectRoot;
125
+ }
126
+ if (!docsRoot) {
127
+ const rel = docsRootFromConfig(config);
128
+ docsRoot = resolveHubPath(rel, projectRoot);
129
+ }
130
+
131
+ let testsDocs = extracted.testsDocs ?? resolveTestsDocsRoot(null);
132
+ if (!testsDocs) {
133
+ const rel = testsRootFromConfig(config);
134
+ testsDocs = resolveHubPath(rel, projectRoot);
135
+ }
136
+
137
+ let e2eRoot = extracted.e2eRoot;
138
+ if (!e2eRoot && env[ENV_E2E_ROOT]) {
139
+ e2eRoot = path.resolve(env[ENV_E2E_ROOT]);
140
+ }
141
+ if (!e2eRoot) {
142
+ const rel = e2eRootFromConfig(config);
143
+ e2eRoot = resolveHubPath(rel, projectRoot);
144
+ }
145
+
146
+ return {
147
+ projectRoot,
148
+ docsRoot,
149
+ testsDocs,
150
+ e2eRoot,
151
+ argv: extracted.argv,
152
+ config,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * @param {{ cwd?: string; config?: object | null; argv?: string[]; env?: NodeJS.ProcessEnv }} [opts]
158
+ * @returns {string | null}
159
+ */
160
+ export function resolveDocsRoot(opts = {}) {
161
+ return resolveFlowgridContext(opts).docsRoot;
162
+ }
163
+
164
+ /**
165
+ * Apply resolved hub env for child processes (only sets when value known).
166
+ * @param {ReturnType<typeof resolveFlowgridContext>} ctx
167
+ * @param {NodeJS.ProcessEnv} [base]
168
+ */
169
+ export function flowgridContextToEnv(ctx, base = {}) {
170
+ const env = { ...base };
171
+ if (ctx.docsRoot) env[ENV_DOCS_ROOT] = ctx.docsRoot;
172
+ if (ctx.testsDocs) env[ENV_TESTS_DOC] = ctx.testsDocs;
173
+ if (ctx.e2eRoot) env[ENV_E2E_ROOT] = ctx.e2eRoot;
174
+ env[ENV_PROJECT_ROOT] = ctx.projectRoot;
175
+ return env;
176
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * audit-api-gaps.mjs
5
- * Zero-dependency static audit script for FlowGrid API Contracts (backend-api.bundle.yaml, backend-api-integration.yaml).
5
+ * Zero-dependency static audit for API contracts — target SSOT: `01-backend-spec.yaml` (legacy bundle-with-spec.api still parseable).
6
6
  * Ensures 100% adherence to API payload rules, SLA, async events, and resilience policies.
7
7
  */
8
8
 
@@ -13,13 +13,15 @@
13
13
  * pageType: list | create | detail | admin-crud | auth | change-password | public | not-found | error
14
14
  * If --type is omitted, script auto-detects from codegen.profile or design.shell.tag.
15
15
  *
16
- * Output: JSON with gaps[] (missing required) + confirms[] (optional, agent must ask member).
17
- * UX affordance checks (flowgrid-ux-common) merge in with category "ux" and codes UX_* / CONFIRM_UX_*.
16
+ * Output: JSON with gaps[] (missing required) + confirms[] (optional wizard) + warnings[] (quality — never blocks split).
17
+ * UX affordance: category "ux" (UX_* / CONFIRM_UX_*). DB tables: category "db" (CONFIRM_DB_*).
18
18
  */
19
19
 
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
+ import { parse } from 'yaml';
22
23
  import { auditUxAffordance } from './audit-ux-affordance.mjs';
24
+ import { auditDbTables, resolveBackendSpecForBundle } from './audit-db-tables.mjs';
23
25
 
24
26
  // ---------------------------------------------------------------------------
25
27
  // Helpers
@@ -111,6 +113,7 @@ function hasMutationActions(rawText) {
111
113
  function auditBundleContent(rawText, filePath, pageType) {
112
114
  const gaps = [];
113
115
  const confirms = [];
116
+ const warnings = [];
114
117
 
115
118
  function addGap(code, severity, fieldPath, message, suggestedFix) {
116
119
  gaps.push({ code, severity, type: 'gap', path: fieldPath, message, suggestedFix });
@@ -127,6 +130,17 @@ function auditBundleContent(rawText, filePath, pageType) {
127
130
  });
128
131
  }
129
132
 
133
+ function addWarning(code, fieldPath, message, suggestedFix) {
134
+ warnings.push({
135
+ code,
136
+ type: 'warning',
137
+ category: 'quality',
138
+ path: fieldPath,
139
+ message,
140
+ suggestedFix
141
+ });
142
+ }
143
+
130
144
  // =========================================================================
131
145
  // ALL PAGES — Required fields
132
146
  // =========================================================================
@@ -528,6 +542,51 @@ function auditBundleContent(rawText, filePath, pageType) {
528
542
  const uxGaps = gaps.filter((g) => g.category === 'ux').length;
529
543
  const uxConfirms = confirms.filter((c) => c.category === 'ux').length;
530
544
 
545
+ // =========================================================================
546
+ // Quality warnings (non-blocking — does not affect totalGaps for structure)
547
+ // =========================================================================
548
+
549
+ if (!has(rawText, 'successMetrics:')) {
550
+ addWarning(
551
+ 'WARN_NO_SUCCESS_METRICS',
552
+ 'successMetrics',
553
+ 'successMetrics not declared — add when PO has measurable or qualitative targets.',
554
+ 'Declare successMetrics: | with 1–3 bullets (see bundle-authoring.md).'
555
+ );
556
+ }
557
+
558
+ if (!has(rawText, 'nonGoals:')) {
559
+ addWarning(
560
+ 'WARN_NO_NON_GOALS',
561
+ 'nonGoals',
562
+ 'nonGoals not declared — clarify out-of-scope for this screen/phase.',
563
+ 'Declare nonGoals: | with explicit non-scope bullets.'
564
+ );
565
+ }
566
+
567
+ const placeholderCue = /\[[^\]]{8,}\]/;
568
+ if (has(rawText, 'summary:') && placeholderCue.test(rawText)) {
569
+ addWarning(
570
+ 'WARN_SUMMARY_PLACEHOLDER',
571
+ 'summary',
572
+ 'Summary or related prose still contains bracket placeholders `[...]`.',
573
+ 'Replace template brackets with real Vietnamese business copy before stakeholder sign-off.'
574
+ );
575
+ }
576
+
577
+ if (
578
+ has(rawText, 'messages:') &&
579
+ has(rawText, 'required:') &&
580
+ !hasAny(rawText, ['Trường này', 'bắt buộc', 'không được để trống', 'Vui lòng'])
581
+ ) {
582
+ addWarning(
583
+ 'WARN_VALIDATION_MSG_GENERIC',
584
+ 'design.sections',
585
+ 'Validation messages may still use generic English — prefer explicit Vietnamese copy per field.',
586
+ 'Set messages.* on each validated field (see bundle-authoring.md).'
587
+ );
588
+ }
589
+
531
590
  // =========================================================================
532
591
  // Result
533
592
  // =========================================================================
@@ -537,13 +596,15 @@ function auditBundleContent(rawText, filePath, pageType) {
537
596
  detectedType: pageType,
538
597
  totalGaps: gaps.length,
539
598
  totalConfirms: confirms.length,
599
+ totalWarnings: warnings.length,
540
600
  uxAffordanceGaps: uxGaps,
541
601
  uxAffordanceConfirms: uxConfirms,
542
602
  criticalGaps: gaps.filter(g => g.severity === 'critical').length,
543
603
  warningGaps: gaps.filter(g => g.severity === 'warning').length,
544
604
  infoGaps: gaps.filter(g => g.severity === 'info').length,
545
605
  gaps,
546
- confirms
606
+ confirms,
607
+ warnings
547
608
  };
548
609
  }
549
610
 
@@ -579,4 +640,32 @@ if (!fs.existsSync(targetFile)) {
579
640
  const rawText = fs.readFileSync(targetFile, 'utf8');
580
641
  const pageType = typeArg || detectPageType(rawText);
581
642
  const report = auditBundleContent(rawText, targetFile, pageType);
643
+
644
+ try {
645
+ const bundle = parse(rawText);
646
+ const backendResolved = resolveBackendSpecForBundle(targetFile, null);
647
+ const backendText =
648
+ backendResolved && fs.existsSync(backendResolved)
649
+ ? fs.readFileSync(backendResolved, 'utf8')
650
+ : '';
651
+ const dbReport = auditDbTables({
652
+ bundle,
653
+ backendText,
654
+ backendSpecFound: !!(backendResolved && fs.existsSync(backendResolved)),
655
+ filePath: targetFile,
656
+ });
657
+ report.confirms.push(...dbReport.confirms);
658
+ report.warnings.push(...dbReport.warnings);
659
+ report.totalConfirms = report.confirms.length;
660
+ report.totalWarnings = report.warnings.length;
661
+ report.dbTables = {
662
+ tables: dbReport.tables,
663
+ bindingCount: dbReport.bindings?.length ?? 0,
664
+ dbConfirms: dbReport.confirms?.length ?? 0,
665
+ backendSpec: backendResolved ? path.relative(process.cwd(), backendResolved) : null,
666
+ };
667
+ } catch {
668
+ // YAML parse errors are handled elsewhere; skip DB pass
669
+ }
670
+
582
671
  console.log(JSON.stringify(report, null, 2));