@shanyucoder/flowgrid 0.1.4 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +4 -1
  2. package/adapters/laravel/registries/codegen.registry.json +9 -9
  3. package/bin/flowgrid.mjs +353 -116
  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
@@ -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
+ }
@@ -0,0 +1,107 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { syncFeDesignRegistry } from './fe-design-sync.mjs';
5
+ import { syncBeCapabilitiesRegistry } from './be-capabilities-sync.mjs';
6
+
7
+ function packageRootFromMeta(metaUrl) {
8
+ return path.resolve(path.dirname(metaUrl), '../..');
9
+ }
10
+
11
+ async function rebuildArtifactGraphIndex(projectRoot) {
12
+ try {
13
+ const { loadEffectiveRepoConfig } = await import('../../dist/graph/config/load-config.js');
14
+ const { IndexStore } = await import('../../dist/graph/db/index-store.js');
15
+ const { loadRegistries, indexRegistries } = await import('../../dist/graph/registry/load-registries.js');
16
+ const { indexLexicons } = await import('../../dist/graph/lexicon/load-lexicon.js');
17
+ const cfg = loadEffectiveRepoConfig(projectRoot);
18
+ const store = new IndexStore(projectRoot);
19
+ store.transaction(() => {
20
+ const loaded = loadRegistries(projectRoot, cfg);
21
+ indexRegistries(store, loaded, projectRoot, cfg);
22
+ indexLexicons(store, projectRoot, cfg);
23
+ });
24
+ store.close();
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {string[]} argv
33
+ * @param {{ packageRoot?: string, projectRoot?: string, config?: object }} ctx
34
+ */
35
+ export async function runRegistrySync(argv = [], ctx = {}) {
36
+ const projectRoot = ctx.projectRoot ?? process.cwd();
37
+ const packageRoot = ctx.packageRoot ?? packageRootFromMeta(import.meta.url);
38
+ const dryRun = argv.includes('--dry-run');
39
+ const feOnly = argv.includes('--fe');
40
+ const beOnly = argv.includes('--be');
41
+
42
+ const configPath = path.join(projectRoot, '.flowgrid/config.json');
43
+ let config = ctx.config ?? {};
44
+ if (!ctx.config && fs.existsSync(configPath)) {
45
+ try {
46
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
47
+ } catch {
48
+ config = {};
49
+ }
50
+ }
51
+
52
+ const feAdapter = config.frontend?.adapter;
53
+ const beAdapter = config.backend?.adapter;
54
+ const type = config.type ?? '';
55
+ const baseProfile = config.baseProfile ?? 'standard';
56
+
57
+ const runFe =
58
+ !beOnly &&
59
+ feAdapter &&
60
+ ['nuxt4', 'nextjs'].includes(feAdapter) &&
61
+ (feOnly || ['Frontend', 'Fullstack'].includes(type) || argv.includes('--fe'));
62
+ const runBe =
63
+ !feOnly &&
64
+ beAdapter &&
65
+ (beOnly || ['Backend', 'Fullstack'].includes(type) || argv.includes('--be'));
66
+
67
+ if (baseProfile === 'custom' && !argv.includes('--force')) {
68
+ console.log(
69
+ pc.yellow(
70
+ '[registry:sync] baseProfile=custom — skip FE scan (use build-template-code). Pass --force to scan anyway.',
71
+ ),
72
+ );
73
+ if (!runBe) return { code: 0 };
74
+ }
75
+
76
+ console.log(pc.cyan('\n=== flowgrid registry:sync ==='));
77
+ console.log(` root: ${projectRoot}`);
78
+ if (dryRun) console.log(pc.magenta(' mode: dry-run'));
79
+
80
+ if (runFe) {
81
+ const fe = syncFeDesignRegistry(projectRoot, feAdapter, packageRoot, { dryRun });
82
+ console.log(pc.green('✔ FE design registry'));
83
+ console.log(
84
+ ` ${fe.report.registryPath}: ui=${fe.report.uiComponents} composables=${fe.report.composables} helpers=${fe.report.helpers} widgets=${fe.report.fieldWidgets} shells=${fe.report.shellsImplemented}`,
85
+ );
86
+ } else if (!beOnly) {
87
+ console.log(pc.gray(' (skip FE — no nuxt4/nextjs frontend in config)'));
88
+ }
89
+
90
+ if (runBe) {
91
+ const be = syncBeCapabilitiesRegistry(projectRoot, beAdapter, packageRoot, { dryRun });
92
+ console.log(pc.green('✔ BE capabilities registry'));
93
+ console.log(
94
+ ` ${be.report.registryPath}: entries=${be.report.capabilityKeys} middleware=${be.report.middleware}`,
95
+ );
96
+ } else if (!feOnly) {
97
+ console.log(pc.gray(' (skip BE — no backend adapter in config)'));
98
+ }
99
+
100
+ if (!dryRun && (runFe || runBe)) {
101
+ const ok = await rebuildArtifactGraphIndex(projectRoot);
102
+ if (ok) console.log(pc.gray(' + ArtifactGraph index rebuilt (.flowgrid/index.db)'));
103
+ }
104
+
105
+ console.log('');
106
+ return { code: 0 };
107
+ }
@@ -0,0 +1,68 @@
1
+ import path from 'node:path';
2
+
3
+ import {
4
+ API_E2E_ROOT_DIR_PARTS,
5
+ DEFAULT_E2E_ROOT_BACKEND_REL,
6
+ DEFAULT_E2E_ROOT_REL,
7
+ E2E_ROOT_DIR_PARTS,
8
+ } from './flowgrid-e2e-root.mjs';
9
+
10
+ /**
11
+ * Relative codegen + preflight trees from project root, given resolved automation root.
12
+ * @param {string} projectRoot
13
+ * @param {string | null | undefined} e2eRootAbs
14
+ * @param {{ projectType?: string }} [opts]
15
+ */
16
+ export function codegenOutputLayout(projectRoot, e2eRootAbs, opts = {}) {
17
+ const resolvedProject = path.resolve(projectRoot);
18
+ let e2eAbs = e2eRootAbs ? path.resolve(e2eRootAbs) : null;
19
+ if (!e2eAbs) {
20
+ const rel =
21
+ opts.projectType === 'Backend' ? DEFAULT_E2E_ROOT_BACKEND_REL : DEFAULT_E2E_ROOT_REL;
22
+ e2eAbs = path.join(resolvedProject, rel);
23
+ }
24
+
25
+ const uiPrefix = path.relative(resolvedProject, e2eAbs).split(path.sep).join('/');
26
+ const defaultApi = path.join(...API_E2E_ROOT_DIR_PARTS);
27
+ let apiPrefix = defaultApi;
28
+
29
+ const uiNorm = uiPrefix.replace(/\\/g, '/');
30
+ if (uiNorm === 'tests') {
31
+ apiPrefix = defaultApi;
32
+ } else if (uiNorm === 'tests/e2e' || uiNorm.endsWith('/e2e')) {
33
+ const parent = path.dirname(uiNorm);
34
+ apiPrefix = parent === '.' ? 'api-e2e' : `${parent}/api-e2e`;
35
+ }
36
+
37
+ const allowedRelRoots = [
38
+ path.join(...E2E_ROOT_DIR_PARTS),
39
+ path.join(...API_E2E_ROOT_DIR_PARTS),
40
+ uiPrefix,
41
+ apiPrefix,
42
+ ]
43
+ .map((p) => p.replace(/\\/g, '/'))
44
+ .filter((p, i, arr) => p && arr.indexOf(p) === i);
45
+
46
+ return {
47
+ projectRoot: resolvedProject,
48
+ e2eRootAbs: e2eAbs,
49
+ uiPrefix,
50
+ apiPrefix,
51
+ allowedRelRoots,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Directories to scan for Playwright specs (UI + api-e2e).
57
+ * @param {string} projectRoot
58
+ * @param {string | null | undefined} e2eRootAbs
59
+ */
60
+ export function playwrightScanRoots(projectRoot, e2eRootAbs) {
61
+ const layout = codegenOutputLayout(projectRoot, e2eRootAbs);
62
+ const roots = [layout.e2eRootAbs];
63
+ const apiAbs = path.join(layout.projectRoot, layout.apiPrefix);
64
+ if (apiAbs !== layout.e2eRootAbs) {
65
+ roots.push(apiAbs);
66
+ }
67
+ return roots;
68
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Playwright automation roots (relative to code repo).
3
+ * testcase:gen (UI) → tests/e2e; testcase:gen:api → tests/api-e2e.
4
+ * audit e2e --e2e-root scans recursively for *.spec.ts / *.api.spec.ts.
5
+ * Distinct from tests-docs hub (YAML plan) and playwright.config (often repo root).
6
+ */
7
+ export const E2E_ROOT_DIR_PARTS = ['tests', 'e2e'];
8
+ export const API_E2E_ROOT_DIR_PARTS = ['tests', 'api-e2e'];
9
+
10
+ export const DEFAULT_E2E_ROOT_REL = 'tests/e2e';
11
+ /** Backend / API-hook lane: scan parent so api-e2e + optional UI specs both count. */
12
+ export const DEFAULT_E2E_ROOT_BACKEND_REL = 'tests';
13
+
14
+ /**
15
+ * @param {'Frontend' | 'Backend' | 'Fullstack' | string} projectType
16
+ */
17
+ export function defaultE2eRootRel(projectType) {
18
+ return projectType === 'Backend' ? DEFAULT_E2E_ROOT_BACKEND_REL : DEFAULT_E2E_ROOT_REL;
19
+ }