@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
|
@@ -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
|
|
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
|
|
17
|
-
* UX affordance
|
|
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));
|