@shanyucoder/flowgrid 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flowgrid.mjs +43 -9
- package/bin/lib/audit-run.mjs +5 -1
- package/bin/lib/docs-hub-locale.mjs +9 -0
- package/bin/lib/init-scaffold.mjs +30 -2
- package/dist/docs/mcp/tools.js +4 -4
- package/dist/docs/mcp/tools.js.map +1 -1
- package/dist/docs/scan/ids.d.ts +1 -1
- package/dist/docs/scan/ids.js +6 -6
- package/dist/docs/scan/ids.js.map +1 -1
- package/dist/docs/scan/route.js +2 -2
- package/dist/docs/scan/route.js.map +1 -1
- package/engines/cases/render-cases.mjs +33 -25
- package/engines/docs/lib/audit-hub-prd.mjs +136 -0
- package/engines/docs/lib/audit-risks-catalog.mjs +142 -0
- package/engines/docs/lib/docs-hub-locale.mjs +100 -0
- package/engines/docs/lib/render-bundle-markdown.mjs +8 -1
- package/engines/docs/vitepress/config.ts +4 -4
- package/engines/spec/lib/audit-bundle-gaps.mjs +38 -7
- package/engines/spec/lib/audit-flow-gaps.mjs +2 -2
- package/engines/spec/lib/bundle-schema.mjs +3 -1
- package/engines/testcase/runners/lib/resolve-hub-id.mjs +3 -3
- package/harness/common/skills/legacy/SKILL.md +2 -2
- package/harness/docs/extracts/common-scope.md +8 -8
- package/harness/docs/extracts/spec-core.md +1 -1
- package/harness/docs/extracts/spec-prd-lite.md +11 -13
- package/harness/docs/extracts/tpl-module.md +9 -40
- package/harness/docs/extracts/tpl-overview-prd.md +11 -0
- package/harness/docs/extracts/tpl-risk-register.md +28 -0
- package/harness/docs/extracts/tpl-surface-prd.md +7 -0
- package/harness/docs/rules/docs-hub.mdc +1 -1
- package/harness/docs/rules/flowgrid-process.mdc +1 -1
- package/harness/docs/skills/adopt/SKILL.md +1 -1
- package/harness/docs/skills/background-logic/SKILL.md +1 -1
- package/harness/docs/skills/common-spec/SKILL.md +1 -1
- package/harness/docs/skills/cross-service/SKILL.md +1 -1
- package/harness/docs/skills/db-erd/SKILL.md +1 -1
- package/harness/docs/skills/grill/SKILL.md +2 -0
- package/harness/docs/skills/grill-hub-prd/SKILL.md +38 -0
- package/harness/docs/skills/module/SKILL.md +8 -5
- package/harness/docs/skills/overview/SKILL.md +7 -6
- package/harness/docs/skills/risk-register/SKILL.md +29 -0
- package/harness/docs/skills/spec/SKILL.md +3 -3
- package/harness/docs/skills/surfaces/SKILL.md +3 -1
- package/harness/docs/skills/{business-process → user-flow}/SKILL.md +8 -8
- package/harness/fe/skills/gen-common/SKILL.md +1 -1
- package/harness/tests/extracts/grill-scenario-flow.md +2 -2
- package/harness/tests/skills/grill-testcase/SKILL.md +1 -1
- package/harness/tests/skills/scenario/SKILL.md +11 -11
- package/harness/tests/skills/testcase/SKILL.md +1 -1
- package/harness/tests/templates/SC.example.md +16 -16
- package/lexicon/registry-tags.en.txt +2 -2
- package/package.json +1 -1
- package/templates/project-skeleton/architecture/03-business-process/index.md +3 -0
- package/templates/project-skeleton/architecture/{03-business-process → 03-user-flows}/FLOW-login.md +6 -6
- package/templates/project-skeleton/architecture/{03-business-process → 03-user-flows}/FLOW-template.md +6 -7
- package/templates/project-skeleton/architecture/11-risks/index.md +7 -17
- package/templates/project-skeleton/architecture/11-risks/risk-register.md +34 -0
- package/templates/project-skeleton/architecture/12-glossary/index.md +2 -1
- package/templates/project-skeleton/overview/index.md +21 -43
- package/templates/project-skeleton/overview/operational-areas/_template.md +15 -22
- package/templates/project-skeleton/surfaces/_module-index.template.md +40 -0
- package/templates/project-skeleton/surfaces/_surface-index.template.md +43 -0
- package/templates/shared/bundle-authoring.md +5 -2
- package/templates/shared/default-layout.ejs +108 -62
- package/templates/shared/feature.bundle.yaml +15 -6
- package/templates/shared/ir/generated/spec.md +24 -24
- package/templates/shared/tpl-api-contract.md +5 -5
- package/templates/tests-skeleton/catalog/locale.yaml +16 -15
- package/templates/tests-skeleton/tpl-testcase-plan.md +6 -6
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PRD hub Markdown audit — section titles must be English (structureLocale: en).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const PLACEHOLDER_RE = /\[[^\]]{4,}\]/;
|
|
11
|
+
|
|
12
|
+
function hubKind(filePath) {
|
|
13
|
+
const norm = filePath.split(path.sep).join('/');
|
|
14
|
+
if (norm.endsWith('overview/index.md')) return 'overview';
|
|
15
|
+
if (norm.endsWith('_surface-index.template.md')) return 'surface';
|
|
16
|
+
if (norm.endsWith('_module-index.template.md')) return 'module';
|
|
17
|
+
if (/\/surfaces\/[^/]+\/index\.md$/.test(norm) && !norm.includes('/CMP-')) return 'surface';
|
|
18
|
+
if (/\/CMP-[^/]+\/index\.md$/.test(norm)) return 'module';
|
|
19
|
+
if (norm.includes('/operational-areas/') && norm.endsWith('.md')) return 'operational';
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasEnglishGoals(text) {
|
|
24
|
+
return /##\s+Goals\b/i.test(text);
|
|
25
|
+
}
|
|
26
|
+
function hasEnglishBackground(text) {
|
|
27
|
+
return /##\s+Background\b/i.test(text);
|
|
28
|
+
}
|
|
29
|
+
function hasEnglishScope(text) {
|
|
30
|
+
return /##\s+Scope\b/i.test(text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function auditHubPrdContent(rawText, filePath) {
|
|
34
|
+
const gaps = [];
|
|
35
|
+
const warnings = [];
|
|
36
|
+
const kind = hubKind(filePath);
|
|
37
|
+
|
|
38
|
+
function addGap(code, fieldPath, message, suggestedFix) {
|
|
39
|
+
gaps.push({ code, severity: 'critical', path: fieldPath, message, suggestedFix });
|
|
40
|
+
}
|
|
41
|
+
function addWarning(code, fieldPath, message, suggestedFix) {
|
|
42
|
+
warnings.push({ code, path: fieldPath, message, suggestedFix });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const needsGoals = kind === 'overview' || kind === 'surface' || kind === 'module';
|
|
46
|
+
const needsBackground = kind === 'overview' || kind === 'surface';
|
|
47
|
+
const needsScope = kind !== 'unknown';
|
|
48
|
+
|
|
49
|
+
if (needsGoals && !hasEnglishGoals(rawText)) {
|
|
50
|
+
addGap(
|
|
51
|
+
'HUB_MISSING_GOALS',
|
|
52
|
+
'section.goals',
|
|
53
|
+
'Missing `## Goals` (English section key).',
|
|
54
|
+
'Add ## Goals {#goals} with business bullets (prose in contentLocale).'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (needsBackground && !hasEnglishBackground(rawText)) {
|
|
59
|
+
addGap(
|
|
60
|
+
'HUB_MISSING_BACKGROUND',
|
|
61
|
+
'section.background',
|
|
62
|
+
'Missing `## Background`.',
|
|
63
|
+
'Add ## Background {#background}.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (needsScope && !hasEnglishScope(rawText)) {
|
|
68
|
+
addGap(
|
|
69
|
+
'HUB_MISSING_SCOPE',
|
|
70
|
+
'section.scope',
|
|
71
|
+
'Missing `## Scope`.',
|
|
72
|
+
'Add ## Scope {#scope} with In scope / Out of scope bullets.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (/##\s+Mục tiêu/i.test(rawText) || /##\s+Bối cảnh/i.test(rawText) || /##\s+Phạm vi/i.test(rawText)) {
|
|
77
|
+
addWarning(
|
|
78
|
+
'HUB_VI_SECTION_TITLE',
|
|
79
|
+
'section.locale',
|
|
80
|
+
'Vietnamese section titles detected — use English keys (Goals, Background, Scope).',
|
|
81
|
+
'Rename headings to English; keep Vietnamese in body only.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (kind === 'module' && !/Features overview|Goals/i.test(rawText)) {
|
|
86
|
+
addWarning(
|
|
87
|
+
'HUB_MODULE_FEATURES_THIN',
|
|
88
|
+
'section.features',
|
|
89
|
+
'Module index missing Features overview / Goals.',
|
|
90
|
+
'Add ## Features overview and link function folders.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (PLACEHOLDER_RE.test(rawText)) {
|
|
95
|
+
addWarning(
|
|
96
|
+
'HUB_PLACEHOLDER',
|
|
97
|
+
'body',
|
|
98
|
+
'Template placeholders `[...]` remain.',
|
|
99
|
+
'Replace with real copy in contentLocale before sign-off.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (/##\s+Personas/i.test(rawText) || /Success metrics/i.test(rawText)) {
|
|
104
|
+
addWarning(
|
|
105
|
+
'HUB_DEPRECATED_SECTION',
|
|
106
|
+
'body',
|
|
107
|
+
'Personas or Success metrics sections — not SSOT on hub.',
|
|
108
|
+
'Remove or manage outside hub.'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
target: filePath,
|
|
114
|
+
hubKind: kind,
|
|
115
|
+
structureLocale: 'en',
|
|
116
|
+
totalGaps: gaps.length,
|
|
117
|
+
totalWarnings: warnings.length,
|
|
118
|
+
gaps,
|
|
119
|
+
warnings,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const args = process.argv.slice(2);
|
|
124
|
+
if (args.length === 0) {
|
|
125
|
+
console.log('Usage: node engines/docs/lib/audit-hub-prd.mjs <path-to-hub.md>');
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const targetFile = path.resolve(process.cwd(), args[0]);
|
|
130
|
+
if (!fs.existsSync(targetFile)) {
|
|
131
|
+
console.error(JSON.stringify({ error: `File not found: ${targetFile}` }));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rawText = fs.readFileSync(targetFile, 'utf8');
|
|
136
|
+
console.log(JSON.stringify(auditHubPrdContent(rawText, targetFile), null, 2));
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Audit risk SSOT: architecture/11-risks/risk-register.md (member register)
|
|
5
|
+
* or legacy short catalog on index.md.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
function isRiskRegister(filePath) {
|
|
12
|
+
return path.basename(filePath) === 'risk-register.md';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function auditRisksCatalogContent(rawText, filePath) {
|
|
16
|
+
const gaps = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
const register = isRiskRegister(filePath);
|
|
19
|
+
|
|
20
|
+
function addGap(code, fieldPath, message, suggestedFix) {
|
|
21
|
+
gaps.push({ code, severity: 'warning', path: fieldPath, message, suggestedFix });
|
|
22
|
+
}
|
|
23
|
+
function addWarning(code, fieldPath, message, suggestedFix) {
|
|
24
|
+
warnings.push({ code, path: fieldPath, message, suggestedFix });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hasRiskCol = /\|\s*Rủi ro\s*\|/i.test(rawText) || /\|\s*Mô tả\s*\|/i.test(rawText);
|
|
28
|
+
const hasTable =
|
|
29
|
+
/\|\s*Risk\s*\|/i.test(rawText) ||
|
|
30
|
+
/\|\s*ID\s*\|/i.test(rawText) ||
|
|
31
|
+
hasRiskCol;
|
|
32
|
+
|
|
33
|
+
if (!hasTable) {
|
|
34
|
+
addGap(
|
|
35
|
+
'RISK_CATALOG_NO_TABLE',
|
|
36
|
+
'risks.table',
|
|
37
|
+
register
|
|
38
|
+
? 'Chưa có bảng sổ rủi ro (risk register).'
|
|
39
|
+
: 'Chưa có bảng rủi ro (cột Risk/Rủi ro + Phương án).',
|
|
40
|
+
register
|
|
41
|
+
? 'Dùng template risk-register.md: cột Hạn mức, Nhu cầu, Chênh lệch.'
|
|
42
|
+
: 'Thêm bảng markdown hoặc link sang risk-register.md.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (register) {
|
|
47
|
+
const needCols = [
|
|
48
|
+
{ re: /\bLimit\b/i, code: 'RISK_REG_MISSING_LIMIT_COL', label: 'Limit' },
|
|
49
|
+
{ re: /\bNeed\b/i, code: 'RISK_REG_MISSING_NEED_COL', label: 'Need' },
|
|
50
|
+
{ re: /\bGap\b/i, code: 'RISK_REG_MISSING_GAP_COL', label: 'Gap' },
|
|
51
|
+
{ re: /Mitigation/i, code: 'RISK_REG_MISSING_MITIGATION_COL', label: 'Mitigation' },
|
|
52
|
+
];
|
|
53
|
+
for (const col of needCols) {
|
|
54
|
+
if (!col.re.test(rawText)) {
|
|
55
|
+
addGap(
|
|
56
|
+
col.code,
|
|
57
|
+
'riskRegister.columns',
|
|
58
|
+
`Thiếu cột «${col.label}» trong bảng risk register.`,
|
|
59
|
+
`Thêm cột ${col.label} để so sánh quota/hạn mức với nhu cầu peak.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hasRiskId = /RISK-[A-Z0-9_-]+/i.test(rawText);
|
|
65
|
+
if (!hasRiskId) {
|
|
66
|
+
addWarning(
|
|
67
|
+
'RISK_REG_NO_ID',
|
|
68
|
+
'riskRegister.ids',
|
|
69
|
+
'Chưa có mã RISK-* trong sổ (chỉ placeholder mẫu?).',
|
|
70
|
+
'Thêm ít nhất một dòng rủi ro thật với mã RISK-….'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rows = rawText.match(/^\|[^|]+\|[^|]+\|[^|]+\|/gm) || [];
|
|
75
|
+
const dataRows = rows.filter(
|
|
76
|
+
(r) => !r.includes('---') && !/^\|\s*[-:]+\s*\|/.test(r) && !/^\|\s*Mã\s*\|/i.test(r)
|
|
77
|
+
);
|
|
78
|
+
const realRows = dataRows.filter((r) => /RISK-/i.test(r) && !/RISK-EXAMPLE/i.test(r));
|
|
79
|
+
if (hasTable && realRows.length < 1) {
|
|
80
|
+
addWarning(
|
|
81
|
+
'RISK_REG_EMPTY',
|
|
82
|
+
'riskRegister.rows',
|
|
83
|
+
'Sổ rủi ro chưa có dòng nghiệp vụ (ngoài ví dụ RISK-EXAMPLE).',
|
|
84
|
+
'Thêm rủi ro thật — vd. quota email, rate limit API, license.'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const rows = rawText.match(/^\|[^|]+\|[^|]+\|/gm) || [];
|
|
89
|
+
const dataRows = rows.filter((r) => !r.includes('---') && !/^\|\s*[-:]+\s*\|/.test(r));
|
|
90
|
+
if (hasTable && dataRows.length < 2) {
|
|
91
|
+
addWarning(
|
|
92
|
+
'RISK_CATALOG_EMPTY',
|
|
93
|
+
'risks.rows',
|
|
94
|
+
'Bảng rủi ro chưa có dòng nghiệp vụ (chỉ header).',
|
|
95
|
+
'Ưu tiên điền architecture/11-risks/risk-register.md.'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!/risk-register\.md/i.test(rawText)) {
|
|
99
|
+
addWarning(
|
|
100
|
+
'RISK_INDEX_NO_REGISTER_LINK',
|
|
101
|
+
'risks.index',
|
|
102
|
+
'index.md chưa trỏ sang risk-register.md.',
|
|
103
|
+
'Link [risk-register.md](./risk-register.md) là SSOT member.'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (/\[(?:\.\.\.|TBD|Tên|VD:)/i.test(rawText)) {
|
|
109
|
+
addWarning(
|
|
110
|
+
'RISK_PLACEHOLDER',
|
|
111
|
+
'body',
|
|
112
|
+
'Còn placeholder template trong file rủi ro.',
|
|
113
|
+
'Điền rủi ro thật hoặc defer qa/.'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
target: filePath,
|
|
119
|
+
documentKind: register ? 'risk-register' : 'risks-index',
|
|
120
|
+
totalGaps: gaps.length,
|
|
121
|
+
totalWarnings: warnings.length,
|
|
122
|
+
gaps,
|
|
123
|
+
warnings,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const args = process.argv.slice(2);
|
|
128
|
+
if (args.length === 0) {
|
|
129
|
+
console.log(
|
|
130
|
+
'Usage: node engines/docs/lib/audit-risks-catalog.mjs <architecture/11-risks/risk-register.md|index.md>'
|
|
131
|
+
);
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const targetFile = path.resolve(process.cwd(), args[0]);
|
|
136
|
+
if (!fs.existsSync(targetFile)) {
|
|
137
|
+
console.error(JSON.stringify({ error: `File not found: ${targetFile}` }));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rawText = fs.readFileSync(targetFile, 'utf8');
|
|
142
|
+
console.log(JSON.stringify(auditRisksCatalogContent(rawText, targetFile), null, 2));
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
|
|
5
|
+
/** Headings / YAML keys / audit anchors — always English. */
|
|
6
|
+
export const STRUCTURE_LOCALE = 'en';
|
|
7
|
+
|
|
8
|
+
/** @param {string} [code] @param {string} [fallback] */
|
|
9
|
+
export function normalizeContentLocale(code, fallback = 'vi') {
|
|
10
|
+
const s = String(code ?? '').trim();
|
|
11
|
+
if (!s) return fallback;
|
|
12
|
+
return s.replace(/\s+/g, '-').toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ text: Function, isCancel: Function, cancel: Function }} clack
|
|
17
|
+
* @returns {Promise<string>}
|
|
18
|
+
*/
|
|
19
|
+
export async function promptDocsContentLocale(clack) {
|
|
20
|
+
const { text, isCancel, cancel } = clack;
|
|
21
|
+
const ans = await text({
|
|
22
|
+
message:
|
|
23
|
+
'Member prose locale for document spec + test documents (BCP-47 tag, e.g. vi, en, ja — keys/headings stay English):',
|
|
24
|
+
defaultValue: 'vi',
|
|
25
|
+
placeholder: 'vi',
|
|
26
|
+
});
|
|
27
|
+
if (isCancel(ans)) {
|
|
28
|
+
cancel('Cancelled.');
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
return normalizeContentLocale(ans, 'vi');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} docsRootAbs
|
|
36
|
+
* @param {{ structureLocale: string, contentLocale: string }} locales
|
|
37
|
+
*/
|
|
38
|
+
export function writeDocsHubLocaleManifest(docsRootAbs, locales) {
|
|
39
|
+
const file = path.join(docsRootAbs, 'docs-hub.locale.yaml');
|
|
40
|
+
const body = `# SSOT — structureLocale is always en (headings/keys). Prose follows contentLocale.\nstructureLocale: ${locales.structureLocale}\ncontentLocale: ${locales.contentLocale}\n`;
|
|
41
|
+
fs.writeFileSync(file, body, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** English section labels for tests-docs render (never translated per locale). */
|
|
45
|
+
export const TESTS_HUB_HEADINGS_EN = {
|
|
46
|
+
home: 'Home',
|
|
47
|
+
cases: 'Test cases',
|
|
48
|
+
scenarios: 'Scenarios',
|
|
49
|
+
plans: 'Test plans',
|
|
50
|
+
preconditions: 'Preconditions',
|
|
51
|
+
steps: 'Steps',
|
|
52
|
+
expected: 'Expected results',
|
|
53
|
+
traceability: 'Traceability (docs SSOT)',
|
|
54
|
+
testMatrix: 'Test matrix (equivalence)',
|
|
55
|
+
crossRefDocs: 'Docs hub cross-reference',
|
|
56
|
+
technical: 'Technical details',
|
|
57
|
+
testData: 'Test data',
|
|
58
|
+
coverage: 'Coverage',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} testsRootAbs
|
|
63
|
+
* @param {{ structureLocale: string, contentLocale: string }} locales
|
|
64
|
+
*/
|
|
65
|
+
export function writeTestsHubLocaleCatalog(testsRootAbs, locales) {
|
|
66
|
+
const catalogDir = path.join(testsRootAbs, 'catalog');
|
|
67
|
+
if (!fs.existsSync(catalogDir)) {
|
|
68
|
+
fs.mkdirSync(catalogDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
const file = path.join(catalogDir, 'locale.yaml');
|
|
71
|
+
const lines = [
|
|
72
|
+
'# SSOT — headings are English; TC prose in YAML follows contentLocale.',
|
|
73
|
+
`structureLocale: ${locales.structureLocale}`,
|
|
74
|
+
`contentLocale: ${locales.contentLocale}`,
|
|
75
|
+
'headings:',
|
|
76
|
+
...Object.entries(TESTS_HUB_HEADINGS_EN).map(([k, v]) => ` ${k}: ${v}`),
|
|
77
|
+
];
|
|
78
|
+
fs.writeFileSync(file, `${lines.join('\n')}\n`, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} projectRoot
|
|
83
|
+
* @param {string} [docsRootRel]
|
|
84
|
+
*/
|
|
85
|
+
export function loadDocsHubLocale(projectRoot, docsRootRel) {
|
|
86
|
+
const rel = docsRootRel || process.env.FLOWGRID_DOCS_ROOT || 'docs';
|
|
87
|
+
const manifest = path.join(path.resolve(projectRoot), rel, 'docs-hub.locale.yaml');
|
|
88
|
+
if (!fs.existsSync(manifest)) {
|
|
89
|
+
return { structureLocale: STRUCTURE_LOCALE, contentLocale: 'vi' };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const data = parse(fs.readFileSync(manifest, 'utf8')) ?? {};
|
|
93
|
+
return {
|
|
94
|
+
structureLocale: data.structureLocale || STRUCTURE_LOCALE,
|
|
95
|
+
contentLocale: normalizeContentLocale(data.contentLocale, 'vi'),
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return { structureLocale: STRUCTURE_LOCALE, contentLocale: 'vi' };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -6,6 +6,7 @@ import { renderSpecMarkdown } from './render-spec-markdown.mjs'
|
|
|
6
6
|
import { renderDataModelMarkdown } from './render-data-model-markdown.mjs'
|
|
7
7
|
import { renderApiSummaryMarkdown } from './render-api-summary-markdown.mjs'
|
|
8
8
|
import { resolveBackendSpecForBundle } from '../../spec/lib/audit-db-tables.mjs'
|
|
9
|
+
import { loadDocsHubLocale } from './docs-hub-locale.mjs'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Flatten bundle → spec shape (tests / fallback). Prefer ir/spec.yaml for site MD.
|
|
@@ -17,7 +18,11 @@ export function bundleToSpecShape(bundle) {
|
|
|
17
18
|
status: bundle.status,
|
|
18
19
|
owner: bundle.owner,
|
|
19
20
|
summary: bundle.summary ?? bundle.review?.summary,
|
|
20
|
-
|
|
21
|
+
scopeIn: bundle.scopeIn,
|
|
22
|
+
nonGoals: bundle.nonGoals,
|
|
23
|
+
userFlows: bundle.userFlows,
|
|
24
|
+
nfr: bundle.nfr,
|
|
25
|
+
userStories: bundle.userStories ?? bundle.spec?.userStories,
|
|
21
26
|
}
|
|
22
27
|
return {
|
|
23
28
|
...meta,
|
|
@@ -158,11 +163,13 @@ export async function writeIrSpecMarkdownFile(specFile, mdOut, context = {}) {
|
|
|
158
163
|
const design = existsSync(designFile) ? parse(await readFile(designFile, 'utf8')) ?? {} : {}
|
|
159
164
|
const specShape = mergeDesignIntoSpecForRender(spec, design)
|
|
160
165
|
const projectRoot = context.projectRoot ?? process.cwd()
|
|
166
|
+
const { contentLocale: docsContentLocale } = loadDocsHubLocale(projectRoot)
|
|
161
167
|
const markdown = renderIrSpecMarkdown(specShape, {
|
|
162
168
|
testcases: [],
|
|
163
169
|
output: { specFile: path.basename(mdOut), testcasesDir: 'testcases' },
|
|
164
170
|
devAppBaseUrl: context.devAppBaseUrl ?? '',
|
|
165
171
|
projectRoot,
|
|
172
|
+
docsContentLocale,
|
|
166
173
|
})
|
|
167
174
|
await mkdir(path.dirname(mdOut), { recursive: true })
|
|
168
175
|
await writeFile(mdOut, markdown, 'utf8')
|
|
@@ -115,7 +115,7 @@ function getOverviewSidebar(root: string, docsDir: string, prefix: string) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
function getBusinessProcessSidebarItems(root: string, prefix: string) {
|
|
118
|
-
const processDir = path.join(root, prefix.replace(/^\//, ''), '03-
|
|
118
|
+
const processDir = path.join(root, prefix.replace(/^\//, ''), '03-user-flows')
|
|
119
119
|
if (!fs.existsSync(processDir)) return []
|
|
120
120
|
try {
|
|
121
121
|
const entries = fs.readdirSync(processDir, { withFileTypes: true })
|
|
@@ -129,7 +129,7 @@ function getBusinessProcessSidebarItems(root: string, prefix: string) {
|
|
|
129
129
|
title = markdownNavText(entry.name, content) || nameWithoutExt
|
|
130
130
|
items.push({
|
|
131
131
|
text: title,
|
|
132
|
-
link: `${prefix}/03-
|
|
132
|
+
link: `${prefix}/03-user-flows/${nameWithoutExt}`
|
|
133
133
|
})
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -399,10 +399,10 @@ export default () => {
|
|
|
399
399
|
{ text: '01 Introduction', link: `${archPrefix}/01-introduction/` },
|
|
400
400
|
{ text: '02 Constraints', link: `${archPrefix}/02-constraints/` },
|
|
401
401
|
{
|
|
402
|
-
text: '03
|
|
402
|
+
text: '03 Luồng người dùng',
|
|
403
403
|
collapsed: true,
|
|
404
404
|
items: [
|
|
405
|
-
{ text: 'Catalog', link: `${archPrefix}/03-
|
|
405
|
+
{ text: 'Catalog', link: `${archPrefix}/03-user-flows/` },
|
|
406
406
|
...getBusinessProcessSidebarItems(projectRoot, archPrefix),
|
|
407
407
|
],
|
|
408
408
|
},
|
|
@@ -546,12 +546,12 @@ function auditBundleContent(rawText, filePath, pageType) {
|
|
|
546
546
|
// Quality warnings (non-blocking — does not affect totalGaps for structure)
|
|
547
547
|
// =========================================================================
|
|
548
548
|
|
|
549
|
-
if (!has(rawText, '
|
|
549
|
+
if (!has(rawText, 'scopeIn:')) {
|
|
550
550
|
addWarning(
|
|
551
|
-
'
|
|
552
|
-
'
|
|
553
|
-
'
|
|
554
|
-
'Declare
|
|
551
|
+
'WARN_NO_SCOPE_IN',
|
|
552
|
+
'scopeIn',
|
|
553
|
+
'scopeIn not declared — generated spec.md missing “Trong phạm vi” (PRD in-scope).',
|
|
554
|
+
'Declare scopeIn: | with bullets for what this screen/feature includes.'
|
|
555
555
|
);
|
|
556
556
|
}
|
|
557
557
|
|
|
@@ -559,8 +559,39 @@ function auditBundleContent(rawText, filePath, pageType) {
|
|
|
559
559
|
addWarning(
|
|
560
560
|
'WARN_NO_NON_GOALS',
|
|
561
561
|
'nonGoals',
|
|
562
|
-
'nonGoals not declared —
|
|
563
|
-
'Declare nonGoals: | with explicit
|
|
562
|
+
'nonGoals not declared — generated spec.md missing “Ngoài phạm vi” (PRD out-of-scope).',
|
|
563
|
+
'Declare nonGoals: | with explicit out-of-scope bullets.'
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!has(rawText, 'nfr:')) {
|
|
568
|
+
addWarning(
|
|
569
|
+
'WARN_NO_NFR',
|
|
570
|
+
'nfr',
|
|
571
|
+
'nfr not declared — generated spec.md missing non-functional section (perf/security).',
|
|
572
|
+
'Declare nfr: | or link architecture/08-cross-cutting in prose.'
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const multiScreen =
|
|
577
|
+
has(rawText, 'nextScreenOnSuccess') ||
|
|
578
|
+
has(rawText, 'contextualAction') ||
|
|
579
|
+
has(rawText, 'sourceScreen:');
|
|
580
|
+
if (multiScreen && !has(rawText, 'userFlows:')) {
|
|
581
|
+
addWarning(
|
|
582
|
+
'WARN_NO_USER_FLOWS',
|
|
583
|
+
'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.'
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (has(rawText, 'risks:')) {
|
|
590
|
+
addWarning(
|
|
591
|
+
'WARN_BUNDLE_RISKS_FORBIDDEN',
|
|
592
|
+
'risks',
|
|
593
|
+
'Rủi ro không ghi trên feature bundle — SSOT chỉ `architecture/11-risks/risk-register.md`.',
|
|
594
|
+
'Xóa key `risks:` khỏi bundle; thêm dòng vào risk-register.md (`RISK-*`). Dùng `/risk-register`.'
|
|
564
595
|
);
|
|
565
596
|
}
|
|
566
597
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* audit-flow-gaps.mjs
|
|
5
|
-
* Zero-dependency static audit script for FlowGrid
|
|
6
|
-
* Ensures 100% adherence to the 6-section
|
|
5
|
+
* Zero-dependency static audit script for FlowGrid User flow markdown files (FLOW-*.md).
|
|
6
|
+
* Ensures 100% adherence to the 6-section user flow standard and background logic rules.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import fs from 'fs';
|
|
@@ -138,7 +138,7 @@ function absUnder(root, rel) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function findFlowMarkdown(docsRoot, id) {
|
|
141
|
-
const catalog = path.join(docsRoot, 'architecture', '03-
|
|
141
|
+
const catalog = path.join(docsRoot, 'architecture', '03-user-flows', `${id}.md`)
|
|
142
142
|
if (existsSync(catalog)) return catalog
|
|
143
143
|
const hits = []
|
|
144
144
|
const walk = (dir) => {
|
|
@@ -150,7 +150,7 @@ function findFlowMarkdown(docsRoot, id) {
|
|
|
150
150
|
walk(p)
|
|
151
151
|
continue
|
|
152
152
|
}
|
|
153
|
-
if (ent.name === `${id}.md` && p.split(path.sep).join('/').includes('/common/
|
|
153
|
+
if (ent.name === `${id}.md` && p.split(path.sep).join('/').includes('/common/user-flows/')) {
|
|
154
154
|
hits.push(p)
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -206,7 +206,7 @@ export function resolveHubId(repoRoot, id, mode = 'testcase') {
|
|
|
206
206
|
const docsRoot = getDocsRoot()
|
|
207
207
|
const md = findFlowMarkdown(docsRoot, id)
|
|
208
208
|
if (!md) {
|
|
209
|
-
throw new Error(`Unknown FLOW ${id} — expected architecture/03-
|
|
209
|
+
throw new Error(`Unknown FLOW ${id} — expected architecture/03-user-flows/${id}.md or …/common/user-flows/${id}.md`)
|
|
210
210
|
}
|
|
211
211
|
notes.push(`flow markdown: ${path.relative(repoRoot, md)}`)
|
|
212
212
|
return { kind: 'flow', id, paths: [md], notes }
|
|
@@ -6,7 +6,7 @@ disable-model-invocation: true
|
|
|
6
6
|
|
|
7
7
|
# /legacy — Legacy Context Modifier
|
|
8
8
|
|
|
9
|
-
**Skill Modifier:** When a member uses `/legacy` alongside another specialized skill (e.g., `/legacy /spec`, `/legacy /overview`, `/legacy /
|
|
9
|
+
**Skill Modifier:** When a member uses `/legacy` alongside another specialized skill (e.g., `/legacy /spec`, `/legacy /overview`, `/legacy /user-flow`), the Agent MUST enforce the following behavioral shifts.
|
|
10
10
|
|
|
11
11
|
## Behavioral Shifts (Context Shift)
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ disable-model-invocation: true
|
|
|
18
18
|
- **Tier 1 (Page / API Detail — `/legacy /spec`)**:
|
|
19
19
|
- ONLY audit internal page/API scope: Check **missing Validation** (field validation rules, max length, format regex...) and **Local Security** (e.g. Laravel `@csrf`, Auth guard/middleware on route, input sanitization).
|
|
20
20
|
- Do NOT perform cross-system end-to-end vulnerability audits at this level.
|
|
21
|
-
- **Tier 2 (Cross-Flow /
|
|
21
|
+
- **Tier 2 (Cross-Flow / User flow — `/legacy /user-flow`, Module, Surface)**:
|
|
22
22
|
- Audit **End-to-End Business Flow Gaps**: Check data/status misalignment between Step 1 (Screen 1) and Step 2 (Screen 2), orphan APIs/steps, broken flow steps, and missing rollback/confirmation handling on failure.
|
|
23
23
|
|
|
24
24
|
3. **Metadata Updates (When Applicable):**
|
|
@@ -25,7 +25,7 @@ Inside every `common/`:
|
|
|
25
25
|
```text
|
|
26
26
|
common/
|
|
27
27
|
patterns/ ← /common Markdown (BA/QA rules)
|
|
28
|
-
|
|
28
|
+
user-flows/ ← module/cluster FLOW-*.md (Luồng người dùng; not architecture catalog)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Optional: `data-model/`, `integrations/`, `db-erd.md`, `cross-service.md` at the same LCA (see skills `/db-erd`, `/cross-service`).
|
|
@@ -38,20 +38,20 @@ If the same pattern already exists at a **narrower** `common/`, **reuse it** (re
|
|
|
38
38
|
|
|
39
39
|
## 3. Consume order (`/spec`, `/grill-*`, `/api-spec`)
|
|
40
40
|
|
|
41
|
-
From the function folder, walk **up** and read the **nearest** `common/patterns/*.md` (and FLOW under `
|
|
41
|
+
From the function folder, walk **up** and read the **nearest** `common/patterns/*.md` (and FLOW under `user-flows/` when relevant). Do not author duplicate YAML CMN bundles.
|
|
42
42
|
|
|
43
43
|
Same walk for `patterns/` Markdown.
|
|
44
44
|
|
|
45
|
-
## 4.
|
|
45
|
+
## 4. Luồng người dùng (`FLOW-*`)
|
|
46
46
|
|
|
47
47
|
| Scope of the flow | File |
|
|
48
48
|
|-------------------|------|
|
|
49
|
-
| Org / cross-surface / “hero” catalog | `architecture/03-
|
|
50
|
-
| Many modules, one surface | `surfaces/<surface>/common/
|
|
51
|
-
| Whole module, several clusters | `surfaces/<surface>/<CMP-id>/common/
|
|
52
|
-
| One cluster / submodule only | `surfaces/<surface>/<CMP-id>/<NN>/common/
|
|
49
|
+
| Org / cross-surface / “hero” catalog | `architecture/03-user-flows/FLOW-*.md` (MCP `flowgrid_docs_user_flows`) |
|
|
50
|
+
| Many modules, one surface | `surfaces/<surface>/common/user-flows/FLOW-*.md` |
|
|
51
|
+
| Whole module, several clusters | `surfaces/<surface>/<CMP-id>/common/user-flows/FLOW-*.md` |
|
|
52
|
+
| One cluster / submodule only | `surfaces/<surface>/<CMP-id>/<NN>/common/user-flows/FLOW-*.md` |
|
|
53
53
|
|
|
54
|
-
Do **not** drop `FLOW-*.md` beside a single `W-*` bundle. Do **not** write module-internal flows only under `architecture/03-
|
|
54
|
+
Do **not** drop `FLOW-*.md` beside a single `W-*` bundle. Do **not** write module-internal flows only under `architecture/03-user-flows/` (optional **link** from the catalog to the product path).
|
|
55
55
|
|
|
56
56
|
`/db-erd` and `/cross-service` use the **same** `common/` LCA (`common/db-erd.md`, `common/cross-service.md`), not a second invented folder name.
|
|
57
57
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# Leaf bundle ↔ PRD lite (map for /spec)
|
|
2
2
|
|
|
3
|
-
| PRD section | Bundle /
|
|
3
|
+
| PRD section | Bundle key → `ir/generated/spec.md` |
|
|
4
4
|
| --- | --- |
|
|
5
|
-
|
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| User
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
| Open questions | `qa/*.yaml` |
|
|
5
|
+
| Goals / background | `summary` |
|
|
6
|
+
| In scope | `scopeIn` |
|
|
7
|
+
| Out of scope | `nonGoals` |
|
|
8
|
+
| Features & requirements | `userStories` + `design.*` (render sections) |
|
|
9
|
+
| User flows | `userFlows` (+ FLOW-* docs) |
|
|
10
|
+
| Data & API | `entities` + links `data-model.md` / `api.md` / `01` |
|
|
11
|
+
| NFR | `nfr` |
|
|
12
|
+
| Risks | **Chỉ** `architecture/11-risks/risk-register.md` — không key trên bundle |
|
|
13
|
+
| Open questions | `qa/*.yaml` → `Q&A` + Phụ lục |
|
|
14
14
|
|
|
15
|
-
**
|
|
16
|
-
|
|
17
|
-
**BA deliverable:** `ir/generated/spec.md` (TOC, overview, metrics, non-goals) after `flowgrid split` + `flowgrid render`.
|
|
15
|
+
**BA deliverable:** `flowgrid split` + `flowgrid render` → `ir/generated/spec.md` (TOC PRD). Audit: `WARN_NO_SCOPE_IN`, `WARN_NO_NON_GOALS`, `WARN_NO_NFR`, `WARN_NO_USER_FLOWS` (when handoff). Rủi ro: `/risk-register` + `audit risks` (không `risks:` trên bundle).
|
|
18
16
|
|
|
19
17
|
**Profile trim:** list → giữ Initial Load, Affordances (nếu có), Exceptions; bỏ scenario form nếu không có `ui.form`. create/detail tương tự — không copy 6 scenario khi không áp dụng.
|