@shanyucoder/flowgrid 0.1.4 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/adapters/laravel/registries/codegen.registry.json +9 -9
- package/bin/flowgrid.mjs +353 -116
- 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/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-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/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 +1 -1
- 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 +9 -1
- 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/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 +3 -3
- 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 +7 -6
- package/harness/docs/skills/grill-docs/SKILL.md +10 -3
- 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/spec/SKILL.md +42 -9
- package/harness/docs/skills/update-spec/SKILL.md +4 -1
- 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 +7 -2
- 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/surfaces/common/data-model/index.md +10 -2
- 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 +39 -3
- 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/qa-item.yaml +1 -1
- 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,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* audit-db-tables.mjs — design db bindings vs entities / optional 01-backend-spec.
|
|
5
|
+
* Emits confirms[] (CONFIRM_DB_*) for /spec and /grill-* AskQuestion wizard; optional warnings[] for agent-only hints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { parse } from 'yaml';
|
|
12
|
+
|
|
13
|
+
function walkDesignNodes(nodes, visitor, ctx = {}) {
|
|
14
|
+
for (const node of nodes || []) {
|
|
15
|
+
visitor(node, ctx);
|
|
16
|
+
walkDesignNodes(node.items, visitor, { ...ctx, sectionId: node.id ?? ctx.sectionId });
|
|
17
|
+
walkDesignNodes(node.sections, visitor, { ...ctx, sectionId: node.id ?? ctx.sectionId });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Record<string, unknown>} bundle Parsed bundle YAML
|
|
23
|
+
* @returns {{ bindings: Array<Record<string, unknown>>, tables: Set<string> }}
|
|
24
|
+
*/
|
|
25
|
+
export function collectDesignDbBindings(bundle) {
|
|
26
|
+
const bindings = [];
|
|
27
|
+
const tables = new Set();
|
|
28
|
+
const design = bundle.design ?? {};
|
|
29
|
+
const tree = design.sections?.length ? design.sections : design.zones ?? [];
|
|
30
|
+
|
|
31
|
+
walkDesignNodes(tree, (node, ctx) => {
|
|
32
|
+
const db = node.db;
|
|
33
|
+
const bindField = node.bind?.field != null ? String(node.bind.field).trim() : '';
|
|
34
|
+
const hasDb = db && typeof db === 'object';
|
|
35
|
+
const schema = hasDb && db.schema != null ? String(db.schema).trim() : '';
|
|
36
|
+
const field = hasDb && db.field != null ? String(db.field).trim() : '';
|
|
37
|
+
const row = {
|
|
38
|
+
path: node.id ? `design.${ctx.sectionId ? `${ctx.sectionId}.` : ''}${node.id}` : 'design.sections',
|
|
39
|
+
label: node.label || node.name || node.id || '',
|
|
40
|
+
schema,
|
|
41
|
+
field,
|
|
42
|
+
bindField,
|
|
43
|
+
required: !!(node.validation?.required),
|
|
44
|
+
enumMapping: hasDb ? db.enumMapping : undefined,
|
|
45
|
+
hasDbBlock: hasDb,
|
|
46
|
+
};
|
|
47
|
+
if (hasDb || bindField || node.validation?.required) {
|
|
48
|
+
bindings.push(row);
|
|
49
|
+
}
|
|
50
|
+
if (schema) tables.add(schema);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const listColumns =
|
|
54
|
+
bundle.gen?.ui?.list?.columns ??
|
|
55
|
+
bundle.spec?.ui?.list?.columns ??
|
|
56
|
+
design.ui?.list?.columns ??
|
|
57
|
+
[];
|
|
58
|
+
for (const col of listColumns) {
|
|
59
|
+
if (!col || typeof col !== 'object') continue;
|
|
60
|
+
const db = col.db;
|
|
61
|
+
const hasDb = db && typeof db === 'object';
|
|
62
|
+
const schema = hasDb && db.schema != null ? String(db.schema).trim() : '';
|
|
63
|
+
const field = hasDb && db.field != null ? String(db.field).trim() : '';
|
|
64
|
+
bindings.push({
|
|
65
|
+
path: `spec.ui.list.columns.${col.key ?? '?'}`,
|
|
66
|
+
label: col.title || col.key || '',
|
|
67
|
+
schema,
|
|
68
|
+
field,
|
|
69
|
+
bindField: col.key != null ? String(col.key) : '',
|
|
70
|
+
required: false,
|
|
71
|
+
enumMapping: hasDb ? db.enumMapping : undefined,
|
|
72
|
+
source: 'list-column',
|
|
73
|
+
hasDbBlock: hasDb,
|
|
74
|
+
sortable: !!col.sortable,
|
|
75
|
+
});
|
|
76
|
+
if (schema) tables.add(schema);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { bindings, tables };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeTableKey(name) {
|
|
83
|
+
return String(name)
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/-/g, '_');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} backendText
|
|
91
|
+
* @returns {Map<string, { entityName: string, fields: Set<string> }>}
|
|
92
|
+
*/
|
|
93
|
+
export function indexBackendEntities(backendText) {
|
|
94
|
+
const map = new Map();
|
|
95
|
+
if (!backendText) return map;
|
|
96
|
+
|
|
97
|
+
let doc;
|
|
98
|
+
try {
|
|
99
|
+
doc = parse(backendText);
|
|
100
|
+
} catch {
|
|
101
|
+
return map;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const mod of doc.modules ?? []) {
|
|
105
|
+
for (const ent of mod.entities ?? []) {
|
|
106
|
+
const table = ent.table ?? ent.name;
|
|
107
|
+
if (!table) continue;
|
|
108
|
+
const key = normalizeTableKey(table);
|
|
109
|
+
const fields = new Set((ent.fields ?? []).map((f) => String(f.name).toLowerCase()));
|
|
110
|
+
const altKey = ent.name ? normalizeTableKey(ent.name) : key;
|
|
111
|
+
map.set(key, { entityName: ent.name ?? table, fields });
|
|
112
|
+
if (altKey !== key) map.set(altKey, { entityName: ent.name ?? table, fields });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return map;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function entityNamesFromSpec(bundle) {
|
|
119
|
+
const ents = bundle.spec?.entities ?? bundle.design?.entities ?? [];
|
|
120
|
+
if (!Array.isArray(ents)) return new Set();
|
|
121
|
+
return new Set(
|
|
122
|
+
ents
|
|
123
|
+
.map((e) => (typeof e === 'string' ? e : e?.name ?? e?.table ?? ''))
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.map((n) => normalizeTableKey(n)),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const DEBT_OPTION = 'Log as Tech Debt (Pending)';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {{
|
|
133
|
+
* bundle?: Record<string, unknown>,
|
|
134
|
+
* bundleText?: string,
|
|
135
|
+
* backendText?: string,
|
|
136
|
+
* backendSpecFound?: boolean,
|
|
137
|
+
* filePath?: string,
|
|
138
|
+
* }} input
|
|
139
|
+
*/
|
|
140
|
+
export function auditDbTables(input = {}) {
|
|
141
|
+
const warnings = [];
|
|
142
|
+
const confirms = [];
|
|
143
|
+
const filePath = input.filePath ?? '';
|
|
144
|
+
|
|
145
|
+
function addWarning(code, fieldPath, message, suggestedFix) {
|
|
146
|
+
warnings.push({
|
|
147
|
+
code,
|
|
148
|
+
type: 'warning',
|
|
149
|
+
category: 'db',
|
|
150
|
+
path: fieldPath,
|
|
151
|
+
message,
|
|
152
|
+
suggestedFix,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function addConfirm(code, fieldPath, question, options, defaultRecommend = 0, patchHint = '') {
|
|
157
|
+
confirms.push({
|
|
158
|
+
code,
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
category: 'db',
|
|
161
|
+
path: fieldPath,
|
|
162
|
+
question,
|
|
163
|
+
options,
|
|
164
|
+
defaultRecommend,
|
|
165
|
+
...(patchHint ? { patchHint } : {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let bundle = input.bundle;
|
|
170
|
+
if (!bundle && input.bundleText) {
|
|
171
|
+
try {
|
|
172
|
+
bundle = parse(input.bundleText);
|
|
173
|
+
} catch {
|
|
174
|
+
addConfirm(
|
|
175
|
+
'CONFIRM_DB_PARSE',
|
|
176
|
+
filePath,
|
|
177
|
+
'Bundle YAML failed DB audit parse — how to proceed?',
|
|
178
|
+
[
|
|
179
|
+
'(Recommended) Fix YAML syntax in bundle, then re-run audit spec',
|
|
180
|
+
'Pause authoring until file is valid',
|
|
181
|
+
DEBT_OPTION,
|
|
182
|
+
],
|
|
183
|
+
0,
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
file: filePath,
|
|
187
|
+
totalWarnings: 0,
|
|
188
|
+
totalConfirms: confirms.length,
|
|
189
|
+
warnings,
|
|
190
|
+
confirms,
|
|
191
|
+
bindings: [],
|
|
192
|
+
tables: [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!bundle) {
|
|
198
|
+
return {
|
|
199
|
+
file: filePath,
|
|
200
|
+
totalWarnings: 0,
|
|
201
|
+
totalConfirms: 0,
|
|
202
|
+
warnings: [],
|
|
203
|
+
confirms: [],
|
|
204
|
+
bindings: [],
|
|
205
|
+
tables: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { bindings, tables } = collectDesignDbBindings(bundle);
|
|
210
|
+
const tableList = [...tables];
|
|
211
|
+
const partialDb = bindings.filter((b) => b.hasDbBlock && ((b.schema && !b.field) || (!b.schema && b.field)));
|
|
212
|
+
const persistedWithoutDb = bindings.filter(
|
|
213
|
+
(b) => b.required && (!b.schema || !b.field) && b.bindField,
|
|
214
|
+
);
|
|
215
|
+
const withFullDb = bindings.filter((b) => b.schema && b.field);
|
|
216
|
+
const listColsNoDb = bindings.filter(
|
|
217
|
+
(b) => b.source === 'list-column' && b.bindField && !b.schema && (b.sortable || b.label),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (partialDb.length === 1) {
|
|
221
|
+
const b = partialDb[0];
|
|
222
|
+
addConfirm(
|
|
223
|
+
'CONFIRM_DB_PARTIAL_BINDING',
|
|
224
|
+
b.path,
|
|
225
|
+
`Incomplete db on "${b.label || b.bindField}": schema="${b.schema}" field="${b.field}". How to fix?`,
|
|
226
|
+
[
|
|
227
|
+
'(Recommended) Set both db.schema and db.field per db-erd.md + 01-backend-spec',
|
|
228
|
+
'Field is UI-only / computed — remove db block and document #derived-data',
|
|
229
|
+
DEBT_OPTION,
|
|
230
|
+
],
|
|
231
|
+
0,
|
|
232
|
+
'design.sections[].db',
|
|
233
|
+
);
|
|
234
|
+
} else if (partialDb.length > 1) {
|
|
235
|
+
const ids = partialDb.map((b) => b.bindField || b.path).join(', ');
|
|
236
|
+
addConfirm(
|
|
237
|
+
'CONFIRM_DB_PARTIAL_BINDING',
|
|
238
|
+
'design.sections',
|
|
239
|
+
`${partialDb.length} controls have partial db bindings (${ids}). Batch fix strategy?`,
|
|
240
|
+
[
|
|
241
|
+
'(Recommended) Patch each control with full schema+field from ERD before split',
|
|
242
|
+
'Mark non-persisted fields as #derived-data and remove partial db',
|
|
243
|
+
DEBT_OPTION,
|
|
244
|
+
],
|
|
245
|
+
0,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (persistedWithoutDb.length === 1) {
|
|
250
|
+
const b = persistedWithoutDb[0];
|
|
251
|
+
addConfirm(
|
|
252
|
+
'CONFIRM_DB_REQUIRED_MAPPING',
|
|
253
|
+
b.path,
|
|
254
|
+
`Required field "${b.bindField}" has no db.schema/db.field — where is it stored?`,
|
|
255
|
+
[
|
|
256
|
+
`(Recommended) Add db: { schema, field } aligned with common/db-erd.md`,
|
|
257
|
+
'Not persisted on this screen — relax validation.required or document #derived-data',
|
|
258
|
+
DEBT_OPTION,
|
|
259
|
+
],
|
|
260
|
+
0,
|
|
261
|
+
);
|
|
262
|
+
} else if (persistedWithoutDb.length > 1) {
|
|
263
|
+
const ids = persistedWithoutDb.map((b) => b.bindField).join(', ');
|
|
264
|
+
addConfirm(
|
|
265
|
+
'CONFIRM_DB_REQUIRED_MAPPING',
|
|
266
|
+
'design.sections',
|
|
267
|
+
`${persistedWithoutDb.length} required fields lack db mapping: ${ids}.`,
|
|
268
|
+
[
|
|
269
|
+
'(Recommended) Map all to tables/columns on bundle + spec.entities subset',
|
|
270
|
+
'Split screen scope — some fields are computed (#derived-data)',
|
|
271
|
+
DEBT_OPTION,
|
|
272
|
+
],
|
|
273
|
+
0,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const bindToDb = new Map();
|
|
278
|
+
const conflicts = [];
|
|
279
|
+
for (const b of withFullDb) {
|
|
280
|
+
if (!b.bindField) continue;
|
|
281
|
+
const sig = `${b.schema}.${b.field}`;
|
|
282
|
+
const prev = bindToDb.get(b.bindField);
|
|
283
|
+
if (prev && prev !== sig) {
|
|
284
|
+
conflicts.push({ bindField: b.bindField, prev, sig, path: b.path });
|
|
285
|
+
} else {
|
|
286
|
+
bindToDb.set(b.bindField, sig);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (conflicts.length === 1) {
|
|
290
|
+
const c = conflicts[0];
|
|
291
|
+
addConfirm(
|
|
292
|
+
'CONFIRM_DB_BIND_CONFLICT',
|
|
293
|
+
c.path,
|
|
294
|
+
`bind.field "${c.bindField}" maps to both ${c.prev} and ${c.sig}. Which is correct?`,
|
|
295
|
+
[
|
|
296
|
+
`(Recommended) Keep ${c.sig} — rename other control bind.field`,
|
|
297
|
+
`Keep ${c.prev} — fix the conflicting control`,
|
|
298
|
+
DEBT_OPTION,
|
|
299
|
+
],
|
|
300
|
+
0,
|
|
301
|
+
);
|
|
302
|
+
} else if (conflicts.length > 1) {
|
|
303
|
+
addConfirm(
|
|
304
|
+
'CONFIRM_DB_BIND_CONFLICT',
|
|
305
|
+
'design.sections[].bind.field',
|
|
306
|
+
`${conflicts.length} bind.field keys map to multiple DB columns. Resolve naming?`,
|
|
307
|
+
[
|
|
308
|
+
'(Recommended) One bind.field per DB column; split payload keys per table',
|
|
309
|
+
'Document composite DTO on 01 — align api-update with nested object',
|
|
310
|
+
DEBT_OPTION,
|
|
311
|
+
],
|
|
312
|
+
0,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const entityNames = entityNamesFromSpec(bundle);
|
|
317
|
+
const persistedCount = withFullDb.length;
|
|
318
|
+
|
|
319
|
+
if (persistedCount >= 2 && entityNames.size === 0) {
|
|
320
|
+
addConfirm(
|
|
321
|
+
'CONFIRM_DB_SPEC_ENTITIES',
|
|
322
|
+
'spec.entities',
|
|
323
|
+
`Screen maps ${persistedCount} column(s) across ${tableList.length || 1} table(s) but spec.entities is empty. Declare entity subset?`,
|
|
324
|
+
[
|
|
325
|
+
`(Recommended) Fill spec.entities[] from db-erd (${tableList.join(', ') || 'see ERD'})`,
|
|
326
|
+
'Single aggregate only — one entity entry covering primary table',
|
|
327
|
+
DEBT_OPTION,
|
|
328
|
+
],
|
|
329
|
+
0,
|
|
330
|
+
'spec.entities',
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (tableList.length >= 2) {
|
|
335
|
+
const missing = tableList.filter((t) => !entityNames.has(normalizeTableKey(t)));
|
|
336
|
+
if (entityNames.size > 0 && missing.length > 0) {
|
|
337
|
+
addConfirm(
|
|
338
|
+
'CONFIRM_DB_MULTI_TABLE_ENTITIES',
|
|
339
|
+
'spec.entities',
|
|
340
|
+
`Tables on screen: ${tableList.join(', ')}. spec.entities may miss: ${missing.join(', ')}.`,
|
|
341
|
+
[
|
|
342
|
+
'(Recommended) Add entity row per missing table (name + short description)',
|
|
343
|
+
'Secondary table is join-only — document under design.dataModel.tables role join',
|
|
344
|
+
DEBT_OPTION,
|
|
345
|
+
],
|
|
346
|
+
0,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (!bundle.design?.dataModel?.tables?.length) {
|
|
350
|
+
addConfirm(
|
|
351
|
+
'CONFIRM_DB_MULTI_TABLE_OVERVIEW',
|
|
352
|
+
'design.dataModel.tables',
|
|
353
|
+
`This screen uses ${tableList.length} tables (${tableList.join(', ')}). Add per-table role for reviewers?`,
|
|
354
|
+
[
|
|
355
|
+
'(Recommended) Add design.dataModel.tables[] (read-write / read / join) — tpl-screen-data-model.md',
|
|
356
|
+
'Rely on ir/generated/data-model.md only after split (no YAML overview)',
|
|
357
|
+
DEBT_OPTION,
|
|
358
|
+
],
|
|
359
|
+
0,
|
|
360
|
+
'design.dataModel',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (listColsNoDb.length > 0) {
|
|
366
|
+
const keys = listColsNoDb.map((b) => b.bindField).join(', ');
|
|
367
|
+
addConfirm(
|
|
368
|
+
'CONFIRM_DB_LIST_COLUMN_STORAGE',
|
|
369
|
+
'spec.ui.list.columns',
|
|
370
|
+
`List columns [${keys}] have no db binding — stored in DB or computed?`,
|
|
371
|
+
[
|
|
372
|
+
'(Recommended) Add columns[].db.schema+field when sort/filter/search hits DB',
|
|
373
|
+
'Computed/display-only — tag #derived-data in data-model/derived-data.md',
|
|
374
|
+
DEBT_OPTION,
|
|
375
|
+
],
|
|
376
|
+
0,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const backendIndex = indexBackendEntities(input.backendText ?? '');
|
|
381
|
+
const beTableMiss = [];
|
|
382
|
+
const beFieldMiss = [];
|
|
383
|
+
if (backendIndex.size > 0 && withFullDb.length > 0) {
|
|
384
|
+
for (const b of withFullDb) {
|
|
385
|
+
const ent = backendIndex.get(normalizeTableKey(b.schema));
|
|
386
|
+
if (!ent) {
|
|
387
|
+
beTableMiss.push(b);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (!ent.fields.has(b.field.toLowerCase())) {
|
|
391
|
+
beFieldMiss.push({ ...b, entityName: ent.entityName });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (beTableMiss.length > 0) {
|
|
397
|
+
const schemas = [...new Set(beTableMiss.map((b) => b.schema))].join(', ');
|
|
398
|
+
addConfirm(
|
|
399
|
+
'CONFIRM_DB_BE_TABLE_DRIFT',
|
|
400
|
+
'api/01-backend-spec.yaml',
|
|
401
|
+
`Design db.schema [${schemas}] not found on 01 modules[].entities.table/name.`,
|
|
402
|
+
[
|
|
403
|
+
'(Recommended) /api-update — add entity with matching table on 01',
|
|
404
|
+
'(Recommended) Fix design db.schema to match existing 01 entity.table',
|
|
405
|
+
DEBT_OPTION,
|
|
406
|
+
],
|
|
407
|
+
0,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (beFieldMiss.length > 0) {
|
|
412
|
+
const sample = beFieldMiss
|
|
413
|
+
.slice(0, 4)
|
|
414
|
+
.map((b) => `${b.schema}.${b.field}`)
|
|
415
|
+
.join(', ');
|
|
416
|
+
addConfirm(
|
|
417
|
+
'CONFIRM_DB_BE_FIELD_DRIFT',
|
|
418
|
+
'api/01-backend-spec.yaml',
|
|
419
|
+
`Design columns not on 01 entity fields (e.g. ${sample}${beFieldMiss.length > 4 ? ', …' : ''}).`,
|
|
420
|
+
[
|
|
421
|
+
'(Recommended) Add fields to 01 entities[].fields via /api-update',
|
|
422
|
+
'Fix design db.field to match 01 field names',
|
|
423
|
+
DEBT_OPTION,
|
|
424
|
+
],
|
|
425
|
+
0,
|
|
426
|
+
);
|
|
427
|
+
} else if (
|
|
428
|
+
withFullDb.length > 0 &&
|
|
429
|
+
backendIndex.size === 0 &&
|
|
430
|
+
input.backendSpecFound === false
|
|
431
|
+
) {
|
|
432
|
+
addConfirm(
|
|
433
|
+
'CONFIRM_DB_BE_SPEC_MISSING',
|
|
434
|
+
'api/01-backend-spec.yaml',
|
|
435
|
+
'Design has db mappings but no sibling 01-backend-spec for audit cross-check.',
|
|
436
|
+
[
|
|
437
|
+
'(Recommended) Run /api-spec or /api-update to author 01, then re-audit',
|
|
438
|
+
'Continue design-only — defer BE until grill-dev',
|
|
439
|
+
DEBT_OPTION,
|
|
440
|
+
],
|
|
441
|
+
1,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (persistedCount === 0 && bindings.some((b) => b.required && b.bindField)) {
|
|
446
|
+
addWarning(
|
|
447
|
+
'WARN_DB_NO_BINDINGS_YET',
|
|
448
|
+
'design.sections',
|
|
449
|
+
'Required form fields exist but no complete db.schema+db.field yet.',
|
|
450
|
+
'Complete db bindings or confirm #derived-data via CONFIRM_DB_* wizard.',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
file: filePath,
|
|
456
|
+
totalWarnings: warnings.length,
|
|
457
|
+
totalConfirms: confirms.length,
|
|
458
|
+
warnings,
|
|
459
|
+
confirms,
|
|
460
|
+
bindings: withFullDb,
|
|
461
|
+
tables: tableList,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function resolveBackendSpecForBundle(bundlePath, explicitPath) {
|
|
466
|
+
if (explicitPath) return path.resolve(process.cwd(), explicitPath);
|
|
467
|
+
const dir = path.dirname(path.resolve(bundlePath));
|
|
468
|
+
const candidates = [];
|
|
469
|
+
const walk = (start, depth) => {
|
|
470
|
+
if (depth > 4) return;
|
|
471
|
+
let entries;
|
|
472
|
+
try {
|
|
473
|
+
entries = fs.readdirSync(start, { withFileTypes: true });
|
|
474
|
+
} catch {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
for (const e of entries) {
|
|
478
|
+
const full = path.join(start, e.name);
|
|
479
|
+
if (e.isDirectory()) {
|
|
480
|
+
if (e.name === 'api' || e.name.match(/^\d+$/)) walk(full, depth + 1);
|
|
481
|
+
} else if (e.name === '01-backend-spec.yaml') {
|
|
482
|
+
candidates.push(full);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
walk(dir, 0);
|
|
487
|
+
walk(path.join(dir, 'api'), 0);
|
|
488
|
+
return candidates[0] ?? null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseArgs(argv) {
|
|
492
|
+
let backendSpec = null;
|
|
493
|
+
const positional = [];
|
|
494
|
+
for (let i = 0; i < argv.length; i++) {
|
|
495
|
+
if (argv[i] === '--backend-spec' && argv[i + 1]) {
|
|
496
|
+
backendSpec = argv[++i];
|
|
497
|
+
} else {
|
|
498
|
+
positional.push(argv[i]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { bundlePath: positional[0], backendSpec };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const isMain =
|
|
505
|
+
process.argv[1] &&
|
|
506
|
+
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
507
|
+
const args = process.argv.slice(2);
|
|
508
|
+
if (isMain) {
|
|
509
|
+
if (args.length === 0) {
|
|
510
|
+
console.log(
|
|
511
|
+
'Usage: node audit-db-tables.mjs <bundle.yaml> [--backend-spec path/to/01-backend-spec.yaml]',
|
|
512
|
+
);
|
|
513
|
+
process.exit(0);
|
|
514
|
+
}
|
|
515
|
+
const { bundlePath, backendSpec } = parseArgs(args);
|
|
516
|
+
const resolved = path.resolve(process.cwd(), bundlePath);
|
|
517
|
+
const bundleText = fs.readFileSync(resolved, 'utf8');
|
|
518
|
+
const backendResolved = resolveBackendSpecForBundle(resolved, backendSpec);
|
|
519
|
+
const backendText =
|
|
520
|
+
backendResolved && fs.existsSync(backendResolved)
|
|
521
|
+
? fs.readFileSync(backendResolved, 'utf8')
|
|
522
|
+
: '';
|
|
523
|
+
const report = auditDbTables({
|
|
524
|
+
bundleText,
|
|
525
|
+
backendText,
|
|
526
|
+
filePath: resolved,
|
|
527
|
+
});
|
|
528
|
+
console.log(JSON.stringify(report, null, 2));
|
|
529
|
+
}
|