@shanyucoder/flowgrid 0.1.5 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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
|
@@ -3,6 +3,9 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { parse } from 'yaml'
|
|
5
5
|
import { renderSpecMarkdown } from './render-spec-markdown.mjs'
|
|
6
|
+
import { renderDataModelMarkdown } from './render-data-model-markdown.mjs'
|
|
7
|
+
import { renderApiSummaryMarkdown } from './render-api-summary-markdown.mjs'
|
|
8
|
+
import { resolveBackendSpecForBundle } from '../../spec/lib/audit-db-tables.mjs'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Flatten bundle → spec shape (tests / fallback). Prefer ir/spec.yaml for site MD.
|
|
@@ -67,6 +70,22 @@ export function irGeneratedMarkdownPath(bundleFile) {
|
|
|
67
70
|
return path.join(dir, 'ir', 'generated', 'spec.md')
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/** Reviewer-facing DB tables — `…/<leaf>/ir/generated/data-model.md` */
|
|
74
|
+
export function irGeneratedDataModelPath(bundleFile) {
|
|
75
|
+
const dir = path.dirname(path.resolve(bundleFile))
|
|
76
|
+
return path.join(dir, 'ir', 'generated', 'data-model.md')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** API summary for readers — `…/<leaf>/ir/generated/api.md` */
|
|
80
|
+
export function irGeneratedApiPath(bundleFile) {
|
|
81
|
+
const dir = path.dirname(path.resolve(bundleFile))
|
|
82
|
+
return path.join(dir, 'ir', 'generated', 'api.md')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function designIrPathForBundle(bundleFile) {
|
|
86
|
+
return path.join(path.dirname(path.resolve(bundleFile)), 'ir', 'design.yaml')
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
export function irSpecPathForBundle(bundleFile) {
|
|
71
90
|
return path.join(path.dirname(path.resolve(bundleFile)), 'ir', 'spec.yaml')
|
|
72
91
|
}
|
|
@@ -77,13 +96,69 @@ export function irSpecPathForBundle(bundleFile) {
|
|
|
77
96
|
export async function writeBundleMarkdownFile(bundleFile, context = {}) {
|
|
78
97
|
const specFile = irSpecPathForBundle(bundleFile)
|
|
79
98
|
if (!existsSync(specFile)) return null
|
|
80
|
-
|
|
99
|
+
const specOut = await writeIrSpecMarkdownFile(specFile, irGeneratedMarkdownPath(bundleFile), context)
|
|
100
|
+
await writeDataModelMarkdownFile(bundleFile)
|
|
101
|
+
await writeApiMarkdownFile(bundleFile)
|
|
102
|
+
return specOut
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function writeApiMarkdownFile(bundleFile) {
|
|
106
|
+
const resolved = path.resolve(bundleFile)
|
|
107
|
+
const backendPath = resolveBackendSpecForBundle(resolved, null)
|
|
108
|
+
if (!backendPath || !existsSync(backendPath)) return null
|
|
109
|
+
const backendSpec = parse(await readFile(backendPath, 'utf8')) ?? {}
|
|
110
|
+
const specFile = irSpecPathForBundle(bundleFile)
|
|
111
|
+
const spec = existsSync(specFile) ? parse(await readFile(specFile, 'utf8')) ?? {} : {}
|
|
112
|
+
const pageId = spec['page-id'] ?? spec.id
|
|
113
|
+
const markdown = renderApiSummaryMarkdown(backendSpec, {
|
|
114
|
+
pageId,
|
|
115
|
+
title: spec.title ?? backendSpec.feature?.title,
|
|
116
|
+
backendRelPath: path.relative(path.dirname(resolved), backendPath).split(path.sep).join('/'),
|
|
117
|
+
})
|
|
118
|
+
const out = irGeneratedApiPath(bundleFile)
|
|
119
|
+
await mkdir(path.dirname(out), { recursive: true })
|
|
120
|
+
await writeFile(out, markdown, 'utf8')
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function writeDataModelMarkdownFile(bundleFile) {
|
|
125
|
+
const designFile = designIrPathForBundle(bundleFile)
|
|
126
|
+
if (!existsSync(designFile)) return null
|
|
127
|
+
const design = parse(await readFile(designFile, 'utf8')) ?? {}
|
|
128
|
+
const specFile = irSpecPathForBundle(bundleFile)
|
|
129
|
+
const spec = existsSync(specFile) ? parse(await readFile(specFile, 'utf8')) ?? {} : {}
|
|
130
|
+
const markdown = renderDataModelMarkdown({
|
|
131
|
+
pageId: spec.id ?? design.id,
|
|
132
|
+
title: spec.title ?? design.title,
|
|
133
|
+
design,
|
|
134
|
+
spec,
|
|
135
|
+
})
|
|
136
|
+
const out = irGeneratedDataModelPath(bundleFile)
|
|
137
|
+
await mkdir(path.dirname(out), { recursive: true })
|
|
138
|
+
await writeFile(out, markdown, 'utf8')
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function mergeDesignIntoSpecForRender(spec, design) {
|
|
143
|
+
if (!design || typeof design !== 'object') return spec
|
|
144
|
+
const out = { ...spec }
|
|
145
|
+
if (design.ui?.list) {
|
|
146
|
+
out.ui = { ...(out.ui ?? {}), list: { ...(out.ui?.list ?? {}), ...design.ui.list } }
|
|
147
|
+
}
|
|
148
|
+
if (design.sections && !out.sections) out.sections = design.sections
|
|
149
|
+
if (design.actions && !out.actions) out.actions = design.actions
|
|
150
|
+
if (design.stateMatrix && !out.stateMatrix) out.stateMatrix = design.stateMatrix
|
|
151
|
+
if (design.nav && !out.nav) out.nav = design.nav
|
|
152
|
+
return out
|
|
81
153
|
}
|
|
82
154
|
|
|
83
155
|
export async function writeIrSpecMarkdownFile(specFile, mdOut, context = {}) {
|
|
84
156
|
const spec = parse(await readFile(specFile, 'utf8')) ?? {}
|
|
157
|
+
const designFile = path.join(path.dirname(specFile), 'design.yaml')
|
|
158
|
+
const design = existsSync(designFile) ? parse(await readFile(designFile, 'utf8')) ?? {} : {}
|
|
159
|
+
const specShape = mergeDesignIntoSpecForRender(spec, design)
|
|
85
160
|
const projectRoot = context.projectRoot ?? process.cwd()
|
|
86
|
-
const markdown = renderIrSpecMarkdown(
|
|
161
|
+
const markdown = renderIrSpecMarkdown(specShape, {
|
|
87
162
|
testcases: [],
|
|
88
163
|
output: { specFile: path.basename(mdOut), testcasesDir: 'testcases' },
|
|
89
164
|
devAppBaseUrl: context.devAppBaseUrl ?? '',
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { renderTable } from './markdown-table.mjs'
|
|
2
|
+
import { collectDesignDbBindings } from '../../spec/lib/audit-db-tables.mjs'
|
|
3
|
+
|
|
4
|
+
function roleLabel(role) {
|
|
5
|
+
const map = {
|
|
6
|
+
read: 'Chỉ đọc',
|
|
7
|
+
write: 'Chỉ ghi',
|
|
8
|
+
'read-write': 'Đọc & ghi',
|
|
9
|
+
join: 'Join / lookup',
|
|
10
|
+
aggregate: 'Tổng hợp (read)',
|
|
11
|
+
}
|
|
12
|
+
return map[role] || role || '—'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function groupBindingsByTable(bindings) {
|
|
16
|
+
const byTable = new Map()
|
|
17
|
+
for (const b of bindings) {
|
|
18
|
+
if (!b.schema || !b.field) continue
|
|
19
|
+
if (!byTable.has(b.schema)) byTable.set(b.schema, [])
|
|
20
|
+
byTable.get(b.schema).push(b)
|
|
21
|
+
}
|
|
22
|
+
return byTable
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tableOverviewRows(design, tableNames) {
|
|
26
|
+
const meta = design.dataModel?.tables
|
|
27
|
+
const byName = new Map()
|
|
28
|
+
if (Array.isArray(meta)) {
|
|
29
|
+
for (const t of meta) {
|
|
30
|
+
if (t?.schema) byName.set(String(t.schema), t)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return tableNames.map((schema) => {
|
|
34
|
+
const m = byName.get(schema) ?? {}
|
|
35
|
+
return [
|
|
36
|
+
`\`${schema}\``,
|
|
37
|
+
roleLabel(m.roleOnScreen),
|
|
38
|
+
m.summary || m.purpose || '—',
|
|
39
|
+
m.erdEntity ? `\`${m.erdEntity}\`` : '—',
|
|
40
|
+
]
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Human-readable DB view for reviewers (multi-table safe). SSOT remains bundle / ir/design.yaml + 01.
|
|
46
|
+
* @param {{
|
|
47
|
+
* pageId?: string,
|
|
48
|
+
* title?: string,
|
|
49
|
+
* design?: Record<string, unknown>,
|
|
50
|
+
* spec?: Record<string, unknown>,
|
|
51
|
+
* }} input
|
|
52
|
+
*/
|
|
53
|
+
export function renderDataModelMarkdown(input = {}) {
|
|
54
|
+
const design = input.design ?? {}
|
|
55
|
+
const spec = input.spec ?? {}
|
|
56
|
+
const pageId = input.pageId ?? design.id ?? '—'
|
|
57
|
+
const title = input.title ?? design.title ?? pageId
|
|
58
|
+
const bundleShape = { design, spec: { entities: design.entities ?? spec.entities, ui: design.ui ?? spec.ui } }
|
|
59
|
+
const { bindings, tables } = collectDesignDbBindings(bundleShape)
|
|
60
|
+
const fullBindings = bindings.filter((b) => b.schema && b.field)
|
|
61
|
+
const byTable = groupBindingsByTable(fullBindings)
|
|
62
|
+
const tableNames = tables.length ? tables : [...byTable.keys()]
|
|
63
|
+
|
|
64
|
+
const parts = [
|
|
65
|
+
`# Data model — ${title}`,
|
|
66
|
+
'',
|
|
67
|
+
`> **Page ID:** \`${pageId}\` · Đọc cùng [spec.md](./spec.md) trên VitePress (sidebar **Data model**). SSOT ghi: bundle + \`ir/design.yaml\` · BE: \`api/*/01-backend-spec.yaml\`.`,
|
|
68
|
+
'',
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if (design.dataModel?.erdRef || design.dataModel?.notes) {
|
|
72
|
+
parts.push('## Ghi chú & ERD', '')
|
|
73
|
+
if (design.dataModel.erdRef) {
|
|
74
|
+
parts.push(`- **ERD (Phase 0):** ${design.dataModel.erdRef}`)
|
|
75
|
+
}
|
|
76
|
+
if (design.dataModel.notes) {
|
|
77
|
+
parts.push('', String(design.dataModel.notes), '')
|
|
78
|
+
}
|
|
79
|
+
parts.push('')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const entities = design.entities ?? spec.entities
|
|
83
|
+
if (Array.isArray(entities) && entities.length) {
|
|
84
|
+
parts.push('## Entity trên màn (spec.entities)', '')
|
|
85
|
+
for (const e of entities) {
|
|
86
|
+
if (typeof e === 'string') {
|
|
87
|
+
parts.push(`- **${e}**`)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const name = e.name ?? e.table ?? 'Entity'
|
|
91
|
+
parts.push(`- **${name}**${e.description ? ` — ${e.description}` : ''}`)
|
|
92
|
+
if (e.relatesTo) parts.push(` - Quan hệ: ${e.relatesTo}`)
|
|
93
|
+
}
|
|
94
|
+
parts.push('')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(design.relationships) && design.relationships.length) {
|
|
98
|
+
parts.push('## Quan hệ (relationships)', '')
|
|
99
|
+
for (const r of design.relationships) {
|
|
100
|
+
parts.push(`- ${typeof r === 'string' ? r : formatRecord(r)}`)
|
|
101
|
+
}
|
|
102
|
+
parts.push('')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tableNames.length) {
|
|
106
|
+
parts.push('## Tổng quan bảng (multi-table)', '')
|
|
107
|
+
parts.push(
|
|
108
|
+
renderTable(
|
|
109
|
+
['Bảng (schema)', 'Vai trò trên màn', 'Mô tả review', 'Entity ER (nếu có)'],
|
|
110
|
+
tableOverviewRows(design, tableNames),
|
|
111
|
+
),
|
|
112
|
+
'',
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!fullBindings.length) {
|
|
117
|
+
parts.push(
|
|
118
|
+
'_Chưa có `design.sections[].db` / list column `db` — bổ sung trên bundle rồi chạy `flowgrid split`._',
|
|
119
|
+
'',
|
|
120
|
+
)
|
|
121
|
+
return parts.join('\n')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const schema of tableNames) {
|
|
125
|
+
const rows = (byTable.get(schema) ?? []).map((b) => {
|
|
126
|
+
const enumStr =
|
|
127
|
+
b.enumMapping && typeof b.enumMapping === 'object'
|
|
128
|
+
? Object.entries(b.enumMapping)
|
|
129
|
+
.map(([k, v]) => `\`${k}\` → ${v}`)
|
|
130
|
+
.join('<br>')
|
|
131
|
+
: '—'
|
|
132
|
+
return [
|
|
133
|
+
b.label || '—',
|
|
134
|
+
b.bindField ? `\`${b.bindField}\`` : '—',
|
|
135
|
+
`\`${b.field}\``,
|
|
136
|
+
b.required ? 'Có' : 'Không',
|
|
137
|
+
b.path?.includes('list') ? 'List' : 'Form / section',
|
|
138
|
+
enumStr,
|
|
139
|
+
]
|
|
140
|
+
})
|
|
141
|
+
if (!rows.length) continue
|
|
142
|
+
parts.push(`## Bảng \`${schema}\``, '')
|
|
143
|
+
parts.push(
|
|
144
|
+
renderTable(
|
|
145
|
+
['Nhãn UI', 'bind.field (API/payload)', 'Cột DB', 'Bắt buộc?', 'Nguồn UI', 'enumMapping'],
|
|
146
|
+
rows,
|
|
147
|
+
),
|
|
148
|
+
'',
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
parts.push(
|
|
153
|
+
'## Backend codegen',
|
|
154
|
+
'',
|
|
155
|
+
'- Map `db.schema` ↔ `01-backend-spec.yaml` → `modules[].entities[].table` (hoặc `name`).',
|
|
156
|
+
'- Map `db.field` ↔ `entities[].fields[].name`; quan hệ ORM theo ERD + `entity-relationship.md` extract.',
|
|
157
|
+
'- `bind.field` ↔ request/response DTO trên endpoint (`executionContract.apiRef`).',
|
|
158
|
+
'',
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return `${parts.join('\n').trim()}\n`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatRecord(obj) {
|
|
165
|
+
if (obj == null) return ''
|
|
166
|
+
if (typeof obj !== 'object') return String(obj)
|
|
167
|
+
return Object.entries(obj)
|
|
168
|
+
.filter(([, v]) => v != null && v !== '')
|
|
169
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
170
|
+
.join('; ')
|
|
171
|
+
}
|
|
@@ -120,6 +120,95 @@ export function collectFormFields(nodes = []) {
|
|
|
120
120
|
/**
|
|
121
121
|
* Bảng Từ Điển Dữ Liệu & Quy Tắc Kiểm Tra Hợp Lệ (Data Dictionary Table 6 cột)
|
|
122
122
|
*/
|
|
123
|
+
/**
|
|
124
|
+
* Bảng cột danh sách (list / admin-crud) — từ spec.ui.list, không chỉ form sections.
|
|
125
|
+
* @param {{ columns?: unknown[], filters?: unknown[], pagination?: unknown }} listUi
|
|
126
|
+
*/
|
|
127
|
+
/**
|
|
128
|
+
* @param {{ filters?: unknown[], pagination?: unknown }} listUi
|
|
129
|
+
*/
|
|
130
|
+
export function renderListFiltersTable(listUi = {}) {
|
|
131
|
+
const filters = listUi?.filters
|
|
132
|
+
if (!Array.isArray(filters) || !filters.length) return ''
|
|
133
|
+
|
|
134
|
+
const rows = filters.map((f) => {
|
|
135
|
+
if (!f || typeof f !== 'object') return null
|
|
136
|
+
return [
|
|
137
|
+
f.label || f.name || '—',
|
|
138
|
+
f.name != null ? `\`${f.name}\`` : '—',
|
|
139
|
+
f.type || '—',
|
|
140
|
+
f.widget || '—',
|
|
141
|
+
f.meaning || '—',
|
|
142
|
+
f.purpose || '—',
|
|
143
|
+
f.placeholder || '—',
|
|
144
|
+
]
|
|
145
|
+
}).filter(Boolean)
|
|
146
|
+
|
|
147
|
+
if (!rows.length) return ''
|
|
148
|
+
|
|
149
|
+
return renderTable(
|
|
150
|
+
['Nhãn', 'Tên filter', 'Kiểu', 'Widget', 'Ý nghĩa', 'Mục đích', 'Placeholder'],
|
|
151
|
+
rows,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function renderListPaginationBlock(listUi = {}) {
|
|
156
|
+
const p = listUi?.pagination
|
|
157
|
+
if (!p || typeof p !== 'object') return ''
|
|
158
|
+
const bits = []
|
|
159
|
+
if (p.enabled === false) return '_Phân trang: tắt trên màn này._\n'
|
|
160
|
+
if (p.pageSize != null) bits.push(`Kích thước trang mặc định: **${p.pageSize}**`)
|
|
161
|
+
if (p.pageSizeOptions?.length) {
|
|
162
|
+
bits.push(`Tùy chọn: ${p.pageSizeOptions.map((n) => `\`${n}\``).join(', ')}`)
|
|
163
|
+
}
|
|
164
|
+
if (p.showTotal != null) bits.push(`Hiển thị tổng số bản ghi: ${p.showTotal ? 'Có' : 'Không'}`)
|
|
165
|
+
if (p.infiniteScroll) bits.push('Chế độ: infinite scroll')
|
|
166
|
+
if (!bits.length) {
|
|
167
|
+
return '_Phân trang: bật (chi tiết kỹ thuật trên bundle / design actions)._ \n'
|
|
168
|
+
}
|
|
169
|
+
return bits.map((b) => `- ${b}`).join('\n') + '\n'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function renderListColumnsTable(listUi = {}) {
|
|
173
|
+
const columns = listUi?.columns
|
|
174
|
+
if (!Array.isArray(columns) || !columns.length) return ''
|
|
175
|
+
|
|
176
|
+
const rows = columns.map((col) => {
|
|
177
|
+
if (!col || typeof col !== 'object') return null
|
|
178
|
+
const db = col.db
|
|
179
|
+
let dbCell = '—'
|
|
180
|
+
if (db && typeof db === 'object') {
|
|
181
|
+
if (db.schema && db.field) dbCell = `\`${db.schema}.${db.field}\``
|
|
182
|
+
else if (db.schema) dbCell = `\`${db.schema}\` (thiếu field)`
|
|
183
|
+
else if (db.field) dbCell = `\`${db.field}\` (thiếu schema)`
|
|
184
|
+
}
|
|
185
|
+
return [
|
|
186
|
+
col.title || col.label || '—',
|
|
187
|
+
col.key != null ? `\`${col.key}\`` : '—',
|
|
188
|
+
col.meaning || '—',
|
|
189
|
+
col.purpose || '—',
|
|
190
|
+
[col.widget, col.render].filter(Boolean).join(' / ') || '—',
|
|
191
|
+
col.sortable ? 'Có' : 'Không',
|
|
192
|
+
dbCell,
|
|
193
|
+
]
|
|
194
|
+
}).filter(Boolean)
|
|
195
|
+
|
|
196
|
+
if (!rows.length) return ''
|
|
197
|
+
|
|
198
|
+
return renderTable(
|
|
199
|
+
[
|
|
200
|
+
'Nhãn cột',
|
|
201
|
+
'Key',
|
|
202
|
+
'Ý nghĩa nghiệp vụ',
|
|
203
|
+
'Mục đích UI',
|
|
204
|
+
'Widget / render',
|
|
205
|
+
'Sort',
|
|
206
|
+
'DB (schema.field)',
|
|
207
|
+
],
|
|
208
|
+
rows,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
123
212
|
export function renderValidationDictionaryTable(sections = []) {
|
|
124
213
|
const fields = collectFormFields(sections)
|
|
125
214
|
if (!fields.length) return ''
|
|
@@ -685,11 +774,4 @@ function formatApi(action) {
|
|
|
685
774
|
if (action.api) return String(action.api)
|
|
686
775
|
if (Array.isArray(action.apiRefs)) return action.apiRefs.join(', ')
|
|
687
776
|
return ''
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export {
|
|
691
|
-
renderValidationDictionaryTable,
|
|
692
|
-
renderStateMatrixTable,
|
|
693
|
-
renderActionFlowsDetailed,
|
|
694
|
-
renderCustomWidgetSpecs
|
|
695
777
|
}
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
renderBehaviorTable,
|
|
11
11
|
renderActionsTable,
|
|
12
12
|
renderValidationDictionaryTable,
|
|
13
|
+
renderListColumnsTable,
|
|
14
|
+
renderListFiltersTable,
|
|
15
|
+
renderListPaginationBlock,
|
|
13
16
|
renderStateMatrixTable,
|
|
14
17
|
renderActionFlowsDetailed,
|
|
15
18
|
renderCustomWidgetSpecs
|
|
@@ -66,6 +69,9 @@ export function renderWithTemplate(spec, context) {
|
|
|
66
69
|
renderBehaviorTable,
|
|
67
70
|
renderActionsTable,
|
|
68
71
|
renderValidationDictionaryTable,
|
|
72
|
+
renderListColumnsTable,
|
|
73
|
+
renderListFiltersTable,
|
|
74
|
+
renderListPaginationBlock,
|
|
69
75
|
renderStateMatrixTable,
|
|
70
76
|
renderActionFlowsDetailed,
|
|
71
77
|
renderCustomWidgetSpecs
|
|
@@ -8,7 +8,9 @@ import { renderSpecMarkdown } from './lib/render-spec-markdown.mjs'
|
|
|
8
8
|
import {
|
|
9
9
|
irGeneratedMarkdownPath,
|
|
10
10
|
irSpecPathForBundle,
|
|
11
|
-
writeIrSpecMarkdownFile
|
|
11
|
+
writeIrSpecMarkdownFile,
|
|
12
|
+
writeDataModelMarkdownFile,
|
|
13
|
+
writeApiMarkdownFile,
|
|
12
14
|
} from './lib/render-bundle-markdown.mjs'
|
|
13
15
|
|
|
14
16
|
const projectRoot = process.cwd()
|
|
@@ -34,7 +36,7 @@ const legacyRoot = cliFlag('legacy-root') ? path.resolve(cliFlag('legacy-root'))
|
|
|
34
36
|
|
|
35
37
|
const devAppBaseUrl = resolveDevAppBaseUrl(projectRoot)
|
|
36
38
|
|
|
37
|
-
import { renderFeatureBackendSpec, listBackendSpecFiles } from '
|
|
39
|
+
import { renderFeatureBackendSpec, listBackendSpecFiles } from '../openapi/render-backend-spec.mjs'
|
|
38
40
|
import { writeQaList } from './lib/render-qa-list.mjs'
|
|
39
41
|
|
|
40
42
|
async function main() {
|
|
@@ -63,6 +65,8 @@ async function main() {
|
|
|
63
65
|
try {
|
|
64
66
|
const mdOut = irGeneratedMarkdownPath(bundleFile)
|
|
65
67
|
await writeIrSpecMarkdownFile(specIr, mdOut, { projectRoot, devAppBaseUrl })
|
|
68
|
+
await writeDataModelMarkdownFile(bundleFile)
|
|
69
|
+
await writeApiMarkdownFile(bundleFile)
|
|
66
70
|
} catch (error) {
|
|
67
71
|
failed++
|
|
68
72
|
console.error(`docs:render: FAIL ${path.relative(projectRoot, specIr)}: ${error.message ?? error}`)
|
|
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress'
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
|
-
import { buildSurfacesSidebarItems, SKIP_NAV_DIRS, markdownNavText } from './surfaces-nav.mjs'
|
|
6
|
+
import { buildSurfacesSidebarItems, SKIP_NAV_DIRS, markdownNavText, leafGeneratedNavNodes } from './surfaces-nav.mjs'
|
|
7
7
|
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
|
|
@@ -89,12 +89,12 @@ function mdTitleFromFile(filePath: string, fallback: string) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function attachGeneratedDocs(dirPath: string, urlPrefix: string, items: any[]) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
92
|
+
const toLink = (abs: string) => {
|
|
93
|
+
const rel = path.relative(dirPath, abs).split(path.sep).join('/').replace(/\.md$/, '')
|
|
94
|
+
return `${urlPrefix}${rel}`
|
|
95
|
+
}
|
|
96
|
+
for (const node of leafGeneratedNavNodes(dirPath, toLink)) {
|
|
97
|
+
items.push({ text: node.text, link: node.link })
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -83,6 +83,38 @@ export function isFunctionLeaf(dir) {
|
|
|
83
83
|
return existsSync(path.join(dir, 'ir', 'spec.yaml'))
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* VitePress nav entries for ir/generated under a function leaf.
|
|
88
|
+
* @param {string} leafDir
|
|
89
|
+
* @param {(absPath: string) => string} toHref
|
|
90
|
+
*/
|
|
91
|
+
export function leafGeneratedNavNodes(leafDir, toHref) {
|
|
92
|
+
const pageId = pageIdFromLeaf(leafDir)
|
|
93
|
+
const specMd = path.join(leafDir, 'ir', 'generated', 'spec.md')
|
|
94
|
+
const dmMd = path.join(leafDir, 'ir', 'generated', 'data-model.md')
|
|
95
|
+
const apiMd = path.join(leafDir, 'ir', 'generated', 'api.md')
|
|
96
|
+
const nodes = []
|
|
97
|
+
if (existsSync(specMd)) {
|
|
98
|
+
nodes.push({
|
|
99
|
+
text: `Spec · ${pageId}`,
|
|
100
|
+
link: toHref(specMd),
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
if (existsSync(dmMd)) {
|
|
104
|
+
nodes.push({
|
|
105
|
+
text: 'Data model',
|
|
106
|
+
link: toHref(dmMd),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
if (existsSync(apiMd)) {
|
|
110
|
+
nodes.push({
|
|
111
|
+
text: 'API summary',
|
|
112
|
+
link: toHref(apiMd),
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return nodes
|
|
116
|
+
}
|
|
117
|
+
|
|
86
118
|
export function pageIdFromLeaf(dir) {
|
|
87
119
|
const specFile = path.join(dir, 'ir', 'spec.yaml')
|
|
88
120
|
if (existsSync(specFile)) {
|
|
@@ -119,18 +151,16 @@ export function walkSurfacesNav(dir, toHref) {
|
|
|
119
151
|
if (SKIP_NAV_DIRS.has(ent.name)) continue
|
|
120
152
|
const full = path.join(dir, ent.name)
|
|
121
153
|
if (isNumericSeq(ent.name) && isFunctionLeaf(full)) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
link: href,
|
|
133
|
-
})
|
|
154
|
+
const leafNodes = leafGeneratedNavNodes(full, toHref)
|
|
155
|
+
if (!leafNodes.length) continue
|
|
156
|
+
if (leafNodes.length === 1) {
|
|
157
|
+
children.push(leafNodes[0])
|
|
158
|
+
} else {
|
|
159
|
+
children.push({
|
|
160
|
+
text: `${ent.name} · ${pageIdFromLeaf(full)}`,
|
|
161
|
+
items: leafNodes,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
134
164
|
continue
|
|
135
165
|
}
|
|
136
166
|
const nested = walkSurfacesNav(full, toHref)
|
|
@@ -163,8 +193,9 @@ export function buildSurfacesSidebarItems(surfacesDir, urlPrefix) {
|
|
|
163
193
|
function toVitepressItem(node) {
|
|
164
194
|
const item = { text: node.text }
|
|
165
195
|
if (node.link) item.link = node.link
|
|
166
|
-
|
|
167
|
-
|
|
196
|
+
const nested = node.items || node.children
|
|
197
|
+
if (nested?.length) {
|
|
198
|
+
item.items = nested.map(toVitepressItem)
|
|
168
199
|
item.collapsed = true
|
|
169
200
|
}
|
|
170
201
|
return item
|
|
@@ -44,7 +44,7 @@ export async function checkBackendSpecFiles(options, cwd = process.cwd()) {
|
|
|
44
44
|
return files
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function
|
|
47
|
+
export function parseApiCheckArgs(argv) {
|
|
48
48
|
const options = { spec: null }
|
|
49
49
|
for (let i = 0; i < argv.length; i++) {
|
|
50
50
|
const arg = argv[i]
|
|
@@ -55,7 +55,7 @@ function parseArgs(argv) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
async function main() {
|
|
58
|
-
await checkBackendSpecFiles(
|
|
58
|
+
await checkBackendSpecFiles(parseApiCheckArgs(process.argv.slice(2)), process.cwd())
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (path.resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { stringify } from 'yaml'
|
|
2
|
+
import { renderTable, escapeMarkdownHeading, MD_NONE } from './markdown-table.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Record<string, unknown>} spec
|
|
6
|
+
* @param {{ specFile?: string, slug?: string }} context
|
|
7
|
+
*/
|
|
8
|
+
export function renderBackendSpecMarkdown(spec, context = {}) {
|
|
9
|
+
const feature = spec.feature ?? {}
|
|
10
|
+
const title = feature.title ?? context.slug ?? 'Backend API'
|
|
11
|
+
const lines = [
|
|
12
|
+
`# ${escapeMarkdownHeading(title)}`,
|
|
13
|
+
'',
|
|
14
|
+
`- **Source:** \`${context.specFile ?? '01-backend-spec.yaml'}\``,
|
|
15
|
+
`- **Status:** ${feature.status ?? 'draft'}`,
|
|
16
|
+
'',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
if (feature.source) {
|
|
20
|
+
lines.push('## Traceability', '', '```yaml', stringify(feature.source).trim(), '```', '')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const endpoints = spec.api?.endpoints ?? []
|
|
24
|
+
if (endpoints.length) {
|
|
25
|
+
lines.push('## Endpoints', '')
|
|
26
|
+
lines.push(
|
|
27
|
+
renderTable(
|
|
28
|
+
['id', 'method', 'path', 'action', 'purpose'],
|
|
29
|
+
endpoints.map((ep) => [
|
|
30
|
+
ep.id ?? '',
|
|
31
|
+
ep.method ?? '',
|
|
32
|
+
ep.path ?? '',
|
|
33
|
+
ep.action ?? '',
|
|
34
|
+
ep.purpose ?? '',
|
|
35
|
+
]),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
lines.push('')
|
|
39
|
+
} else {
|
|
40
|
+
lines.push('## Endpoints', '', MD_NONE, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (spec.decisions?.length) {
|
|
44
|
+
lines.push('## Decisions', '')
|
|
45
|
+
for (const d of spec.decisions) {
|
|
46
|
+
lines.push(`- **${d.id ?? 'DEC'}:** ${d.summary ?? ''}`)
|
|
47
|
+
}
|
|
48
|
+
lines.push('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (spec.changeLog?.length) {
|
|
52
|
+
lines.push('## Change log', '')
|
|
53
|
+
for (const c of spec.changeLog) {
|
|
54
|
+
lines.push(`- **${c.id ?? 'CHG'}:** ${c.summary ?? ''}`)
|
|
55
|
+
}
|
|
56
|
+
lines.push('')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lines.join('\n').endsWith('\n') ? lines.join('\n') : `${lines.join('\n')}\n`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Record<string, unknown>} spec
|
|
64
|
+
* @param {{ specFile?: string, slug?: string }} context
|
|
65
|
+
*/
|
|
66
|
+
export function renderCommonSpecMarkdown(spec, context = {}) {
|
|
67
|
+
const title = spec.title ?? context.slug ?? 'Common API'
|
|
68
|
+
return [
|
|
69
|
+
`# ${escapeMarkdownHeading(title)}`,
|
|
70
|
+
'',
|
|
71
|
+
`- **Source:** \`${context.specFile ?? 'common.yaml'}\``,
|
|
72
|
+
'',
|
|
73
|
+
'```yaml',
|
|
74
|
+
stringify(spec).trim(),
|
|
75
|
+
'```',
|
|
76
|
+
'',
|
|
77
|
+
].join('\n')
|
|
78
|
+
}
|