@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
package/bin/self-check.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 3 consumer, 0 producer, sống qua nhiều version mà không lệnh nào, không cổng
|
|
9
9
|
* nào, không test nào bắt được. Đây là cái bắt lớp lỗi đó một cách mechanical.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* 12 rule — áp cho tags · tsv_columns · aux_tables (_seams.tsv) · queues (feedback/*),
|
|
12
|
+
* cộng gate · CI template · lint-trace · dòng changelog:
|
|
12
13
|
* R1 field/hàng đợi có read_by nhưng KHÔNG có producer → ERROR (bắt được G1; queue: G26)
|
|
13
14
|
* R2 field có producer nhưng KHÔNG ai đọc → WARN (bắt được G15, fe_phase, test_type)
|
|
14
15
|
* R3 actor được khai là producer/consumer mà file của
|
|
@@ -20,6 +21,33 @@
|
|
|
20
21
|
* nào dùng → WARN (bắt được G19: integration vs integrated)
|
|
21
22
|
* R7 audit flag thiếu counter {flag}_count trong CẢ
|
|
22
23
|
* Step 7 LẪN khối `summary` của validate-traces → ERROR (bắt được G33)
|
|
24
|
+
* R8 bin/lint-trace.js tụt lại sau schema → ERROR (bắt được G38)
|
|
25
|
+
* R8a vocabulary không khai binding (neo vào đâu để kiểm sổ thật?)
|
|
26
|
+
* R8b binding đã khai mà lint-trace không đọc key đó
|
|
27
|
+
* R8c `n` của tsv_columns trùng/hổng — lint dựng header theo `n`, sai `n` = sai header
|
|
28
|
+
* R8d bảng phụ nằm ngoài phạm vi lint
|
|
29
|
+
* R9 cổng chặn PR neo vào counter/issues_key không
|
|
30
|
+
* tồn tại trong report, hoặc cờ 🟠 lọt vào
|
|
31
|
+
* gate.blocking → ERROR (bắt được G39)
|
|
32
|
+
* R10 templates/ci/trace-gate.yml tụt lại sau danh
|
|
33
|
+
* sách audit_invalidating_tags ⇒ job LUÔN XANH → ERROR (nợ #4 sau Wave 1)
|
|
34
|
+
* R11 lệnh không tự khai mức chặn CHECKPOINT, hoặc
|
|
35
|
+
* nhãn trong file lệch schema → ERROR (nợ sau G41)
|
|
36
|
+
* R12 dòng changelog — producer không có DÒNG TEMPLATE
|
|
37
|
+
* mang {changelog_scope}, hoặc consumer không hiểu
|
|
38
|
+
* [no-behavior] / phép phân giải BR/AC → UC sở hữu → ERROR (bắt được G52, G53, G58)
|
|
39
|
+
*
|
|
40
|
+
* R12 CỐ Ý là USE-check, không phải mention-check: nó đòi token nằm trên ĐÚNG dòng template
|
|
41
|
+
* (row 3-placeholder, ngoài blockquote), không chỉ "có xuất hiện đâu đó trong file". Vì
|
|
42
|
+
* `text.includes()` bị thoả mãn bởi một lần nhắc bất kỳ — kể cả trong phần giải thích — nên
|
|
43
|
+
* gỡ token khỏi đúng dòng format mà vẫn nhắc ở chỗ khác thì checker im lặng và G52 quay lại
|
|
44
|
+
* nguyên vẹn. Đây là điểm yếu đã biết của R3, không lặp lại. Ví dụ đã ĐIỀN CỤ THỂ
|
|
45
|
+
* (`| 1.4 | 2026-08-19 | UC5: … |`) không bị canh — nếu canh thì không ai dám viết ví dụ.
|
|
46
|
+
*
|
|
47
|
+
* RANH GIỚI R1–R7 vs R8: R1–R7 canh CONTRACT (đọc commands/*.tmpl — "lệnh có gọi đúng tên
|
|
48
|
+
* cột không"). Không rule nào mở một `.tsv` THẬT ra xem. Đó là G38: sổ 24 cột do LLM ghi tay,
|
|
49
|
+
* một dấu tab thiếu dồn mọi ô sang trái và ô `status` nhận một ngày tháng — không gì báo.
|
|
50
|
+
* bin/lint-trace.js canh DỮ LIỆU; R8 canh việc nó theo kịp contract.
|
|
23
51
|
*
|
|
24
52
|
* PHẠM VI (G30): trước đó schema chỉ phủ 16/44 tag — R1/R3 mù với 28 cái còn lại, và ĐÓ LÀ
|
|
25
53
|
* cách G31 (@trace.service: 2 consumer khai, 0 nơi chứa) cùng G32 (design-spec: 0 cột/0 tag/
|
|
@@ -100,6 +128,26 @@ function mentions(text, name) {
|
|
|
100
128
|
return new RegExp(`${before}${esc}(?![A-Za-z0-9_])`).test(text);
|
|
101
129
|
}
|
|
102
130
|
|
|
131
|
+
/**
|
|
132
|
+
* `name` có xuất hiện trong PHẦN CODE THẬT của một file .js không — bỏ dòng comment.
|
|
133
|
+
*
|
|
134
|
+
* Vì sao cần (bài học lặp BỐN lần trong loạt G52–G57): một chú thích *giải thích* luật cũng
|
|
135
|
+
* chứa đúng tên/biểu thức mà rule đang tìm. Nên `text.includes("scope.kind")` vẫn ✅ sau khi
|
|
136
|
+
* ai đó thay điều kiện thật bằng `false` — chú thích "Nên: scope.kind !== all ⇒ FAIL" thoả mãn
|
|
137
|
+
* phép kiểm, và cổng chết trong im lặng.
|
|
138
|
+
*
|
|
139
|
+
* Cùng lớp với điểm yếu của R3 (`mentions()` không phân biệt "đọc" với "nhắc tên") — chỉ ở một
|
|
140
|
+
* trục khác: không phân biệt "code" với "chú thích VỀ code". R12/R13/R14/R9(h) đều đã phải
|
|
141
|
+
* chuyển sang use-check vì nó; helper này để lần sau không phải phát minh lại.
|
|
142
|
+
*
|
|
143
|
+
* Cố ý ĐƠN GIẢN: chỉ bỏ dòng bắt đầu bằng `//`. Không parse JS. Comment cuối dòng sau code
|
|
144
|
+
* thật vẫn được giữ — đúng ý, vì dòng đó CÓ code.
|
|
145
|
+
*/
|
|
146
|
+
const codeOf = text => text.split('\n').filter(l => !/^\s*\/\//.test(l)).join('\n');
|
|
147
|
+
function mentionsInCode(text, name) {
|
|
148
|
+
return codeOf(text).includes(name);
|
|
149
|
+
}
|
|
150
|
+
|
|
103
151
|
// ── R1 + R2 + R3 — producer / consumer / prose khớp ───────────────────────────
|
|
104
152
|
function checkField(kind, name, def) {
|
|
105
153
|
const producers = [...(def.written_by || []), ...(def.bumped_by || [])];
|
|
@@ -318,6 +366,748 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
318
366
|
}
|
|
319
367
|
}
|
|
320
368
|
|
|
369
|
+
// ── R8 — lint-trace phải theo kịp schema ──────────────────────────────────────
|
|
370
|
+
//
|
|
371
|
+
// Vì sao (G38): R1–R7 canh CONTRACT — chúng đọc file lệnh và kiểm "lệnh có gọi đúng tên
|
|
372
|
+
// cột không". Không rule nào mở một `.tsv` THẬT ra xem. bin/lint-trace.js làm việc đó,
|
|
373
|
+
// nhưng nó chỉ canh được thứ nó BIẾT. Thêm cột 25 hay vocabulary mới vào schema mà quên
|
|
374
|
+
// dạy nó → checker tụt lại sau contract, im lặng, và 6 tháng sau ta có G38 lần hai.
|
|
375
|
+
// R8 là van đóng đường đó — cùng tinh thần R7: nợ không được phép tái sinh.
|
|
376
|
+
{
|
|
377
|
+
const LINT_REL = 'bin/lint-trace.js';
|
|
378
|
+
const lintAbs = path.join(ROOT, LINT_REL);
|
|
379
|
+
|
|
380
|
+
if (!fs.existsSync(lintAbs)) {
|
|
381
|
+
err('R8', `${LINT_REL} không tồn tại — không có gì kiểm DỮ LIỆU sổ trace`,
|
|
382
|
+
`Contract được canh bằng máy (R1–R7) nhưng sổ thật thì không. Đây chính là G38.`);
|
|
383
|
+
} else {
|
|
384
|
+
const lintText = fs.readFileSync(lintAbs, 'utf8');
|
|
385
|
+
|
|
386
|
+
// ── R8a — mọi vocabulary phải neo được vào một chỗ kiểm cụ thể ──
|
|
387
|
+
const BINDINGS = ['column', 'columns', 'aux_column', 'filename_segment'];
|
|
388
|
+
for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
389
|
+
if (vocab.startsWith('$') || !def || !def.values) continue;
|
|
390
|
+
const bound = BINDINGS.filter(b => def[b] !== undefined);
|
|
391
|
+
if (!bound.length && !def.$lint) {
|
|
392
|
+
err('R8', `vocabulary \`${vocab}\` không khai binding nào cho lint-trace`,
|
|
393
|
+
`Khai đúng một trong: ${BINDINGS.map(b => `\`${b}\``).join(' · ')} — hoặc \`$lint\` kèm LÝ DO\n`
|
|
394
|
+
+ ` nếu nó cố ý ngoài phạm vi (như audit_flags: sống trong trace-report.json, R7 canh).\n`
|
|
395
|
+
+ ` Không khai = ${lintText ? 'lint-trace' : 'checker'} không biết kiểm ở đâu, và enum đó chỉ là chữ trong schema.`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Cột được neo tới phải TỒN TẠI — neo vào một tên cột đã đổi/gõ sai thì lint
|
|
399
|
+
// im lặng bỏ qua cả vocabulary đó (hình dạng G1: consumer trỏ vào chỗ trống).
|
|
400
|
+
const colNames = new Set(schema.tsv_columns.map(c => c.name));
|
|
401
|
+
for (const c of (def.columns || (def.column ? [def.column] : []))) {
|
|
402
|
+
if (!colNames.has(c)) {
|
|
403
|
+
err('R8', `vocabulary \`${vocab}\` neo vào cột \`${c}\` — cột này KHÔNG có trong tsv_columns`,
|
|
404
|
+
`Cột đã đổi tên, hay gõ sai? Neo trỏ vào chỗ trống ⇒ lint bỏ qua enum này trong im lặng.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (def.aux_column) {
|
|
408
|
+
const aux = (schema.aux_tables || []).find(t => t.name === def.aux_table);
|
|
409
|
+
if (!aux) {
|
|
410
|
+
err('R8', `vocabulary \`${vocab}\` khai \`aux_table: "${def.aux_table}"\` — không có bảng phụ nào tên đó`);
|
|
411
|
+
} else if (!aux.columns.includes(def.aux_column)) {
|
|
412
|
+
err('R8', `vocabulary \`${vocab}\` neo vào \`${def.aux_table}.${def.aux_column}\` — cột này không có trong bảng đó`,
|
|
413
|
+
`Cột hiện có: ${aux.columns.join(' · ')}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── R8b — kiểu binding đã khai thì lint-trace phải thực sự ĐỌC nó ──
|
|
418
|
+
for (const b of bound) {
|
|
419
|
+
if (!mentionsKey(lintText, b)) {
|
|
420
|
+
err('R8', `schema khai binding \`${b}\` (ở \`${vocab}\`) mà ${LINT_REL} KHÔNG đọc key đó`,
|
|
421
|
+
`Khai binding rồi mà checker không dùng = enum không được kiểm ở đâu cả.\n`
|
|
422
|
+
+ ` Dạy lint-trace đọc \`${b}\`, hoặc đổi sang một binding nó đã hiểu.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── R8c — `n` phải là 1..N liền mạch, không trùng ──
|
|
428
|
+
//
|
|
429
|
+
// lint-trace dựng header bằng cách SORT theo `n` (thứ tự array KHÔNG phải thứ tự cột —
|
|
430
|
+
// ba entry cuối trong schema hiện là n:23, n:24, n:22). Nên `n` trùng hoặc hổng làm
|
|
431
|
+
// header dẫn xuất sai ⇒ lint đối chiếu sổ thật với một contract sai. Kiểm cơ học.
|
|
432
|
+
const ns = schema.tsv_columns.map(c => c.n);
|
|
433
|
+
const dupes = ns.filter((n, i) => ns.indexOf(n) !== i);
|
|
434
|
+
if (dupes.length) {
|
|
435
|
+
err('R8', `tsv_columns có \`n\` trùng: ${[...new Set(dupes)].join(', ')}`,
|
|
436
|
+
`\`n\` là thứ tự cột authoritative — lint-trace sort theo nó để dựng header.\n`
|
|
437
|
+
+ ` Trùng ⇒ header dẫn xuất sai ⇒ lint đối chiếu sổ thật với contract sai.`);
|
|
438
|
+
}
|
|
439
|
+
const expected = Array.from({ length: schema.tsv_columns.length }, (_, i) => i + 1);
|
|
440
|
+
const missingN = expected.filter(n => !ns.includes(n));
|
|
441
|
+
if (missingN.length) {
|
|
442
|
+
err('R8', `tsv_columns hổng số thứ tự: thiếu n = ${missingN.join(', ')}`,
|
|
443
|
+
`${schema.tsv_columns.length} cột thì \`n\` phải là 1..${schema.tsv_columns.length} liền mạch.\n`
|
|
444
|
+
+ ` Bỏ một cột thì đánh số lại, đừng để lỗ — lint dựng header theo \`n\`.`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── R8d — bảng phụ cũng phải nằm trong phạm vi lint ──
|
|
448
|
+
for (const t of schema.aux_tables || []) {
|
|
449
|
+
if (!lintText.includes(t.name)) {
|
|
450
|
+
err('R8', `bảng phụ \`${t.name}\` không được ${LINT_REL} kiểm`,
|
|
451
|
+
`Nó là NGUỒN của SEAM_UNWIRED/STUB_UNRESOLVED (hai cờ 🔴 chặn PR).\n`
|
|
452
|
+
+ ` Header lệch ở đây thì hai cờ đó mù — đúng lý do bảng này được đưa vào SoT ở G30.`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── R9 — cổng chặn PR phải neo đúng vào report ────────────────────────────────
|
|
459
|
+
//
|
|
460
|
+
// Vì sao (G39): gate đọc `summary[counter]`. Counter bị đổi tên mà gate không biết →
|
|
461
|
+
// `undefined > 0` là false → GATE PASS TRÊN CODE HỎNG, kèm dấu ✅. Đây là false-green
|
|
462
|
+
// im lặng, đúng hình dạng G1 (consumer trỏ vào chỗ trống) nhưng hậu quả nặng hơn: một
|
|
463
|
+
// PR hỏng được merge. gate-trace.js tự phòng bằng cách fail khi counter vắng mặt trong
|
|
464
|
+
// REPORT; R9 canh đầu còn lại — schema có neo đúng vào thứ validate-traces THỰC SỰ ghi.
|
|
465
|
+
{
|
|
466
|
+
const GATE_REL = 'bin/gate-trace.js';
|
|
467
|
+
const gateAbs = path.join(ROOT, GATE_REL);
|
|
468
|
+
const gate = schema.gate;
|
|
469
|
+
|
|
470
|
+
if (!gate) {
|
|
471
|
+
err('R9', 'schema thiếu khối `gate` — không khai được cờ nào chặn PR',
|
|
472
|
+
`README khai 4 cờ 🔴 "chặn PR". Không có khối này thì lời đó không có cơ chế nào (G39).`);
|
|
473
|
+
} else if (!fs.existsSync(gateAbs)) {
|
|
474
|
+
err('R9', `${GATE_REL} không tồn tại — cờ 🔴 không có gì chặn`,
|
|
475
|
+
`Khai blocking trong schema mà không có cổng đọc nó = vẫn honor-system.`);
|
|
476
|
+
} else {
|
|
477
|
+
const gateText = fs.readFileSync(gateAbs, 'utf8');
|
|
478
|
+
const vtName = 'validate-traces';
|
|
479
|
+
const vtText = actorText[vtName];
|
|
480
|
+
|
|
481
|
+
const slice = (from, to) => {
|
|
482
|
+
if (vtText === undefined) return null;
|
|
483
|
+
const a = vtText.indexOf(from);
|
|
484
|
+
if (a < 0) return null;
|
|
485
|
+
const b = vtText.indexOf(to, a + from.length);
|
|
486
|
+
return vtText.slice(a, b < 0 ? undefined : b);
|
|
487
|
+
};
|
|
488
|
+
const summaryZone = slice('"summary": {', '"prds"');
|
|
489
|
+
// `"issues"` nằm SAU `"prds"` trong file, nên mốc kết thúc `"prds"` không tồn tại phía sau
|
|
490
|
+
// ⇒ slice() rơi về "tới hết file" và zone phình 4.6× (25790 ký tự thay vì 5625). Hệ quả:
|
|
491
|
+
// R9(a) sẽ PASS khi `issues_key` chỉ xuất hiện ở khối report terminal chứ không có trong
|
|
492
|
+
// `issues[]` — đúng lớp lỗi R7 đã phải siết ở G30 lô 8 ("tên xuất hiện Ở ĐÂU ĐÓ trong file").
|
|
493
|
+
// Dùng dấu đóng của chính object `issues` (2 space + `}`) làm mốc.
|
|
494
|
+
const issuesZone = slice('"issues": {', '\n }');
|
|
495
|
+
|
|
496
|
+
if (summaryZone === null || issuesZone === null) {
|
|
497
|
+
warn('R9', `Không định vị được khối \`summary\`/\`issues\` trong ${schema.actors[vtName]} — R9 bỏ qua`,
|
|
498
|
+
'Mốc cắt đã đổi? Cập nhật `slice()` trong self-check.js, đừng để R9 mù trong im lặng.');
|
|
499
|
+
} else {
|
|
500
|
+
// (a) counter + issues_key phải CÓ THẬT trong report mà validate-traces sinh ra
|
|
501
|
+
for (const b of gate.blocking || []) {
|
|
502
|
+
if (!mentionsKey(summaryZone, b.counter)) {
|
|
503
|
+
err('R9', `\`gate.blocking\` chờ counter \`${b.counter}\` mà khối \`summary\` của ${schema.actors[vtName]} KHÔNG có`,
|
|
504
|
+
`Gate đọc summary[${b.counter}]. Không có ⇒ gate MÙ với cờ này.\n`
|
|
505
|
+
+ ` Hoặc counter đã đổi tên (sửa schema), hoặc validate-traces thiếu ghi nó.`);
|
|
506
|
+
}
|
|
507
|
+
if (!mentionsKey(issuesZone, b.issues_key)) {
|
|
508
|
+
err('R9', `\`gate.blocking\` chờ \`issues.${b.issues_key}\` mà khối \`issues\` KHÔNG có`,
|
|
509
|
+
`Không có ⇒ gate chặn được nhưng KHÔNG nêu được thủ phạm — người đọc CI thấy\n`
|
|
510
|
+
+ ` một con số và không biết sửa gì. Cờ chặn phải kèm đường ra.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// (b) mốc đối chiếu độ tươi cũng phải tồn tại — thiếu thì G2 mất tác dụng và gate
|
|
515
|
+
// quay về đọc một file có thể cũ, tức trở lại đúng thứ G39 định bịt.
|
|
516
|
+
for (const key of [...Object.keys((gate.reconcile || {}).hard || {}),
|
|
517
|
+
...Object.keys((gate.reconcile || {}).soft || {})]) {
|
|
518
|
+
if (!mentionsKey(summaryZone, key)) {
|
|
519
|
+
err('R9', `\`gate.reconcile\` dùng \`summary.${key}\` — khối \`summary\` không có key này`,
|
|
520
|
+
`Thiếu mốc đối chiếu ⇒ gate không phát hiện được report cũ.`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// (c) soft map trỏ vào giá trị status có thật
|
|
525
|
+
const statusVals = (schema.vocabularies.trace_status || {}).values || [];
|
|
526
|
+
for (const [key, val] of Object.entries((gate.reconcile || {}).soft || {})) {
|
|
527
|
+
if (!statusVals.includes(val)) {
|
|
528
|
+
err('R9', `\`gate.reconcile.soft.${key}\` trỏ tới status "${val}" — không thuộc vocabulary \`trace_status\``,
|
|
529
|
+
`Giá trị cho phép: ${statusVals.join(' | ')}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// (h) KEY Ở CẤP GỐC BIÊN BẢN — cả hai đầu, không đầu nào được lửng (G57).
|
|
534
|
+
//
|
|
535
|
+
// Vì sao cần: `report.domain` từng có 1 consumer (gate-trace CHẶN PR nếu nó khác 'all')
|
|
536
|
+
// và 0 producer — validate-traces không có cờ nào đặt nó, nên ô đó LUÔN 'all' và phần
|
|
537
|
+
// kiểm của gate chưa bao giờ chạy. Đúng hình dạng R1 fail build vì nó; sống được vì
|
|
538
|
+
// R1/R2 chỉ canh field trong SỔ TSV + tag trong CODE, không canh key cấp gốc BIÊN BẢN.
|
|
539
|
+
// Cùng điểm mù R13 vừa bịt cho `spec_baseline`, chỉ ở một key khác.
|
|
540
|
+
{
|
|
541
|
+
const RRK = gate.report_root_keys;
|
|
542
|
+
if (RRK) {
|
|
543
|
+
for (const k of RRK.keys || []) {
|
|
544
|
+
// Đầu PRODUCER: phải là một KEY THẬT trong skeleton JSON, không chỉ được nhắc tên.
|
|
545
|
+
// Dạng `"key":` chỉ xuất hiện ở skeleton — prose nhắc tên dùng backtick.
|
|
546
|
+
if (!vtText.includes(`"${k.key}":`)) {
|
|
547
|
+
err('R9', `\`report_root_keys\` khai \`${k.key}\` mà ${schema.actors[vtName]} không ghi nó`,
|
|
548
|
+
`${k.read_by} ĐỌC \`report.${k.key}\` và phán theo nó. Không ai ghi ⇒ giá trị luôn\n`
|
|
549
|
+
+ ` là mặc định ⇒ phần kiểm đó KHÔNG BAO GIỜ chạy, mà dấu ✅ vẫn đầy đủ.`);
|
|
550
|
+
}
|
|
551
|
+
// Đầu CONSUMER: phải thực sự đọc `report.{key}`, không phải chỉ có chữ đó ở đâu đó.
|
|
552
|
+
// Escape PHẢI double trong template literal: `\s` là "s" và `\b` là ký tự backspace.
|
|
553
|
+
// mentionsInCode: đầu consumer phải là CODE THẬT. Chú thích của khối này cũng nhắc
|
|
554
|
+
// `report.domain` (nó kể lại chính ca G57), nên một phép kiểm trên toàn văn sẽ được
|
|
555
|
+
// chú thích thoả mãn ngay cả khi gate đã thôi đọc field đó.
|
|
556
|
+
if (!mentionsInCode(gateText, `report.${k.key}`)) {
|
|
557
|
+
err('R9', `\`report_root_keys\` khai \`${k.read_by}\` đọc \`${k.key}\` mà ${GATE_REL} không đọc`,
|
|
558
|
+
'Producer ghi một key mà consumer không đọc = field chết ở phía biên bản (R2 lộn\n'
|
|
559
|
+
+ ' ngược). Hoặc consumer đã bỏ, hoặc key đổi tên — sửa schema cho khớp.');
|
|
560
|
+
}
|
|
561
|
+
if (!k.why) {
|
|
562
|
+
err('R9', `\`report_root_keys\` mục \`${k.key}\` thiếu \`why\``,
|
|
563
|
+
'Một key ở cấp gốc biên bản không nói được vì sao tồn tại sẽ bị dọn đi.');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Luật gate cho scope phải TUYỆT ĐỐI. `if (others.length)` là đúng ca false-green:
|
|
567
|
+
// repo MỘT domain ⇒ others rỗng ⇒ biên bản phiến diện được nhận là toàn bộ.
|
|
568
|
+
const SK = RRK.scope_kinds;
|
|
569
|
+
if (SK) {
|
|
570
|
+
if (!(SK.values || []).includes('all')) {
|
|
571
|
+
err('R9', '`scope_kinds.values` thiếu `all` — không có giá trị nào nghĩa là "toàn bộ"');
|
|
572
|
+
}
|
|
573
|
+
for (const v of SK.values || []) {
|
|
574
|
+
if (!vtText.includes(v)) {
|
|
575
|
+
err('R9', `\`scope_kinds\` khai giá trị \`${v}\` mà ${schema.actors[vtName]} không nhắc`,
|
|
576
|
+
'Giá trị scope khai mà lệnh không biết ⇒ không bao giờ ghi ra được.');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const f of SK.flags || []) {
|
|
580
|
+
// mentions(): khớp ranh giới token. `--prdXX` CHỨA `--prd`, nên includes() trần
|
|
581
|
+
// vẫn ✅ sau khi ai đó đổi tên cờ và scoping đã chết.
|
|
582
|
+
if (!mentions(vtText, f.flag)) {
|
|
583
|
+
err('R9', `\`scope_kinds.flags\` khai \`${f.flag}\` mà ${schema.actors[vtName]} không parse`,
|
|
584
|
+
'Đây chính là G57: biên bản có ô "phạm vi", gate biết đọc ô đó, mà lệnh KHÔNG có\n'
|
|
585
|
+
+ ' cờ nào để đặt nó. Sợi dây đấu một đầu, đầu kia bỏ lửng.');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Neo vào PHÉP SO SÁNH, không phải truy cập thuộc tính: `scope.kind` còn xuất
|
|
589
|
+
// hiện ở chỗ dựng thông điệp (`${scope.kind}="…"`), nên kiểm tên thuộc tính vẫn ✅
|
|
590
|
+
// sau khi ai đó thay `if (scope.kind !== 'all')` bằng `if (false)`. Chuẩn hoá bỏ
|
|
591
|
+
// khoảng trắng để bền với format (`!==` giữ nguyên; ' hay " đều nhận).
|
|
592
|
+
const norm = codeOf(gateText).replace(/\s+/g, '');
|
|
593
|
+
if (!/scope\.kind!==['"]all['"]/.test(norm)) {
|
|
594
|
+
err('R9', `${GATE_REL} không kiểm \`scope.kind\``,
|
|
595
|
+
'Thêm cờ scope mà không siết G2 là biến cổng thành SÂN KHẤU — đúng cái G39 dựng\n'
|
|
596
|
+
+ ' lên để chống. Điều kiện phải TUYỆT ĐỐI: scope.kind !== "all" ⇒ FAIL, không\n'
|
|
597
|
+
+ ' phụ thuộc trên đĩa có bao nhiêu domain (repo một domain ⇒ others rỗng ⇒\n'
|
|
598
|
+
+ ' biên bản phiến diện lọt qua).');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// (f) per_sc (G48) — mọi field dùng làm khoá/so sánh phải CÓ THẬT trong khối
|
|
604
|
+
// `scenarios[]` mà validate-traces sinh ra. Neo vào field không tồn tại thì
|
|
605
|
+
// gate im lặng rơi về so tổng số — tức G48 quay lại y nguyên mà dấu vẫn ✅.
|
|
606
|
+
const perSc = (gate.reconcile || {}).per_sc;
|
|
607
|
+
if (perSc) {
|
|
608
|
+
const scZone = slice('"scenarios": [', '"issues"');
|
|
609
|
+
if (scZone === null) {
|
|
610
|
+
warn('R9', `Không định vị được khối \`scenarios[]\` trong ${schema.actors[vtName]} — R9(f) bỏ qua`,
|
|
611
|
+
'Mốc cắt đã đổi? Cập nhật `slice()`, đừng để R9 mù trong im lặng.');
|
|
612
|
+
} else {
|
|
613
|
+
const needed = [...(perSc.key_fields || []), perSc.compare, perSc.orphaned_flag]
|
|
614
|
+
.filter(Boolean);
|
|
615
|
+
for (const f of needed) {
|
|
616
|
+
if (!mentionsKey(scZone, f)) {
|
|
617
|
+
err('R9', `\`gate.reconcile.per_sc\` dùng field \`${f}\` mà khối \`scenarios[]\` KHÔNG có`,
|
|
618
|
+
`Gate sẽ im lặng rơi về so tổng số ⇒ mất chính thứ G48 vừa thêm,\n`
|
|
619
|
+
+ ` mà report vẫn hiện ✅. Thêm field vào Step 8, hoặc sửa schema.`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (perSc.orphaned_status && !statusVals.includes(perSc.orphaned_status)) {
|
|
623
|
+
err('R9', `\`per_sc.orphaned_status\` = "${perSc.orphaned_status}" không thuộc \`trace_status\``,
|
|
624
|
+
`Giá trị cho phép: ${statusVals.join(' | ')}`);
|
|
625
|
+
}
|
|
626
|
+
// Khoá KHÔNG có platform = quay lại đúng ca G48 (sc_id nhập nhằng giữa platform).
|
|
627
|
+
if (!(perSc.key_fields || []).includes('platform')) {
|
|
628
|
+
err('R9', '`per_sc.key_fields` thiếu `platform` — sc_id một mình không định danh được scenario',
|
|
629
|
+
`validate-traces Step 2: "sc_id trùng số giữa các platform là 2 scenario khác nhau".\n`
|
|
630
|
+
+ ` Khoá thiếu platform ⇒ web và app đè nhau khi đối chiếu (G48).`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// (g) config_preconditions (tầng G0) — rule được NÂNG thành chặn phải TỒN TẠI THẬT
|
|
637
|
+
// trong lint-trace.js. Neo vào một rule id không có ⇒ `warns.filter(...)` không
|
|
638
|
+
// khớp gì ⇒ G0 luôn xanh ⇒ cổng mù, im lặng. Cùng hình dạng false-green của R9(a).
|
|
639
|
+
const precond = (gate.config_preconditions || {}).promote_lint_rules || [];
|
|
640
|
+
if (precond.length) {
|
|
641
|
+
const lintRuleIds = new Set(
|
|
642
|
+
(fs.readFileSync(gateAbs.replace(/gate-trace\.js$/, 'lint-trace.js'), 'utf8')
|
|
643
|
+
.match(/\b(?:err|warn)\('(T\d+)'/g) || []).map(m => /T\d+/.exec(m)[0]));
|
|
644
|
+
for (const p of precond) {
|
|
645
|
+
if (!lintRuleIds.has(p.rule)) {
|
|
646
|
+
err('R9', `\`gate.config_preconditions\` nâng rule \`${p.rule}\` mà bin/lint-trace.js KHÔNG phát ra rule đó`,
|
|
647
|
+
`Rule id đổi tên? Gate filter theo id, không khớp ⇒ G0 LUÔN XANH ⇒ cổng mù.\n`
|
|
648
|
+
+ ` Rule lint hiện có: ${[...lintRuleIds].sort().join(' · ')}`);
|
|
649
|
+
}
|
|
650
|
+
if (!p.why) {
|
|
651
|
+
err('R9', `\`config_preconditions\` rule \`${p.rule}\` thiếu \`why\``,
|
|
652
|
+
`Cổng chặn phải nói được VÌ SAO nó chặn — không thì người bị chặn chỉ thấy một id.`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!mentionsKey(gateText, 'config_preconditions')) {
|
|
656
|
+
err('R9', `schema khai \`gate.config_preconditions\` mà ${GATE_REL} KHÔNG đọc nó`,
|
|
657
|
+
'Khai trong schema rồi hard-code lại trong code = hai nguồn-sự-thật, và chúng sẽ lệch.');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// (d) gate-trace.js phải thực sự ĐỌC khối gate, không hard-code danh sách cờ
|
|
662
|
+
for (const key of ['blocking', 'reconcile', 'report_path']) {
|
|
663
|
+
if (!mentionsKey(gateText, key)) {
|
|
664
|
+
err('R9', `schema khai \`gate.${key}\` mà ${GATE_REL} KHÔNG đọc nó`,
|
|
665
|
+
`Khai trong schema rồi hard-code lại trong code = hai nguồn-sự-thật, và chúng sẽ lệch.`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// (e) cờ 🟠 không được lọt vào danh sách chặn — chặn PR vì NỢ là cách làm cổng bị tắt
|
|
670
|
+
const blockingFlags = new Set((gate.blocking || [])
|
|
671
|
+
.map(b => b.counter.replace(/_count$/, '').toUpperCase()));
|
|
672
|
+
for (const f of blockingFlags) {
|
|
673
|
+
// `_UNROUTED$` cũng thuộc nhóm nợ (G51): "chưa ai quyết repo" là trạng thái HỢP LỆ ở
|
|
674
|
+
// feature đầu tiên của một domain mới, không phải code hỏng. Chặn PR vì nó sẽ đỏ liên tục
|
|
675
|
+
// trên mọi feature mới ⇒ cổng bị tắt ⇒ mất cả 4 cờ 🔴 thật.
|
|
676
|
+
if (/_DRIFT$|_STALE_REF$|_PENDING$|_STALE_VS_|_UNROUTED$/.test(f)) {
|
|
677
|
+
err('R9', `\`${f}\` nằm trong \`gate.blocking\` nhưng nó là cờ NỢ (🟠), không phải code hỏng (🔴)`,
|
|
678
|
+
`README §Traceability: 🟠 = "code chưa theo kịp spec", cố ý KHÔNG chặn.\n`
|
|
679
|
+
+ ` Chặn PR vì nợ ⇒ cổng đỏ liên tục ⇒ người ta tắt cổng ⇒ mất cả 🔴.`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ── R10 — file CI phải theo kịp danh sách tag làm audit hết tươi ──────────────
|
|
686
|
+
//
|
|
687
|
+
// Vì sao (nợ #4 sau Wave 1): job `require-fresh-audit` grep 7 tag trong diff để biết
|
|
688
|
+
// "PR có đổi thứ report đang khẳng định không". Danh sách đó nằm HAI chỗ: schema (nguồn)
|
|
689
|
+
// và file YAML (nơi grep thật chạy). Tag đổi tên mà YAML không biết ⇒ grep không khớp gì
|
|
690
|
+
// ⇒ job LUÔN XANH ⇒ cổng mù trong im lặng. Đúng hình dạng false-green của R9(a)/R9(g),
|
|
691
|
+
// chỉ ở một file khác loại.
|
|
692
|
+
//
|
|
693
|
+
// Không gộp một nguồn được: YAML chạy trong CI, không đọc JSON lúc runtime mà không thêm
|
|
694
|
+
// một lệnh CLI chỉ để phục vụ nó. Nên chấp nhận hai chỗ, và canh chúng khớp bằng máy.
|
|
695
|
+
{
|
|
696
|
+
const inv = (schema.gate || {}).audit_invalidating_tags;
|
|
697
|
+
if (inv) {
|
|
698
|
+
const rel = inv.ci_template;
|
|
699
|
+
if (!rel) {
|
|
700
|
+
err('R10', '`gate.audit_invalidating_tags` thiếu `ci_template` — không biết canh file nào');
|
|
701
|
+
} else {
|
|
702
|
+
const abs = path.join(ROOT, rel);
|
|
703
|
+
if (!fs.existsSync(abs)) {
|
|
704
|
+
err('R10', `\`audit_invalidating_tags.ci_template\` trỏ tới ${rel} — file không tồn tại`,
|
|
705
|
+
'Template đổi tên/bị xoá? Không có nó thì lỗ "sửa code mà không audit lại" hở lại.');
|
|
706
|
+
} else {
|
|
707
|
+
const yml = fs.readFileSync(abs, 'utf8');
|
|
708
|
+
const declared = (inv.tags || []).map(t => t.tag);
|
|
709
|
+
if (!declared.length) {
|
|
710
|
+
err('R10', '`audit_invalidating_tags.tags` rỗng — job sẽ không grep gì và LUÔN XANH');
|
|
711
|
+
}
|
|
712
|
+
// YAML viết tag trong một alternation regex (`implements|verifies|…`), nên so theo
|
|
713
|
+
// phần sau `@trace.` thay vì so nguyên chuỗi có dấu chấm.
|
|
714
|
+
for (const full of declared) {
|
|
715
|
+
const bare = full.replace(/^@trace\./, '');
|
|
716
|
+
if (!new RegExp(`\\b${bare}\\b`).test(yml)) {
|
|
717
|
+
err('R10', `${rel} KHÔNG nhắc tag \`${full}\``,
|
|
718
|
+
`Job grep theo danh sách này. Thiếu một tag ⇒ đổi tag đó không bị đòi audit lại,\n`
|
|
719
|
+
+ ` và không gì báo. Tag phải canh: ${declared.join(' · ')}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Ngược lại: cờ mà tag khai là nguồn phải có thật trong vocabulary audit_flags,
|
|
723
|
+
// else danh sách này đang neo vào một cờ không tồn tại.
|
|
724
|
+
const flagVals = new Set(((schema.vocabularies.audit_flags || {}).values) || []);
|
|
725
|
+
const statusVals2 = new Set(((schema.vocabularies.trace_status || {}).values) || []);
|
|
726
|
+
for (const t of inv.tags || []) {
|
|
727
|
+
for (const f of t.flags || []) {
|
|
728
|
+
if (!flagVals.has(f) && !statusVals2.has(f)) {
|
|
729
|
+
err('R10', `tag \`${t.tag}\` khai sinh ra cờ \`${f}\` — cờ này không có trong vocabulary`,
|
|
730
|
+
`audit_flags: ${[...flagVals].join(' · ')}\n trace_status: ${[...statusVals2].join(' · ')}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ── R11 — mức chặn CHECKPOINT phải được KHAI, không để agent tự suy ──────────
|
|
740
|
+
//
|
|
741
|
+
// Vì sao: G41 viết ba mức vào gate.md Bước 3a nhưng không lệnh nào tự khai mình thuộc
|
|
742
|
+
// mức nào ⇒ agent tự suy mỗi lần đọc. Suy sai một lần theo hướng nhẹ hơn thì `--yes` bỏ
|
|
743
|
+
// qua được một cổng đáng ra KHÔNG BAO GIỜ được bỏ.
|
|
744
|
+
//
|
|
745
|
+
// Đây là hình dạng đã gặp bốn lần: luật ĐÚNG, viết RÕ, KHÔNG AI CANH — G1, G28, G41, và
|
|
746
|
+
// cái này. Ba lần trước đều chữa bằng "khai tường minh + để máy canh". R11 là phần sau.
|
|
747
|
+
{
|
|
748
|
+
const CL = (schema.gate || {}).checkpoint_levels;
|
|
749
|
+
if (CL) {
|
|
750
|
+
const marker = CL.label_marker || 'Checkpoint:';
|
|
751
|
+
const check = (list, levelWord, label) => {
|
|
752
|
+
for (const e of list || []) {
|
|
753
|
+
const rel = `commands/${e.cmd}.tmpl`;
|
|
754
|
+
const abs = path.join(ROOT, rel);
|
|
755
|
+
if (!fs.existsSync(abs)) {
|
|
756
|
+
err('R11', `\`checkpoint_levels.${label}\` khai lệnh \`${e.cmd}\` — ${rel} không tồn tại`,
|
|
757
|
+
'Lệnh bị đổi tên/bỏ? Khai một lệnh không có nghĩa là mức chặn đó không áp cho ai.');
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
761
|
+
if (!text.includes(marker)) {
|
|
762
|
+
err('R11', `${rel} thiếu dòng khai mức checkpoint (\`${marker}\`)`,
|
|
763
|
+
`Schema xếp nó vào \`${label}\`. Không khai trong file thì agent phải TỰ SUY từ\n`
|
|
764
|
+
+ ` bảng ở gate Bước 3a — và suy sai theo hướng nhẹ hơn thì --yes bỏ qua được\n`
|
|
765
|
+
+ ` một cổng đáng ra không bao giờ được bỏ.`);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (!new RegExp(levelWord).test(text)) {
|
|
769
|
+
err('R11', `${rel} khai mức checkpoint KHÁC với schema (schema: \`${label}\`)`,
|
|
770
|
+
`File phải nói "${levelWord.replace(/\\/g, '')}". Hai nguồn nói khác nhau thì\n`
|
|
771
|
+
+ ` agent theo cái nào là tuỳ lúc — đúng hình dạng mâu thuẫn G41 vừa vá.`);
|
|
772
|
+
}
|
|
773
|
+
if (!e.why) {
|
|
774
|
+
err('R11', `\`checkpoint_levels.${label}\` mục \`${e.cmd}\` thiếu \`why\``,
|
|
775
|
+
'Một mức chặn không nói được VÌ SAO sẽ bị người sau coi là tuỳ ý và gỡ đi.');
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
check(CL.hard, 'chặn CỨNG', 'hard');
|
|
780
|
+
check(CL.none, 'không chặn', 'none');
|
|
781
|
+
|
|
782
|
+
// Một lệnh không được vừa `hard` vừa `none`.
|
|
783
|
+
const hardSet = new Set((CL.hard || []).map(e => e.cmd));
|
|
784
|
+
for (const e of CL.none || []) {
|
|
785
|
+
if (hardSet.has(e.cmd)) {
|
|
786
|
+
err('R11', `lệnh \`${e.cmd}\` khai ở CẢ \`hard\` LẪN \`none\``,
|
|
787
|
+
'Mâu thuẫn trực tiếp — chọn một.');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Mức `normal` cố ý KHÔNG liệt kê (nó là mặc định). Nếu ai thêm khoá đó vào schema,
|
|
791
|
+
// báo ngay: một danh sách phải bảo trì mãi là chỗ nợ sẽ tích lại.
|
|
792
|
+
if (CL.normal) {
|
|
793
|
+
err('R11', '`checkpoint_levels.normal` không nên tồn tại',
|
|
794
|
+
'`normal` là MẶC ĐỊNH. Liệt kê nó ra tạo một danh sách phải bảo trì mãi — thêm lệnh\n'
|
|
795
|
+
+ ' mới mà quên thêm vào đó là sai lần nữa. Chỉ khai `hard` và `none`.');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ── R12 — dòng changelog: producer phải khai scope, consumer phải HIỂU ────────
|
|
801
|
+
//
|
|
802
|
+
// Vì sao: dòng changelog là contract máy đọc (validate-traces Step 4/5 phân loại 🟠 vs ⓘ
|
|
803
|
+
// dựa vào nó), nhưng trước G52/G53/G58 nó là contract DUY NHẤT có consumer được canh
|
|
804
|
+
// (R1–R11 canh tên cột/field) mà KHÔNG producer nào được canh. Bốn producer, ba hình dạng,
|
|
805
|
+
// hỏng theo cả hai hướng: /review-context --fix ghi 0 scope ⇒ 🟠 cho MỌI UC (ồn tới mức
|
|
806
|
+
// cờ mất giá trị); /extend-prd ghi `sửa BR8` không nêu UC ⇒ UC đổi thật thành ⓘ và
|
|
807
|
+
// `--realign-prd-version` dán nhãn lại (im lặng).
|
|
808
|
+
//
|
|
809
|
+
// R12 là phần "máy canh" — cùng khuôn R11: khai tường minh + để máy canh.
|
|
810
|
+
// Điểm quan trọng nhất là R12d: một marker được GHI mà consumer không HIỂU thì TỆ HƠN
|
|
811
|
+
// không có marker — nó bị đọc như văn xuôi và lọt im lặng, đúng hình dạng G1.
|
|
812
|
+
{
|
|
813
|
+
const C = schema.changelog_row_contract;
|
|
814
|
+
if (C) {
|
|
815
|
+
const tok = C.scope_token;
|
|
816
|
+
if (!tok) {
|
|
817
|
+
err('R12', '`changelog_row_contract` thiếu `scope_token` — R12 không có gì để neo',
|
|
818
|
+
'Không có token thì "producer có khai scope không" là câu hỏi không kiểm được bằng máy.');
|
|
819
|
+
}
|
|
820
|
+
const readActor = (cmd, rule, label) => {
|
|
821
|
+
const rel = `commands/${cmd}.tmpl`;
|
|
822
|
+
const abs = path.join(ROOT, rel);
|
|
823
|
+
if (!fs.existsSync(abs)) {
|
|
824
|
+
err(rule, `\`changelog_row_contract.${label}\` khai lệnh \`${cmd}\` — ${rel} không tồn tại`,
|
|
825
|
+
'Lệnh bị đổi tên/bỏ? Khai một lệnh không có nghĩa là contract đó không áp cho ai.');
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
return { rel, text: fs.readFileSync(abs, 'utf8') };
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// R12a/R12b — mỗi producer phải nhắc scope_token trong format nó ghi.
|
|
832
|
+
for (const p of C.producers || []) {
|
|
833
|
+
const f = readActor(p.cmd, 'R12', 'producers');
|
|
834
|
+
if (!f) continue;
|
|
835
|
+
// Canh DÒNG TEMPLATE THẬT, không phải "có nhắc token ở đâu đó trong file".
|
|
836
|
+
//
|
|
837
|
+
// Vì sao phân biệt: `text.includes(tok)` bị thoả mãn bởi MỘT lần nhắc bất kỳ — kể cả
|
|
838
|
+
// trong phần giải thích. Nên gỡ token khỏi ĐÚNG dòng format mà vẫn còn nhắc ở chỗ khác
|
|
839
|
+
// thì checker im lặng, và G52 quay lại nguyên vẹn. Đây đúng là điểm yếu của R3
|
|
840
|
+
// (`mentions()` không phân biệt "đọc" với "nhắc tên") — không lặp lại nó ở R12.
|
|
841
|
+
//
|
|
842
|
+
// "Dòng template changelog" = row markdown có PLACEHOLDER ở CẢ BA ô (version · ngày ·
|
|
843
|
+
// mô tả). Ví dụ đã điền cụ thể (`| 1.4 | 2026-08-19 | UC5: … |`) KHÔNG khớp — cố ý:
|
|
844
|
+
// chúng minh hoạ output, không phải khuôn để ghi. Blockquote `>` cũng bị loại — khối
|
|
845
|
+
// "vì sao đổi" được phép trích nguyên dòng cũ (cùng cách test rules/workflow.md làm).
|
|
846
|
+
const ROW_TEMPLATE = /\|[^|]*\{[^}]*\}[^|]*\|[^|]*\{[^}]*\}[^|]*\|[^|]*\{[^}]*\}[^|]*\|/;
|
|
847
|
+
const rows = f.text.split('\n')
|
|
848
|
+
.filter(l => !/^\s*>/.test(l))
|
|
849
|
+
.filter(l => ROW_TEMPLATE.test(l));
|
|
850
|
+
if (!rows.length) {
|
|
851
|
+
err('R12', `${f.rel} là producer dòng changelog nhưng không có DÒNG TEMPLATE nào`,
|
|
852
|
+
'Không có khuôn `| {version} | {ngày} | {mô tả} |` thì agent tự nghĩ ra format, và\n'
|
|
853
|
+
+ ' mỗi lệnh nghĩ một kiểu — đúng trạng thái trước G52/G53/G58.');
|
|
854
|
+
}
|
|
855
|
+
for (const r of rows.filter(l => tok && !l.includes(tok))) {
|
|
856
|
+
err('R12', `${f.rel} có dòng template changelog KHÔNG chứa \`${tok}\``,
|
|
857
|
+
`Dòng: ${r.trim().slice(0, 90)}\n`
|
|
858
|
+
+ ' Format phải có chỗ cho scope. Thiếu ⇒ row mơ hồ ⇒ consumer gắn 🟠 cho MỌI UC\n'
|
|
859
|
+
+ ' (G52), hoặc nêu BR mà không nêu UC sở hữu ⇒ UC đổi thật thành ⓘ (G53).\n'
|
|
860
|
+
+ ' Muốn minh hoạ output? Điền GIÁ TRỊ CỤ THỂ (`| 1.4 | 2026-08-19 | UC5: … |`)\n'
|
|
861
|
+
+ ' — dòng đã điền không bị canh, dòng còn placeholder thì bị.');
|
|
862
|
+
}
|
|
863
|
+
if (!p.scope_source) {
|
|
864
|
+
err('R12', `\`changelog_row_contract.producers\` mục \`${p.cmd}\` thiếu \`scope_source\``,
|
|
865
|
+
'Không nói được scope LẤY TỪ ĐÂU thì người sửa sau sẽ ghi bừa cho đủ token.');
|
|
866
|
+
}
|
|
867
|
+
// R12c — producer nào khai tự phát marker trung tính thì phải nhắc nó.
|
|
868
|
+
if (p.emits_neutral_marker && C.neutral_marker && !f.text.includes(C.neutral_marker)) {
|
|
869
|
+
err('R12', `${f.rel} khai \`emits_neutral_marker\` nhưng KHÔNG nhắc \`${C.neutral_marker}\``,
|
|
870
|
+
'Khai tự phát một marker mà file không nói bao giờ phát ⇒ không bao giờ phát.');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// R12d — consumer phải hiểu ĐỦ: token + marker + phép phân giải BR/AC → UC.
|
|
875
|
+
for (const c of C.consumers || []) {
|
|
876
|
+
const f = readActor(c.cmd, 'R12', 'consumers');
|
|
877
|
+
if (!f) continue;
|
|
878
|
+
if (tok && !f.text.includes(tok)) {
|
|
879
|
+
err('R12', `${f.rel} là consumer dòng changelog nhưng KHÔNG nhắc \`${tok}\``,
|
|
880
|
+
'Consumer không biết token thì không parse được thứ producer vừa ghi.');
|
|
881
|
+
}
|
|
882
|
+
if (C.neutral_marker && !f.text.includes(C.neutral_marker)) {
|
|
883
|
+
err('R12', `${f.rel} KHÔNG nhắc \`${C.neutral_marker}\` — marker được GHI mà không ai HIỂU`,
|
|
884
|
+
'Đây là ca TỆ HƠN không có marker: producer ghi nó, consumer đọc như văn xuôi rồi\n'
|
|
885
|
+
+ ' bỏ qua ⇒ hoặc mất tác dụng lọc, hoặc bị coi là mơ hồ. Hỏng im lặng (G1).');
|
|
886
|
+
}
|
|
887
|
+
const rq = C.resolve_to_uc || {};
|
|
888
|
+
if (rq.required && rq.anchor && !f.text.includes(rq.anchor)) {
|
|
889
|
+
err('R12', `${f.rel} KHÔNG nhắc phép phân giải \`${rq.anchor}\``,
|
|
890
|
+
'Thiếu bước này thì phép thử "UC này có trong tập?" luôn FALSE cho một row chỉ nêu\n'
|
|
891
|
+
+ ' BR/AC ⇒ UC đổi thật bị xếp ⓘ và --realign dán nhãn lại. Đó là G53.');
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// R12e — một check không được vừa trung tính vừa không trung tính.
|
|
896
|
+
const neu = new Set((C.neutral_checks || []).map(e => e.check));
|
|
897
|
+
for (const e of C.not_neutral_checks || []) {
|
|
898
|
+
if (neu.has(e.check)) {
|
|
899
|
+
err('R12', `check \`${e.check}\` khai ở CẢ \`neutral_checks\` LẪN \`not_neutral_checks\``,
|
|
900
|
+
'Mâu thuẫn trực tiếp — chọn một. Trung tính là lời khẳng định CHỨNG MINH ĐƯỢC.');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const e of [...(C.neutral_checks || []), ...(C.not_neutral_checks || [])]) {
|
|
904
|
+
if (!e.why) {
|
|
905
|
+
err('R12', `check \`${e.check}\` thiếu \`why\``,
|
|
906
|
+
'Xếp hạng trung tính mà không nói lý do sẽ bị người sau coi là tuỳ ý và đổi đi.');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// Có marker mà không có danh sách trung tính = marker dùng theo cảm tính.
|
|
910
|
+
if (C.neutral_marker && !(C.neutral_checks || []).length) {
|
|
911
|
+
err('R12', '`neutral_marker` được khai nhưng `neutral_checks` rỗng',
|
|
912
|
+
'Không liệt kê check nào ĐƯỢC PHÉP mang marker thì nó thành cửa sau: mọi thay đổi\n'
|
|
913
|
+
+ ' đều tự khai "không đổi hành vi" và bộ lọc mất luôn tác dụng.');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ── R13 — phát hiện sửa spec ngoài đường: nửa GHI MỐC phải có thật ────────────
|
|
919
|
+
//
|
|
920
|
+
// Vì sao cần một rule riêng (G54): R6 canh "giá trị enum có xuất hiện ở đâu không" và R7
|
|
921
|
+
// canh "cờ có counter trong Step 7 + summary không" — cả hai đều là nửa BÁO CÁO. Không rule
|
|
922
|
+
// nào canh nửa GHI MỐC. Nên có thể khai đủ cờ + counter + khối report mà KHÔNG BAO GIỜ ghi
|
|
923
|
+
// `spec_baseline`, và cờ đó **không bao giờ bật**: dấu ✅ đầy đủ trên một detector đã chết.
|
|
924
|
+
//
|
|
925
|
+
// Đúng hình dạng G1 (`@trace.sc_version`: 3 consumer, 0 producer) — chỉ khác là lần này
|
|
926
|
+
// "producer" không phải một field trong sổ mà là một khối trong report, nên R1 không thấy.
|
|
927
|
+
{
|
|
928
|
+
const SED = schema.spec_edit_detection;
|
|
929
|
+
if (SED) {
|
|
930
|
+
const vtRel = schema.actors['validate-traces'];
|
|
931
|
+
const vt = vtRel ? actorText['validate-traces'] : null;
|
|
932
|
+
if (!vt) {
|
|
933
|
+
err('R13', '`spec_edit_detection` khai mà không định vị được validate-traces',
|
|
934
|
+
'Không đọc được actor thì R13 mù — sửa `actors` trong schema.');
|
|
935
|
+
} else {
|
|
936
|
+
const bf = SED.baseline_fields || {};
|
|
937
|
+
// (a) nửa GHI: khối baseline + mọi key của nó phải được validate-traces nhắc.
|
|
938
|
+
if (!bf.path) {
|
|
939
|
+
err('R13', '`spec_edit_detection.baseline_fields` thiếu `path` — R13 không có gì để neo');
|
|
940
|
+
} else {
|
|
941
|
+
if (!mentions(vt, bf.path)) {
|
|
942
|
+
err('R13', `${vtRel} KHÔNG nhắc \`${bf.path}\` — cờ \`${SED.flag}\` sẽ không bao giờ bật`,
|
|
943
|
+
'Nửa BÁO CÁO (cờ + counter + khối report) có thể đủ mà nửa GHI MỐC thì vắng, và\n'
|
|
944
|
+
+ ' khi đó detector đã chết trong khi mọi rule khác vẫn ✅. R6 canh giá trị enum,\n'
|
|
945
|
+
+ ' R7 canh counter — không rule nào canh việc mốc có được GHI.');
|
|
946
|
+
}
|
|
947
|
+
// Nửa GHI phải hiện diện như một KEY trong JSON skeleton của report, không chỉ được
|
|
948
|
+
// nhắc trong prose. Vì sao phân biệt: phần ĐỌC mốc (Step 3.9) cũng nhắc `spec_baseline`,
|
|
949
|
+
// nên một `mentions()` trần bị nửa-đọc thoả mãn và nửa-GHI có thể vắng hoàn toàn —
|
|
950
|
+
// detector chết trong khi R13 vẫn ✅. Đúng điểm yếu của R3, và của chính bản R12 đầu tiên.
|
|
951
|
+
// Dạng key JSON (`"spec_baseline"`, có ngoặc kép) chỉ xuất hiện ở skeleton report.
|
|
952
|
+
if (!vt.includes(`"${bf.path}"`)) {
|
|
953
|
+
err('R13', `${vtRel} không khai \`"${bf.path}"\` như một KEY trong JSON report`,
|
|
954
|
+
'Đọc mốc mà không GHI mốc thì lần chạy sau không có gì để so ⇒ cờ im lặng mãi.\n'
|
|
955
|
+
+ ' Prose nhắc tên là chưa đủ — key phải có thật trong skeleton `trace-report.json`,\n'
|
|
956
|
+
+ ' vì đó là thứ lần chạy sau đọc.');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
for (const k of bf.keys || []) {
|
|
960
|
+
if (!mentions(vt, k)) {
|
|
961
|
+
err('R13', `${vtRel} KHÔNG nhắc key mốc \`${k}\``,
|
|
962
|
+
`\`${bf.path}\` thiếu key này ⇒ lần chạy sau không so được, và cờ im lặng.`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// (a2) MỌI command khai là reader của mốc phải nhắc nó. Khai một consumer mà file của nó
|
|
966
|
+
// không biết gì là đúng hình dạng G31/G32 (schema khai, thực tế không có) — và ở đây
|
|
967
|
+
// nó còn im lặng theo hướng xấu: `sync` được khai đọc mốc để trả lời "đổi gì kể từ
|
|
968
|
+
// lần AUDIT", mà không đọc thì nó rơi về câu "kể từ lần PULL" — reset mỗi lần pull.
|
|
969
|
+
for (const cmd of SED.read_by || []) {
|
|
970
|
+
const rel2 = schema.actors[cmd];
|
|
971
|
+
if (!rel2) {
|
|
972
|
+
err('R13', `\`spec_edit_detection.read_by\` khai \`${cmd}\` — không có trong \`actors\``,
|
|
973
|
+
'Không định vị được file thì R13 mù với consumer đó.');
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (!mentions(actorText[cmd] || '', bf.path)) {
|
|
977
|
+
err('R13', `${rel2} khai là reader của \`${bf.path}\` nhưng KHÔNG nhắc nó`,
|
|
978
|
+
'Khai một consumer mà file của nó không biết gì = schema nói một chuyện, thực tế một\n'
|
|
979
|
+
+ ' chuyện khác. Ở đây còn hỏng theo hướng xấu: mốc audit tích luỹ đúng qua nhiều\n'
|
|
980
|
+
+ ' lần pull, còn mốc pull-to-pull thì reset — dùng sai mốc là ẩn nợ đã tích.');
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// (b) cờ phải nằm trong vocabulary — nếu không thì R6/R7 không phủ nó.
|
|
985
|
+
const flags = (schema.vocabularies.audit_flags || {}).values || [];
|
|
986
|
+
if (SED.flag && !flags.includes(SED.flag)) {
|
|
987
|
+
err('R13', `\`${SED.flag}\` không có trong \`vocabularies.audit_flags\``,
|
|
988
|
+
'Ngoài vocabulary thì R6 (giá trị chết) và R7 (đủ ba tầng quan sát) đều KHÔNG phủ nó.');
|
|
989
|
+
}
|
|
990
|
+
// (c) mỗi nguồn bằng chứng phải được nhắc — thiếu một nguồn là thiếu một lớp ca.
|
|
991
|
+
// `git status` bắt ca sửa CHƯA commit, thường gặp hơn ca đã commit.
|
|
992
|
+
for (const e of SED.evidence || []) {
|
|
993
|
+
if (!vt.includes(e.source)) {
|
|
994
|
+
err('R13', `${vtRel} KHÔNG nhắc nguồn bằng chứng \`${e.source}\``,
|
|
995
|
+
`Nó bắt: ${e.catches}. Thiếu nguồn này là mù với cả lớp ca đó.`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// (d) ca degraded phải được khai — im lặng khi không kiểm được là tệ nhất trong ba
|
|
999
|
+
// lựa chọn (bịa cờ · im lặng · nói rõ đang mù).
|
|
1000
|
+
if (!SED.degraded) {
|
|
1001
|
+
err('R13', '`spec_edit_detection` thiếu `degraded`',
|
|
1002
|
+
'Không phải git repo / history bị rewrite là ca THẬT. Không khai cách xử thì agent\n'
|
|
1003
|
+
+ ' tự chọn, và lựa chọn rẻ nhất là im lặng — tức detector tắt mà không ai biết.');
|
|
1004
|
+
}
|
|
1005
|
+
// (e) KHÔNG được lặng lẽ trôi vào gate.blocking. Nếu team muốn chặn thì đó là quyết
|
|
1006
|
+
// định có chủ ý của họ, và phải kèm lý do trong schema như mọi entry blocking khác.
|
|
1007
|
+
const inBlocking = ((schema.gate || {}).blocking || [])
|
|
1008
|
+
.some(b => b.counter === `${SED.flag.toLowerCase()}_count`);
|
|
1009
|
+
if (inBlocking) {
|
|
1010
|
+
const entry = schema.gate.blocking.find(b => b.counter === `${SED.flag.toLowerCase()}_count`);
|
|
1011
|
+
if (!entry.why) {
|
|
1012
|
+
err('R13', `\`${SED.flag}\` đã vào \`gate.blocking\` mà thiếu \`why\``,
|
|
1013
|
+
'Đưa một cờ SPEC vào cổng "code đang hỏng" là mở rộng nghĩa của gate.blocking —\n'
|
|
1014
|
+
+ ' phải nói được vì sao, và phải chắc nợ tồn đã dọn (xem $comment của khối).');
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ── R14 — quyền khẳng định `pass`: chủ cột phải ĐỌC status, và T12 phải tồn tại ─
|
|
1022
|
+
//
|
|
1023
|
+
// Vì sao cần rule riêng (G55): schema khai cột 21 `status` có `read_by` gồm dev-run-test và
|
|
1024
|
+
// qc-run-test — và R3 xác nhận ✅ vì cả hai file CÓ nhắc `status`… trong đúng câu khai rằng
|
|
1025
|
+
// chúng "trực giao" với nó, tức cố ý KHÔNG dùng. Số lần đọc thật: 0.
|
|
1026
|
+
//
|
|
1027
|
+
// Đây là lần thứ ba cùng một điểm yếu xuất hiện (R3 `mentions()` không phân biệt "đọc" với
|
|
1028
|
+
// "nhắc tên"; R12 và R13 đều đã phải chuyển sang use-check vì nó). R14 neo vào một thứ chỉ
|
|
1029
|
+
// tồn tại khi guard được implement THẬT: file phải nhắc từng giá trị `status` bị chặn VÀ giá
|
|
1030
|
+
// trị hạ xuống — một câu tuyên bố "trực giao" không thể thoả mãn cả hai.
|
|
1031
|
+
{
|
|
1032
|
+
const PAG = schema.positive_assertion_guards;
|
|
1033
|
+
if (PAG) {
|
|
1034
|
+
if (!PAG.blocked_when_status || !PAG.blocked_when_status.length) {
|
|
1035
|
+
err('R14', '`positive_assertion_guards.blocked_when_status` rỗng — guard không chặn gì cả');
|
|
1036
|
+
}
|
|
1037
|
+
const statusVals = (schema.vocabularies.trace_status || {}).values || [];
|
|
1038
|
+
for (const st of PAG.blocked_when_status || []) {
|
|
1039
|
+
if (statusVals.length && !statusVals.includes(st)) {
|
|
1040
|
+
err('R14', `\`blocked_when_status\` nêu "${st}" — không thuộc vocabulary \`trace_status\``,
|
|
1041
|
+
`Giá trị cho phép: ${statusVals.join(' | ')}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
for (const g of PAG.guards || []) {
|
|
1046
|
+
const rel = `commands/${g.owner}.tmpl`;
|
|
1047
|
+
const abs = path.join(ROOT, rel);
|
|
1048
|
+
if (!fs.existsSync(abs)) {
|
|
1049
|
+
err('R14', `\`positive_assertion_guards\` khai chủ cột \`${g.column}\` là \`${g.owner}\` — ${rel} không tồn tại`);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
1053
|
+
// (a) chủ cột phải nhắc TỪNG status bị chặn — bằng chứng nó thực sự rẽ nhánh theo status.
|
|
1054
|
+
// Dùng mentions() (khớp ranh giới token), KHÔNG includes(): `DRIFTED` CHỨA `DRIFT`,
|
|
1055
|
+
// nên một includes() trần vẫn ✅ sau khi ai đó đổi tên giá trị và guard đã chết.
|
|
1056
|
+
// Đây đúng cái bẫy mentions() được viết ra để chặn — xem doc-comment của nó.
|
|
1057
|
+
for (const st of PAG.blocked_when_status || []) {
|
|
1058
|
+
if (!mentions(text, st)) {
|
|
1059
|
+
err('R14', `${rel} KHÔNG nhắc status \`${st}\` — nó ghi \`${g.column}\` mà không đọc \`status\``,
|
|
1060
|
+
`Schema khai nó là consumer của cột \`status\`, và R3 xác nhận ✅ vì file có nhắc\n`
|
|
1061
|
+
+ ` chữ "status" — nhưng nhắc trong câu "trực giao" là cố ý KHÔNG dùng. Trên row\n`
|
|
1062
|
+
+ ` ${st}, "${g.positive_values.join('/')}" là lời khẳng định SAI: ${g.why}`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
// (b) phải nhắc giá trị HẠ XUỐNG — nếu không thì biết chặn mà không biết ghi gì thay.
|
|
1066
|
+
if (g.downgrade_to && !mentions(text, g.downgrade_to)) {
|
|
1067
|
+
err('R14', `${rel} KHÔNG nhắc giá trị hạ xuống \`${g.downgrade_to}\` cho \`${g.column}\``,
|
|
1068
|
+
'Biết phải chặn mà không nói ghi gì thay thì agent tự chọn, và lựa chọn rẻ nhất là\n'
|
|
1069
|
+
+ ' giữ nguyên giá trị cũ — tức guard không có tác dụng.');
|
|
1070
|
+
}
|
|
1071
|
+
// (c) `fail` KHÔNG được nằm trong positive_values. Chặn tin xấu là biến một guard
|
|
1072
|
+
// chống-báo-cáo-sai thành một guard CHE tin xấu — hỏng theo hướng nguy hiểm hơn.
|
|
1073
|
+
if ((g.positive_values || []).includes('fail')) {
|
|
1074
|
+
err('R14', `\`${g.column}\`: \`fail\` nằm trong \`positive_values\``,
|
|
1075
|
+
'`fail` là tin xấu THẬT, luôn hợp lệ trên mọi status. Chặn nó là che lỗi.');
|
|
1076
|
+
}
|
|
1077
|
+
if (!g.why) {
|
|
1078
|
+
err('R14', `\`positive_assertion_guards\` mục \`${g.column}\` thiếu \`why\``,
|
|
1079
|
+
'Một guard không nói được VÌ SAO sẽ bị người sau coi là tuỳ ý và gỡ đi.');
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// (d) TẦNG HAI phải tồn tại: lint-trace thực sự phát ra rule id đã khai. Guard ở lệnh chỉ
|
|
1084
|
+
// chặn NGUỒN; sổ sửa tay / sổ sinh bởi version cũ / lệnh tương lai quên luật đều lọt.
|
|
1085
|
+
if (PAG.lint_rule) {
|
|
1086
|
+
const ltRel = 'bin/lint-trace.js';
|
|
1087
|
+
const lt = fs.existsSync(path.join(ROOT, ltRel))
|
|
1088
|
+
? fs.readFileSync(path.join(ROOT, ltRel), 'utf8') : '';
|
|
1089
|
+
const emits = new Set((lt.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
|
|
1090
|
+
.map(m => /T\d+/.exec(m)[0]));
|
|
1091
|
+
if (!emits.has(PAG.lint_rule)) {
|
|
1092
|
+
err('R14', `schema khai \`lint_rule: ${PAG.lint_rule}\` mà ${ltRel} KHÔNG phát ra rule đó`,
|
|
1093
|
+
'Chỉ có guard ở lệnh là chỉ chặn được NGUỒN. Sổ ghi tay, sổ sinh bởi version cũ, hay\n'
|
|
1094
|
+
+ ' một lệnh tương lai quên luật đều lọt — và không gì bắt được, vì T1–T8 chỉ kiểm\n'
|
|
1095
|
+
+ ' hình dạng TỪNG Ô, không kiểm nhất quán GIỮA các ô.');
|
|
1096
|
+
}
|
|
1097
|
+
// Neo vào TRUY CẬP THUỘC TÍNH (`schema.positive_assertion_guards`), không phải tên
|
|
1098
|
+
// xuất hiện ở đâu đó: doc-comment của T12 cũng nhắc tên khối, nên một includes() trần
|
|
1099
|
+
// vẫn ✅ sau khi ai đó thay bằng danh sách hard-code. Lần thứ ba cùng một bẫy —
|
|
1100
|
+
// R12 và R13 đều đã phải chuyển sang use-check vì nó.
|
|
1101
|
+
if (!/schema\s*\.\s*positive_assertion_guards/.test(lt)) {
|
|
1102
|
+
err('R14', `${ltRel} không ĐỌC \`schema.positive_assertion_guards\``,
|
|
1103
|
+
'Hard-code lại danh sách cột/status trong lint = hai nguồn-sự-thật, và chúng sẽ lệch\n'
|
|
1104
|
+
+ ' âm thầm: thêm một cột nghiệm thu mới vào schema thì lint không biết, và T12\n'
|
|
1105
|
+
+ ' chỉ canh một nửa số cột trong khi dấu ✅ vẫn đầy đủ.');
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
321
1111
|
// ── report ────────────────────────────────────────────────────────────────────
|
|
322
1112
|
const tagCount = schema.tags.length;
|
|
323
1113
|
const colCount = schema.tsv_columns.length;
|
|
@@ -337,7 +1127,19 @@ show(errors, '❌');
|
|
|
337
1127
|
show(warns, '⚠️ ');
|
|
338
1128
|
|
|
339
1129
|
if (!errors.length && !warns.length) {
|
|
340
|
-
|
|
1130
|
+
// Đếm bằng cách LOẠI key `$…` (metadata/comment), không phải `- 1`: schema có nhiều hơn
|
|
1131
|
+
// một key `$` kể từ khi thêm `$lint_binding_comment`, nên hằng số cũ sẽ đếm lố.
|
|
1132
|
+
const vocabCount = Object.keys(schema.vocabularies).filter(k => !k.startsWith('$')).length;
|
|
1133
|
+
console.log(` ✅ ${tagCount} tag + ${colCount} cột TSV + ${auxCount} bảng phụ + ${queueCount} hàng đợi + ${vocabCount} vocabulary — contract khớp mọi lệnh`);
|
|
1134
|
+
// ĐẾM rule từ chính lint-trace.js, không hard-code: một con số phát biểu ở hai chỗ rồi
|
|
1135
|
+
// lệch nhau là đúng lớp lỗi G23 được dựng lên để chặn. Đếm id `Tn` mà nó thực sự phát ra.
|
|
1136
|
+
let lintRules = '?';
|
|
1137
|
+
try {
|
|
1138
|
+
const txt = fs.readFileSync(path.join(ROOT, 'bin/lint-trace.js'), 'utf8');
|
|
1139
|
+
lintRules = new Set((txt.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
|
|
1140
|
+
.map(m => /T\d+/.exec(m)[0])).size;
|
|
1141
|
+
} catch {}
|
|
1142
|
+
console.log(` + sổ trace thật được canh bởi bin/lint-trace.js (${lintRules} rule) — R8 giữ nó theo kịp schema`);
|
|
341
1143
|
}
|
|
342
1144
|
console.log('');
|
|
343
1145
|
|