@shanyucoder/flowgrid 0.1.5 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/adapters/laravel/registries/codegen.registry.json +9 -9
  3. package/bin/flowgrid.mjs +326 -115
  4. package/bin/lib/agent-mcp.mjs +4 -0
  5. package/bin/lib/agent-profiles.mjs +30 -6
  6. package/bin/lib/audit-run.mjs +1 -1
  7. package/bin/lib/cli-update.mjs +48 -8
  8. package/bin/lib/doctor.mjs +90 -3
  9. package/bin/lib/harness-overlay.mjs +12 -5
  10. package/bin/lib/harness-sync.mjs +17 -3
  11. package/bin/lib/init-adapters.mjs +75 -0
  12. package/bin/lib/init-scaffold.mjs +9 -0
  13. package/bin/lib/inject-consumer-scripts.mjs +115 -0
  14. package/bin/lib/merge-stack-config.mjs +89 -0
  15. package/bin/lib/project-gitignore.mjs +1 -0
  16. package/bin/lib/repo-maps-align.mjs +203 -0
  17. package/dist/graph/config/load-config.js +7 -2
  18. package/dist/graph/config/load-config.js.map +1 -1
  19. package/dist/graph/mcp/tools.js +1 -1
  20. package/dist/graph/mcp/tools.js.map +1 -1
  21. package/dist/graph/registry/load-registries.d.ts +1 -0
  22. package/dist/graph/registry/load-registries.js +14 -1
  23. package/dist/graph/registry/load-registries.js.map +1 -1
  24. package/engines/cases/render-cases.mjs +67 -5
  25. package/engines/docs/lib/qa-item.mjs +91 -0
  26. package/engines/docs/lib/render-api-summary-markdown.mjs +28 -0
  27. package/engines/docs/lib/render-bundle-markdown.mjs +77 -2
  28. package/engines/docs/lib/render-data-model-markdown.mjs +171 -0
  29. package/engines/docs/lib/render-design-tables.mjs +89 -7
  30. package/engines/docs/lib/render-qa-list.mjs +123 -25
  31. package/engines/docs/lib/render-template.mjs +6 -0
  32. package/engines/docs/render-docs.mjs +6 -2
  33. package/engines/docs/vitepress/config.ts +7 -7
  34. package/engines/docs/vitepress/surfaces-nav.mjs +45 -14
  35. package/engines/openapi/check-backend-spec.mjs +2 -2
  36. package/engines/openapi/lib/markdown-table.mjs +8 -0
  37. package/engines/openapi/lib/render-backend-spec-markdown.mjs +78 -0
  38. package/engines/registry-sync/be-capabilities-sync.mjs +118 -0
  39. package/engines/registry-sync/fe-design-sync.mjs +258 -0
  40. package/engines/registry-sync/run-registry-sync.mjs +107 -0
  41. package/engines/shared/e2e-output-layout.mjs +68 -0
  42. package/engines/shared/flowgrid-e2e-root.mjs +19 -0
  43. package/engines/shared/resolve-flowgrid-context.mjs +176 -0
  44. package/engines/spec/lib/audit-api-gaps.mjs +1 -1
  45. package/engines/spec/lib/audit-bundle-gaps.mjs +92 -3
  46. package/engines/spec/lib/audit-db-tables.mjs +529 -0
  47. package/engines/spec/lib/audit-e2e-coverage.mjs +88 -30
  48. package/engines/spec/lib/bundle-schema.mjs +4 -1
  49. package/engines/spec/lib/open-qa.mjs +71 -21
  50. package/engines/spec/split-bundle.mjs +11 -1
  51. package/engines/testcase/runners/generate-api.mjs +23 -23
  52. package/engines/testcase/runners/generate.mjs +19 -17
  53. package/engines/testcase/runners/lib/bootstrap-context.mjs +44 -0
  54. package/engines/testcase/runners/lib/write-files.mjs +37 -9
  55. package/harness/agents/antigravity/rules/antigravity-mcp.mdc +12 -0
  56. package/harness/agents/gemini/rules/gemini-mcp.mdc +11 -0
  57. package/harness/agents/gemini_antigravity/rules/antigravity-mcp.mdc +8 -6
  58. package/harness/be/skills/{grill-api → audit-api}/SKILL.md +8 -7
  59. package/harness/common/extracts/artifact-graph.md +2 -2
  60. package/harness/common/extracts/artifactgraph-phase-hooks.md +2 -2
  61. package/harness/common/extracts/docs-mark-detect.md +2 -2
  62. package/harness/common/extracts/entity-relationship.md +23 -0
  63. package/harness/common/rules/flowgrid-ux-common.mdc +3 -3
  64. package/harness/common/skills/configure-repo-maps/SKILL.md +4 -2
  65. package/harness/docs/extracts/agent-execution-protocol.md +3 -3
  66. package/harness/docs/extracts/api-codegen-readiness.md +34 -0
  67. package/harness/docs/extracts/api-codegen-tags.md +30 -0
  68. package/harness/docs/extracts/api-contract.md +43 -0
  69. package/harness/docs/extracts/api-spec-sync.md +35 -0
  70. package/harness/docs/extracts/artifactgraph-hooks-docs.md +1 -1
  71. package/harness/docs/extracts/call-external.md +16 -0
  72. package/harness/docs/extracts/common-scope.md +9 -10
  73. package/harness/docs/extracts/db-audit-wizard.md +45 -0
  74. package/harness/docs/extracts/derived-data.md +18 -0
  75. package/harness/docs/extracts/design-leaf-signoff.md +16 -0
  76. package/harness/docs/extracts/extract-registry.docs.json +11 -2
  77. package/harness/docs/extracts/qa-inbox.md +19 -10
  78. package/harness/docs/extracts/qa-team.md +32 -0
  79. package/harness/docs/extracts/spec-core.md +7 -3
  80. package/harness/docs/extracts/spec-evolution.md +21 -0
  81. package/harness/docs/extracts/spec-prd-lite.md +19 -0
  82. package/harness/docs/extracts/spec-requirement.md +6 -2
  83. package/harness/docs/extracts/spec-ssot-prep.md +25 -0
  84. package/harness/docs/extracts/tpl-module.md +12 -0
  85. package/harness/docs/extracts/verify-gate.md +33 -0
  86. package/harness/docs/extracts/wire-spec-feedback.md +31 -0
  87. package/harness/docs/rules/agent-compliance.mdc +1 -1
  88. package/harness/docs/rules/team-flow-spec.mdc +3 -4
  89. package/harness/docs/schemas/flowgrid-docs/qa-item.schema.json +65 -0
  90. package/harness/docs/skills/adopt/SKILL.md +2 -0
  91. package/harness/docs/skills/api/SKILL.md +4 -5
  92. package/harness/docs/skills/api-spec/SKILL.md +19 -6
  93. package/harness/docs/skills/api-update/SKILL.md +4 -4
  94. package/harness/docs/skills/architecture/SKILL.md +1 -1
  95. package/harness/docs/skills/business-process/SKILL.md +2 -0
  96. package/harness/docs/skills/common/SKILL.md +2 -2
  97. package/harness/docs/skills/common-spec/SKILL.md +10 -47
  98. package/harness/docs/skills/db-erd/SKILL.md +26 -0
  99. package/harness/docs/skills/grill/SKILL.md +28 -22
  100. package/harness/docs/skills/grill-api/SKILL.md +4 -6
  101. package/harness/docs/skills/grill-api-spec/SKILL.md +44 -24
  102. package/harness/docs/skills/grill-bqa/SKILL.md +28 -13
  103. package/harness/docs/skills/grill-common-spec/SKILL.md +10 -35
  104. package/harness/docs/skills/grill-dev/SKILL.md +8 -7
  105. package/harness/docs/skills/grill-docs/SKILL.md +11 -4
  106. package/harness/docs/skills/module/SKILL.md +3 -1
  107. package/harness/docs/skills/openapi/SKILL.md +2 -1
  108. package/harness/docs/skills/overview/SKILL.md +7 -1
  109. package/harness/docs/skills/qa-resolve/SKILL.md +13 -12
  110. package/harness/docs/skills/qa-review/SKILL.md +45 -0
  111. package/harness/docs/skills/spec/SKILL.md +43 -10
  112. package/harness/docs/skills/update-spec/SKILL.md +5 -2
  113. package/harness/fe/extracts/wire-audit-loop.md +72 -0
  114. package/harness/fe/extracts/wire-phase.md +45 -0
  115. package/harness/fe/rules/platform-design-vocabulary.mdc +1 -1
  116. package/harness/fe/rules/team-flow-prototype.mdc +8 -4
  117. package/harness/fe/skills/gen-common/SKILL.md +11 -84
  118. package/harness/fe/skills/grill-prototype/SKILL.md +51 -25
  119. package/harness/fe/skills/grill-test/SKILL.md +78 -20
  120. package/harness/fe/skills/grill-wire/SKILL.md +81 -0
  121. package/harness/fe/skills/prototype/SKILL.md +3 -2
  122. package/harness/fe/skills/wire/SKILL.md +8 -3
  123. package/harness/shared/AGENTS.md +3 -3
  124. package/harness/shared/SSOT_AGENT_PROTOCOL.md +3 -3
  125. package/harness/tests/extracts/grill-api-hook.md +69 -0
  126. package/harness/tests/extracts/grill-scenario-flow.md +39 -0
  127. package/harness/tests/extracts/grill-screen-tc.md +40 -0
  128. package/harness/tests/extracts/testcase-gen-cli.md +57 -0
  129. package/harness/tests/extracts/testcase-plan.md +29 -0
  130. package/harness/tests/extracts/tests-verify-gate.md +29 -0
  131. package/harness/tests/extracts/wire-test-handoff.md +37 -0
  132. package/harness/tests/skills/grill-testcase/SKILL.md +1 -0
  133. package/harness/tests/skills/test-api/SKILL.md +14 -6
  134. package/harness/tests/skills/testcase/SKILL.md +3 -1
  135. package/harness/tests/templates/TC.example-api.yaml +7 -1
  136. package/harness/tests/templates/TC.example.yaml +4 -1
  137. package/harness/tests/templates/tpl-testcase-plan.md +75 -0
  138. package/package.json +1 -1
  139. package/stacks/fastapi.json +1 -0
  140. package/stacks/laravel.json +1 -0
  141. package/stacks/nestjs.json +72 -0
  142. package/stacks/nextjs-nest.json +1 -0
  143. package/stacks/nuxt4-nest.json +1 -0
  144. package/templates/project-skeleton/architecture/03-business-process/FLOW-template.md +2 -0
  145. package/templates/project-skeleton/overview/index.md +68 -2
  146. package/templates/project-skeleton/overview/operational-areas/_template.md +37 -0
  147. package/templates/project-skeleton/qa/README.md +4 -8
  148. package/templates/project-skeleton/surfaces/common/data-model/index.md +10 -2
  149. package/templates/schemas/qa-item.schema.json +98 -0
  150. package/templates/shared/api-03-mock.stub.yaml +14 -0
  151. package/templates/shared/backend-api.bundle.yaml +3 -0
  152. package/templates/shared/backend-api.yaml +4 -0
  153. package/templates/shared/be-capabilities.registry.base.json +8 -0
  154. package/templates/shared/bundle-authoring.md +44 -7
  155. package/templates/shared/default-layout.ejs +131 -18
  156. package/templates/shared/design-spec.yaml +2 -3
  157. package/templates/shared/design.registry.base.json +38 -0
  158. package/templates/shared/feature.bundle.yaml +16 -9
  159. package/templates/shared/ir/generated/spec.md +281 -0
  160. package/templates/shared/ir-spec.yaml +1 -1
  161. package/templates/shared/qa-authoring.md +78 -0
  162. package/templates/shared/qa-item.yaml +35 -14
  163. package/templates/shared/tpl-api-contract.md +133 -0
  164. package/templates/shared/tpl-screen-data-model.md +76 -0
  165. package/templates/tests-skeleton/cases/README.md +4 -0
  166. package/templates/tests-skeleton/catalog/locale.yaml +9 -0
  167. package/templates/tests-skeleton/tpl-testcase-plan.md +9 -0
  168. package/harness/docs/skills/api-integration/SKILL.md +0 -110
  169. package/harness/docs/skills/grill-integration-spec/SKILL.md +0 -51
@@ -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
+ }
@@ -0,0 +1,118 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ const REGISTRY_REL = 'registries/be-capabilities.registry.json';
5
+
6
+ const BE_SCAN_RULES = {
7
+ laravel: [
8
+ { kind: 'exceptionHandler', globs: ['app/Exceptions/Handler.php'], tags: ['#err:validation', '#err:not-found', '#err:permission-denied'] },
9
+ { kind: 'middleware', dir: 'app/Http/Middleware', tags: ['#api: middleware'] },
10
+ { kind: 'helpers', dir: 'app/Support', tags: ['#helper: support'] },
11
+ { kind: 'helpers', dir: 'app/Helpers', tags: ['#helper: app'] },
12
+ { kind: 'auth', globs: ['app/Http/Controllers/Controller.php', 'routes/api.php'], tags: ['#api: auth'] },
13
+ ],
14
+ nestjs: [
15
+ { kind: 'exceptionHandler', globs: ['src/common/filters/http-exception.filter.ts', 'src/filters/http-exception.filter.ts'], tags: ['#err:validation'] },
16
+ { kind: 'middleware', dir: 'src/common/middleware', tags: ['#api: middleware'] },
17
+ { kind: 'auth', globs: ['src/auth/auth.guard.ts', 'src/common/guards/jwt-auth.guard.ts'], tags: ['#api: auth', '#err:permission-denied'] },
18
+ { kind: 'helpers', dir: 'src/common', tags: ['#helper: common'] },
19
+ ],
20
+ fastapi: [
21
+ { kind: 'exceptionHandler', globs: ['app/core/exceptions.py', 'app/exceptions.py'], tags: ['#err:validation'] },
22
+ { kind: 'middleware', dir: 'app/middleware', tags: ['#api: middleware'] },
23
+ { kind: 'auth', globs: ['app/core/security.py', 'app/auth/deps.py'], tags: ['#api: auth'] },
24
+ ],
25
+ };
26
+
27
+ function loadJson(abs, fallback) {
28
+ if (!fs.existsSync(abs)) return fallback;
29
+ try {
30
+ return JSON.parse(fs.readFileSync(abs, 'utf8'));
31
+ } catch {
32
+ return fallback;
33
+ }
34
+ }
35
+
36
+ function listFilesInDir(root, relDir, max = 80) {
37
+ const dir = path.join(root, relDir);
38
+ if (!fs.existsSync(dir)) return [];
39
+ const out = [];
40
+ const walk = (d, depth) => {
41
+ if (depth > 4 || out.length >= max) return;
42
+ for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
43
+ const full = path.join(d, ent.name);
44
+ if (ent.isDirectory()) walk(full, depth + 1);
45
+ else if (/\.(php|ts|py)$/.test(ent.name)) {
46
+ out.push(path.relative(root, full).split(path.sep).join('/'));
47
+ }
48
+ }
49
+ };
50
+ walk(dir, 0);
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * @param {string} projectRoot
56
+ * @param {string} beAdapter
57
+ * @param {string} packageRoot
58
+ * @param {{ dryRun?: boolean }} opts
59
+ */
60
+ export function syncBeCapabilitiesRegistry(projectRoot, beAdapter, packageRoot, opts = {}) {
61
+ const rules = BE_SCAN_RULES[beAdapter] ?? BE_SCAN_RULES.laravel;
62
+ const registryPath = path.join(projectRoot, REGISTRY_REL);
63
+ const baseTemplate = path.join(packageRoot, 'templates/shared/be-capabilities.registry.base.json');
64
+ const registry = loadJson(registryPath, loadJson(baseTemplate, { version: 1, capabilities: {} }));
65
+
66
+ const capabilities = { ...(registry.capabilities ?? {}) };
67
+ const middleware = [];
68
+ const exceptionHandlers = [];
69
+ const helpers = [];
70
+
71
+ for (const rule of rules) {
72
+ if (rule.globs) {
73
+ for (const g of rule.globs) {
74
+ const abs = path.join(projectRoot, g);
75
+ if (!fs.existsSync(abs)) continue;
76
+ const entry = { path: g, tags: rule.tags, kind: rule.kind };
77
+ capabilities[g] = entry;
78
+ if (rule.kind === 'exceptionHandler') exceptionHandlers.push(entry);
79
+ else if (rule.kind === 'auth') helpers.push(entry);
80
+ }
81
+ }
82
+ if (rule.dir) {
83
+ const files = listFilesInDir(projectRoot, rule.dir);
84
+ for (const f of files) {
85
+ const entry = { path: f, tags: rule.tags, kind: rule.kind };
86
+ capabilities[f] = entry;
87
+ if (rule.kind === 'middleware') middleware.push(entry);
88
+ else helpers.push(entry);
89
+ }
90
+ }
91
+ }
92
+
93
+ const merged = {
94
+ ...registry,
95
+ adapter: beAdapter,
96
+ capabilities,
97
+ middleware,
98
+ exceptionHandlers,
99
+ helpers,
100
+ scannedAt: new Date().toISOString(),
101
+ };
102
+
103
+ const report = {
104
+ registryPath: REGISTRY_REL,
105
+ capabilityKeys: Object.keys(capabilities).length,
106
+ middleware: middleware.length,
107
+ exceptionHandlers: exceptionHandlers.length,
108
+ };
109
+
110
+ if (opts.dryRun) {
111
+ return { ok: true, dryRun: true, report, registry: merged };
112
+ }
113
+
114
+ const dir = path.dirname(registryPath);
115
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
116
+ fs.writeFileSync(registryPath, JSON.stringify(merged, null, 2) + '\n');
117
+ return { ok: true, report, registryPath };
118
+ }
@@ -0,0 +1,258 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ const REGISTRY_REL = 'registries/design.registry.json';
5
+
6
+ const UI_CANDIDATES = {
7
+ nuxt4: ['components/ui'],
8
+ nextjs: ['src/components/ui'],
9
+ };
10
+
11
+ const MOLECULE_CANDIDATES = {
12
+ nuxt4: ['components/molecules', 'components/organisms'],
13
+ nextjs: ['src/components/molecules', 'src/components/organisms', 'src/components/common'],
14
+ };
15
+
16
+ /** Vue/Nuxt composables + React hooks / shared logic */
17
+ const COMPOSABLE_CANDIDATES = {
18
+ nuxt4: ['composables', 'app/composables', 'utils'],
19
+ nextjs: ['src/hooks', 'src/composables', 'composables', 'src/lib'],
20
+ };
21
+
22
+ const SHELL_CANDIDATES = {
23
+ nuxt4: {
24
+ DataListPage: ['components/organisms/DataListPage.vue', 'components/pages/DataListPage.vue'],
25
+ DataFormPage: ['components/organisms/DataFormPage.vue'],
26
+ DataDetailPage: ['components/organisms/DataDetailPage.vue'],
27
+ },
28
+ nextjs: {
29
+ DataListPage: ['src/components/organisms/data-list-page.tsx', 'src/components/templates/data-list-page.tsx'],
30
+ DataFormPage: ['src/components/organisms/data-form-page.tsx'],
31
+ DataDetailPage: ['src/components/organisms/data-detail-page.tsx'],
32
+ },
33
+ };
34
+
35
+ function folderToPascalCase(kebab) {
36
+ return String(kebab)
37
+ .split('-')
38
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
39
+ .join('');
40
+ }
41
+
42
+ function existsFile(root, rel) {
43
+ return fs.existsSync(path.join(root, rel));
44
+ }
45
+
46
+ function loadJson(abs, fallback) {
47
+ if (!fs.existsSync(abs)) return fallback;
48
+ try {
49
+ return JSON.parse(fs.readFileSync(abs, 'utf8'));
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ }
54
+
55
+ function deepMergeRegistry(base, patch) {
56
+ const out = { ...base, ...patch };
57
+ out.shells = { ...(base.shells ?? {}), ...(patch.shells ?? {}) };
58
+ out.fieldWidgets = { ...(base.fieldWidgets ?? {}), ...(patch.fieldWidgets ?? {}) };
59
+ out.components = { ...(base.components ?? {}), ...(patch.components ?? {}) };
60
+ out.aliasIndex = { ...(base.aliasIndex ?? {}), ...(patch.aliasIndex ?? {}) };
61
+ out.componentAliases = { ...(base.componentAliases ?? {}), ...(patch.componentAliases ?? {}) };
62
+ out.composables = { ...(base.composables ?? {}), ...(patch.composables ?? {}) };
63
+ out.helpers = { ...(base.helpers ?? {}), ...(patch.helpers ?? {}) };
64
+ return out;
65
+ }
66
+
67
+ /** @param {string} base filename without ext */
68
+ function composableExportName(base) {
69
+ if (/^use[A-Z]/.test(base)) return base;
70
+ if (base.startsWith('use-')) {
71
+ const rest = base.slice(4).split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
72
+ return `use${rest}`;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function walkDirFiles(dir, exts, max = 500) {
78
+ const found = [];
79
+ if (!fs.existsSync(dir)) return found;
80
+ const stack = [dir];
81
+ while (stack.length && found.length < max) {
82
+ const current = stack.pop();
83
+ let entries;
84
+ try {
85
+ entries = fs.readdirSync(current, { withFileTypes: true });
86
+ } catch {
87
+ continue;
88
+ }
89
+ for (const ent of entries) {
90
+ const full = path.join(current, ent.name);
91
+ if (ent.isDirectory()) {
92
+ if (!['node_modules', '.git', 'dist', '.flowgrid'].includes(ent.name)) stack.push(full);
93
+ } else if (ent.isFile() && exts.some((e) => ent.name.endsWith(e))) {
94
+ found.push(full);
95
+ }
96
+ }
97
+ }
98
+ return found;
99
+ }
100
+
101
+ /**
102
+ * @param {string} projectRoot
103
+ * @param {string} feAdapter nuxt4 | nextjs
104
+ * @param {string} packageRoot flowgrid package (for base template)
105
+ * @param {{ dryRun?: boolean }} opts
106
+ */
107
+ export function syncFeDesignRegistry(projectRoot, feAdapter, packageRoot, opts = {}) {
108
+ const adapter = feAdapter === 'nuxt4' ? 'nuxt4' : 'nextjs';
109
+ const registryDir = path.join(projectRoot, 'registries');
110
+ const registryPath = path.join(projectRoot, REGISTRY_REL);
111
+ const baseTemplate = path.join(packageRoot, 'templates/shared/design.registry.base.json');
112
+ let registry = loadJson(registryPath, loadJson(baseTemplate, { version: 1, shells: {}, fieldWidgets: {}, components: {} }));
113
+
114
+ const components = { ...(registry.components ?? {}) };
115
+ const fieldWidgets = { ...(registry.fieldWidgets ?? {}) };
116
+ const shells = { ...(registry.shells ?? {}) };
117
+ const composables = { ...(registry.composables ?? {}) };
118
+ const helpers = { ...(registry.helpers ?? {}) };
119
+
120
+ for (const rel of UI_CANDIDATES[adapter] ?? UI_CANDIDATES.nextjs) {
121
+ const uiDir = path.join(projectRoot, rel);
122
+ if (!fs.existsSync(uiDir)) continue;
123
+ const entries = fs.readdirSync(uiDir, { withFileTypes: true });
124
+ for (const entry of entries) {
125
+ let canonical;
126
+ let relPath;
127
+ if (adapter === 'nuxt4' && entry.isDirectory()) {
128
+ canonical = folderToPascalCase(entry.name);
129
+ relPath = `${rel}/${entry.name}`;
130
+ } else if (adapter === 'nextjs' && entry.isFile() && /\.(tsx|ts|vue)$/.test(entry.name)) {
131
+ const base = entry.name.replace(/\.(tsx|ts|vue)$/, '');
132
+ canonical = folderToPascalCase(base);
133
+ relPath = `${rel}/${entry.name}`;
134
+ } else {
135
+ continue;
136
+ }
137
+ components[canonical] = {
138
+ canonical,
139
+ category: 'ui',
140
+ tags: [`#ui: ${canonical}`],
141
+ portal: { layer: 'ui', path: relPath, status: 'implemented' },
142
+ source: 'registry:sync',
143
+ };
144
+ fieldWidgets[canonical] = fieldWidgets[canonical] ?? {
145
+ tag: `#widget: ${canonical}`,
146
+ component: canonical,
147
+ portal: { ui: canonical, status: 'implemented' },
148
+ source: 'registry:sync',
149
+ };
150
+ }
151
+ }
152
+
153
+ const molRoots = MOLECULE_CANDIDATES[adapter] ?? [];
154
+ const molFiles = molRoots.flatMap((r) => walkDirFiles(path.join(projectRoot, r), ['.vue', '.tsx', '.ts']));
155
+ for (const abs of molFiles) {
156
+ const base = path.basename(abs).replace(/\.(vue|tsx|ts)$/, '');
157
+ const moName = base.startsWith('Mo') ? base : base.match(/^mo-/) ? `Mo${folderToPascalCase(base.slice(3))}` : null;
158
+ const key = moName ?? (base.startsWith('Data') ? base : null);
159
+ if (!key) continue;
160
+ const rel = path.relative(projectRoot, abs).split(path.sep).join('/');
161
+ fieldWidgets[key] = {
162
+ tag: `#widget: ${key}`,
163
+ component: key,
164
+ portal: { molecule: key, path: rel, status: 'implemented' },
165
+ source: 'registry:sync',
166
+ };
167
+ if (key.startsWith('Mo')) {
168
+ components[key] = {
169
+ canonical: key,
170
+ category: 'molecule',
171
+ tags: [`#needs-component: ${key}`],
172
+ portal: { layer: 'molecule', path: rel, status: 'implemented' },
173
+ source: 'registry:sync',
174
+ };
175
+ }
176
+ }
177
+
178
+ const compRoots = COMPOSABLE_CANDIDATES[adapter] ?? [];
179
+ const compFiles = compRoots.flatMap((r) =>
180
+ walkDirFiles(path.join(projectRoot, r), ['.ts', '.js', '.mts']),
181
+ );
182
+ for (const abs of compFiles) {
183
+ const base = path.basename(abs).replace(/\.(ts|js|mts)$/, '');
184
+ if (base === 'index' || base.endsWith('.test') || base.endsWith('.spec')) continue;
185
+ const rel = path.relative(projectRoot, abs).split(path.sep).join('/');
186
+ const useName = composableExportName(base);
187
+ if (useName) {
188
+ composables[useName] = {
189
+ export: useName,
190
+ tags: [`#composable: ${useName}`],
191
+ path: rel,
192
+ status: 'implemented',
193
+ source: 'registry:sync',
194
+ };
195
+ continue;
196
+ }
197
+ const helperKey = folderToPascalCase(base);
198
+ if (rel.includes('/utils/') || rel.startsWith('utils/') || rel.includes('/lib/')) {
199
+ helpers[helperKey] = {
200
+ symbol: helperKey,
201
+ tags: [`#helper: ${helperKey}`],
202
+ path: rel,
203
+ status: 'implemented',
204
+ source: 'registry:sync',
205
+ };
206
+ }
207
+ }
208
+
209
+ for (const [shellName, candidates] of Object.entries(SHELL_CANDIDATES[adapter] ?? {})) {
210
+ const hit = candidates.find((c) => existsFile(projectRoot, c));
211
+ const prev = shells[shellName] ?? registry.shells?.[shellName] ?? { tag: `#shell: ${shellName}` };
212
+ shells[shellName] = {
213
+ ...prev,
214
+ tag: prev.tag ?? `#shell: ${shellName}`,
215
+ portal: {
216
+ ...(prev.portal ?? {}),
217
+ status: hit ? 'implemented' : prev.portal?.status ?? 'planned',
218
+ path: hit ?? prev.portal?.path,
219
+ },
220
+ };
221
+ }
222
+
223
+ const componentsJson = path.join(projectRoot, 'components.json');
224
+ if (fs.existsSync(componentsJson)) {
225
+ const cj = loadJson(componentsJson, {});
226
+ const uiAlias = cj?.aliases?.ui ?? cj?.aliases?.components;
227
+ if (typeof uiAlias === 'string') {
228
+ registry._shadcnUiAlias = uiAlias;
229
+ }
230
+ }
231
+
232
+ const merged = deepMergeRegistry(registry, {
233
+ components,
234
+ fieldWidgets,
235
+ shells,
236
+ composables,
237
+ helpers,
238
+ scannedAt: new Date().toISOString(),
239
+ scannedAdapter: adapter,
240
+ });
241
+
242
+ const report = {
243
+ registryPath: REGISTRY_REL,
244
+ uiComponents: Object.keys(components).length,
245
+ composables: Object.keys(merged.composables ?? {}).length,
246
+ helpers: Object.keys(merged.helpers ?? {}).length,
247
+ fieldWidgets: Object.keys(merged.fieldWidgets ?? {}).length,
248
+ shellsImplemented: Object.values(merged.shells ?? {}).filter((s) => s.portal?.status === 'implemented').length,
249
+ };
250
+
251
+ if (opts.dryRun) {
252
+ return { ok: true, dryRun: true, report, registry: merged };
253
+ }
254
+
255
+ if (!fs.existsSync(registryDir)) fs.mkdirSync(registryDir, { recursive: true });
256
+ fs.writeFileSync(registryPath, JSON.stringify(merged, null, 2) + '\n');
257
+ return { ok: true, report, registryPath };
258
+ }