@shanyucoder/flowgrid 0.1.10 → 0.1.12

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 (76) hide show
  1. package/adapters/nextjs/codegen/runners/lib/resolve-hub-id.mjs +7 -4
  2. package/adapters/nextjs/unitgen/runners/README.md +4 -4
  3. package/adapters/nextjs/unitgen/runners/generate.mjs +1 -1
  4. package/adapters/nuxt4/codegen/runners/lib/resolve-hub-id.mjs +7 -4
  5. package/adapters/nuxt4/unitgen/runners/README.md +4 -4
  6. package/adapters/nuxt4/unitgen/runners/generate.mjs +1 -1
  7. package/adapters/shared/common-gen.mjs +1 -1
  8. package/adapters/shared/resolve-hub-id.mjs +12 -5
  9. package/dist/docs/mcp/tools.js +59 -6
  10. package/dist/docs/mcp/tools.js.map +1 -1
  11. package/dist/docs/scan/ids.js +3 -2
  12. package/dist/docs/scan/ids.js.map +1 -1
  13. package/dist/docs/scan/screen-to-flows.d.ts +3 -0
  14. package/dist/docs/scan/screen-to-flows.js +18 -0
  15. package/dist/docs/scan/screen-to-flows.js.map +1 -0
  16. package/dist/graph/mcp/tools.js +1 -1
  17. package/dist/graph/mcp/tools.js.map +1 -1
  18. package/engines/cases/render-cases.mjs +1 -1
  19. package/engines/docs/lib/render-bundle-markdown.mjs +19 -0
  20. package/engines/docs/lib/render-design-markdown.mjs +56 -0
  21. package/engines/docs/lib/screen-to-flows-index.mjs +121 -0
  22. package/engines/docs/vitepress/surfaces-nav.mjs +7 -0
  23. package/engines/spec/lib/audit-bundle-gaps.mjs +15 -3
  24. package/engines/spec/lib/audit-interaction-cases.mjs +185 -0
  25. package/engines/spec/lib/audit-legacy-gaps.mjs +10 -0
  26. package/engines/spec/lib/bundle-ir.mjs +7 -0
  27. package/engines/spec/lib/bundle-schema.mjs +1 -0
  28. package/engines/spec/lib/interaction-cases.mjs +46 -0
  29. package/engines/spec/split-bundle.mjs +1 -1
  30. package/engines/testcase/_staging-from-portal/runners/README.md +3 -3
  31. package/engines/testcase/_staging-from-portal/runners/generate.mjs +2 -2
  32. package/engines/testcase/_staging-from-portal/runners/lib/read-testcase.mjs +1 -1
  33. package/engines/testcase/runners/README.md +3 -3
  34. package/engines/testcase/runners/generate.mjs +2 -2
  35. package/engines/testcase/runners/lib/read-testcase.mjs +1 -1
  36. package/engines/testcase/runners/lib/resolve-hub-id.mjs +3 -3
  37. package/harness/be/skills/audit-api/SKILL.md +1 -1
  38. package/harness/be/skills/grill-api-unit/SKILL.md +1 -1
  39. package/harness/docs/extracts/agent-design-context.md +124 -0
  40. package/harness/docs/extracts/architecture-core.md +2 -0
  41. package/harness/docs/extracts/extract-registry.docs.json +11 -4
  42. package/harness/docs/extracts/product-id-convention.md +58 -0
  43. package/harness/docs/extracts/spec-requirement.md +1 -1
  44. package/harness/docs/extracts/tpl-adoption-inventory.md +32 -0
  45. package/harness/docs/extracts/tpl-module.md +1 -1
  46. package/harness/docs/rules/agent-compliance.mdc +1 -1
  47. package/harness/docs/skills/adopt/SKILL.md +42 -10
  48. package/harness/docs/skills/api-spec/SKILL.md +4 -0
  49. package/harness/docs/skills/api-update/SKILL.md +1 -1
  50. package/harness/docs/skills/grill-dev/SKILL.md +22 -9
  51. package/harness/docs/skills/module/SKILL.md +1 -1
  52. package/harness/docs/skills/spec/SKILL.md +10 -6
  53. package/harness/docs/skills/surfaces/SKILL.md +1 -0
  54. package/harness/docs/skills/user-flow/SKILL.md +4 -2
  55. package/harness/fe/skills/grill-prototype/SKILL.md +1 -1
  56. package/harness/fe/skills/prototype/SKILL.md +28 -16
  57. package/harness/fe/skills/unit/SKILL.md +4 -4
  58. package/harness/shared/SSOT_AGENT_PROTOCOL.md +1 -1
  59. package/harness/tests/skills/grill-testcase/SKILL.md +1 -1
  60. package/harness/tests/skills/scenario/SKILL.md +2 -2
  61. package/harness/tests/skills/testcase/SKILL.md +1 -1
  62. package/harness/tests/templates/SC.example.md +11 -11
  63. package/harness/tests/templates/TC.example.yaml +3 -3
  64. package/package.json +1 -1
  65. package/templates/project-skeleton/architecture/03-user-flows/FLOW-login.md +10 -10
  66. package/templates/project-skeleton/architecture/04-solution-strategy/index.md +1 -1
  67. package/templates/project-skeleton/architecture/08-cross-cutting/security.md +1 -1
  68. package/templates/project-skeleton/architecture/12-glossary/index.md +3 -2
  69. package/templates/project-skeleton/architecture/model/workspace.dsl +7 -7
  70. package/templates/project-skeleton/surfaces/_module-index.template.md +2 -2
  71. package/templates/project-skeleton/surfaces/_surface-index.template.md +1 -0
  72. package/templates/shared/bundle-authoring.md +3 -2
  73. package/templates/shared/default-layout.ejs +47 -2
  74. package/templates/shared/feature.bundle.yaml +29 -1
  75. package/templates/shared/tpl-runtime-sequence.md +56 -0
  76. package/templates/tests-skeleton/cases/README.md +1 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * ir/generated/design.md — interaction case sequence diagrams only (from ir/design.yaml).
3
+ */
4
+ import path from 'node:path'
5
+
6
+ function stripMermaidFence(text) {
7
+ const t = String(text ?? '').trim()
8
+ const m = t.match(/^```(?:mermaid)?\s*([\s\S]*?)```$/m)
9
+ if (m) return m[1].trim()
10
+ return t
11
+ }
12
+
13
+ /**
14
+ * @param {object} design ir/design.yaml shape
15
+ * @param {{ pageId?: string, title?: string }} meta
16
+ */
17
+ export function renderDesignMarkdown(design, meta = {}) {
18
+ const items = design?.interactionCases?.items
19
+ if (!Array.isArray(items) || !items.length) return null
20
+
21
+ const lines = []
22
+ lines.push(`# Design sequences — ${meta.pageId || design.id || 'screen'}`)
23
+ lines.push('')
24
+ if (meta.title) {
25
+ lines.push(`> ${meta.title}`)
26
+ lines.push('')
27
+ }
28
+ lines.push(
29
+ '> Generated from `ir/design.yaml` (`interactionCases`). Prose: [spec.md](./spec.md#interaction-cases). Do not edit this file.',
30
+ )
31
+ lines.push('')
32
+
33
+ for (const item of items) {
34
+ const id = item?.id || 'IC-UNKNOWN'
35
+ lines.push(`## ${id} {#${id.toLowerCase()}}`)
36
+ lines.push('')
37
+ const body = stripMermaidFence(item.sequenceDiagram)
38
+ if (!body.includes('sequenceDiagram')) {
39
+ lines.push('```mermaid')
40
+ lines.push(body)
41
+ lines.push('```')
42
+ } else {
43
+ lines.push('```mermaid')
44
+ lines.push(body)
45
+ lines.push('```')
46
+ }
47
+ lines.push('')
48
+ }
49
+
50
+ return lines.join('\n')
51
+ }
52
+
53
+ export function irGeneratedDesignMarkdownPath(bundleFile) {
54
+ const dir = path.dirname(path.resolve(bundleFile))
55
+ return path.join(dir, 'ir', 'generated', 'design.md')
56
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Reverse index: W-* screen id → FLOW-* ids (from bundle userFlows + FLOW markdown traceability).
3
+ */
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+
7
+ const SCREEN_ID_RE = /W-[A-Z0-9][\w-]*/gi
8
+
9
+ function addLink(map, screenId, flowId) {
10
+ const flow = normalizeFlowToken(flowId)
11
+ if (!screenId || !flow) return
12
+ if (!map.has(screenId)) map.set(screenId, new Set())
13
+ map.get(screenId).add(flow)
14
+ }
15
+
16
+ function normalizeFlowToken(raw) {
17
+ const m = raw.match(/FLOW-[A-Z0-9][\w-]*/i)
18
+ if (!m) return null
19
+ const tail = m[0].slice(5)
20
+ return `FLOW-${tail}`
21
+ }
22
+
23
+ /** Parse bullets from userFlows block text (YAML literal or list lines). */
24
+ export function parseUserFlowsText(text) {
25
+ const links = []
26
+ if (!text || !text.trim()) return links
27
+ for (const line of text.split('\n')) {
28
+ const trimmed = line.replace(/^\s*-\s*/, '').trim()
29
+ if (!/FLOW-/i.test(trimmed)) continue
30
+ const flowId = normalizeFlowToken(trimmed)
31
+ if (!flowId) continue
32
+ const screens = []
33
+ const screensPart = trimmed.match(/screens:\s*(.+)/i)?.[1] ?? trimmed
34
+ for (const sm of screensPart.match(SCREEN_ID_RE) ?? []) screens.push(sm)
35
+ links.push({ flowId, screenIds: screens })
36
+ }
37
+ return links
38
+ }
39
+
40
+ function extractUserFlowsFromBundleFile(raw) {
41
+ const block = raw.match(/^userFlows:\s*\|\s*\n([\s\S]*?)(?=^[a-zA-Z][\w.-]*:|\s*$)/m)?.[1]
42
+ if (block) return parseUserFlowsText(block)
43
+ const inline = raw.match(/^userFlows:\s*(.+)$/m)?.[1]
44
+ if (inline) return parseUserFlowsText(inline)
45
+ const listBlock = raw.match(/^userFlows:\s*\n((?:\s+-\s+.+\n?)+)/m)?.[1]
46
+ if (listBlock) return parseUserFlowsText(listBlock)
47
+ return []
48
+ }
49
+
50
+ function walkBundleFiles(docsRoot, onBundle) {
51
+ const roots = [
52
+ path.join(docsRoot, 'surfaces'),
53
+ path.join(docsRoot, 'product'),
54
+ ]
55
+ const visit = (dir) => {
56
+ if (!fs.existsSync(dir)) return
57
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
58
+ if (ent.name.startsWith('.') || ent.name === 'node_modules') continue
59
+ const p = path.join(dir, ent.name)
60
+ if (ent.isDirectory()) visit(p)
61
+ else if (ent.name.endsWith('.bundle.yaml')) onBundle(p)
62
+ }
63
+ }
64
+ for (const r of roots) visit(r)
65
+ }
66
+
67
+ function walkFlowMarkdown(docsRoot, onFile) {
68
+ const catalog = path.join(docsRoot, 'architecture/03-user-flows')
69
+ if (fs.existsSync(catalog)) {
70
+ for (const n of fs.readdirSync(catalog)) {
71
+ if (n.startsWith('FLOW-') && n.endsWith('.md')) onFile(path.join(catalog, n))
72
+ }
73
+ }
74
+ const walk = (dir) => {
75
+ if (!fs.existsSync(dir)) return
76
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ if (ent.name.startsWith('.') || ent.name === 'node_modules') continue
78
+ const p = path.join(dir, ent.name)
79
+ if (ent.isDirectory()) {
80
+ if (ent.name === 'user-flows' && path.basename(dir) === 'common') {
81
+ for (const n of fs.readdirSync(p)) {
82
+ if (n.startsWith('FLOW-') && n.endsWith('.md')) onFile(path.join(p, n))
83
+ }
84
+ }
85
+ walk(p)
86
+ }
87
+ }
88
+ }
89
+ walk(path.join(docsRoot, 'surfaces'))
90
+ }
91
+
92
+ /**
93
+ * @returns {Record<string, string[]>} screenToFlows sorted flow ids per W-*
94
+ */
95
+ export function buildScreenToFlows(docsRoot) {
96
+ const map = new Map()
97
+
98
+ walkBundleFiles(docsRoot, (bundlePath) => {
99
+ const raw = fs.readFileSync(bundlePath, 'utf8')
100
+ for (const { flowId, screenIds } of extractUserFlowsFromBundleFile(raw)) {
101
+ for (const sid of screenIds) addLink(map, sid, flowId)
102
+ }
103
+ })
104
+
105
+ walkFlowMarkdown(docsRoot, (flowPath) => {
106
+ const flowId = path.basename(flowPath, '.md')
107
+ const body = fs.readFileSync(flowPath, 'utf8')
108
+ for (const sid of body.match(SCREEN_ID_RE) ?? []) addLink(map, sid, flowId)
109
+ })
110
+
111
+ const out = {}
112
+ for (const [screen, flows] of map) {
113
+ out[screen] = [...flows].sort((a, b) => a.localeCompare(b))
114
+ }
115
+ return out
116
+ }
117
+
118
+ export function linkedFlowsForScreen(docsRoot, screenId, screenToFlows) {
119
+ const idx = screenToFlows ?? buildScreenToFlows(docsRoot)
120
+ return idx[screenId] ?? []
121
+ }
@@ -93,6 +93,7 @@ export function leafGeneratedNavNodes(leafDir, toHref) {
93
93
  const specMd = path.join(leafDir, 'ir', 'generated', 'spec.md')
94
94
  const dmMd = path.join(leafDir, 'ir', 'generated', 'data-model.md')
95
95
  const apiMd = path.join(leafDir, 'ir', 'generated', 'api.md')
96
+ const designMd = path.join(leafDir, 'ir', 'generated', 'design.md')
96
97
  const nodes = []
97
98
  if (existsSync(specMd)) {
98
99
  nodes.push({
@@ -112,6 +113,12 @@ export function leafGeneratedNavNodes(leafDir, toHref) {
112
113
  link: toHref(apiMd),
113
114
  })
114
115
  }
116
+ if (existsSync(designMd)) {
117
+ nodes.push({
118
+ text: 'Design sequences',
119
+ link: toHref(designMd),
120
+ })
121
+ }
115
122
  return nodes
116
123
  }
117
124
 
@@ -20,6 +20,7 @@
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
22
  import { parse } from 'yaml';
23
+ import { auditInteractionCases } from './audit-interaction-cases.mjs';
23
24
  import { auditUxAffordance } from './audit-ux-affordance.mjs';
24
25
  import { auditDbTables, resolveBackendSpecForBundle } from './audit-db-tables.mjs';
25
26
 
@@ -581,11 +582,10 @@ function auditBundleContent(rawText, filePath, pageType) {
581
582
  addWarning(
582
583
  'WARN_NO_USER_FLOWS',
583
584
  'userFlows',
584
- 'Màn có handoff / multi-screen nhưng chưa khai báo luồng người dùng (userFlows).',
585
- 'Declare userFlows: | — link FLOW-* hoặc mô tả journey ngắn.'
585
+ 'Screen has handoff / multi-screen navigation but userFlows is not declared.',
586
+ 'Declare userFlows: | — FLOW-<id> — role: … — screens: W-* (this leaf).'
586
587
  );
587
588
  }
588
-
589
589
  if (has(rawText, 'risks:')) {
590
590
  addWarning(
591
591
  'WARN_BUNDLE_RISKS_FORBIDDEN',
@@ -618,6 +618,18 @@ function auditBundleContent(rawText, filePath, pageType) {
618
618
  );
619
619
  }
620
620
 
621
+ // =========================================================================
622
+ // Interaction cases (L2) — two-step confirm + item keys
623
+ // =========================================================================
624
+
625
+ let bundleParsed = {};
626
+ try {
627
+ bundleParsed = parse(rawText) ?? {};
628
+ } catch {
629
+ /* keep empty */
630
+ }
631
+ auditInteractionCases(bundleParsed, { addGap, addConfirm, addWarning });
632
+
621
633
  // =========================================================================
622
634
  // Result
623
635
  // =========================================================================
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Two-step audit for bundle interactionCases (see harness/docs/extracts/agent-design-context.md).
3
+ */
4
+
5
+ function countMutationActions(design) {
6
+ const actions = design?.actions
7
+ if (!Array.isArray(actions)) return 0
8
+ return actions.filter((a) => a && (a.executionContract || a.apiRef)).length
9
+ }
10
+
11
+ export function interactionCasesComplexityTriggers(bundle) {
12
+ const design = bundle?.design ?? {}
13
+ const statuses = design?.stateMatrix?.recordStatuses
14
+ const statusCount = Array.isArray(statuses) ? statuses.length : 0
15
+ const mutations = countMutationActions(design)
16
+ const scenarios = bundle?.userStories?.scenarios
17
+ const scenarioCount = Array.isArray(scenarios) ? scenarios.length : 0
18
+ const listOnly =
19
+ design?.codegen?.profile === 'list' ||
20
+ String(bundle?.gen?.codegen?.profile || '') === 'list'
21
+
22
+ return {
23
+ complex:
24
+ statusCount >= 3 ||
25
+ mutations >= 2 ||
26
+ (!listOnly && scenarioCount >= 4) ||
27
+ Boolean(design?.stateMatrix?.behaviors?.length >= 3),
28
+ statusCount,
29
+ mutations,
30
+ scenarioCount,
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @param {Record<string, unknown>} bundle parsed bundle YAML
36
+ * @param {{ addGap: Function, addConfirm: Function, addWarning: Function }} hooks
37
+ */
38
+ export function auditInteractionCases(bundle, hooks) {
39
+ const { addGap, addConfirm, addWarning } = hooks
40
+ const ic = bundle?.interactionCases
41
+ const { complex, statusCount, mutations } = interactionCasesComplexityTriggers(bundle)
42
+
43
+ if (ic == null) {
44
+ if (!complex) return
45
+ addConfirm(
46
+ 'CONFIRM_INTERACTION_CASES_POLICY',
47
+ 'interactionCases',
48
+ 'Screen has complex state/actions — declare interactionCases (IC-* + sequenceDiagram)?',
49
+ [
50
+ '(Recommended) Yes — set interactionCases.policy: required and items[]',
51
+ 'No — simple screen (set interactionCases.policy: skip + skipReason)',
52
+ 'Other — note in qa/',
53
+ ],
54
+ 0,
55
+ )
56
+ return
57
+ }
58
+
59
+ const policy = ic.policy
60
+ if (policy === 'skip') {
61
+ if (!String(ic.skipReason || '').trim()) {
62
+ addWarning(
63
+ 'INTERACTION_CASES_SKIP_NO_REASON',
64
+ 'interactionCases.skipReason',
65
+ 'interactionCases.policy is skip but skipReason is empty.',
66
+ 'Declare skipReason explaining why L2 sequences are not needed.',
67
+ )
68
+ }
69
+ return
70
+ }
71
+
72
+ const items = Array.isArray(ic.items) ? ic.items : []
73
+
74
+ if (policy === 'required' && items.length === 0) {
75
+ addGap(
76
+ 'INTERACTION_CASES_ITEMS_MISSING',
77
+ 'critical',
78
+ 'interactionCases.items',
79
+ 'interactionCases.policy is required but items[] is empty.',
80
+ 'Add at least one IC-* item with description and sequenceDiagram.',
81
+ )
82
+ return
83
+ }
84
+
85
+ if (items.length === 0) return
86
+
87
+ const actionIds = new Set(
88
+ (bundle?.design?.actions || [])
89
+ .map((a) => a?.id)
90
+ .filter(Boolean),
91
+ )
92
+ const statuses = new Set(bundle?.design?.stateMatrix?.recordStatuses || [])
93
+
94
+ for (let i = 0; i < items.length; i++) {
95
+ const item = items[i]
96
+ const base = `interactionCases.items[${i}]`
97
+ if (!item?.id) {
98
+ addGap(
99
+ 'INTERACTION_CASES_MISSING_ID',
100
+ 'critical',
101
+ `${base}.id`,
102
+ 'Interaction case missing id (use IC-*).',
103
+ 'Declare id: IC-SUBMIT-HAPPY',
104
+ )
105
+ } else if (!/^IC-[A-Z0-9][A-Z0-9-]*$/i.test(String(item.id))) {
106
+ addWarning(
107
+ 'INTERACTION_CASES_ID_FORMAT',
108
+ `${base}.id`,
109
+ `Interaction case id "${item.id}" should match IC-* pattern.`,
110
+ 'Rename to IC-<DOMAIN>-<CASE> (e.g. IC-SUBMIT-409).',
111
+ )
112
+ }
113
+ if (!String(item?.title || '').trim()) {
114
+ addGap(
115
+ 'INTERACTION_CASES_MISSING_TITLE',
116
+ 'critical',
117
+ `${base}.title`,
118
+ 'Interaction case missing title.',
119
+ 'Add short title for reviewers and agents.',
120
+ )
121
+ }
122
+ if (!String(item?.description || '').trim()) {
123
+ addGap(
124
+ 'INTERACTION_CASES_MISSING_DESCRIPTION',
125
+ 'critical',
126
+ `${base}.description`,
127
+ 'Interaction case missing description (user case prose).',
128
+ 'Describe user steps and expected UI/API feedback.',
129
+ )
130
+ }
131
+ const seq = String(item?.sequenceDiagram || '')
132
+ if (!seq.includes('sequenceDiagram')) {
133
+ addGap(
134
+ 'INTERACTION_CASES_MISSING_SEQUENCE',
135
+ 'critical',
136
+ `${base}.sequenceDiagram`,
137
+ 'Interaction case missing Mermaid sequenceDiagram.',
138
+ 'Add sequenceDiagram with alt/else for error paths when applicable.',
139
+ )
140
+ } else if (!/alt\s+/i.test(seq) && complex) {
141
+ addWarning(
142
+ 'INTERACTION_CASES_NO_ALT',
143
+ `${base}.sequenceDiagram`,
144
+ 'Sequence diagram has no alt/else block — recommend at least one error branch.',
145
+ 'Model validation, 409, or timeout with alt/else.',
146
+ )
147
+ }
148
+ const actionId = item?.links?.actionId
149
+ if (actionId && actionIds.size && !actionIds.has(actionId)) {
150
+ addWarning(
151
+ 'INTERACTION_CASES_UNKNOWN_ACTION',
152
+ `${base}.links.actionId`,
153
+ `links.actionId "${actionId}" not found in design.actions.`,
154
+ 'Align with an existing action id or add the action.',
155
+ )
156
+ }
157
+ const st = item?.links?.recordStatus
158
+ if (st && statuses.size && !statuses.has(st)) {
159
+ addWarning(
160
+ 'INTERACTION_CASES_UNKNOWN_STATUS',
161
+ `${base}.links.recordStatus`,
162
+ `links.recordStatus "${st}" not in stateMatrix.recordStatuses.`,
163
+ `Use one of: ${[...statuses].join(', ')}`,
164
+ )
165
+ }
166
+ }
167
+
168
+ if (policy === 'required' && mutations >= 2 && items.length < mutations) {
169
+ addWarning(
170
+ 'INTERACTION_CASES_FEW_ITEMS',
171
+ 'interactionCases.items',
172
+ `Only ${items.length} interaction case(s) for ${mutations} mutation action(s).`,
173
+ 'Consider one IC-* per critical mutation path (happy + conflict).',
174
+ )
175
+ }
176
+
177
+ if (complex && !policy) {
178
+ addWarning(
179
+ 'INTERACTION_CASES_POLICY_UNSET',
180
+ 'interactionCases.policy',
181
+ 'interactionCases present but policy not set to required or skip.',
182
+ 'Set policy: required or policy: skip with skipReason.',
183
+ )
184
+ }
185
+ }
@@ -42,6 +42,16 @@ function auditLegacyInventory(inventoryPath, targetId) {
42
42
 
43
43
  const rawText = fs.readFileSync(inventoryPath, 'utf8');
44
44
 
45
+ if (targetId?.startsWith('FLOW-') && !/##\s+4\.\s+User flows/i.test(rawText)) {
46
+ addGap(
47
+ 'ADOPT_MISSING_USER_FLOW_SECTION',
48
+ 'warning',
49
+ 'adoption-inventory.md#user-flows',
50
+ 'Inventory has no "## 4. User flows" section — cross-surface FLOW-* may be missing.',
51
+ 'Re-run /adopt with user-flow discovery; see tpl-adoption-inventory.md (tiers A/B/C).'
52
+ );
53
+ }
54
+
45
55
  if (targetId) {
46
56
  if (!rawText.includes(targetId)) {
47
57
  addGap(
@@ -20,6 +20,7 @@ import {
20
20
  import { applyOpenQaField } from './open-qa.mjs'
21
21
  import { applyDesignApiFrom01 } from './hydrate-design-api.mjs'
22
22
  import { isPlaceholderLegacy, projectBusinessPage } from './project-business-layout.mjs'
23
+ import { applyInteractionCasesToIr } from './interaction-cases.mjs'
23
24
 
24
25
  export function bundlePageId(bundle) {
25
26
  const v = bundle?.['page-id'] ?? bundle?.pageId ?? bundle?.id
@@ -39,6 +40,12 @@ export function buildIrFromBundle(bundle) {
39
40
  const pageId = bundlePageId(bundle)
40
41
  const legacy = { id: pageId, ...(bundle.legacy ?? {}) }
41
42
  const design = buildDesignIr(bundle, designSpec, gen)
43
+ if (bundle.interactionCases != null) {
44
+ delete spec.interactionCases
45
+ applyInteractionCasesToIr(spec, design, bundle.interactionCases)
46
+ } else if (spec.interactionCases) {
47
+ delete spec.interactionCases
48
+ }
42
49
 
43
50
  return { spec, legacy, design, designSpec, gen }
44
51
  }
@@ -15,6 +15,7 @@ export const BUNDLE_META_KEYS = [
15
15
  'scopeIn',
16
16
  'nonGoals',
17
17
  'userFlows',
18
+ 'interactionCases',
18
19
  'nfr',
19
20
  'userStories',
20
21
  'specOrigin',
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Split bundle interactionCases → ir/spec (prose) + ir/design (sequenceDiagram only).
3
+ */
4
+
5
+ /**
6
+ * @param {unknown} interactionCases
7
+ * @returns {{ specPart: object|null, designPart: object|null }}
8
+ */
9
+ export function partitionInteractionCasesForIr(interactionCases) {
10
+ if (!interactionCases || typeof interactionCases !== 'object') {
11
+ return { specPart: null, designPart: null }
12
+ }
13
+ const { policy, skipReason, items } = interactionCases
14
+ const specPart = {
15
+ ...(policy != null ? { policy } : {}),
16
+ ...(skipReason != null && String(skipReason).trim() ? { skipReason } : {}),
17
+ items: Array.isArray(items)
18
+ ? items.map((item) => {
19
+ if (!item || typeof item !== 'object') return item
20
+ const { id, title, description, links } = item
21
+ return { id, title, description, links }
22
+ })
23
+ : [],
24
+ }
25
+ const designItems = Array.isArray(items)
26
+ ? items
27
+ .filter((item) => item && typeof item === 'object' && item.sequenceDiagram)
28
+ .map(({ id, sequenceDiagram }) => ({ id, sequenceDiagram }))
29
+ : []
30
+ const designPart = designItems.length ? { items: designItems } : null
31
+ return { specPart, designPart }
32
+ }
33
+
34
+ /**
35
+ * @param {Record<string, unknown>} spec
36
+ * @param {Record<string, unknown>} design
37
+ * @param {unknown} interactionCases from bundle
38
+ */
39
+ export function applyInteractionCasesToIr(spec, design, interactionCases) {
40
+ const { specPart, designPart } = partitionInteractionCasesForIr(interactionCases)
41
+ if (!specPart && !designPart) return
42
+ if (specPart && (specPart.policy || specPart.skipReason || specPart.items?.length)) {
43
+ spec.interactionCases = specPart
44
+ }
45
+ if (designPart) design.interactionCases = designPart
46
+ }
@@ -34,7 +34,7 @@ async function main() {
34
34
  const mdRel = mdOut ? path.relative(process.cwd(), mdOut) : null
35
35
  const genDir = mdOut ? path.dirname(mdOut) : null
36
36
  const extra = genDir
37
- ? ['data-model.md', 'api.md']
37
+ ? ['data-model.md', 'api.md', 'design.md']
38
38
  .filter((name) => fs.existsSync(path.join(genDir, name)))
39
39
  .map((name) => path.relative(process.cwd(), path.join(genDir, name)))
40
40
  : []
@@ -4,10 +4,10 @@ Reads **plans** from sibling `base-tests/` → writes `tests/e2e/` on this FE re
4
4
 
5
5
  ```bash
6
6
  pnpm testcase:gen:dry --id TC-LOGIN-VALID
7
- pnpm testcase:gen --id W-AD-AUTH-001
7
+ pnpm testcase:gen --id W-ADM-AUTH-01
8
8
  pnpm testcase:gen --id smoke
9
- pnpm testcase:gen --testcase ../base-tests/cases/W-AD-AUTH-001/TC-LOGIN-VALID.yaml
10
- pnpm testcase:gen --feature W-AD-AUTH-001 # all TC-* under cases/{id}/
9
+ pnpm testcase:gen --testcase ../base-tests/cases/W-ADM-AUTH-01/TC-LOGIN-VALID.yaml
10
+ pnpm testcase:gen --feature W-ADM-AUTH-01 # all TC-* under cases/{id}/
11
11
  ```
12
12
 
13
13
  Author/edit plans on **base-tests** (`/testcase`), not on this code repo.
@@ -37,8 +37,8 @@ function parseArgs(argv) {
37
37
 
38
38
  if (!options.testcase && !options.feature && !options.id) {
39
39
  throw new Error(
40
- 'Usage: pnpm testcase:gen --id W-AD-AUTH-001|TC-LOGIN-VALID|smoke|CMP-01|SC-LOGIN\n' +
41
- ' pnpm testcase:gen --testcase <path> | --feature W-AD-AUTH-001 (screen under base-tests/cases)',
40
+ 'Usage: pnpm testcase:gen --id W-ADM-AUTH-01|TC-LOGIN-VALID|smoke|CMP-ADM-AUTH-01|SC-LOGIN\n' +
41
+ ' pnpm testcase:gen --testcase <path> | --feature W-ADM-AUTH-01 (screen under base-tests/cases)',
42
42
  )
43
43
  }
44
44
 
@@ -145,7 +145,7 @@ export async function readTestcaseFile(testcasePath, opts = {}) {
145
145
  /**
146
146
  * List TC-*.yaml under base-tests/cases/{screenId}/ (prefer --id).
147
147
  * @param {string} root FE repo root
148
- * @param {string} screenId e.g. W-AD-AUTH-001
148
+ * @param {string} screenId e.g. W-ADM-AUTH-01
149
149
  */
150
150
  export async function listFeatureTestcases(root, screenId) {
151
151
  const testsRoot = resolveProjectRoot(root, 'base-tests')
@@ -4,10 +4,10 @@ Reads **plans** from the tests hub → writes `tests/e2e/` in the FE project.
4
4
 
5
5
  ```bash
6
6
  pnpm testcase:gen:dry --id TC-LOGIN-VALID
7
- pnpm testcase:gen --id W-AD-AUTH-001
7
+ pnpm testcase:gen --id W-ADM-AUTH-01
8
8
  pnpm testcase:gen --id smoke
9
- pnpm testcase:gen --testcase "$FLOWGRID_TESTS_DOC/cases/W-AD-AUTH-001/TC-LOGIN-VALID.yaml"
10
- pnpm testcase:gen --feature W-AD-AUTH-001 # all TC-* under cases/{id}/
9
+ pnpm testcase:gen --testcase "$FLOWGRID_TESTS_DOC/cases/W-ADM-AUTH-01/TC-LOGIN-VALID.yaml"
10
+ pnpm testcase:gen --feature W-ADM-AUTH-01 # all TC-* under cases/{id}/
11
11
  ```
12
12
 
13
13
  Author/edit plans in the **tests hub** (`/testcase`), not in the code project.
@@ -51,8 +51,8 @@ function parseArgs(argv) {
51
51
 
52
52
  if (!options.testcase && !options.feature && !options.id && !options.all) {
53
53
  throw new Error(
54
- 'Usage: pnpm testcase:gen --id W-AD-AUTH-001|TC-LOGIN-VALID|smoke|CMP-01|SC-LOGIN\n' +
55
- ' pnpm testcase:gen --testcase <path> | --feature W-AD-AUTH-001 (screen under tests hub cases/)\n' +
54
+ 'Usage: pnpm testcase:gen --id W-ADM-AUTH-01|TC-LOGIN-VALID|smoke|CMP-ADM-AUTH-01|SC-LOGIN\n' +
55
+ ' pnpm testcase:gen --testcase <path> | --feature W-ADM-AUTH-01 (screen under tests hub cases/)\n' +
56
56
  ' pnpm testcase:gen --all\n' +
57
57
  ' Optional: --tests-docs <hub> or FLOWGRID_TESTS_DOC',
58
58
  )
@@ -158,7 +158,7 @@ export async function readTestcaseFile(testcasePath, opts = {}) {
158
158
  /**
159
159
  * List TC-*.yaml under the tests hub cases/{screenId}/ (prefer --id).
160
160
  * @param {string} root FE repo root
161
- * @param {string} screenId e.g. W-AD-AUTH-001
161
+ * @param {string} screenId e.g. W-ADM-AUTH-01
162
162
  */
163
163
  export async function listFeatureTestcases(root, screenId) {
164
164
  const testsRoot = process.env.FLOWGRID_TESTS_DOC ? path.resolve(process.env.FLOWGRID_TESTS_DOC) : resolveProjectRoot(root, 'tests')
@@ -263,8 +263,8 @@ export function resolveHubId(repoRoot, id, mode = 'testcase') {
263
263
  return { kind: 'code', id, paths: [spec], notes, codeDir }
264
264
  }
265
265
 
266
- // Module (e.g. CMP-ADM-001) → all code children
267
- if (/^[A-Z]+-[A-Z]+-\d{3}$/i.test(id)) {
266
+ // Module (e.g. CMP-ADM-AUTH-01) → all code children
267
+ if (/^CMP-[A-Z0-9]+(?:-[A-Z0-9]+)+$/i.test(id)) {
268
268
  const { index: docsIdx } = getDocs()
269
269
  const cmp = (docsIdx.modules || []).find(
270
270
  (c) => c.id === id || c.id.startsWith(id) || (c.slug && id.toLowerCase().includes(c.slug)),
@@ -319,6 +319,6 @@ export function resolveHubId(repoRoot, id, mode = 'testcase') {
319
319
  }
320
320
 
321
321
  throw new Error(
322
- `Unrecognized id "${id}". Use W-|API-|UI-|FLOW-|DEP-|TC-|SC-* or hierarchical ID (e.g. cmp-adm-001-01-01), or module ID (CMP-ADM-001), or suite id (smoke, regression-auth).`,
322
+ `Unrecognized id "${id}". Use W-|API-|UI-|FLOW-|DEP-|TC-|SC-* or hierarchical ID (e.g. cmp-adm-auth-01-01-01), or module ID (CMP-ADM-AUTH-01), or suite id (smoke, regression-auth).`,
323
323
  )
324
324
  }
@@ -12,7 +12,7 @@ Run after `npm run codegen:api:dry` / `flowgrid api-gen:dry` and after implement
12
12
 
13
13
  ## Target / ID Resolution Rule
14
14
 
15
- - User prompt MAY specify a function ID, API slug, or short name (e.g. `API-AUTH-001`, `login`, `CMP-ADM-009`).
15
+ - User prompt MAY specify a function ID, API slug, or short name (e.g. `API-AUTH-001`, `login`, `CMP-ADM-DEMO-01`).
16
16
  - Agent MUST resolve **`…/api/<seq>/01-backend-spec.yaml`** via `FLOWGRID_DOCS_ROOT` or `flowgrid_docs_route` (screen leaf `CMP-*/<NN…>/api/<seq>/`, or `common/yaml/<slug>/`, or external-channel surface `…/api/<seq>/`).
17
17
  - **Read the entire `01-backend-spec.yaml`**. Do **not** use `ir/design.yaml` or `ir/spec.yaml` as BE contract.
18
18
  - Compare generated routes/code against that 01 file. Missing 01 → STOP, hand off to docs `/grill-api-spec`.
@@ -13,7 +13,7 @@ before committing generated test code.
13
13
 
14
14
  ## Target / ID Resolution Rule
15
15
 
16
- - User prompt MAY specify a function ID, API slug, or short name (e.g. `API-AUTH-001`, `login`, `CMP-ADM-009`).
16
+ - User prompt MAY specify a function ID, API slug, or short name (e.g. `API-AUTH-001`, `login`, `CMP-ADM-DEMO-01`).
17
17
  - Agent MUST resolve **`…/api/<seq>/01-backend-spec.yaml`** via `FLOWGRID_DOCS_ROOT` or `flowgrid_docs_route`.
18
18
  - **Read the entire `01-backend-spec.yaml`**. Do **not** use `ir/design.yaml` or `ir/spec.yaml` as BE contract.
19
19
  - Compare generated unit tests against that 01 file. Missing 01 → STOP, hand off to docs `/grill-api-spec`.