@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.
Files changed (157) hide show
  1. package/README.md +1 -1
  2. package/adapters/laravel/registries/codegen.registry.json +9 -9
  3. package/bin/flowgrid.mjs +326 -115
  4. package/bin/lib/agent-mcp.mjs +4 -0
  5. package/bin/lib/agent-profiles.mjs +30 -6
  6. package/bin/lib/audit-run.mjs +1 -1
  7. package/bin/lib/cli-update.mjs +48 -8
  8. package/bin/lib/doctor.mjs +90 -3
  9. package/bin/lib/harness-overlay.mjs +12 -5
  10. package/bin/lib/harness-sync.mjs +17 -3
  11. package/bin/lib/init-adapters.mjs +75 -0
  12. package/bin/lib/init-scaffold.mjs +9 -0
  13. package/bin/lib/inject-consumer-scripts.mjs +115 -0
  14. package/bin/lib/merge-stack-config.mjs +89 -0
  15. package/bin/lib/project-gitignore.mjs +1 -0
  16. package/bin/lib/repo-maps-align.mjs +203 -0
  17. package/dist/graph/config/load-config.js +7 -2
  18. package/dist/graph/config/load-config.js.map +1 -1
  19. package/dist/graph/mcp/tools.js +1 -1
  20. package/dist/graph/mcp/tools.js.map +1 -1
  21. package/dist/graph/registry/load-registries.d.ts +1 -0
  22. package/dist/graph/registry/load-registries.js +14 -1
  23. package/dist/graph/registry/load-registries.js.map +1 -1
  24. package/engines/cases/render-cases.mjs +67 -5
  25. package/engines/docs/lib/render-api-summary-markdown.mjs +28 -0
  26. package/engines/docs/lib/render-bundle-markdown.mjs +77 -2
  27. package/engines/docs/lib/render-data-model-markdown.mjs +171 -0
  28. package/engines/docs/lib/render-design-tables.mjs +89 -7
  29. package/engines/docs/lib/render-template.mjs +6 -0
  30. package/engines/docs/render-docs.mjs +6 -2
  31. package/engines/docs/vitepress/config.ts +7 -7
  32. package/engines/docs/vitepress/surfaces-nav.mjs +45 -14
  33. package/engines/openapi/check-backend-spec.mjs +2 -2
  34. package/engines/openapi/lib/markdown-table.mjs +8 -0
  35. package/engines/openapi/lib/render-backend-spec-markdown.mjs +78 -0
  36. package/engines/registry-sync/be-capabilities-sync.mjs +118 -0
  37. package/engines/registry-sync/fe-design-sync.mjs +258 -0
  38. package/engines/registry-sync/run-registry-sync.mjs +107 -0
  39. package/engines/shared/e2e-output-layout.mjs +68 -0
  40. package/engines/shared/flowgrid-e2e-root.mjs +19 -0
  41. package/engines/shared/resolve-flowgrid-context.mjs +176 -0
  42. package/engines/spec/lib/audit-api-gaps.mjs +1 -1
  43. package/engines/spec/lib/audit-bundle-gaps.mjs +92 -3
  44. package/engines/spec/lib/audit-db-tables.mjs +529 -0
  45. package/engines/spec/lib/audit-e2e-coverage.mjs +88 -30
  46. package/engines/spec/lib/bundle-schema.mjs +4 -1
  47. package/engines/spec/split-bundle.mjs +11 -1
  48. package/engines/testcase/runners/generate-api.mjs +23 -23
  49. package/engines/testcase/runners/generate.mjs +19 -17
  50. package/engines/testcase/runners/lib/bootstrap-context.mjs +44 -0
  51. package/engines/testcase/runners/lib/write-files.mjs +37 -9
  52. package/harness/agents/antigravity/rules/antigravity-mcp.mdc +12 -0
  53. package/harness/agents/gemini/rules/gemini-mcp.mdc +11 -0
  54. package/harness/agents/gemini_antigravity/rules/antigravity-mcp.mdc +8 -6
  55. package/harness/be/skills/{grill-api → audit-api}/SKILL.md +8 -7
  56. package/harness/common/extracts/artifact-graph.md +2 -2
  57. package/harness/common/extracts/artifactgraph-phase-hooks.md +2 -2
  58. package/harness/common/extracts/docs-mark-detect.md +2 -2
  59. package/harness/common/extracts/entity-relationship.md +23 -0
  60. package/harness/common/rules/flowgrid-ux-common.mdc +3 -3
  61. package/harness/common/skills/configure-repo-maps/SKILL.md +4 -2
  62. package/harness/docs/extracts/agent-execution-protocol.md +1 -1
  63. package/harness/docs/extracts/api-codegen-readiness.md +34 -0
  64. package/harness/docs/extracts/api-codegen-tags.md +30 -0
  65. package/harness/docs/extracts/api-contract.md +43 -0
  66. package/harness/docs/extracts/api-spec-sync.md +35 -0
  67. package/harness/docs/extracts/artifactgraph-hooks-docs.md +1 -1
  68. package/harness/docs/extracts/call-external.md +16 -0
  69. package/harness/docs/extracts/common-scope.md +9 -10
  70. package/harness/docs/extracts/db-audit-wizard.md +45 -0
  71. package/harness/docs/extracts/derived-data.md +18 -0
  72. package/harness/docs/extracts/design-leaf-signoff.md +16 -0
  73. package/harness/docs/extracts/extract-registry.docs.json +9 -1
  74. package/harness/docs/extracts/spec-core.md +7 -3
  75. package/harness/docs/extracts/spec-evolution.md +21 -0
  76. package/harness/docs/extracts/spec-prd-lite.md +19 -0
  77. package/harness/docs/extracts/spec-requirement.md +6 -2
  78. package/harness/docs/extracts/spec-ssot-prep.md +25 -0
  79. package/harness/docs/extracts/tpl-module.md +12 -0
  80. package/harness/docs/extracts/verify-gate.md +33 -0
  81. package/harness/docs/extracts/wire-spec-feedback.md +31 -0
  82. package/harness/docs/rules/agent-compliance.mdc +1 -1
  83. package/harness/docs/rules/team-flow-spec.mdc +3 -4
  84. package/harness/docs/skills/adopt/SKILL.md +2 -0
  85. package/harness/docs/skills/api/SKILL.md +4 -5
  86. package/harness/docs/skills/api-spec/SKILL.md +19 -6
  87. package/harness/docs/skills/api-update/SKILL.md +3 -3
  88. package/harness/docs/skills/architecture/SKILL.md +1 -1
  89. package/harness/docs/skills/business-process/SKILL.md +2 -0
  90. package/harness/docs/skills/common/SKILL.md +2 -2
  91. package/harness/docs/skills/common-spec/SKILL.md +10 -47
  92. package/harness/docs/skills/db-erd/SKILL.md +26 -0
  93. package/harness/docs/skills/grill/SKILL.md +28 -22
  94. package/harness/docs/skills/grill-api/SKILL.md +4 -6
  95. package/harness/docs/skills/grill-api-spec/SKILL.md +44 -24
  96. package/harness/docs/skills/grill-bqa/SKILL.md +28 -13
  97. package/harness/docs/skills/grill-common-spec/SKILL.md +10 -35
  98. package/harness/docs/skills/grill-dev/SKILL.md +7 -6
  99. package/harness/docs/skills/grill-docs/SKILL.md +10 -3
  100. package/harness/docs/skills/module/SKILL.md +3 -1
  101. package/harness/docs/skills/openapi/SKILL.md +2 -1
  102. package/harness/docs/skills/overview/SKILL.md +7 -1
  103. package/harness/docs/skills/spec/SKILL.md +42 -9
  104. package/harness/docs/skills/update-spec/SKILL.md +4 -1
  105. package/harness/fe/extracts/wire-audit-loop.md +72 -0
  106. package/harness/fe/extracts/wire-phase.md +45 -0
  107. package/harness/fe/rules/platform-design-vocabulary.mdc +1 -1
  108. package/harness/fe/rules/team-flow-prototype.mdc +8 -4
  109. package/harness/fe/skills/gen-common/SKILL.md +11 -84
  110. package/harness/fe/skills/grill-prototype/SKILL.md +51 -25
  111. package/harness/fe/skills/grill-test/SKILL.md +78 -20
  112. package/harness/fe/skills/grill-wire/SKILL.md +81 -0
  113. package/harness/fe/skills/prototype/SKILL.md +3 -2
  114. package/harness/fe/skills/wire/SKILL.md +7 -2
  115. package/harness/shared/AGENTS.md +3 -3
  116. package/harness/shared/SSOT_AGENT_PROTOCOL.md +3 -3
  117. package/harness/tests/extracts/grill-api-hook.md +69 -0
  118. package/harness/tests/extracts/grill-scenario-flow.md +39 -0
  119. package/harness/tests/extracts/grill-screen-tc.md +40 -0
  120. package/harness/tests/extracts/testcase-gen-cli.md +57 -0
  121. package/harness/tests/extracts/testcase-plan.md +29 -0
  122. package/harness/tests/extracts/tests-verify-gate.md +29 -0
  123. package/harness/tests/extracts/wire-test-handoff.md +37 -0
  124. package/harness/tests/skills/grill-testcase/SKILL.md +1 -0
  125. package/harness/tests/skills/test-api/SKILL.md +14 -6
  126. package/harness/tests/skills/testcase/SKILL.md +3 -1
  127. package/harness/tests/templates/TC.example-api.yaml +7 -1
  128. package/harness/tests/templates/TC.example.yaml +4 -1
  129. package/harness/tests/templates/tpl-testcase-plan.md +75 -0
  130. package/package.json +1 -1
  131. package/stacks/fastapi.json +1 -0
  132. package/stacks/laravel.json +1 -0
  133. package/stacks/nestjs.json +72 -0
  134. package/stacks/nextjs-nest.json +1 -0
  135. package/stacks/nuxt4-nest.json +1 -0
  136. package/templates/project-skeleton/architecture/03-business-process/FLOW-template.md +2 -0
  137. package/templates/project-skeleton/overview/index.md +68 -2
  138. package/templates/project-skeleton/overview/operational-areas/_template.md +37 -0
  139. package/templates/project-skeleton/surfaces/common/data-model/index.md +10 -2
  140. package/templates/shared/api-03-mock.stub.yaml +14 -0
  141. package/templates/shared/backend-api.bundle.yaml +3 -0
  142. package/templates/shared/backend-api.yaml +4 -0
  143. package/templates/shared/be-capabilities.registry.base.json +8 -0
  144. package/templates/shared/bundle-authoring.md +39 -3
  145. package/templates/shared/default-layout.ejs +131 -18
  146. package/templates/shared/design-spec.yaml +2 -3
  147. package/templates/shared/design.registry.base.json +38 -0
  148. package/templates/shared/feature.bundle.yaml +16 -9
  149. package/templates/shared/ir/generated/spec.md +281 -0
  150. package/templates/shared/qa-item.yaml +1 -1
  151. package/templates/shared/tpl-api-contract.md +133 -0
  152. package/templates/shared/tpl-screen-data-model.md +76 -0
  153. package/templates/tests-skeleton/cases/README.md +4 -0
  154. package/templates/tests-skeleton/catalog/locale.yaml +9 -0
  155. package/templates/tests-skeleton/tpl-testcase-plan.md +9 -0
  156. package/harness/docs/skills/api-integration/SKILL.md +0 -110
  157. 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
- return writeIrSpecMarkdownFile(specFile, irGeneratedMarkdownPath(bundleFile), context)
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(spec, {
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 '../../scripts/backend-api/render-backend-spec.mjs'
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 spec = path.join(dirPath, 'ir', 'generated', 'spec.md')
93
- if (fs.existsSync(spec)) {
94
- items.push({
95
- text: mdTitleFromFile(spec, 'spec'),
96
- link: `${urlPrefix}ir/generated/spec`,
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 pageId = pageIdFromLeaf(full)
123
- const specMd = path.join(full, 'ir', 'generated', 'spec.md')
124
-
125
- // Bỏ qua nếu chưa chạy split (chưa có spec.md)
126
- if (!existsSync(specMd)) continue
127
-
128
- const href = toHref(specMd)
129
- children.push({
130
- text: `${ent.name} + ${pageId}`,
131
- href,
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
- if (node.items?.length) {
167
- item.items = node.items.map(toVitepressItem)
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 parseArgs(argv) {
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(parseArgs(process.argv.slice(2)), process.cwd())
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,8 @@
1
+ export {
2
+ MD_NONE,
3
+ escapeMarkdownHeading,
4
+ escapeCell,
5
+ renderTable,
6
+ objectRows,
7
+ formatCellValue,
8
+ } from '../../docs/lib/markdown-table.mjs'
@@ -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
+ }