@shanyucoder/flowgrid 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/engines/docs/lib/qa-item.mjs +91 -0
  2. package/engines/docs/lib/render-qa-list.mjs +123 -25
  3. package/engines/spec/lib/open-qa.mjs +71 -21
  4. package/harness/docs/extracts/agent-execution-protocol.md +2 -2
  5. package/harness/docs/extracts/api-codegen-readiness.md +1 -1
  6. package/harness/docs/extracts/api-spec-sync.md +1 -1
  7. package/harness/docs/extracts/call-external.md +1 -1
  8. package/harness/docs/extracts/design-leaf-signoff.md +2 -2
  9. package/harness/docs/extracts/extract-registry.docs.json +2 -1
  10. package/harness/docs/extracts/qa-inbox.md +19 -10
  11. package/harness/docs/extracts/qa-team.md +32 -0
  12. package/harness/docs/extracts/spec-evolution.md +1 -1
  13. package/harness/docs/extracts/spec-prd-lite.md +1 -1
  14. package/harness/docs/schemas/flowgrid-docs/qa-item.schema.json +65 -0
  15. package/harness/docs/skills/api-spec/SKILL.md +1 -1
  16. package/harness/docs/skills/api-update/SKILL.md +1 -1
  17. package/harness/docs/skills/grill-bqa/SKILL.md +2 -2
  18. package/harness/docs/skills/grill-dev/SKILL.md +1 -1
  19. package/harness/docs/skills/grill-docs/SKILL.md +2 -2
  20. package/harness/docs/skills/qa-resolve/SKILL.md +13 -12
  21. package/harness/docs/skills/qa-review/SKILL.md +45 -0
  22. package/harness/docs/skills/spec/SKILL.md +1 -1
  23. package/harness/docs/skills/update-spec/SKILL.md +2 -2
  24. package/harness/fe/extracts/wire-audit-loop.md +1 -1
  25. package/harness/fe/skills/grill-wire/SKILL.md +1 -1
  26. package/harness/fe/skills/wire/SKILL.md +1 -1
  27. package/package.json +1 -1
  28. package/templates/project-skeleton/qa/README.md +4 -8
  29. package/templates/schemas/qa-item.schema.json +98 -0
  30. package/templates/shared/bundle-authoring.md +5 -4
  31. package/templates/shared/ir-spec.yaml +1 -1
  32. package/templates/shared/qa-authoring.md +78 -0
  33. package/templates/shared/qa-item.yaml +35 -14
  34. package/templates/shared/tpl-api-contract.md +1 -1
@@ -0,0 +1,91 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import Ajv2020 from 'ajv/dist/2020.js'
5
+ import { parse } from 'yaml'
6
+
7
+ export const QA_ITEM_SCHEMA_ID = 'flowgrid-qa-item/v1'
8
+
9
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
10
+
11
+ let compiledValidator = null
12
+
13
+ function loadSchema() {
14
+ const schemaPath = path.join(packageRoot, 'templates/schemas/qa-item.schema.json')
15
+ return JSON.parse(readFileSync(schemaPath, 'utf8'))
16
+ }
17
+
18
+ /**
19
+ * @param {Date} [date]
20
+ * @returns {string} YYYYMMDD HH:mm
21
+ */
22
+ export function formatQaAt(date = new Date()) {
23
+ const d = date
24
+ const y = d.getFullYear()
25
+ const m = String(d.getMonth() + 1).padStart(2, '0')
26
+ const day = String(d.getDate()).padStart(2, '0')
27
+ const h = String(d.getHours()).padStart(2, '0')
28
+ const min = String(d.getMinutes()).padStart(2, '0')
29
+ return `${y}${m}${day} ${h}:${min}`
30
+ }
31
+
32
+ /**
33
+ * @param {string} screen e.g. W-HOTEL-LIST
34
+ */
35
+ export function suggestQaShort(screen) {
36
+ const s = String(screen ?? '').trim()
37
+ if (!s) return 'QA'
38
+ if (/^W-/i.test(s)) return s.slice(2).toUpperCase()
39
+ return s.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toUpperCase()
40
+ }
41
+
42
+ /**
43
+ * @param {string} hubRoot
44
+ * @param {string} shortSlug e.g. HOTEL-LIST
45
+ * @returns {string} four-digit sequence
46
+ */
47
+ export function nextQaSequence(hubRoot, shortSlug) {
48
+ const qaDir = path.join(path.resolve(hubRoot), 'qa')
49
+ const prefix = `${shortSlug}_`
50
+ let max = 0
51
+ const scan = (dir) => {
52
+ if (!existsSync(dir)) return
53
+ for (const name of readdirSync(dir)) {
54
+ if (!name.toLowerCase().startsWith(prefix.toLowerCase())) continue
55
+ const m = name.match(/_(\d{4})\.ya?ml$/i)
56
+ if (m) max = Math.max(max, Number.parseInt(m[1], 10))
57
+ }
58
+ }
59
+ scan(qaDir)
60
+ scan(path.join(qaDir, 'open'))
61
+ return String(max + 1).padStart(4, '0')
62
+ }
63
+
64
+ /**
65
+ * @param {string} hubRoot
66
+ * @param {string} shortSlug
67
+ */
68
+ export function nextQaId(hubRoot, shortSlug) {
69
+ return `${shortSlug}_${nextQaSequence(hubRoot, shortSlug)}`
70
+ }
71
+
72
+ /**
73
+ * @param {unknown} doc parsed YAML object
74
+ * @returns {{ ok: true } | { ok: false, errors: import('ajv').ErrorObject[] }}
75
+ */
76
+ export function validateQaItem(doc) {
77
+ if (!compiledValidator) {
78
+ compiledValidator = new Ajv2020({ allErrors: true }).compile(loadSchema())
79
+ }
80
+ const ok = compiledValidator(doc)
81
+ if (ok) return { ok: true }
82
+ return { ok: false, errors: compiledValidator.errors ?? [] }
83
+ }
84
+
85
+ /**
86
+ * @param {string} filePath
87
+ */
88
+ export function validateQaItemFile(filePath) {
89
+ const doc = parse(readFileSync(filePath, 'utf8'))
90
+ return validateQaItem(doc)
91
+ }
@@ -4,10 +4,23 @@ import { parse } from 'yaml'
4
4
 
5
5
  export const QA_LIST_FILE = 'qa/index.md'
6
6
 
7
- function listOpenYaml(openDir) {
7
+ function listQaYamlFiles(qaDir) {
8
+ if (!existsSync(qaDir)) return []
9
+ const out = []
10
+ for (const name of readdirSync(qaDir)) {
11
+ if (!/\.ya?ml$/i.test(name)) continue
12
+ if (name === 'index.yaml') continue
13
+ out.push(path.join(qaDir, name))
14
+ }
15
+ return out.sort()
16
+ }
17
+
18
+ function listLegacyOpenYaml(qaDir) {
19
+ const openDir = path.join(qaDir, 'open')
8
20
  if (!existsSync(openDir)) return []
9
21
  return readdirSync(openDir)
10
- .filter((n) => /^QA-.*\.ya?ml$/i.test(n))
22
+ .filter((n) => /\.ya?ml$/i.test(n))
23
+ .map((n) => path.join(openDir, n))
11
24
  .sort()
12
25
  }
13
26
 
@@ -16,41 +29,126 @@ function oneLine(text) {
16
29
  return String(text).trim().split(/\n/)[0].trim()
17
30
  }
18
31
 
32
+ function escapeCell(s) {
33
+ return String(s ?? '')
34
+ .replace(/\|/g, '\\|')
35
+ .replace(/\n/g, ' ')
36
+ .trim()
37
+ }
38
+
39
+ function lastUpdate(doc) {
40
+ const updates = Array.isArray(doc?.updates) ? doc.updates : []
41
+ if (updates.length) return updates[updates.length - 1]
42
+ return null
43
+ }
44
+
45
+ function summaryFromDoc(doc) {
46
+ const last = lastUpdate(doc)
47
+ if (last?.text) return oneLine(last.text)
48
+ if (doc?.question) return oneLine(doc.question)
49
+ if (doc?.resolution?.summary) return oneLine(doc.resolution.summary)
50
+ return oneLine(doc?.kind) || '—'
51
+ }
52
+
53
+ function lastAt(doc) {
54
+ const last = lastUpdate(doc)
55
+ if (last?.at) return String(last.at)
56
+ if (doc?.resolution?.decidedAt) return String(doc.resolution.decidedAt)
57
+ return '—'
58
+ }
59
+
60
+ function isOpen(doc) {
61
+ if (doc?.status === 'closed') return false
62
+ if (doc?.status === 'open') return true
63
+ if (doc?.resolution) return false
64
+ return true
65
+ }
66
+
19
67
  /**
20
- * Always write qa/index.md (empty body when no qa/open files).
68
+ * @param {string} full — absolute path to yaml
69
+ * @param {string} linkName — basename for markdown link
70
+ */
71
+ function catalogRow(full, linkName) {
72
+ let id = linkName.replace(/\.ya?ml$/i, '')
73
+ let screen = ''
74
+ let kind = ''
75
+ let summary = ''
76
+ let lastAtStr = '—'
77
+ let state = 'open'
78
+ try {
79
+ const doc = parse(readFileSync(full, 'utf8')) ?? {}
80
+ if (doc.id) id = String(doc.id)
81
+ if (doc.screen) screen = String(doc.screen)
82
+ if (doc.kind) kind = String(doc.kind)
83
+ summary = summaryFromDoc(doc)
84
+ lastAtStr = lastAt(doc)
85
+ state = isOpen(doc) ? 'open' : 'closed'
86
+ } catch {
87
+ /* still list */
88
+ }
89
+
90
+ const relLink = full.includes(`${path.sep}open${path.sep}`)
91
+ ? `open/${linkName}`
92
+ : linkName
93
+
94
+ return {
95
+ id,
96
+ link: `[${id}](${relLink})`,
97
+ state,
98
+ screen: screen || '—',
99
+ lastAt: lastAtStr,
100
+ summary: summary || kind || '—',
101
+ sortKey: `${lastAtStr}\t${id}`,
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Always write qa/index.md — catalog table only (SSOT = per-id YAML files).
21
107
  */
22
108
  export function writeQaList(root = process.cwd()) {
23
109
  const hub = path.resolve(root)
24
110
  const qaDir = path.join(hub, 'qa')
25
- const openDir = path.join(qaDir, 'open')
26
111
  mkdirSync(qaDir, { recursive: true })
27
112
 
28
- const lines = ['# QA', '', '_Sinh bởi `flowgrid render`. Đóng bằng `/qa-resolve`._', '']
29
- const files = listOpenYaml(openDir)
30
- if (!files.length) {
31
- lines.push('_Không có câu hỏi mở._', '')
113
+ const seen = new Set()
114
+ const rows = []
115
+ for (const full of [...listQaYamlFiles(qaDir), ...listLegacyOpenYaml(qaDir)]) {
116
+ const name = path.basename(full)
117
+ if (seen.has(name)) continue
118
+ seen.add(name)
119
+ rows.push(catalogRow(full, name))
120
+ }
121
+ rows.sort((a, b) => a.sortKey.localeCompare(b.sortKey))
122
+
123
+ const lines = [
124
+ '# QA — catalog',
125
+ '',
126
+ '_Sinh bởi `flowgrid render` — **chỉ trạng thái + link**. Nội dung SSOT: một file YAML / mã QA._',
127
+ '',
128
+ 'Workflow: [qa-team.md](../workflows/qa-team.md) · `/qa-resolve` · `/qa-review`',
129
+ '',
130
+ '| Mã | Status | Screen | Cập nhật cuối | Tóm tắt |',
131
+ '| --- | --- | --- | --- | --- |',
132
+ ]
133
+
134
+ if (!rows.length) {
135
+ lines.push('| _—_ | _empty_ | — | — | Không có QA file |', '')
32
136
  } else {
33
- for (const name of files) {
34
- const full = path.join(openDir, name)
35
- let id = name.replace(/\.ya?ml$/i, '')
36
- let kind = ''
37
- let question = ''
38
- try {
39
- const doc = parse(readFileSync(full, 'utf8')) ?? {}
40
- if (doc.id) id = String(doc.id)
41
- if (doc.kind) kind = String(doc.kind)
42
- question = oneLine(doc.question)
43
- } catch {
44
- /* list the file anyway */
45
- }
46
- const bits = [`[${id}](open/${name})`]
47
- if (kind) bits.push(kind)
48
- if (question) bits.push(question)
49
- lines.push(`- ${bits.join(' — ')}`)
137
+ for (const r of rows) {
138
+ lines.push(
139
+ `| ${r.link} | ${escapeCell(r.state)} | ${escapeCell(r.screen)} | ${escapeCell(r.lastAt)} | ${escapeCell(r.summary)} |`,
140
+ )
50
141
  }
51
142
  lines.push('')
52
143
  }
53
144
 
145
+ lines.push(
146
+ '## Đọc timeline',
147
+ '',
148
+ 'Mở file YAML — đọc `updates[]` từ trên xuống (append-only). Không copy full log vào index.',
149
+ '',
150
+ )
151
+
54
152
  const out = path.join(hub, ...QA_LIST_FILE.split('/'))
55
153
  writeFileSync(out, lines.join('\n'))
56
154
  return out
@@ -2,11 +2,20 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { parse } from 'yaml'
4
4
 
5
- /** Walk up from a bundle dir to the docs hub (`qa/open` or `architecture/`). */
5
+ function qaDirHasFiles(qaDir) {
6
+ if (!existsSync(qaDir)) return false
7
+ for (const name of readdirSync(qaDir)) {
8
+ if (/\.ya?ml$/i.test(name)) return true
9
+ if (name === 'open' && existsSync(path.join(qaDir, 'open'))) return true
10
+ }
11
+ return existsSync(path.join(qaDir, 'index.md'))
12
+ }
13
+
14
+ /** Walk up from a bundle dir to the docs hub (`qa/` or `architecture/`). */
6
15
  export function findDocsHubRoot(fromDir) {
7
16
  let dir = path.resolve(fromDir)
8
17
  for (let i = 0; i < 40; i++) {
9
- if (existsSync(path.join(dir, 'qa', 'open')) || existsSync(path.join(dir, 'architecture'))) {
18
+ if (existsSync(path.join(dir, 'architecture')) || qaDirHasFiles(path.join(dir, 'qa'))) {
10
19
  return dir
11
20
  }
12
21
  const parent = path.dirname(dir)
@@ -16,32 +25,73 @@ export function findDocsHubRoot(fromDir) {
16
25
  return null
17
26
  }
18
27
 
19
- export function openQaCsv(hubRoot, bundleId) {
20
- return getOpenQaDetails(hubRoot, bundleId).csv
28
+ function listQaYamlPaths(hubRoot) {
29
+ const qaDir = path.join(hubRoot, 'qa')
30
+ const paths = []
31
+ if (!existsSync(qaDir)) return paths
32
+ for (const name of readdirSync(qaDir)) {
33
+ if (/\.ya?ml$/i.test(name)) paths.push(path.join(qaDir, name))
34
+ }
35
+ const openDir = path.join(qaDir, 'open')
36
+ if (existsSync(openDir)) {
37
+ for (const name of readdirSync(openDir)) {
38
+ if (/\.ya?ml$/i.test(name)) paths.push(path.join(openDir, name))
39
+ }
40
+ }
41
+ return paths
42
+ }
43
+
44
+ function qaAppliesToBundle(doc, stem, bundleId, bundlePath) {
45
+ if (stem.startsWith(`QA-${bundleId}-`)) return true
46
+ if (doc?.screen && String(doc.screen) === bundleId) return true
47
+ const tp = doc?.target?.path
48
+ if (tp && bundlePath) {
49
+ const base = path.basename(bundlePath)
50
+ if (String(tp).includes(base)) return true
51
+ }
52
+ return false
53
+ }
54
+
55
+ function isOpenQa(doc) {
56
+ if (doc?.status === 'closed') return false
57
+ if (doc?.status === 'open') return true
58
+ if (doc?.resolution) return false
59
+ return true
60
+ }
61
+
62
+ function questionLine(doc) {
63
+ if (typeof doc?.question === 'string' && doc.question.trim()) {
64
+ return doc.question.trim().split('\n')[0]
65
+ }
66
+ const updates = Array.isArray(doc?.updates) ? doc.updates : []
67
+ const q = updates.find((u) => u?.kind === 'question')
68
+ if (q?.text) return String(q.text).trim().split('\n')[0]
69
+ const last = updates[updates.length - 1]
70
+ if (last?.text) return String(last.text).trim().split('\n')[0]
71
+ return ''
72
+ }
73
+
74
+ export function openQaCsv(hubRoot, bundleId, bundlePath = '') {
75
+ return getOpenQaDetails(hubRoot, bundleId, bundlePath).csv
21
76
  }
22
77
 
23
- export function getOpenQaDetails(hubRoot, bundleId) {
78
+ export function getOpenQaDetails(hubRoot, bundleId, bundlePath = '') {
24
79
  if (!hubRoot || !bundleId) return { csv: '', techDebt: [] }
25
- const openDir = path.join(hubRoot, 'qa', 'open')
26
- if (!existsSync(openDir)) return { csv: '', techDebt: [] }
27
- const prefix = `QA-${bundleId}-`
28
80
  const ids = []
29
81
  const techDebt = []
30
- for (const name of readdirSync(openDir)) {
31
- if (!/\.ya?ml$/i.test(name)) continue
32
- const stem = name.replace(/\.ya?ml$/i, '')
33
- if (!stem.startsWith(prefix)) continue
34
- let id = stem
35
- let kind = ''
36
- let question = ''
82
+ for (const full of listQaYamlPaths(hubRoot)) {
83
+ const stem = path.basename(full).replace(/\.ya?ml$/i, '')
84
+ let doc = {}
37
85
  try {
38
- const parsed = parse(readFileSync(path.join(openDir, name), 'utf8'))
39
- if (parsed && typeof parsed.id === 'string' && parsed.id.trim()) id = parsed.id.trim()
40
- if (parsed && typeof parsed.kind === 'string') kind = parsed.kind
41
- if (parsed && typeof parsed.question === 'string') question = parsed.question.trim().split('\n')[0]
86
+ doc = parse(readFileSync(full, 'utf8')) ?? {}
42
87
  } catch {
43
- /* filename */
88
+ continue
44
89
  }
90
+ if (!isOpenQa(doc)) continue
91
+ if (!qaAppliesToBundle(doc, stem, bundleId, bundlePath)) continue
92
+ const id = typeof doc.id === 'string' && doc.id.trim() ? doc.id.trim() : stem
93
+ const kind = typeof doc.kind === 'string' ? doc.kind : ''
94
+ const question = questionLine(doc)
45
95
  ids.push(id)
46
96
  if (kind === 'tech-debt') {
47
97
  techDebt.push({ id, status: 'open', summary: question })
@@ -57,7 +107,7 @@ const QA_FIELD = 'Q&A'
57
107
  /** Set or remove `Q&A` on a business spec object (comma-separated ids). */
58
108
  export function applyOpenQaField(spec, bundlePath, bundleId) {
59
109
  const hub = findDocsHubRoot(path.dirname(path.resolve(bundlePath)))
60
- const qa = getOpenQaDetails(hub, bundleId)
110
+ const qa = getOpenQaDetails(hub, bundleId, bundlePath)
61
111
  if (qa.csv) spec[QA_FIELD] = qa.csv
62
112
  else delete spec[QA_FIELD]
63
113
  if (qa.techDebt && qa.techDebt.length > 0) spec.pendingTechDebt = qa.techDebt
@@ -17,7 +17,7 @@ surfaces/<surface>/CMP-*/<slug>/ # no modules/ segment
17
17
 
18
18
  1. **PRE-FLIGHT:** First action = `{{FLOWGRID_READ_TOOL}}` / read target `SKILL.md`. Never memory.
19
19
  4. **NO RAM CACHING:** Durable results → disk immediately. Prior file = next input.
20
- 5. **ZERO BUSINESS HALLUCINATION:** Data only from User prompt or ArtifactGraph. Gaps → AskQuestion (MUST include "Log as Tech Debt" option); if member selects "Log as Tech Debt" → `qa/open/QA-<page-id>-NNNN.yaml` + `#missing_info QA-…`. No invented business fields.
20
+ 5. **ZERO BUSINESS HALLUCINATION:** Data only from User prompt or ArtifactGraph. Gaps → AskQuestion (MUST include "Log as Tech Debt" option); if member selects "Log as Tech Debt" → `qa/<SHORT>_NNNN.yaml` + `#missing_info QA-…`. No invented business fields.
21
21
  6. **GRILL HARD GATE:** AG re-check → micro-scope → propose → **STOP for Confirm** before product SSOT write.
22
22
  7. **HUMAN DSL:** Common/DSL only via `/common`, `/docs-mark`, custom-base, or grill Confirm. `/spec` consumes only (no `common/yaml`).
23
23
 
@@ -26,6 +26,6 @@ surfaces/<surface>/CMP-*/<slug>/ # no modules/ segment
26
26
  ```text
27
27
  {{FLOWGRID_READ_TOOL}} SKILL.md
28
28
  → durable writes immediately (No RAM)
29
- → AskQuestion or qa/open + grill Confirm before SSOT fill
29
+ → AskQuestion or qa + grill Confirm before SSOT fill
30
30
  → common/DSL only when human-gated
31
31
  ```
@@ -29,6 +29,6 @@ flowgrid render
29
29
 
30
30
  ## Still missing after grill?
31
31
 
32
- `#tech-debt:QA-*` + `qa/open/` — or `/qa-resolve`. Do not ship silent gaps.
32
+ `#tech-debt:QA-*` + `qa/` — or `/qa-resolve`. Do not ship silent gaps.
33
33
 
34
34
  Registry: BE checkout `registries/codegen.registry.json` (after `registry:sync`).
@@ -28,7 +28,7 @@ Path: `…/common/yaml/<slug>/01-backend-spec.yaml` — same trio rules, scan be
28
28
 
29
29
  ## Missing facts
30
30
 
31
- AskQuestion (Recommended / Other / Tech Debt) → `qa/open/QA-*` — **no** `openQuestions` in YAML.
31
+ AskQuestion (Recommended / Other / Tech Debt) → `qa/*` — **no** `openQuestions` in YAML.
32
32
 
33
33
  ## Field rename guard
34
34
 
@@ -10,7 +10,7 @@ Hashtag only (via `/api-spec`, `/grill-api-spec`, `/api-update`) — **not** a s
10
10
  ## Rules
11
11
 
12
12
  - Never edit `ir/design.yaml` for external contract detail.
13
- - No invented secrets — AskQuestion or `qa/open/`.
13
+ - No invented secrets — AskQuestion or `qa/`.
14
14
  - Grill: timeout/retry/idempotency must be explicit before `api-gen`.
15
15
 
16
16
  Hub: `docs/references/skills/call-external.md`.
@@ -4,12 +4,12 @@ Hub SSOT: `docs/workflows/design-leaf-signoff.md` — **không** engine gate.
4
4
 
5
5
  ## Rubric 6 mục (lead/BA)
6
6
 
7
- 1. `flowgrid audit spec <bundle> --type <profile>` — `gaps[]` xử lý; `confirms[]` chốt hoặc `qa/open`.
7
+ 1. `flowgrid audit spec <bundle> --type <profile>` — `gaps[]` xử lý; `confirms[]` chốt hoặc `qa`.
8
8
  2. VitePress `ir/generated/spec.md` (+ `data-model.md`) — stories, list columns, AC không placeholder.
9
9
  3. Spot 2–3 scenario ↔ `design.actions` / validation messages VI.
10
10
  4. `api/.../01-backend-spec.yaml` ↔ `ir/design.yaml` apiRef; `audit fe-be` nếu có BE.
11
11
  5. `grillStatus.dev: done` (theo flow); `gen:dry` pass; `bundle.gen` đầy đủ.
12
- 6. `qa/open` rỗng hoặc debt đã accept.
12
+ 6. `qa` rỗng hoặc debt đã accept.
13
13
 
14
14
  Optional: **Data model** — `db-erd` LCA + `design.sections[].db` / `spec.entities` (xem hub `architecture-data.md`). `/grill-prototype` SSOT issues; `registry:sync` trên FE standard base.
15
15
 
@@ -15,7 +15,8 @@
15
15
  "docs-hub": [
16
16
  ".cursor/extracts/docs-phase-hooks.md",
17
17
  ".cursor/extracts/agent-execution-protocol.md",
18
- ".cursor/extracts/qa-inbox.md"
18
+ ".cursor/extracts/qa-inbox.md",
19
+ ".cursor/extracts/qa-team.md"
19
20
  ],
20
21
  "spec-requirement": [
21
22
  ".cursor/extracts/spec-requirement.md",
@@ -7,28 +7,37 @@ There is **no** `openQuestions` on YAML. Gaps use **AskQuestion** in the skill s
7
7
  AskQuestion: MUST include your Recommended options AND an explicit "Log as Tech Debt (Pending)" option. (The UI automatically provides the 3rd "Other" option). **STOP**.
8
8
 
9
9
  - Member picks A/B/C (Recommended) → write that fact into the real spec field (`design`, `01`, …).
10
- - Member picks **Log as Tech Debt (Pending)** → create a QA file (below). Do not invent the answer.
10
+ - Member picks **Log as Tech Debt (Pending)** → create **one** `qa/<SHORT>_NNNN.yaml` with first `updates[]` line `kind: question`. Do not invent the answer.
11
11
  - Member picks **Other** and types a decision → write that text into the spec field.
12
12
 
13
13
  ## When to create a file
14
14
 
15
- 1. Read `page-id` on the feature bundle / `ir/spec.yaml` (or `feature.id` on a backend `01`). Legacy bundles may still have `id`.
16
- 2. Glob `qa/open/QA-<id>-*.yaml`. Next seq = max `NNNN` + 1, pad 4 digits. First file → `0001`.
17
- 3. Write `qa/open/QA-<id>-NNNN.yaml` from `.flowgrid/templates/qa-item.yaml`.
18
- 4. Point the spec field at the **same** id: `#missing_info QA-<id>-NNNN` and/or `#tech-debt:QA-<id>-NNNN` and/or `pendingTechDebt[].id`.
19
- 5. `flowgrid split` — `ir/spec.yaml` gets `"Q&A": QA-…-0001, QA-…-0002`.
15
+ **[MANDATORY]** Read whole `.flowgrid/templates/qa-item.yaml` + `.flowgrid/templates/qa-authoring.md` before writing.
16
+
17
+ 1. Pick **SHORT** slug (stable) — see `suggestQaShort(W-*)` in authoring doc.
18
+ 2. Next id: glob `qa/<SHORT>_*.yaml` **or** `nextQaId(hub, SHORT)` (`engines/docs/lib/qa-item.mjs`).
19
+ 3. Copy golden template → `qa/<SHORT>_NNNN.yaml`:
20
+ - `schema: flowgrid-qa-item/v1`
21
+ - `id` = basename; `status: open`; `target.path` + `target.at`; `screen` / `bundleId` / `skill` / `kind`
22
+ 4. First `updates[]`: `at` = `YYYYMMDD HH:mm` (`formatQaAt`), `kind: question`, `text` = open question.
23
+ 5. Point spec: `#missing_info <id>` / `#tech-debt:<id>` on `target.at`.
24
+ 6. `flowgrid split` — `ir/spec.yaml` Q&A lists open ids.
25
+
26
+ Optional: `validateQaItem` / schema `templates/schemas/qa-item.schema.json`.
20
27
 
21
28
  ## Close (`/qa-resolve`)
22
29
 
23
- Prompt: `/qa-resolve QA-<page-id>-NNNN` plus the **solution** on the following lines.
30
+ Prompt: `/qa-resolve <id>` plus **solution** on following lines.
31
+
32
+ Append `kind: answer` to **same file**; patch bundle/01; `status: closed`; gỡ tags; `flowgrid split`.
24
33
 
25
- That is Confirm — do **not** AskQuestion again. Skill patches `target.path`, deletes this file, gỡ tag / `pendingTechDebt`, `flowgrid split` (and `openapi:gen` if 01).
34
+ ## Review (team)
26
35
 
27
- Unknown answer → grill / `/api-spec`, not this extract.
36
+ `/qa-review <id>` → append `kind: review` to **same file** — see `qa-team.md`.
28
37
 
29
38
  ## Do not block grill
30
39
 
31
- `grillStatus` may be `done` while `qa/open/` still has files. Short `summary` prose is member review, not a QA file unless **keys** are missing.
40
+ `grillStatus` may be `done` while `status: open` QA files exist.
32
41
 
33
42
  ## Not this folder
34
43
 
@@ -0,0 +1,32 @@
1
+ # QA — one file, append timeline (docs hub)
2
+
3
+ Hub: `docs/workflows/qa-team.md`.
4
+
5
+ ## File naming
6
+
7
+ `qa/<SHORT>_<NNNN>.yaml` — e.g. `HOTEL-LIST_0001.yaml`. Next index: glob same prefix, max NNNN + 1.
8
+
9
+ `id` inside file = basename without `.yaml`.
10
+
11
+ ## Rules
12
+
13
+ 1. **One case = one file** for life of that question thread.
14
+ 2. **`updates[]` append-only** — `at` as `YYYYMMDD HH:mm`, `kind`: question | answer | review | note.
15
+ 3. **`qa/index.md`** is render-only catalog — do not put business log in index.
16
+ 4. **needs-change:** append `kind: review` with text; set `status: open`; do **not** create a second file.
17
+
18
+ ## Open (Tech Debt)
19
+
20
+ Template: `.flowgrid/templates/qa-item.yaml` · rules: `qa-authoring.md` · `schema: flowgrid-qa-item/v1`.
21
+
22
+ Create file + first update `kind: question`. Tag field: `#missing_info <id>` / `#tech-debt:<id>`.
23
+
24
+ ## /qa-resolve
25
+
26
+ Append `kind: answer` with user solution text; patch `target.path` / `target.at`; remove QA tags from spec; `status: closed`; `flowgrid split`.
27
+
28
+ ## /qa-review
29
+
30
+ Append `kind: review` only. If needs-change, `status: open`.
31
+
32
+ Legacy `qa/` paths: migrate to flat `qa/<id>.yaml` when touching old hubs.
@@ -14,7 +14,7 @@ Set `breaking: true`; require re-grill (`/grill-api-spec` or `/grill-dev`) befor
14
14
 
15
15
  ## Deferred work
16
16
 
17
- Use `pendingTechDebt[]` with `id: QA-<feature.id>-NNNN` + matching `qa/open/` file — not prose in `requirements`.
17
+ Use `pendingTechDebt[]` with `id: QA-<feature.id>-NNNN` + matching `qa/` file — not prose in `requirements`.
18
18
 
19
19
  ## OpenAPI
20
20
 
@@ -10,7 +10,7 @@
10
10
  | Functional scope | `spec.requirements`, `design.sections`, `design.actions` |
11
11
  | API | `api/<seq>/01-backend-spec.yaml` — **not** `spec.api` |
12
12
  | Tech / codegen | `gen`, `grill-dev` — not in first `/spec` pass |
13
- | Open questions | `qa/open/QA-*.yaml` |
13
+ | Open questions | `qa/*.yaml` |
14
14
 
15
15
  **Author path:** `.flowgrid/templates/feature.bundle.yaml` + `bundle-authoring.md` only. **`design-spec.yaml` deprecated.**
16
16
 
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://platform.local/schemas/flowgrid-docs/qa-item.schema.json",
4
+ "title": "FlowGrid QA inbox item",
5
+ "type": "object",
6
+ "required": ["schema", "id", "status", "target", "updates"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "schema": {
10
+ "type": "string",
11
+ "const": "flowgrid-qa-item/v1"
12
+ },
13
+ "id": {
14
+ "type": "string",
15
+ "pattern": "^[A-Z0-9][A-Z0-9._-]*_\\d{4}$"
16
+ },
17
+ "screen": { "type": "string" },
18
+ "bundleId": { "type": "string" },
19
+ "status": { "type": "string", "enum": ["open", "closed"] },
20
+ "kind": {
21
+ "type": "string",
22
+ "enum": ["customer", "choice", "tech-debt", "integration", "coverage"]
23
+ },
24
+ "skill": { "type": "string" },
25
+ "tags": { "type": "array", "items": { "type": "string" } },
26
+ "target": {
27
+ "type": "object",
28
+ "required": ["path", "at"],
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "path": { "type": "string" },
32
+ "at": { "type": "string" }
33
+ }
34
+ },
35
+ "options": {
36
+ "type": "array",
37
+ "items": {
38
+ "type": "object",
39
+ "required": ["label"],
40
+ "properties": {
41
+ "label": { "type": "string" },
42
+ "recommended": { "type": "boolean" }
43
+ }
44
+ }
45
+ },
46
+ "updates": {
47
+ "type": "array",
48
+ "minItems": 1,
49
+ "items": { "$ref": "#/$defs/update" }
50
+ }
51
+ },
52
+ "$defs": {
53
+ "update": {
54
+ "type": "object",
55
+ "required": ["at", "kind", "text"],
56
+ "additionalProperties": false,
57
+ "properties": {
58
+ "at": { "type": "string", "pattern": "^\\d{8} \\d{2}:\\d{2}$" },
59
+ "by": { "type": "string" },
60
+ "kind": { "type": "string", "enum": ["question", "answer", "review", "note"] },
61
+ "text": { "type": "string", "minLength": 1 }
62
+ }
63
+ }
64
+ }
65
+ }
@@ -110,7 +110,7 @@ Use the **same** skill (`/api-spec`) and same trio layout when there is no porta
110
110
  - **[MANDATORY]** Set `feature.source.kind`, `base: none`, `integrationRefs[]` (or equivalent refs), empty `portalRefs`, `contexts.portalLayout: none`, and `contexts.auth` (API key / HMAC / OAuth).
111
111
  - **[MANDATORY]** Domain tags: `#webhook-inbound`, `#webhook-outbound`, `#partner-api`, `#public-api`, `#call-external`, `#err:*` (include `#err:signature-invalid`, `#err:rate-limit`, `#err:unauthorized` when relevant).
112
112
  - **[MANDATORY]** Scan sibling `…/api/<seq>/` and surface `common/yaml/` before creating new endpoints; reuse via `#reuse-api` when applicable.
113
- - **[STRICTLY FORBIDDEN]** Do not invent HMAC secrets or partner validation logic; use AskQuestion or `qa/open/` tech debt.
113
+ - **[STRICTLY FORBIDDEN]** Do not invent HMAC secrets or partner validation logic; use AskQuestion or `qa/` tech debt.
114
114
 
115
115
  ---
116
116
 
@@ -66,7 +66,7 @@ Shared extracts: `api-spec-sync.md`, `spec-evolution.md`, `entity-relationship.m
66
66
 
67
67
  ## Done Criteria
68
68
 
69
- - Portal delta in `01`; or if deferred: `pendingTechDebt` with `id: QA-<feature.id>-NNNN` + `qa/open/` file.
69
+ - Portal delta in `01`; or if deferred: `pendingTechDebt` with `id: QA-<feature.id>-NNNN` + `qa/` file.
70
70
  - `source.portalRefs` current.
71
71
  - `changeLog` + version bumped.
72
72
  - Handoff: `/grill-api-spec {slug}` (re-run gates + codegen tags).
@@ -32,7 +32,7 @@ disable-model-invocation: true
32
32
  - `<pageType>` from `gen.codegen.profile` when set; else infer from prompt (list | create | detail | auth | admin-crud | …) — same table as `docs/workflows/grill-and-human-review.md`.
33
33
  - If profile unknown → AskQuestion to lock profile **before** audit (do not use `--type unknown`).
34
34
  - **[MANDATORY]** Consume `gaps[]` (patch bundle) and `confirms[]` (AskQuestion with `(Recommended)` from audit): `CONFIRM_UX_*` + **`CONFIRM_DB_*`** (`db-audit-wizard.md`).
35
- - **[MANDATORY]** After any bundle patch in Step A or B: **re-run** `flowgrid audit spec` until structural `gaps[]` empty or deferred via `qa/open/QA-<bundle.id>-NNNN.yaml`.
35
+ - **[MANDATORY]** After any bundle patch in Step A or B: **re-run** `flowgrid audit spec` until structural `gaps[]` empty or deferred via `qa/<SHORT>_NNNN.yaml`.
36
36
  - **[RECOMMENDED]** Resolve `warnings[]` (placeholders, missing metrics/non-goals) when BQA has answers; sync `userStories` if UX copy changed.
37
37
  - **[MANDATORY]** After reconcile: `flowgrid split` + `flowgrid render` — stakeholder review uses `ir/generated/spec.md`.
38
38
  - **[STRICTLY FORBIDDEN]** Skip audit and rely on manual zone review only.
@@ -44,7 +44,7 @@ disable-model-invocation: true
44
44
  - **[MANDATORY]** For `#missing_info` / open gaps: re-check ArtifactGraph → micro-scope → evaluate total gap volume:
45
45
  - **Small Scope (≤5 questions):** `AskQuestion` wizard in chat thread — **one question at a time**, **≥3 options**: (1) `(Recommended)`, (2) `Other` (free text), (3) `Log as Tech Debt (Pending)`.
46
46
  - **Large Scope (≥10 gaps):** **[MANDATORY HARD STOP IN CHAT]**. Do not spam single questions in chat. Generate an implementation plan / Plan Mode document partitioned into sequential Phases (3–5 gaps per phase) with disk offloading at boundaries.
47
- - **[MANDATORY]** If member selects "Log as Tech Debt" → create `qa/open/QA-<bundle.id>-NNNN.yaml`. Close later with `/qa-resolve`.
47
+ - **[MANDATORY]** If member selects "Log as Tech Debt" → copy from `.flowgrid/templates/qa-item.yaml` per `qa-authoring.md`; create `qa/<SHORT>_NNNN.yaml`. Close later with `/qa-resolve`.
48
48
  - **[STRICTLY FORBIDDEN]** Never write `openQuestions` in YAML. Never silently overwrite settled SSOT without explicit confirmation.
49
49
 
50
50
  ---
@@ -54,7 +54,7 @@ disable-model-invocation: true
54
54
  → **Proactively brainstorm** logical suggestions from business context in Vietnamese (e.g. login page → suggest `module: auth, entity: user`).
55
55
  - **Small Scope (≤5 questions):** Trigger `AskQuestion` wizard — **one question at a time**, ≥3 options: (1) `(Recommended)`, (2) `Other` (free text), (3) `Log as Tech Debt (Pending)`. Wait for member answer before showing next question.
56
56
  - **Large Scope (≥10 gaps/endpoints):** **[MANDATORY HARD STOP IN CHAT]**. Do not spam single questions in chat. Generate an implementation plan / Plan Mode document partitioned into sequential Phases (3–5 endpoints/gaps per phase) with disk offloading at boundaries.
57
- - ✅ If member selects "Log as Tech Debt" → create `qa/open/QA-<bundle.id>-NNNN.yaml`; maintain `grillStatus.dev: pending`.
57
+ - ✅ If member selects "Log as Tech Debt" → create `qa/<SHORT>_NNNN.yaml`; maintain `grillStatus.dev: pending`.
58
58
  - ❌ Do NOT set `grillStatus.dev: done` until profile + entity/module + endpoint actions are all verified and confirmed.
59
59
 
60
60
  ---
@@ -42,7 +42,7 @@ disable-model-invocation: true
42
42
  ## Rule: Audit interlock (sau reconcile)
43
43
 
44
44
  - **[MANDATORY]** After any bundle patch that touches `design.*`, `userStories`, `bundle.gen`, or `api` refs: run `flowgrid audit spec <bundle> --type <pageType>` (same profile rules as `/grill-dev`). Resolve `CONFIRM_DB_*` via `db-audit-wizard.md` before handoff.
45
- - Patch structural `gaps[]`; defer remainder via `qa/open/` per Law 2.
45
+ - Patch structural `gaps[]`; defer remainder via `qa/` per Law 2.
46
46
  - Address `warnings[]` when reconcile changes business prose (`summary`, `successMetrics`, `nonGoals`, `userStories`).
47
47
  - ❌ Do not hand off FE until audit has been re-run post-reconcile.
48
48
 
@@ -81,6 +81,6 @@ disable-model-invocation: true
81
81
 
82
82
  ## Verification Checklist
83
83
 
84
- - [ ] Conflicts reconciled in `*.bundle.yaml`, or deferred with `qa/open/QA-…`. No `openQuestions`.
84
+ - [ ] Conflicts reconciled in `*.bundle.yaml`, or deferred with `qa/<SHORT>_…`. No `openQuestions`.
85
85
  - [ ] `bundle.gen.codegen.profile` present and correct.
86
86
  - [ ] `flowgrid audit spec` re-run after reconcile; `flowgrid split` succeeded with zero errors.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: qa-resolve
3
3
  extractBundle: docs-hub
4
- description: EXCLUSIVE /qa-resolve — close one qa/open file. Prompt is QA id + solution. Do not use for full-screen grill or first-time /spec.
4
+ description: EXCLUSIVE /qa-resolve — append answer + close one qa/<id>.yaml. Prompt is QA id + solution. Do not use for full-screen grill or first-time /spec.
5
5
  disable-model-invocation: true
6
6
  ---
7
7
 
@@ -10,14 +10,14 @@ disable-model-invocation: true
10
10
 
11
11
  # /qa-resolve — Close One Open QA
12
12
 
13
- **When:** Member provides `QA-<page-id>-NNNN` (or `QA-<feature.id>-NNNN`) **along with** an explicit decision/solution.
13
+ **When:** Member provides `HOTEL-LIST_0001` (or legacy `QA-<page-id>-NNNN`) **along with** an explicit decision/solution.
14
14
 
15
15
  **Not this skill:**
16
16
  - Unknown answers needing brainstorming → `/grill-bqa` / `/grill-dev` / `/grill-docs` / `/api-spec`
17
17
  - FE delta without an existing QA file → `/update-spec`
18
18
  - Portal/BE sync without closing QA files → `/api-update`
19
19
 
20
- **Extract:** `.cursor/extracts/qa-inbox.md`
20
+ **Extract:** `.cursor/extracts/qa-inbox.md` · `qa-team.md`
21
21
 
22
22
  ---
23
23
 
@@ -25,8 +25,8 @@ disable-model-invocation: true
25
25
 
26
26
  | Read (whole file) | Write | NEVER do |
27
27
  |---|---|---|
28
- | `qa/open/<id>.yaml` | Patch `target.path` only | Read generated `*.md` as SSOT |
29
- | Target bundle **or** `01-backend-spec.yaml` (entire file) | Delete QA file after patch | Author `openQuestions` |
28
+ | `qa/<id>.yaml` (legacy: `qa/open/<id>.yaml`) | Patch `target.path` only | Read generated `*.md` as SSOT |
29
+ | Target bundle **or** `01-backend-spec.yaml` (entire file) | **Append** `updates[]` — never delete prior lines | Author `openQuestions` |
30
30
  | `ir/design.yaml` — ONLY to locate field if `at` is a design pointer | `flowgrid split` after patch | Full-screen rewrite (use `/spec`) |
31
31
 
32
32
  ---
@@ -41,10 +41,10 @@ disable-model-invocation: true
41
41
 
42
42
  ## Rule: Resolving the QA File
43
43
 
44
- - **[MANDATORY]** Step 1: Locate `qa/open/<id>.yaml`. If missing, glob `qa/open/QA-*-NNNN.yaml` matching `id:`.
45
- - Zero matches → **STOP**, list available `qa/open/` IDs to user.
44
+ - **[MANDATORY]** Step 1: Locate `qa/<id>.yaml`. If missing, glob `qa/*` and legacy `qa/*` matching `id:`.
45
+ - Zero matches → **STOP**, list available QA ids to user.
46
46
  - Multiple matches → **STOP**, prompt user to clarify which file to close.
47
- - **[MANDATORY]** Step 2: Read `target.path`, `target.at`, `kind`, `skill`, and `question` from the QA file.
47
+ - **[MANDATORY]** Step 2: Read `target.path`, `target.at`, `kind`, `skill`, and latest `question` from `updates[]` (or legacy `question` field).
48
48
 
49
49
  ---
50
50
 
@@ -60,9 +60,10 @@ disable-model-invocation: true
60
60
 
61
61
  - **[MANDATORY]** Post-patch execution:
62
62
  1. Write solution into field at `target.at` (replacing `#missing_info` / empty / placeholder).
63
- 2. Remove this ID from `#missing_info QA-…`, `#tech-debt:QA-…`, and all tag lists.
64
- 3. **Delete** `qa/open/<id>.yaml`.
63
+ 2. Remove this ID from `#missing_info QA-…`, `#tech-debt:QA-…`, `#missing_info <id>`, and all tag lists.
64
+ 3. **Same file:** Append `updates[]` entry `kind: answer`, `at` now (`YYYYMMDD HH:mm`), `text` = solution; set `status: closed`.
65
65
  4. Run `flowgrid split` / `pnpm docs:split` so `ir/spec.yaml` Q&A removes this ID.
66
+ 5. Remind `flowgrid render` to refresh `qa/index.md`.
66
67
  - **[MANDATORY]** Preserve existing error matrices (`onSuccess` / `onCommonError` / `onSpecificError`, `#err:*`) unless solution specifically alters those fields.
67
68
 
68
69
  ---
@@ -84,8 +85,8 @@ disable-model-invocation: true
84
85
 
85
86
  ## Verification Checklist
86
87
 
87
- - [ ] Read `qa/open/<id>.yaml`; patched only `target.path` field.
88
+ - [ ] Read `qa/<id>.yaml`; patched only `target.path` field.
88
89
  - [ ] Solution sourced strictly from user prompt (or single AskQuestion turn).
89
- - [ ] QA file deleted; `pendingTechDebt` + `#missing_info` references removed for this ID.
90
+ - [ ] Appended `kind: answer`; `status: closed`; tags removed for this ID.
90
91
  - [ ] `flowgrid split` executed; `ir/spec.yaml` Q&A reflects closed status.
91
92
  - [ ] Did not author `openQuestions` or `bundle.spec.api`.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: qa-review
3
+ description: /qa-review — append review line on qa/<id>.yaml (team).
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ > [!CRITICAL] MANDATORY PRE-FLIGHT
8
+ > **[MANDATORY]** Read `.cursor/extracts/qa-team.md` and hub `docs/workflows/qa-team.md`.
9
+
10
+ # /qa-review — Review QA timeline (team)
11
+
12
+ **Owner:** docs hub (`--type=Document` or consumer `FLOWGRID_DOCS_ROOT`)
13
+
14
+ **When:** After `/qa-resolve` appended `kind: answer`; senior checks decision quality and spec patch.
15
+
16
+ **Not this skill:** First answer + patch (`/qa-resolve`); silent spec edits (`/update-spec` without review note).
17
+
18
+ ---
19
+
20
+ ## Input
21
+
22
+ - `HOTEL-LIST_0001` (or legacy `QA-<page-id>-NNNN`)
23
+ - Optional: reviewer id + verdict in user prompt
24
+
25
+ ---
26
+
27
+ ## Steps
28
+
29
+ 1. Read **whole** `qa/<id>.yaml` (legacy: `qa/` or same basename under `qa/`).
30
+ 2. Read **whole** target `*.bundle.yaml` or `01-backend-spec.yaml` at `target.path`; verify field at `target.at` matches latest `kind: answer` text.
31
+ 3. **Append only** to `updates[]` (never edit or delete prior lines):
32
+ - `at`: now (`YYYYMMDD HH:mm`)
33
+ - `by`: reviewer
34
+ - `kind`: `review`
35
+ - `text`: `approved: …` or `needs-change: …` (substantive notes)
36
+ 4. If `needs-change` in text → set `status: open` on **same file** (middle fixes spec + `/qa-resolve` or append another `answer` later).
37
+ 5. Remind: `flowgrid render` to refresh `qa/index.md`.
38
+
39
+ ---
40
+
41
+ ## Verification
42
+
43
+ - [ ] File exists; timeline append-only.
44
+ - [ ] Review notes substantive for `needs-change`.
45
+ - [ ] No invented business beyond comparing answer vs spec.
@@ -26,7 +26,7 @@ disable-model-invocation: true
26
26
  - **Small Scope (≤5 gaps):** Run `AskQuestion` wizard — **one question at a time**, **≥3 options**: (1) `(Recommended)`, (2) Alternative, (3) `Log as Tech Debt (Pending)`. Show next question only after member answers current one.
27
27
  - **Large Scope (≥10 gaps OR multi-screen scope):** **[MANDATORY HARD STOP IN CHAT]**. Do not spam single questions in chat. Generate an implementation plan / Plan Mode document partitioned into Phases (3–5 questions/fields per phase) to prevent session token overflow.
28
28
  - ❌ Never invent, assume, or silently skip missing fields.
29
- - **[MANDATORY]** If member chooses "Log as Tech Debt" → create `qa/open/QA-<page-id>-NNNN.yaml` + tag `#missing_info QA-…`. Do not block on it.
29
+ - **[MANDATORY]** If member chooses "Log as Tech Debt" → read `.flowgrid/templates/qa-item.yaml` + `qa-authoring.md`; create `qa/<SHORT>_NNNN.yaml` (`schema: flowgrid-qa-item/v1`) + tag `#missing_info <id>`. Do not block on it.
30
30
  - **[RECOMMENDED]** Brainstorm business text (context, input, output, screen descriptions) proactively in Vietnamese for Non-tech audience — do not wait to be told.
31
31
 
32
32
  ---
@@ -27,7 +27,7 @@ Doc hub: `platform/toolchain/UPDATE-SPEC-FLOW.md` · `platform/toolchain/FEATURE
27
27
  ## Rule: Scope Boundaries
28
28
 
29
29
  - **[MANDATORY]** Scope: patch bundle (delta only); emit `#update:*` tags; bump `specRevision`; run `flowgrid split/check`.
30
- - **[STRICTLY FORBIDDEN]** Full rewrite → `/spec`. Close `qa/open` item → `/qa-resolve`. Production code → NOT this skill.
30
+ - **[STRICTLY FORBIDDEN]** Full rewrite → `/spec`. Close `qa` item → `/qa-resolve`. Production code → NOT this skill.
31
31
  - **[MANDATORY]** Legacy re-mine / trace lại từ code cũ: dùng **`/legacy /spec`** (adopt lại) hoặc **`/update-spec`** với delta `legacy` / `legacyEvidence` + `#update:*` — **không** skill riêng.
32
32
 
33
33
  ---
@@ -37,7 +37,7 @@ Doc hub: `platform/toolchain/UPDATE-SPEC-FLOW.md` · `platform/toolchain/FEATURE
37
37
  - **[MANDATORY]** Gaps or ambiguity regarding delta scope, evaluate total gap volume:
38
38
  - **Small Scope (≤5 questions):** Trigger `AskQuestion` wizard — one question at a time, **≥3 options**: (1) `(Recommended)`, (2) `Other`, (3) `Log as Tech Debt (Pending)`.
39
39
  - **Large Scope (≥10 gaps):** **[MANDATORY HARD STOP IN CHAT]**. Do not spam single questions in chat. Generate an implementation plan / Plan Mode document partitioned into sequential Phases (3–5 gaps per phase) with disk offloading at boundaries.
40
- - ✅ If "Log as Tech Debt" is selected → create `qa/open/` entry; do not invent business data.
40
+ - ✅ If "Log as Tech Debt" is selected → create `qa/` entry; do not invent business data.
41
41
  - ❌ Never invent delta scope or novel business fields without explicit user confirmation.
42
42
  - Path SSOT: `surfaces/<surface>/CMP-*/<slug>/` — NO `modules/` segment.
43
43
 
@@ -47,7 +47,7 @@ Re-run **entire chain** after fixes in any lane (code, plan, or spec).
47
47
  | Business/spec/UX wrong vs what API actually returns | docs hub | `/update-spec` (paste-ready `/docs-hub` prompt) |
48
48
  | `01` / OpenAPI wrong; BE field/status codes | docs hub | `/api-update` → BE `/audit-api` → re-wire |
49
49
  | SC screen uncovered | tests-docs | `/testcase` or `/scenario` + `audit scenario` |
50
- | Intentional defer | docs | `qa/open` + `QA-*` / `coverage_deferred` on SC |
50
+ | Intentional defer | docs | `qa` + `QA-*` / `coverage_deferred` on SC |
51
51
 
52
52
  **Forbidden:** patch `ir/*` or `01` from FE repo; invent AC on tests hub; skip `audit e2e` because manual QA passed.
53
53
 
@@ -54,7 +54,7 @@ Re-run full chain after downstream lanes fix gaps.
54
54
  | UX/spec/AC wrong vs observed behaviour | docs-hub `/update-spec` (paste-ready prompt per `grill-testcase` rule) |
55
55
  | `FEBE_*`, wrong endpoint/field on `01` | docs-hub `/api-update` → BE `/audit-api` → member re-runs `/wire` |
56
56
  | `SC_SCREEN_NO_TC` | tests-hub `/testcase` or `/scenario` |
57
- | Defer only | `qa/open` + team policy |
57
+ | Defer only | `qa` + team policy |
58
58
 
59
59
  - **[STRICTLY FORBIDDEN]** Close `#update:*` / `#wire-only` here — only `/wire` after spec merge confirms.
60
60
  - **[STRICTLY FORBIDDEN]** Run `/api` codegen or full product regression from this skill.
@@ -56,7 +56,7 @@ flowgrid audit e2e --e2e-root <playwright-dir> --tests-docs "$FLOWGRID_TESTS_DOC
56
56
  Optional: pass explicit `cases/**/TC-*.yaml` paths instead of `--tests-docs`.
57
57
 
58
58
  - Parse JSON: `missingInPlaywright`, `matrixRowsUncovered`, `orphanTestCases`, `orphanSpecs`, `gaps[]`.
59
- - **[MANDATORY]** Fix or defer every `critical` / `warning` gap before marking wire done; log deferrals in `qa/open` if team policy allows.
59
+ - **[MANDATORY]** Fix or defer every `critical` / `warning` gap before marking wire done; log deferrals in `qa` if team policy allows.
60
60
  - **[MANDATORY]** Portal leaf with `apiRef`: `flowgrid audit fe-be <bundle.yaml>`.
61
61
  - **[MANDATORY]** When `SC-*` covers screen: `flowgrid audit scenario <SC.yaml> --tests-docs "$FLOWGRID_TESTS_DOC"`.
62
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanyucoder/flowgrid",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Unified Local MCP Toolkit (Graph, DNA, Docs, Test, Codegen)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,11 +1,7 @@
1
- # QA inbox (docs hub)
1
+ # QA inbox
2
2
 
3
- Open customer / choice / tech-debt questions live here — **not** in `*.bundle.yaml`.
3
+ One file per case: `qa/<SHORT>_0001.yaml`.
4
4
 
5
- - One file per open issue: `qa/open/QA-<bundle.id>-NNNN.yaml`
6
- - `bundle.id` is the screen leaf id from `/spec` (e.g. `cmp-adm-002-02-01-02`)
7
- - Sequence is **per screen** (`0001`, `0002`, …). Different screens never share a counter.
8
- - Close with **`/qa-resolve QA-<bundle.id>-NNNN`** plus the decision (skill deletes the file).
5
+ After `flowgrid init`, copy from `.flowgrid/templates/qa-item.yaml` and follow `.flowgrid/templates/qa-authoring.md`.
9
6
 
10
- Template: `.flowgrid/templates/qa-item.yaml`
11
- Rules: `.cursor/extracts/qa-inbox.md`
7
+ Append-only `updates[]` · `flowgrid render` refreshes `index.md`.
@@ -0,0 +1,98 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://platform.local/schemas/flowgrid-docs/qa-item.schema.json",
4
+ "title": "FlowGrid QA inbox item",
5
+ "type": "object",
6
+ "required": ["schema", "id", "status", "target", "updates"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "schema": {
10
+ "type": "string",
11
+ "const": "flowgrid-qa-item/v1"
12
+ },
13
+ "id": {
14
+ "type": "string",
15
+ "pattern": "^[A-Z0-9][A-Z0-9._-]*_\\d{4}$",
16
+ "description": "Same as filename without .yaml — SHORT_NNNN"
17
+ },
18
+ "screen": {
19
+ "type": "string",
20
+ "description": "W-* or bundle page-id for humans and split matching"
21
+ },
22
+ "bundleId": {
23
+ "type": "string",
24
+ "description": "Legacy QA-<bundleId>-NNNN prefix; optional when screen/target.path enough"
25
+ },
26
+ "status": {
27
+ "type": "string",
28
+ "enum": ["open", "closed"]
29
+ },
30
+ "kind": {
31
+ "type": "string",
32
+ "enum": ["customer", "choice", "tech-debt", "integration", "coverage"]
33
+ },
34
+ "skill": {
35
+ "type": "string",
36
+ "description": "Skill that opened the gap — spec, grill-dev, api-spec, …"
37
+ },
38
+ "tags": {
39
+ "type": "array",
40
+ "items": { "type": "string" }
41
+ },
42
+ "target": {
43
+ "type": "object",
44
+ "required": ["path", "at"],
45
+ "additionalProperties": false,
46
+ "properties": {
47
+ "path": {
48
+ "type": "string",
49
+ "description": "Repo-relative path to bundle or 01-backend-spec.yaml"
50
+ },
51
+ "at": {
52
+ "type": "string",
53
+ "description": "YAML pointer — field gets #missing_info <id> until resolve"
54
+ }
55
+ }
56
+ },
57
+ "options": {
58
+ "type": "array",
59
+ "description": "Optional AskQuestion options captured when opening (audit trail)",
60
+ "items": {
61
+ "type": "object",
62
+ "required": ["label"],
63
+ "properties": {
64
+ "label": { "type": "string" },
65
+ "recommended": { "type": "boolean" }
66
+ }
67
+ }
68
+ },
69
+ "updates": {
70
+ "type": "array",
71
+ "minItems": 1,
72
+ "items": { "$ref": "#/$defs/update" }
73
+ }
74
+ },
75
+ "$defs": {
76
+ "update": {
77
+ "type": "object",
78
+ "required": ["at", "kind", "text"],
79
+ "additionalProperties": false,
80
+ "properties": {
81
+ "at": {
82
+ "type": "string",
83
+ "pattern": "^\\d{8} \\d{2}:\\d{2}$",
84
+ "description": "YYYYMMDD HH:mm (team TZ)"
85
+ },
86
+ "by": { "type": "string" },
87
+ "kind": {
88
+ "type": "string",
89
+ "enum": ["question", "answer", "review", "note"]
90
+ },
91
+ "text": {
92
+ "type": "string",
93
+ "minLength": 1
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
@@ -303,13 +303,14 @@ actions:
303
303
 
304
304
  YAML only per schema. No explanation. No markdown.
305
305
 
306
- ## Questions: AskQuestion wizard or hub `qa/open/`
306
+ ## Questions: AskQuestion wizard or hub `qa/`
307
307
 
308
308
  Do **not** write `openQuestions` anywhere. Schema/render không còn field này.
309
309
 
310
310
  - Member trả lời **ngay:** AskQuestion (options + Recommended + **Other**) → **STOP** → ghi field thật.
311
- - Member chọn **Other** mà **chưa quyết:** `.cursor/extracts/qa-inbox.md` → `qa/open/QA-<bundle.id>-NNNN.yaml`. Đóng sau bằng **`/qa-resolve <id>` + giải pháp**.
312
- - `grillStatus` có thể `done` khi vẫn còn file QA.
311
+ - Member chọn **Log as Tech Debt:** copy `.flowgrid/templates/qa-item.yaml` — quy tắc field/timeline: **`qa-authoring.md`** · extract `qa-inbox.md`.
312
+ - Đóng sau bằng **`/qa-resolve <id>` + giải pháp** (append `kind: answer`, `status: closed`).
313
+ - `grillStatus` có thể `done` khi vẫn còn file QA `status: open`.
313
314
 
314
315
  ## YAML Syntax & Escaping Rules
315
316
 
@@ -318,7 +319,7 @@ Do **not** write `openQuestions` anywhere. Schema/render không còn field này.
318
319
 
319
320
  ## ir/spec.yaml vs ir/design.yaml
320
321
 
321
- `pnpm spec:split` ghi hai file. FlowGrid bộ docs grill **không** đọc `ir/*` khi author — chỉ bundle. Split ghi `"Q&A":` trên **`ir/spec.yaml`** (id `QA-<bundle.id>-NNNN` cách nhau bởi `, `) từ `qa/open/` — không nhét list vào bundle.
322
+ `pnpm spec:split` ghi hai file. FlowGrid bộ docs grill **không** đọc `ir/*` khi author — chỉ bundle. Split ghi `"Q&A":` trên **`ir/spec.yaml`** (id `QA-<bundle.id>-NNNN` cách nhau bởi `, `) từ `qa/` — không nhét list vào bundle.
322
323
 
323
324
  Downstream (UI gen, API gen, testcase) đọc **chỉ `ir/design.yaml`**. Thiếu file = split/spec chưa xong. Story/copy chưa đủ thì **bổ sung design** (bundle.gen + split), không đọc `ir/spec.yaml`. `ir/spec.yaml` chỉ văn mô tả + `legacy` + `qa` (id treo).
324
325
 
@@ -14,5 +14,5 @@ actors: []
14
14
  requirements: []
15
15
  acceptance: []
16
16
 
17
- # Filled by spec:split from qa/open/QA-<id>-NNNN.yaml (comma-separated). Omit when none.
17
+ # Filled by spec:split from qa/<SHORT>_NNNN.yaml (comma-separated). Omit when none.
18
18
  # "Q&A": QA-role-domain-function-0001, QA-role-domain-function-0002
@@ -0,0 +1,78 @@
1
+ # flowgrid-qa-item/v1 — authoring rules (QA inbox)
2
+
3
+ Hub template: `.flowgrid/templates/qa-item.yaml` (sau `flowgrid init`) · Workflow: `docs/workflows/qa-team.md`
4
+
5
+ ## File naming
6
+
7
+ | Part | Rule | Example |
8
+ |------|------|---------|
9
+ | **SHORT** | Slug màn/module, ổn định, `A-Z0-9` + `-` | `HOTEL-LIST`, `ADM-AUTH` |
10
+ | **NNNN** | Tăng dần theo SHORT (0001…) | `0001`, `0002` |
11
+ | **Path** | `qa/<SHORT>_<NNNN>.yaml` | `qa/HOTEL-LIST_0001.yaml` |
12
+ | **id** | **Bắt buộc** = basename không `.yaml` | `HOTEL-LIST_0001` |
13
+
14
+ Legacy id `QA-<page-id>-NNNN` vẫn đọc được — file mới dùng `<SHORT>_NNNN`.
15
+
16
+ Gợi ý SHORT từ `W-HOTEL-LIST` → `HOTEL-LIST` (bỏ prefix `W-`). Team có thể chọn slug module cố định (vd. `ADM-AUTH` cho nhiều màn auth).
17
+
18
+ ## Top-level fields
19
+
20
+ | Key | Required | Purpose |
21
+ |-----|----------|---------|
22
+ | `schema` | yes | Luôn `flowgrid-qa-item/v1` |
23
+ | `id` | yes | Khớp tên file |
24
+ | `status` | yes | `open` \| `closed` |
25
+ | `target.path` | yes | Bundle hoặc `01-backend-spec.yaml` (repo-relative) |
26
+ | `target.at` | yes | Pointer field — gắn `#missing_info <id>` / `#tech-debt:<id>` |
27
+ | `updates` | yes | Timeline append-only (≥1 dòng) |
28
+ | `screen` | khuyến nghị | `W-*` hoặc `page-id` — catalog + split |
29
+ | `bundleId` | khi legacy | `page-id` bundle để `Q&A` trên `ir/spec.yaml` |
30
+ | `kind` | khuyến nghị | `customer` \| `choice` \| `tech-debt` \| `integration` \| `coverage` |
31
+ | `skill` | khuyến nghị | Skill mở gap: `spec`, `grill-dev`, `api-spec`, … |
32
+ | `options` | optional | Copy options AskQuestion khi mở (audit) |
33
+ | `tags` | optional | Mirror tag đã gắn trên spec |
34
+
35
+ ## `updates[]` (append-only)
36
+
37
+ **Không** sửa/xóa dòng cũ. Chỉ **thêm** cuối mảng.
38
+
39
+ | `kind` | Ai / khi |
40
+ |--------|----------|
41
+ | `question` | Mở QA (Log Tech Debt) — **dòng đầu** |
42
+ | `answer` | `/qa-resolve` — sau khi patch `target.at` |
43
+ | `review` | `/qa-review` — `approved:` hoặc `needs-change:` |
44
+ | `note` | Ghi chú, không đổi spec |
45
+
46
+ | `at` | Format **bắt buộc** | `20260930 08:00` (YYYYMMDD HH:mm) |
47
+ | `by` | Member / role | `ba-lead`, `middle-dev` |
48
+ | `text` | Nội dung | Block `\|` multiline |
49
+
50
+ ### Vòng đời status
51
+
52
+ - Mở file: `status: open`, `updates[0].kind: question`
53
+ - `/qa-resolve`: append `answer`, `status: closed`, gỡ tag trên spec
54
+ - `/qa-review` + `needs-change`: append `review`, **`status: open`** lại (cùng file)
55
+
56
+ ## Agent workflow (mở QA)
57
+
58
+ 1. Đọc **whole** `.flowgrid/templates/qa-item.yaml` + file này.
59
+ 2. Chọn SHORT (team convention hoặc từ `screen`).
60
+ 3. `nextQaId(hub, SHORT)` hoặc glob `qa/<SHORT>_*.yaml` → max NNNN + 1.
61
+ 4. Copy template → `qa/<SHORT>_<NNNN>.yaml`; set `id`, `target`, `at` now, `updates[0]`.
62
+ 5. Patch spec: `#missing_info <id>` trên field `target.at`.
63
+ 6. `flowgrid split` + `flowgrid render`.
64
+
65
+ ## Agent output
66
+
67
+ - **YAML only** khi tạo/sửa QA file — không markdown giải thích trong repo.
68
+ - Quote string có `:`; multiline dùng `|`.
69
+
70
+ ## Không dùng QA file cho
71
+
72
+ - ADR → `architecture/09-decisions`
73
+ - Risk dài hạn → `architecture/11-risks`
74
+ - `openQuestions` trên bundle — **cấm**
75
+
76
+ ## Schema
77
+
78
+ Validate (optional CI): `templates/schemas/qa-item.schema.json` · `flowgrid` test `validateQaItem`.
@@ -1,15 +1,36 @@
1
- # Copy to qa/open/QA-<bundle.id>-NNNN.yaml — do not leave this file in qa/open/.
2
- id: QA-cmp-adm-002-02-01-02-0001
3
- kind: customer # customer | choice | tech-debt
4
- skill: spec # spec | grill-bqa | grill-dev | grill-docs | api-spec | api-update
1
+ # flowgrid-qa-item/v1 — Authoring: qa-authoring.md · Hub: docs/workflows/qa-team.md
2
+ # Copy to qa/<SHORT>_NNNN.yaml (id = basename without .yaml). Append-only updates[].
3
+
4
+ schema: flowgrid-qa-item/v1
5
+ id: HOTEL-LIST_0001
6
+ screen: W-HOTEL-LIST
7
+ bundleId: cmp-adm-000-01-01
8
+ status: open
9
+ kind: tech-debt
10
+ skill: spec
11
+ tags:
12
+ - "#missing_info HOTEL-LIST_0001"
5
13
  target:
6
- path: surfaces/<surface>/CMP-*/<NN…>/<slug>.bundle.yaml
7
- at: design.zones.main.items.title.purpose
8
- question: |
9
- What is still unknown?
10
- options: []
11
- # - id: a
12
- # text: Option A
13
- # recommended: true
14
- # - id: other
15
- # text: Member supplies copy
14
+ path: surfaces/<surface>/CMP-*/<slug>.bundle.yaml
15
+ at: design.sections.main.items.filter_timezone.purpose
16
+ options:
17
+ - label: "(Recommended) Theo timezone property"
18
+ recommended: true
19
+ - label: Theo locale user
20
+ updates:
21
+ - at: "20260930 08:00"
22
+ by: <member-id>
23
+ kind: question
24
+ text: |
25
+ Filter timezone lấy theo property hay user locale?
26
+ # Append only — never delete or rewrite prior lines:
27
+ # - at: "20260930 10:00"
28
+ # by: ba-lead
29
+ # kind: answer
30
+ # text: |
31
+ # Theo property TZ; label UTC+7 trên UI.
32
+ # - at: "20261001 08:00"
33
+ # by: senior
34
+ # kind: review
35
+ # text: |
36
+ # needs-change: thêm AC khi property chưa set TZ.
@@ -112,7 +112,7 @@ Skill: `/openapi` · Redoc/Swagger UI: `openapi_build_ui` (tùy hub).
112
112
 
113
113
  - Chuỗi có `:` → bọc `"..."`.
114
114
  - Chạy `flowgrid api:check` trước handoff.
115
- - Thiếu fact → AskQuestion hoặc `qa/open/` — **không** `openQuestions` trong YAML.
115
+ - Thiếu fact → AskQuestion hoặc `qa/` — **không** `openQuestions` trong YAML.
116
116
 
117
117
  ---
118
118