@shanyucoder/flowgrid 0.1.5 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/adapters/laravel/registries/codegen.registry.json +9 -9
- package/bin/flowgrid.mjs +326 -115
- package/bin/lib/agent-mcp.mjs +4 -0
- package/bin/lib/agent-profiles.mjs +30 -6
- package/bin/lib/audit-run.mjs +1 -1
- package/bin/lib/cli-update.mjs +48 -8
- package/bin/lib/doctor.mjs +90 -3
- package/bin/lib/harness-overlay.mjs +12 -5
- package/bin/lib/harness-sync.mjs +17 -3
- package/bin/lib/init-adapters.mjs +75 -0
- package/bin/lib/init-scaffold.mjs +9 -0
- package/bin/lib/inject-consumer-scripts.mjs +115 -0
- package/bin/lib/merge-stack-config.mjs +89 -0
- package/bin/lib/project-gitignore.mjs +1 -0
- package/bin/lib/repo-maps-align.mjs +203 -0
- package/dist/graph/config/load-config.js +7 -2
- package/dist/graph/config/load-config.js.map +1 -1
- package/dist/graph/mcp/tools.js +1 -1
- package/dist/graph/mcp/tools.js.map +1 -1
- package/dist/graph/registry/load-registries.d.ts +1 -0
- package/dist/graph/registry/load-registries.js +14 -1
- package/dist/graph/registry/load-registries.js.map +1 -1
- package/engines/cases/render-cases.mjs +67 -5
- package/engines/docs/lib/qa-item.mjs +91 -0
- package/engines/docs/lib/render-api-summary-markdown.mjs +28 -0
- package/engines/docs/lib/render-bundle-markdown.mjs +77 -2
- package/engines/docs/lib/render-data-model-markdown.mjs +171 -0
- package/engines/docs/lib/render-design-tables.mjs +89 -7
- package/engines/docs/lib/render-qa-list.mjs +123 -25
- package/engines/docs/lib/render-template.mjs +6 -0
- package/engines/docs/render-docs.mjs +6 -2
- package/engines/docs/vitepress/config.ts +7 -7
- package/engines/docs/vitepress/surfaces-nav.mjs +45 -14
- package/engines/openapi/check-backend-spec.mjs +2 -2
- package/engines/openapi/lib/markdown-table.mjs +8 -0
- package/engines/openapi/lib/render-backend-spec-markdown.mjs +78 -0
- package/engines/registry-sync/be-capabilities-sync.mjs +118 -0
- package/engines/registry-sync/fe-design-sync.mjs +258 -0
- package/engines/registry-sync/run-registry-sync.mjs +107 -0
- package/engines/shared/e2e-output-layout.mjs +68 -0
- package/engines/shared/flowgrid-e2e-root.mjs +19 -0
- package/engines/shared/resolve-flowgrid-context.mjs +176 -0
- package/engines/spec/lib/audit-api-gaps.mjs +1 -1
- package/engines/spec/lib/audit-bundle-gaps.mjs +92 -3
- package/engines/spec/lib/audit-db-tables.mjs +529 -0
- package/engines/spec/lib/audit-e2e-coverage.mjs +88 -30
- package/engines/spec/lib/bundle-schema.mjs +4 -1
- package/engines/spec/lib/open-qa.mjs +71 -21
- package/engines/spec/split-bundle.mjs +11 -1
- package/engines/testcase/runners/generate-api.mjs +23 -23
- package/engines/testcase/runners/generate.mjs +19 -17
- package/engines/testcase/runners/lib/bootstrap-context.mjs +44 -0
- package/engines/testcase/runners/lib/write-files.mjs +37 -9
- package/harness/agents/antigravity/rules/antigravity-mcp.mdc +12 -0
- package/harness/agents/gemini/rules/gemini-mcp.mdc +11 -0
- package/harness/agents/gemini_antigravity/rules/antigravity-mcp.mdc +8 -6
- package/harness/be/skills/{grill-api → audit-api}/SKILL.md +8 -7
- package/harness/common/extracts/artifact-graph.md +2 -2
- package/harness/common/extracts/artifactgraph-phase-hooks.md +2 -2
- package/harness/common/extracts/docs-mark-detect.md +2 -2
- package/harness/common/extracts/entity-relationship.md +23 -0
- package/harness/common/rules/flowgrid-ux-common.mdc +3 -3
- package/harness/common/skills/configure-repo-maps/SKILL.md +4 -2
- package/harness/docs/extracts/agent-execution-protocol.md +3 -3
- package/harness/docs/extracts/api-codegen-readiness.md +34 -0
- package/harness/docs/extracts/api-codegen-tags.md +30 -0
- package/harness/docs/extracts/api-contract.md +43 -0
- package/harness/docs/extracts/api-spec-sync.md +35 -0
- package/harness/docs/extracts/artifactgraph-hooks-docs.md +1 -1
- package/harness/docs/extracts/call-external.md +16 -0
- package/harness/docs/extracts/common-scope.md +9 -10
- package/harness/docs/extracts/db-audit-wizard.md +45 -0
- package/harness/docs/extracts/derived-data.md +18 -0
- package/harness/docs/extracts/design-leaf-signoff.md +16 -0
- package/harness/docs/extracts/extract-registry.docs.json +11 -2
- package/harness/docs/extracts/qa-inbox.md +19 -10
- package/harness/docs/extracts/qa-team.md +32 -0
- package/harness/docs/extracts/spec-core.md +7 -3
- package/harness/docs/extracts/spec-evolution.md +21 -0
- package/harness/docs/extracts/spec-prd-lite.md +19 -0
- package/harness/docs/extracts/spec-requirement.md +6 -2
- package/harness/docs/extracts/spec-ssot-prep.md +25 -0
- package/harness/docs/extracts/tpl-module.md +12 -0
- package/harness/docs/extracts/verify-gate.md +33 -0
- package/harness/docs/extracts/wire-spec-feedback.md +31 -0
- package/harness/docs/rules/agent-compliance.mdc +1 -1
- package/harness/docs/rules/team-flow-spec.mdc +3 -4
- package/harness/docs/schemas/flowgrid-docs/qa-item.schema.json +65 -0
- package/harness/docs/skills/adopt/SKILL.md +2 -0
- package/harness/docs/skills/api/SKILL.md +4 -5
- package/harness/docs/skills/api-spec/SKILL.md +19 -6
- package/harness/docs/skills/api-update/SKILL.md +4 -4
- package/harness/docs/skills/architecture/SKILL.md +1 -1
- package/harness/docs/skills/business-process/SKILL.md +2 -0
- package/harness/docs/skills/common/SKILL.md +2 -2
- package/harness/docs/skills/common-spec/SKILL.md +10 -47
- package/harness/docs/skills/db-erd/SKILL.md +26 -0
- package/harness/docs/skills/grill/SKILL.md +28 -22
- package/harness/docs/skills/grill-api/SKILL.md +4 -6
- package/harness/docs/skills/grill-api-spec/SKILL.md +44 -24
- package/harness/docs/skills/grill-bqa/SKILL.md +28 -13
- package/harness/docs/skills/grill-common-spec/SKILL.md +10 -35
- package/harness/docs/skills/grill-dev/SKILL.md +8 -7
- package/harness/docs/skills/grill-docs/SKILL.md +11 -4
- package/harness/docs/skills/module/SKILL.md +3 -1
- package/harness/docs/skills/openapi/SKILL.md +2 -1
- package/harness/docs/skills/overview/SKILL.md +7 -1
- package/harness/docs/skills/qa-resolve/SKILL.md +13 -12
- package/harness/docs/skills/qa-review/SKILL.md +45 -0
- package/harness/docs/skills/spec/SKILL.md +43 -10
- package/harness/docs/skills/update-spec/SKILL.md +5 -2
- package/harness/fe/extracts/wire-audit-loop.md +72 -0
- package/harness/fe/extracts/wire-phase.md +45 -0
- package/harness/fe/rules/platform-design-vocabulary.mdc +1 -1
- package/harness/fe/rules/team-flow-prototype.mdc +8 -4
- package/harness/fe/skills/gen-common/SKILL.md +11 -84
- package/harness/fe/skills/grill-prototype/SKILL.md +51 -25
- package/harness/fe/skills/grill-test/SKILL.md +78 -20
- package/harness/fe/skills/grill-wire/SKILL.md +81 -0
- package/harness/fe/skills/prototype/SKILL.md +3 -2
- package/harness/fe/skills/wire/SKILL.md +8 -3
- package/harness/shared/AGENTS.md +3 -3
- package/harness/shared/SSOT_AGENT_PROTOCOL.md +3 -3
- package/harness/tests/extracts/grill-api-hook.md +69 -0
- package/harness/tests/extracts/grill-scenario-flow.md +39 -0
- package/harness/tests/extracts/grill-screen-tc.md +40 -0
- package/harness/tests/extracts/testcase-gen-cli.md +57 -0
- package/harness/tests/extracts/testcase-plan.md +29 -0
- package/harness/tests/extracts/tests-verify-gate.md +29 -0
- package/harness/tests/extracts/wire-test-handoff.md +37 -0
- package/harness/tests/skills/grill-testcase/SKILL.md +1 -0
- package/harness/tests/skills/test-api/SKILL.md +14 -6
- package/harness/tests/skills/testcase/SKILL.md +3 -1
- package/harness/tests/templates/TC.example-api.yaml +7 -1
- package/harness/tests/templates/TC.example.yaml +4 -1
- package/harness/tests/templates/tpl-testcase-plan.md +75 -0
- package/package.json +1 -1
- package/stacks/fastapi.json +1 -0
- package/stacks/laravel.json +1 -0
- package/stacks/nestjs.json +72 -0
- package/stacks/nextjs-nest.json +1 -0
- package/stacks/nuxt4-nest.json +1 -0
- package/templates/project-skeleton/architecture/03-business-process/FLOW-template.md +2 -0
- package/templates/project-skeleton/overview/index.md +68 -2
- package/templates/project-skeleton/overview/operational-areas/_template.md +37 -0
- package/templates/project-skeleton/qa/README.md +4 -8
- package/templates/project-skeleton/surfaces/common/data-model/index.md +10 -2
- package/templates/schemas/qa-item.schema.json +98 -0
- package/templates/shared/api-03-mock.stub.yaml +14 -0
- package/templates/shared/backend-api.bundle.yaml +3 -0
- package/templates/shared/backend-api.yaml +4 -0
- package/templates/shared/be-capabilities.registry.base.json +8 -0
- package/templates/shared/bundle-authoring.md +44 -7
- package/templates/shared/default-layout.ejs +131 -18
- package/templates/shared/design-spec.yaml +2 -3
- package/templates/shared/design.registry.base.json +38 -0
- package/templates/shared/feature.bundle.yaml +16 -9
- package/templates/shared/ir/generated/spec.md +281 -0
- package/templates/shared/ir-spec.yaml +1 -1
- package/templates/shared/qa-authoring.md +78 -0
- package/templates/shared/qa-item.yaml +35 -14
- package/templates/shared/tpl-api-contract.md +133 -0
- package/templates/shared/tpl-screen-data-model.md +76 -0
- package/templates/tests-skeleton/cases/README.md +4 -0
- package/templates/tests-skeleton/catalog/locale.yaml +9 -0
- package/templates/tests-skeleton/tpl-testcase-plan.md +9 -0
- package/harness/docs/skills/api-integration/SKILL.md +0 -110
- package/harness/docs/skills/grill-integration-spec/SKILL.md +0 -51
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|