@educa-corp/sdd-framework 0.5.0 → 0.7.0
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/build.js +113 -19
- package/bin/gate-trace.js +487 -0
- package/bin/index.js +445 -146
- package/bin/lint-trace.js +643 -0
- package/bin/self-check.js +804 -2
- package/bin/trace-schema.json +621 -10
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/README.md +20 -0
- package/core/commands/amend-prd.md +518 -0
- package/core/commands/debug.md +123 -511
- package/core/commands/define-product.md +86 -510
- package/core/commands/dev-gen-test.md +86 -510
- package/core/commands/dev-run-test.md +133 -519
- package/core/commands/dev-smoke-test.md +86 -510
- package/core/commands/extend-prd.md +128 -522
- package/core/commands/fix-bug.md +118 -509
- package/core/commands/generate-architecture.md +94 -515
- package/core/commands/generate-bdd.md +128 -513
- package/core/commands/generate-code.md +119 -510
- package/core/commands/generate-design-spec.md +86 -510
- package/core/commands/generate-prd.md +89 -510
- package/core/commands/generate-spec-manifest.md +86 -510
- package/core/commands/generate-tech-docs.md +120 -512
- package/core/commands/learn.md +172 -496
- package/core/commands/map-testids.md +86 -510
- package/core/commands/propose-scenario.md +86 -510
- package/core/commands/qc-analyze.md +86 -510
- package/core/commands/qc-design-test.md +86 -510
- package/core/commands/qc-plan.md +86 -510
- package/core/commands/qc-report.md +86 -510
- package/core/commands/qc-review.md +86 -510
- package/core/commands/qc-run-test.md +115 -513
- package/core/commands/refine-prd.md +112 -522
- package/core/commands/report-bug.md +86 -510
- package/core/commands/review-code.md +123 -511
- package/core/commands/review-context.md +136 -522
- package/core/commands/review-tech-docs.md +90 -511
- package/core/commands/setup-ai-first.md +166 -138
- package/core/commands/sync.md +155 -107
- package/core/commands/update-framework.md +16 -103
- package/core/commands/validate-traces.md +426 -511
- package/core/hooks/data-guard.js +174 -83
- package/core/hooks/settings.json +2 -1
- package/core/rules/workflow.md +64 -4
- package/core/steps/capture-lesson.md +34 -1
- package/core/steps/context-loader.md +50 -8
- package/core/steps/gate.md +92 -35
- package/core/steps/report-footer.md +23 -0
- package/core/templates/README.md +24 -1
- package/core/templates/ci/trace-gate.yml +146 -0
- package/core/templates/feature.template +1 -1
- package/core/templates/hooks/pre-push +61 -0
- package/docs/02-concepts/architecture.md +61 -6
- package/docs/02-concepts/traceability.md +57 -0
- package/docs/03-guides/architect.md +63 -0
- package/docs/04-reference/commands.md +148 -134
- package/docs/04-reference/model-selection.md +32 -19
- package/docs/04-reference/trace-schema.md +39 -0
- package/docs/explain/02b-extend-prd.md +1 -1
- package/docs/explain/02c-amend-prd.md +152 -0
- package/docs/explain/21-validate-traces.md +2 -1
- package/docs/explain/27-learn.md +5 -3
- package/docs/explain/28-sync.md +25 -0
- package/docs/explain/README.md +136 -135
- package/package.json +5 -9
- package/commands/debug.md +0 -917
- package/commands/debug.tmpl +0 -257
- package/commands/define-product.md +0 -862
- package/commands/define-product.tmpl +0 -225
- package/commands/dev-gen-test.md +0 -1124
- package/commands/dev-gen-test.tmpl +0 -490
- package/commands/dev-run-test.md +0 -859
- package/commands/dev-run-test.tmpl +0 -225
- package/commands/dev-smoke-test.md +0 -798
- package/commands/dev-smoke-test.tmpl +0 -217
- package/commands/extend-prd.md +0 -907
- package/commands/extend-prd.tmpl +0 -270
- package/commands/fix-bug.md +0 -910
- package/commands/fix-bug.tmpl +0 -197
- package/commands/generate-architecture.md +0 -775
- package/commands/generate-architecture.tmpl +0 -194
- package/commands/generate-bdd.md +0 -1347
- package/commands/generate-bdd.tmpl +0 -590
- package/commands/generate-code.md +0 -1283
- package/commands/generate-code.tmpl +0 -649
- package/commands/generate-design-spec.md +0 -1161
- package/commands/generate-design-spec.tmpl +0 -524
- package/commands/generate-prd.md +0 -1143
- package/commands/generate-prd.tmpl +0 -223
- package/commands/generate-spec-manifest.md +0 -745
- package/commands/generate-spec-manifest.tmpl +0 -164
- package/commands/generate-tech-docs.md +0 -1344
- package/commands/generate-tech-docs.tmpl +0 -273
- package/commands/learn.md +0 -723
- package/commands/learn.tmpl +0 -63
- package/commands/map-testids.md +0 -662
- package/commands/map-testids.tmpl +0 -81
- package/commands/propose-scenario.md +0 -783
- package/commands/propose-scenario.tmpl +0 -202
- package/commands/qc-analyze.md +0 -693
- package/commands/qc-analyze.tmpl +0 -112
- package/commands/qc-design-test.md +0 -650
- package/commands/qc-design-test.tmpl +0 -69
- package/commands/qc-plan.md +0 -630
- package/commands/qc-plan.tmpl +0 -49
- package/commands/qc-report.md +0 -641
- package/commands/qc-report.tmpl +0 -60
- package/commands/qc-review.md +0 -634
- package/commands/qc-review.tmpl +0 -53
- package/commands/qc-run-test.md +0 -750
- package/commands/qc-run-test.tmpl +0 -116
- package/commands/refine-prd.md +0 -1074
- package/commands/refine-prd.tmpl +0 -278
- package/commands/report-bug.md +0 -729
- package/commands/report-bug.tmpl +0 -148
- package/commands/review-code.md +0 -803
- package/commands/review-code.tmpl +0 -143
- package/commands/review-context.md +0 -1323
- package/commands/review-context.tmpl +0 -527
- package/commands/review-tech-docs.md +0 -982
- package/commands/review-tech-docs.tmpl +0 -401
- package/commands/setup-ai-first.md +0 -574
- package/commands/setup-ai-first.tmpl +0 -378
- package/commands/sync.md +0 -486
- package/commands/sync.tmpl +0 -384
- package/commands/update-framework.md +0 -290
- package/commands/update-framework.tmpl +0 -188
- package/commands/validate-traces.md +0 -1435
- package/commands/validate-traces.tmpl +0 -854
- package/hooks/data-guard.js +0 -141
- package/hooks/settings.json +0 -18
- package/modules/android-compose/module.yaml +0 -13
- package/modules/android-compose/stack-profile.yaml +0 -57
- package/modules/angular/architecture-snippets/component-patterns.md +0 -187
- package/modules/angular/module.yaml +0 -6
- package/modules/angular/stack-profile.yaml +0 -38
- package/modules/context-engineering/architecture-snippets/context-design.md +0 -119
- package/modules/context-engineering/module.yaml +0 -9
- package/modules/context-engineering/stack-profile.yaml +0 -61
- package/modules/dotnet/architecture-snippets/clean-arch.md +0 -160
- package/modules/dotnet/module.yaml +0 -6
- package/modules/dotnet/stack-profile.yaml +0 -50
- package/modules/flutter/module.yaml +0 -14
- package/modules/flutter/stack-profile.yaml +0 -59
- package/modules/golang/architecture-snippets/domain-layout.md +0 -283
- package/modules/golang/module.yaml +0 -6
- package/modules/golang/stack-profile.yaml +0 -40
- package/modules/ios-swiftui/module.yaml +0 -13
- package/modules/ios-swiftui/stack-profile.yaml +0 -55
- package/modules/java-spring/architecture-snippets/layered-arch.md +0 -201
- package/modules/java-spring/module.yaml +0 -15
- package/modules/java-spring/stack-profile.yaml +0 -28
- package/modules/nextjs/architecture-snippets/app-router-patterns.md +0 -269
- package/modules/nextjs/module.yaml +0 -14
- package/modules/nextjs/stack-profile.yaml +0 -74
- package/modules/nuxt/module.yaml +0 -14
- package/modules/nuxt/stack-profile.yaml +0 -58
- package/modules/phaser-game/architecture-snippets/phaser-scene-patterns.md +0 -646
- package/modules/phaser-game/module.yaml +0 -15
- package/modules/phaser-game/stack-profile.yaml +0 -90
- package/modules/php-laravel/architecture-snippets/service-repository.md +0 -302
- package/modules/php-laravel/module.yaml +0 -15
- package/modules/php-laravel/stack-profile.yaml +0 -56
- package/modules/qc-playwright/stack-profile.yaml +0 -66
- package/modules/react/architecture-snippets/hooks-query-patterns.md +0 -254
- package/modules/react/module.yaml +0 -14
- package/modules/react/stack-profile.yaml +0 -63
- package/modules/react-native/module.yaml +0 -14
- package/modules/react-native/stack-profile.yaml +0 -56
- package/modules/vue/module.yaml +0 -14
- package/modules/vue/stack-profile.yaml +0 -65
- package/rules/data-protection.md +0 -80
- package/rules/workflow.md +0 -73
- package/scripts/init.sh +0 -49
- package/scripts/upgrade.sh +0 -94
- package/skills/code/SKILL.md +0 -19
- package/skills/code/SKILL.tmpl +0 -19
- package/skills/debug/SKILL.md +0 -19
- package/skills/debug/SKILL.tmpl +0 -19
- package/skills/design-spec/SKILL.md +0 -11
- package/skills/design-spec/SKILL.tmpl +0 -11
- package/skills/discovery/SKILL.md +0 -14
- package/skills/discovery/SKILL.tmpl +0 -14
- package/skills/prd/SKILL.md +0 -19
- package/skills/prd/SKILL.tmpl +0 -19
- package/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
- package/skills/qc/qa-analyst/acceptance-criteria.md +0 -60
- package/skills/qc/qa-analyst/business-rules.md +0 -59
- package/skills/qc/qa-analyst/data-flow.md +0 -64
- package/skills/qc/qa-analyst/spec-breakdown.md +0 -61
- package/skills/qc/qa-designer/e2e/journey.md +0 -41
- package/skills/qc/qa-designer/exploratory/charter.md +0 -68
- package/skills/qc/qa-designer/exploratory/explore-to-functional.md +0 -43
- package/skills/qc/qa-designer/functional/api.md +0 -45
- package/skills/qc/qa-designer/functional/gui-feature.md +0 -46
- package/skills/qc/qa-designer/functional/gui-screen.md +0 -52
- package/skills/qc/qa-designer/integration/api.md +0 -42
- package/skills/qc/qa-designer/integration/db.md +0 -39
- package/skills/qc/qa-designer/integration/gui.md +0 -40
- package/skills/qc/qa-designer/integration/kafka.md +0 -40
- package/skills/qc/qa-designer/non-functional.md +0 -40
- package/skills/qc/qa-planner/test-plan.md +0 -120
- package/skills/qc/qa-reviewer/script/e2e.md +0 -87
- package/skills/qc/qa-reviewer/script/exploratory.md +0 -45
- package/skills/qc/qa-reviewer/script/functional.md +0 -101
- package/skills/qc/qa-reviewer/script/integration.md +0 -91
- package/skills/qc/qa-reviewer/script/non-functional.md +0 -126
- package/skills/qc/qa-reviewer/test-case/e2e.md +0 -73
- package/skills/qc/qa-reviewer/test-case/exploratory.md +0 -43
- package/skills/qc/qa-reviewer/test-case/functional.md +0 -76
- package/skills/qc/qa-reviewer/test-case/integration.md +0 -69
- package/skills/qc/qa-reviewer/test-case/non-functional.md +0 -73
- package/skills/qc/qa-runner/e2e.md +0 -49
- package/skills/qc/qa-runner/exploratory/session.md +0 -36
- package/skills/qc/qa-runner/functional/api.md +0 -35
- package/skills/qc/qa-runner/functional/gui-feature.md +0 -51
- package/skills/qc/qa-runner/functional/gui-screen.md +0 -55
- package/skills/qc/qa-runner/integration.md +0 -47
- package/skills/qc/qa-runner/non-functional.md +0 -49
- package/skills/qc/qa-runner/report/report.md +0 -37
- package/skills/setup-ai-first/SKILL.md +0 -19
- package/skills/setup-ai-first/SKILL.tmpl +0 -19
- package/skills/spec/SKILL.md +0 -19
- package/skills/spec/SKILL.tmpl +0 -19
- package/skills/test/SKILL.md +0 -18
- package/skills/test/SKILL.tmpl +0 -18
- package/steps/business-language.md +0 -56
- package/steps/capture-lesson.md +0 -79
- package/steps/context-loader.md +0 -385
- package/steps/gate.md +0 -94
- package/steps/report-footer.md +0 -102
- package/steps/review-fanout.md +0 -159
- package/steps/spawn-agent.md +0 -129
- package/steps/trace-mirror.md +0 -53
- package/templates/README.md +0 -47
- package/templates/architecture.template.md +0 -394
- package/templates/design-spec.template.md +0 -217
- package/templates/feature.template +0 -123
- package/templates/platform-guide.template.md +0 -145
- package/templates/prd.template.md +0 -283
- package/templates/product-definition.template.md +0 -188
- package/templates/project-context.yaml +0 -212
- package/templates/tech-design.template.md +0 -490
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gate-trace — CỔNG CHẶN PR. Exit 1 khi trace có cờ 🔴.
|
|
4
|
+
*
|
|
5
|
+
* Vì sao tồn tại (GAPS-v3 G39): README khai bằng chữ in đậm rằng 4 cờ (ORPHANED ·
|
|
6
|
+
* TRACE_ORPHAN · SEAM_UNWIRED · STUB_UNRESOLVED) "chặn PR", kèm câu "build xanh + test
|
|
7
|
+
* từng-UC xanh KHÔNG phát hiện được" — đó là điểm bán hàng cốt lõi của framework.
|
|
8
|
+
* Nhưng trước file này KHÔNG CÓ GÌ CHẶN: 0 CI, 0 git hook, 0 exit code. /validate-traces
|
|
9
|
+
* in report vào terminal Claude Code, nơi không CI nào đọc được. Kỷ luật là honor-system,
|
|
10
|
+
* và cái gì không chặn thì sau sprint thứ ba không ai làm.
|
|
11
|
+
*
|
|
12
|
+
* Bốn tầng, theo thứ tự — mỗi tầng vô nghĩa nếu tầng trước chưa xanh:
|
|
13
|
+
*
|
|
14
|
+
* G0 Sổ có ĐƯỢC BẢO VỆ không? → nằm trong git? git biết cách gộp?
|
|
15
|
+
* Kiểm cấu trúc của một quyển sổ không nằm trong git thì vô nghĩa. Hai điều kiện
|
|
16
|
+
* này lint đã phát hiện dưới dạng WARN; ở đây chúng được NÂNG thành lỗi chặn —
|
|
17
|
+
* cố ý: pre-push nhắc, CI chặn (chặn push vì một dòng cấu hình sẽ dạy dev gõ
|
|
18
|
+
* --no-verify, và mất luôn phần lint thật).
|
|
19
|
+
* G1 Sổ có ĐÚNG HÌNH DẠNG không? → gọi bin/lint-trace.js (G38)
|
|
20
|
+
* Phán trạng thái trên sổ lệch cột là phán trên dữ liệu rác.
|
|
21
|
+
* G2 Report có TƯƠI không? → đối chiếu report ↔ sổ TSV
|
|
22
|
+
* Không có tầng này thì gate là sân khấu: chạy /validate-traces một lần, commit
|
|
23
|
+
* report, rồi sửa gì cũng được — CI đọc report cũ và cho qua mãi mãi.
|
|
24
|
+
* G3 Có cờ 🔴 nào không? → đếm counter khai trong schema `gate.blocking`
|
|
25
|
+
*
|
|
26
|
+
* PHẠM VI — nói thẳng để không ai tin quá:
|
|
27
|
+
* Gate chứng minh "report khớp SỔ". Chỉ /validate-traces chứng minh được "sổ khớp CODE"
|
|
28
|
+
* (nó cần quét tag trong source, đọc .feature, so version — việc của LLM, không phải
|
|
29
|
+
* của file này). Nên gate bắt được ca phổ biến "quên chạy lại", nhưng KHÔNG bắt được
|
|
30
|
+
* ca "sửa code mà không đụng sổ". Đường bịt: CI chạy /validate-traces, hoặc yêu cầu
|
|
31
|
+
* report có trong diff khi src đổi. §Output in ra đúng giới hạn này mỗi lần chạy.
|
|
32
|
+
*
|
|
33
|
+
* Cờ 🟠 (PRD_DRIFT, BDD_DRIFT, TECHDOC_DRIFT…) KHÔNG chặn — cố ý. Chúng là "code chưa
|
|
34
|
+
* theo kịp spec", một loại nợ khác. Chặn PR vì nợ là làm cổng bị tắt.
|
|
35
|
+
*
|
|
36
|
+
* Chạy: node bin/gate-trace.js [--trace DIR[,DIR]] [--report PATH]
|
|
37
|
+
* [--skip-lint] [--no-reconcile] [--warn-only] [--json]
|
|
38
|
+
* exit 1 nếu FAIL (0 với --warn-only)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('fs');
|
|
42
|
+
const path = require('path');
|
|
43
|
+
|
|
44
|
+
const schema = JSON.parse(fs.readFileSync(path.join(__dirname, 'trace-schema.json'), 'utf8'));
|
|
45
|
+
const GATE = schema.gate;
|
|
46
|
+
|
|
47
|
+
if (!GATE || !Array.isArray(GATE.blocking) || !GATE.blocking.length) {
|
|
48
|
+
console.error('❌ bin/trace-schema.json thiếu khối `gate.blocking` — không biết chặn cờ nào.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── CLI ───────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const argv = process.argv.slice(2);
|
|
55
|
+
const flag = (name, def) => {
|
|
56
|
+
const i = argv.indexOf(name);
|
|
57
|
+
return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : def;
|
|
58
|
+
};
|
|
59
|
+
const WARN_ONLY = argv.includes('--warn-only');
|
|
60
|
+
const AS_JSON = argv.includes('--json');
|
|
61
|
+
const SKIP_LINT = argv.includes('--skip-lint');
|
|
62
|
+
const NO_RECONCILE = argv.includes('--no-reconcile');
|
|
63
|
+
const TRACE_DIRS = flag('--trace', '.trace').split(',').map(s => s.trim()).filter(Boolean);
|
|
64
|
+
const REPORT_ARG = flag('--report', null);
|
|
65
|
+
|
|
66
|
+
const fails = [];
|
|
67
|
+
const warns = [];
|
|
68
|
+
const oks = [];
|
|
69
|
+
const fail = (stage, msg, hint) => fails.push({ stage, msg, hint });
|
|
70
|
+
const warn = (stage, msg, hint) => warns.push({ stage, msg, hint });
|
|
71
|
+
|
|
72
|
+
const rel = p => path.relative(process.cwd(), p).replace(/\\/g, '/');
|
|
73
|
+
|
|
74
|
+
// ── Chạy lint MỘT LẦN — G0 và G1 cùng đọc kết quả này ─────────────────────────
|
|
75
|
+
|
|
76
|
+
let lintSummary = 'bỏ qua (--skip-lint)';
|
|
77
|
+
let lintParsed = null;
|
|
78
|
+
let lintStderr = '';
|
|
79
|
+
|
|
80
|
+
if (!SKIP_LINT) {
|
|
81
|
+
const lintPath = path.join(__dirname, 'lint-trace.js');
|
|
82
|
+
const args = ['--trace', TRACE_DIRS.join(','), '--json'];
|
|
83
|
+
const r = require('child_process').spawnSync(process.execPath, [lintPath, ...args],
|
|
84
|
+
{ encoding: 'utf8' });
|
|
85
|
+
lintStderr = r.stderr || '';
|
|
86
|
+
try { lintParsed = JSON.parse(r.stdout); } catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Tầng G0 — sổ có ĐƯỢC BẢO VỆ không (GAPS-v3 T10-debt) ──────────────────────
|
|
90
|
+
//
|
|
91
|
+
// Hỏi TRƯỚC G1 có lý do: G1 kiểm rất kỹ cấu trúc của một quyển sổ mà có thể KHÔNG NẰM
|
|
92
|
+
// TRONG GIT. Kiểm cấu trúc của thứ sắp mất thì vô nghĩa.
|
|
93
|
+
//
|
|
94
|
+
// Hai điều kiện đều đã được lint phát hiện dưới dạng WARN; ở đây chúng được NÂNG thành
|
|
95
|
+
// lỗi chặn. Một implementation, hai mức nghiêm trọng — cố ý: pre-push nhắc, CI chặn.
|
|
96
|
+
// Danh sách rule đọc từ schema (`gate.config_preconditions`), không hard-code — R9 canh.
|
|
97
|
+
const PRECOND = (GATE.config_preconditions || {}).promote_lint_rules || [];
|
|
98
|
+
|
|
99
|
+
if (SKIP_LINT) {
|
|
100
|
+
warn('G0', 'bỏ qua kiểm cấu hình (--skip-lint) — không biết sổ có được bảo vệ hay không');
|
|
101
|
+
} else if (!lintParsed) {
|
|
102
|
+
fail('G0', 'không chạy được bin/lint-trace.js — không xác nhận được sổ có được bảo vệ',
|
|
103
|
+
lintStderr.trim().split('\n').slice(0, 3).join('\n ')
|
|
104
|
+
|| `Chạy tay để xem lỗi: node bin/lint-trace.js --trace ${TRACE_DIRS.join(',')}`);
|
|
105
|
+
} else {
|
|
106
|
+
const byRule = new Map(PRECOND.map(p => [p.rule, p.why]));
|
|
107
|
+
const hits = (lintParsed.warns || []).filter(w => byRule.has(w.rule));
|
|
108
|
+
|
|
109
|
+
for (const h of hits) {
|
|
110
|
+
fail('G0', `${h.where} — ${h.msg}`,
|
|
111
|
+
[`Vì sao chặn: ${byRule.get(h.rule)}`,
|
|
112
|
+
h.hint || ''].filter(Boolean).join('\n '));
|
|
113
|
+
}
|
|
114
|
+
if (!hits.length) {
|
|
115
|
+
oks.push(`G0 sổ được bảo vệ — nằm trong git · git biết cách gộp (${PRECOND.length}/${PRECOND.length} điều kiện)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Tầng G1 — sổ có đúng hình dạng không (G38) ────────────────────────────────
|
|
120
|
+
|
|
121
|
+
if (!SKIP_LINT) {
|
|
122
|
+
const parsed = lintParsed;
|
|
123
|
+
if (!parsed) {
|
|
124
|
+
// G0 đã báo lý do — đừng nhân đôi thông điệp, chỉ ghi là không kiểm được.
|
|
125
|
+
fail('G1', 'không xác nhận được sổ có đúng hình dạng (lint không chạy được — xem G0)');
|
|
126
|
+
} else if (!parsed.ok) {
|
|
127
|
+
fail('G1', `sổ trace HỎNG CẤU TRÚC — ${parsed.errors.length} lỗi, gate không phán được`,
|
|
128
|
+
parsed.errors.slice(0, 6).map(e => `${e.where} — ${e.rule}: ${e.msg}`).join('\n ')
|
|
129
|
+
+ (parsed.errors.length > 6 ? `\n … và ${parsed.errors.length - 6} lỗi nữa` : '')
|
|
130
|
+
+ `\n Chi tiết: node bin/lint-trace.js --trace ${TRACE_DIRS.join(',')}`);
|
|
131
|
+
} else {
|
|
132
|
+
const rest = (parsed.warns || []).filter(w => !PRECOND.some(p => p.rule === w.rule));
|
|
133
|
+
lintSummary = `${parsed.scanned.books} sổ + ${parsed.scanned.seams} bảng phụ`
|
|
134
|
+
+ (rest.length ? ` (${rest.length} warning)` : '');
|
|
135
|
+
oks.push(`G1 sổ đúng hình dạng — ${lintSummary}`);
|
|
136
|
+
// Warn đã được G0 nâng thành fail thì KHÔNG in lại ở đây.
|
|
137
|
+
for (const w of rest) warn('G1', `${w.where} — ${w.rule}: ${w.msg}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Định vị report ────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const REPORT_BASENAME = path.basename(GATE.report_path.replace(/^.*[/\\]/, ''));
|
|
144
|
+
|
|
145
|
+
function findReport() {
|
|
146
|
+
if (REPORT_ARG) return fs.existsSync(REPORT_ARG) ? path.resolve(REPORT_ARG) : null;
|
|
147
|
+
for (const d of TRACE_DIRS) {
|
|
148
|
+
const p = path.resolve(d, REPORT_BASENAME);
|
|
149
|
+
if (fs.existsSync(p)) return p;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const reportPath = findReport();
|
|
155
|
+
let report = null;
|
|
156
|
+
|
|
157
|
+
if (!reportPath) {
|
|
158
|
+
fail('G2', `không tìm thấy ${REPORT_BASENAME} — trace CHƯA TỪNG được audit`,
|
|
159
|
+
[`Đã tìm ở: ${TRACE_DIRS.map(d => rel(path.resolve(d, REPORT_BASENAME))).join(' · ')}`,
|
|
160
|
+
`Gate không thể cho qua khi không có gì để đọc — "không có report" ≠ "không có lỗi".`,
|
|
161
|
+
`Chạy /validate-traces trong Claude Code, rồi commit ${REPORT_BASENAME}.`].join('\n '));
|
|
162
|
+
} else {
|
|
163
|
+
try {
|
|
164
|
+
report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
165
|
+
} catch (e) {
|
|
166
|
+
fail('G2', `${rel(reportPath)} không phải JSON hợp lệ: ${e.message}`,
|
|
167
|
+
`Nghi conflict merge chưa giải. trace-report.json là file SINH LẠI ĐƯỢC —\n`
|
|
168
|
+
+ ` đừng merge tay, chạy lại /validate-traces.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Tầng G2 — report có tươi không ────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Đọc mọi row dữ liệu của các sổ TSV (bỏ _seams.tsv — nó không mang scenario).
|
|
176
|
+
* Mỗi row kèm `platform` lấy từ TÊN FILE (`{UC-ID}-{platform}.tsv`) — đó là nơi duy
|
|
177
|
+
* nhất sổ mang platform, và là thứ làm khoá {sc_id, platform} của G48 dựng được.
|
|
178
|
+
*/
|
|
179
|
+
function readBooks(dirs, domainFilter) {
|
|
180
|
+
const rows = [];
|
|
181
|
+
const pf = (schema.vocabularies.platform || {}).values || [];
|
|
182
|
+
const nameRe = pf.length ? new RegExp(`-(${pf.join('|')})\\.tsv$`) : null;
|
|
183
|
+
|
|
184
|
+
const walk = (d) => {
|
|
185
|
+
if (!fs.existsSync(d)) return;
|
|
186
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
187
|
+
const p = path.join(d, e.name);
|
|
188
|
+
if (e.isDirectory()) { walk(p); continue; }
|
|
189
|
+
if (!e.name.endsWith('.tsv') || e.name === '_seams.tsv') continue;
|
|
190
|
+
const m = nameRe && nameRe.exec(e.name);
|
|
191
|
+
const platform = m ? m[1] : '';
|
|
192
|
+
const lines = fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n').split('\n')
|
|
193
|
+
.filter(l => l.trim() !== '');
|
|
194
|
+
if (lines.length < 2) continue;
|
|
195
|
+
const header = lines[0].split('\t').map(s => s.trim());
|
|
196
|
+
const si = header.indexOf('status');
|
|
197
|
+
const ii = header.indexOf('sc_id');
|
|
198
|
+
for (const l of lines.slice(1)) {
|
|
199
|
+
const f = l.split('\t');
|
|
200
|
+
rows.push({
|
|
201
|
+
file: p, platform,
|
|
202
|
+
sc_id: ii !== -1 ? (f[ii] || '').trim() : '',
|
|
203
|
+
status: si !== -1 ? (f[si] || '').trim() : '',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
for (const d of dirs) {
|
|
209
|
+
const abs = path.resolve(d);
|
|
210
|
+
// Report chỉ phủ một domain → chỉ đọc sổ của domain đó, else so lệch là chắc chắn.
|
|
211
|
+
walk(domainFilter && domainFilter !== 'all' ? path.join(abs, domainFilter) : abs);
|
|
212
|
+
}
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Duỗi cây prds[].ucs[].scenarios[] của report thành danh sách phẳng. */
|
|
217
|
+
function flattenReportScenarios(report) {
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const prd of report.prds || []) {
|
|
220
|
+
for (const uc of prd.ucs || []) {
|
|
221
|
+
for (const sc of uc.scenarios || []) out.push(sc);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Domain nào có sổ trên đĩa — để bắt ca report chỉ phủ một phần repo. */
|
|
228
|
+
function domainsOnDisk(dirs) {
|
|
229
|
+
const out = new Set();
|
|
230
|
+
for (const d of dirs) {
|
|
231
|
+
const abs = path.resolve(d);
|
|
232
|
+
if (!fs.existsSync(abs)) continue;
|
|
233
|
+
for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
234
|
+
if (e.isDirectory() && !e.name.startsWith('.')) out.add(e.name);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return [...out];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (report && !NO_RECONCILE) {
|
|
241
|
+
const domain = report.domain || 'all';
|
|
242
|
+
const summary = report.summary || {};
|
|
243
|
+
|
|
244
|
+
// (a) Report phủ một phần repo → cờ 🔴 ở phần chưa audit hoàn toàn vô hình với gate.
|
|
245
|
+
// Cho qua ở đây là cấp một tờ giấy xanh cho thứ chưa ai xem.
|
|
246
|
+
//
|
|
247
|
+
// G57 — LUẬT TUYỆT ĐỐI, không phụ thuộc trên đĩa có bao nhiêu domain.
|
|
248
|
+
// Bản cũ hỏi "còn domain NÀO KHÁC không": `if (others.length) fail`. Trong repo MỘT domain,
|
|
249
|
+
// `others` là RỖNG ⇒ KHÔNG fail ⇒ một report hẹp-theo-PRD (hoặc theo-UC) được nhận là "toàn
|
|
250
|
+
// bộ". Lúc chưa có cờ scope thì vô hại; nó BẬT NGAY khi scoping thành khả dụng — tức bản fix
|
|
251
|
+
// G57 sẽ tự tay mở đúng cái false-green mà chú thích ngay trên cảnh báo.
|
|
252
|
+
// Nên: scope.kind !== 'all' ⇒ FAIL. Hết.
|
|
253
|
+
const scope = report.scope;
|
|
254
|
+
if (scope && typeof scope === 'object') {
|
|
255
|
+
if (scope.kind !== 'all') {
|
|
256
|
+
const what = scope.value && scope.value !== 'all'
|
|
257
|
+
? `${scope.kind}="${scope.value}"` : String(scope.kind);
|
|
258
|
+
fail('G2', `report chỉ phủ ${what} — đây KHÔNG phải một audit đầy đủ`,
|
|
259
|
+
['Cờ 🔴 ngoài phạm vi đó hoàn toàn vô hình với gate.',
|
|
260
|
+
'Gate cho qua lúc này là khẳng định sạch cho thứ chưa ai xem.',
|
|
261
|
+
'Chạy /validate-traces KHÔNG kèm --domain/--prd/--uc, rồi commit report.'].join('\n '));
|
|
262
|
+
}
|
|
263
|
+
} else if (domain !== 'all') {
|
|
264
|
+
// TƯƠNG THÍCH NGƯỢC: report sinh trước G57 không có `scope`. Rơi về logic cũ theo `domain`
|
|
265
|
+
// và nói rõ kiểm đang ở mức YẾU — KHÔNG fail vì thiếu field. Người dùng chưa chạy lại
|
|
266
|
+
// /validate-traces không phải là lỗi của họ. Cùng khuôn G48 đã dùng cho `platform`.
|
|
267
|
+
warn('G2', 'report không có field `scope` (sinh trước G57) — kiểm độ phủ đang ở mức YẾU',
|
|
268
|
+
'Chạy lại /validate-traces để report mang `scope`; chỉ khi đó gate mới chặn được mọi\n'
|
|
269
|
+
+ ' loại audit phiến diện, kể cả hẹp-theo-PRD trong repo một domain.');
|
|
270
|
+
const others = domainsOnDisk(TRACE_DIRS).filter(d => d !== domain);
|
|
271
|
+
if (others.length) {
|
|
272
|
+
fail('G2', `report chỉ phủ domain "${domain}" — còn ${others.length} domain có sổ mà chưa được audit`,
|
|
273
|
+
[`Chưa phủ: ${others.join(' · ')}`,
|
|
274
|
+
`Gate cho qua lúc này là khẳng định sạch cho thứ chưa ai xem.`,
|
|
275
|
+
`Chạy /validate-traces KHÔNG kèm domain để audit toàn bộ, rồi commit report.`].join('\n '));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rows = readBooks(TRACE_DIRS, domain);
|
|
280
|
+
|
|
281
|
+
// (b) hard — số row là tuyệt đối, không nhập nhằng quy ước.
|
|
282
|
+
const hardKeys = Object.keys(GATE.reconcile.hard || {});
|
|
283
|
+
for (const key of hardKeys) {
|
|
284
|
+
if (!(key in summary)) {
|
|
285
|
+
fail('G2', `report thiếu \`summary.${key}\` — không đối chiếu được độ tươi`,
|
|
286
|
+
`Counter bị đổi tên? Sửa bin/trace-schema.json \`gate.reconcile\` cho khớp\n`
|
|
287
|
+
+ ` (và commands/validate-traces.tmpl khối \`summary\`).`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (summary[key] !== rows.length) {
|
|
291
|
+
fail('G2', `report CŨ — \`summary.${key}\` = ${summary[key]} nhưng sổ TSV đang có ${rows.length} scenario`,
|
|
292
|
+
[`Sổ đã thay đổi sau lần audit cuối (${report.generated_at || 'không rõ thời điểm'}).`,
|
|
293
|
+
`Mọi con số dưới đây — gồm cả các cờ 🔴 — nói về một trạng thái đã qua.`,
|
|
294
|
+
`Chạy /validate-traces rồi commit lại ${REPORT_BASENAME}.`].join('\n '));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// (c) soft — phụ thuộc quy ước đếm, nên WARN chứ không FAIL. Sai ở đây thì tệ nhất là
|
|
299
|
+
// ồn; ép cứng mà lệch quy ước thì cổng bị tắt — hỏng nặng hơn nhiều.
|
|
300
|
+
const byStatus = {};
|
|
301
|
+
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
302
|
+
|
|
303
|
+
for (const [key, statusVal] of Object.entries(GATE.reconcile.soft || {})) {
|
|
304
|
+
if (!(key in summary)) continue;
|
|
305
|
+
const onDisk = byStatus[statusVal] || 0;
|
|
306
|
+
if (summary[key] !== onDisk) {
|
|
307
|
+
warn('G2', `\`summary.${key}\` = ${summary[key]} nhưng sổ có ${onDisk} row status=${statusVal}`,
|
|
308
|
+
`Report cũ, hoặc quy ước đếm khác. Không chặn — chạy /validate-traces để chắc.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// (d) per-SC (G48) — đối chiếu TỪNG scenario, không chỉ tổng số.
|
|
313
|
+
//
|
|
314
|
+
// So tổng bỏ lọt ca thường gặp nhất khi spec tiến hoá: một SC bị xoá + một SC được
|
|
315
|
+
// thêm trong cùng một lần ⇒ total_scs KHÔNG đổi ⇒ report cũ vẫn được coi là tươi.
|
|
316
|
+
// Khoá phải gồm platform: sc_id một mình không định danh được scenario.
|
|
317
|
+
const PS = GATE.reconcile.per_sc;
|
|
318
|
+
if (PS) {
|
|
319
|
+
const scs = flattenReportScenarios(report);
|
|
320
|
+
const hasPlatform = scs.length > 0 && scs.every(s => s[PS.key_fields[1]]);
|
|
321
|
+
|
|
322
|
+
if (!scs.length) {
|
|
323
|
+
warn('G2', 'report không kèm `prds[].ucs[].scenarios[]` — chỉ đối chiếu được tổng số',
|
|
324
|
+
'Kiểm độ tươi đang ở mức yếu: một SC bị xoá + một SC được thêm sẽ không bị phát hiện.');
|
|
325
|
+
} else if (!hasPlatform) {
|
|
326
|
+
// Backward-compat: report sinh bởi version trước G48. Không fail — người dùng chưa
|
|
327
|
+
// chạy lại /validate-traces không phải lỗi của họ.
|
|
328
|
+
warn('G2', 'report thiếu `platform` trong `scenarios[]` — đối chiếu chỉ ở mức tổng số',
|
|
329
|
+
[`Report sinh bởi version trước G48. sc_id một mình không phân biệt được web/app,`,
|
|
330
|
+
`nên không khoá được từng scenario.`,
|
|
331
|
+
`Chạy lại /validate-traces bằng version hiện tại để bật đối chiếu đầy đủ.`].join('\n '));
|
|
332
|
+
} else {
|
|
333
|
+
const key = (o) => PS.key_fields.map(f => o[f]).join('@');
|
|
334
|
+
// Quy đổi ORPHANED: JSON cố ý chỉ có 4 giá trị status; row ORPHANED xuất ra là
|
|
335
|
+
// `status: DRIFT` + `orphaned: true` (validate-traces §Rules). Phải quy đổi TRƯỚC
|
|
336
|
+
// khi so, không thì mọi row ORPHANED đều báo lệch giả.
|
|
337
|
+
const norm = (s) => (s[PS.orphaned_flag] === true ? PS.orphaned_status : s[PS.compare]);
|
|
338
|
+
|
|
339
|
+
const inReport = new Map(scs.map(s => [key(s), norm(s)]));
|
|
340
|
+
const inBook = new Map(rows.map(r => [key(r), r[PS.compare]]));
|
|
341
|
+
|
|
342
|
+
const missing = [...inBook.keys()].filter(k => !inReport.has(k));
|
|
343
|
+
const extra = [...inReport.keys()].filter(k => !inBook.has(k));
|
|
344
|
+
const differ = [...inBook.entries()]
|
|
345
|
+
.filter(([k, v]) => inReport.has(k) && inReport.get(k) !== v)
|
|
346
|
+
.map(([k, v]) => `${k}: sổ=${v} report=${inReport.get(k)}`);
|
|
347
|
+
|
|
348
|
+
const show = (a) => a.slice(0, 6).join(' · ') + (a.length > 6 ? ` … +${a.length - 6}` : '');
|
|
349
|
+
|
|
350
|
+
if (missing.length || extra.length) {
|
|
351
|
+
fail('G2', `report CŨ — lệch ${missing.length + extra.length} scenario so với sổ`,
|
|
352
|
+
[missing.length ? `Có trong sổ, thiếu ở report: ${show(missing)}` : null,
|
|
353
|
+
extra.length ? `Có ở report, không còn trong sổ: ${show(extra)}` : null,
|
|
354
|
+
`Sổ đã thay đổi sau lần audit cuối (${report.generated_at || 'không rõ'}).`,
|
|
355
|
+
`Chạy /validate-traces rồi commit lại ${REPORT_BASENAME}.`].filter(Boolean).join('\n '));
|
|
356
|
+
} else if (differ.length) {
|
|
357
|
+
fail('G2', `report CŨ — ${differ.length} scenario khác trạng thái so với sổ`,
|
|
358
|
+
[show(differ),
|
|
359
|
+
`Cùng tập scenario nhưng status đã đổi ⇒ audit cuối nói về một trạng thái đã qua.`,
|
|
360
|
+
`Chạy /validate-traces rồi commit lại ${REPORT_BASENAME}.`].join('\n '));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!fails.some(f => f.stage === 'G2')) {
|
|
366
|
+
// `[].every()` là true — nên phải đòi CÓ scenario, không thì report rỗng sẽ khoe
|
|
367
|
+
// "đối chiếu từng SC" trong khi nó chẳng đối chiếu gì.
|
|
368
|
+
const scsNow = PS ? flattenReportScenarios(report) : [];
|
|
369
|
+
const PSok = scsNow.length > 0 && scsNow.every(s => s[PS.key_fields[1]]);
|
|
370
|
+
oks.push(`G2 report tươi — ${rows.length} scenario khớp sổ`
|
|
371
|
+
+ (PSok ? ' (đối chiếu từng SC)' : ' (chỉ tổng số)')
|
|
372
|
+
+ ` · audit lúc ${report.generated_at || '?'}`
|
|
373
|
+
+ (domain !== 'all' ? ` · domain "${domain}"` : ''));
|
|
374
|
+
}
|
|
375
|
+
} else if (report && NO_RECONCILE) {
|
|
376
|
+
warn('G2', 'bỏ qua kiểm độ tươi (--no-reconcile) — report có thể đã cũ',
|
|
377
|
+
'Cờ 🔴 dưới đây có thể nói về một trạng thái đã qua.');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Tầng G3 — cờ 🔴 ───────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
const blocking = [];
|
|
383
|
+
|
|
384
|
+
if (report) {
|
|
385
|
+
const summary = report.summary || {};
|
|
386
|
+
const issues = report.issues || {};
|
|
387
|
+
|
|
388
|
+
for (const b of GATE.blocking) {
|
|
389
|
+
// Counter VẮNG MẶT không được coi là 0. `undefined > 0` là false ⇒ gate sẽ PASS trên
|
|
390
|
+
// code hỏng, im lặng. Đúng hình dạng G1 (consumer trỏ vào chỗ trống), và ở đây hậu quả
|
|
391
|
+
// là một PR hỏng được merge kèm dấu ✅.
|
|
392
|
+
if (!(b.counter in summary)) {
|
|
393
|
+
fail('G3', `report thiếu \`summary.${b.counter}\` — cờ này KHÔNG được kiểm`,
|
|
394
|
+
[`Không suy ra là 0: counter vắng mặt nghĩa là gate MÙ với cờ này, không phải sạch.`,
|
|
395
|
+
`Nguyên nhân: report sinh bởi version cũ, hoặc counter đã đổi tên.`,
|
|
396
|
+
`Sửa: chạy lại /validate-traces bằng version framework hiện tại. Nếu counter đổi tên`,
|
|
397
|
+
`thật thì cập nhật bin/trace-schema.json \`gate.blocking\` — self-check R9 canh chỗ đó.`].join('\n '));
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const n = Number(summary[b.counter]) || 0;
|
|
402
|
+
if (n === 0) continue;
|
|
403
|
+
|
|
404
|
+
const rows = Array.isArray(issues[b.issues_key]) ? issues[b.issues_key] : [];
|
|
405
|
+
blocking.push({
|
|
406
|
+
counter: b.counter, count: n, why: b.why, issues_key: b.issues_key,
|
|
407
|
+
items: rows.map(r => ({
|
|
408
|
+
label: (b.label_fields || []).map(f => r[f]).filter(Boolean).join(' · ') || '(không rõ)',
|
|
409
|
+
fix: r.fix || null,
|
|
410
|
+
})),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (blocking.length) {
|
|
415
|
+
const total = blocking.reduce((s, b) => s + b.count, 0);
|
|
416
|
+
fail('G3', `${total} vấn đề 🔴 — code đang hỏng, KHÔNG merge`, null);
|
|
417
|
+
} else if (!fails.length) {
|
|
418
|
+
oks.push(`G3 không có cờ 🔴 — ${GATE.blocking.map(b => b.counter).length}/${GATE.blocking.length} cờ chặn đều bằng 0`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Output ────────────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
const passed = fails.length === 0;
|
|
425
|
+
|
|
426
|
+
if (AS_JSON) {
|
|
427
|
+
console.log(JSON.stringify({
|
|
428
|
+
ok: passed,
|
|
429
|
+
report: reportPath ? rel(reportPath) : null,
|
|
430
|
+
generated_at: report && report.generated_at || null,
|
|
431
|
+
blocking, fails, warns,
|
|
432
|
+
}, null, 2));
|
|
433
|
+
process.exit(passed || WARN_ONLY ? 0 : 1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log('');
|
|
437
|
+
console.log('Trace gate (bin/trace-schema.json → gate) ...');
|
|
438
|
+
console.log('');
|
|
439
|
+
|
|
440
|
+
for (const o of oks) console.log(` ✅ ${o}`);
|
|
441
|
+
if (oks.length) console.log('');
|
|
442
|
+
|
|
443
|
+
for (const w of warns) {
|
|
444
|
+
console.log(` ⚠️ [${w.stage}] ${w.msg}`);
|
|
445
|
+
if (w.hint) console.log(` ${w.hint}`);
|
|
446
|
+
}
|
|
447
|
+
if (warns.length) console.log('');
|
|
448
|
+
|
|
449
|
+
for (const f of fails) {
|
|
450
|
+
console.log(` 🔴 [${f.stage}] ${f.msg}`);
|
|
451
|
+
if (f.hint) console.log(` ${f.hint}`);
|
|
452
|
+
console.log('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (const b of blocking) {
|
|
456
|
+
console.log(` 🔴 ${b.counter} = ${b.count} — ${b.why}`);
|
|
457
|
+
for (const it of b.items.slice(0, 8)) {
|
|
458
|
+
console.log(` • ${it.label}`);
|
|
459
|
+
if (it.fix) console.log(` → ${it.fix}`);
|
|
460
|
+
}
|
|
461
|
+
if (b.items.length > 8) console.log(` … và ${b.items.length - 8} nữa`);
|
|
462
|
+
if (!b.items.length) {
|
|
463
|
+
console.log(` (report không kèm chi tiết trong \`issues.${b.issues_key}\` — chạy /validate-traces để xem)`);
|
|
464
|
+
}
|
|
465
|
+
console.log('');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (passed) {
|
|
469
|
+
console.log(' ✅ TRACE GATE PASSED');
|
|
470
|
+
console.log('');
|
|
471
|
+
// In giới hạn MỖI LẦN, kể cả khi xanh. Một cổng không nói rõ nó chưa chứng minh gì
|
|
472
|
+
// sẽ được người đọc hiểu là đã chứng minh mọi thứ — và đó là cách một cổng thật biến
|
|
473
|
+
// thành một cổng giả mà không ai sửa dòng code nào.
|
|
474
|
+
console.log(' Cổng này chứng minh: report khớp SỔ, và sổ không có cờ 🔴.');
|
|
475
|
+
console.log(' Cổng này KHÔNG chứng minh: sổ khớp CODE — việc đó cần /validate-traces');
|
|
476
|
+
console.log(' (quét tag trong source, đọc .feature, so version). Sửa code mà không đụng');
|
|
477
|
+
console.log(' sổ thì cổng vẫn xanh. Cho CI đầy đủ: chạy /validate-traces trước bước này.');
|
|
478
|
+
} else {
|
|
479
|
+
console.log(' 🔴 TRACE GATE FAILED');
|
|
480
|
+
console.log('');
|
|
481
|
+
console.log(' Đây là lớp lỗi mà build xanh + test từng-UC xanh KHÔNG phát hiện được:');
|
|
482
|
+
console.log(' luồng ghép chạy vào hàm rỗng, hoặc code trỏ vào scenario đã bị xoá.');
|
|
483
|
+
if (WARN_ONLY) console.log(' (--warn-only: exit 0 dù có lỗi)');
|
|
484
|
+
}
|
|
485
|
+
console.log('');
|
|
486
|
+
|
|
487
|
+
process.exit(passed || WARN_ONLY ? 0 : 1);
|