@educa-corp/sdd-framework 0.4.2 → 0.6.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 +464 -0
- package/bin/index.js +418 -146
- package/bin/lint-trace.js +602 -0
- package/bin/self-check.js +499 -6
- package/bin/trace-schema.json +1449 -692
- package/commands/debug.md +123 -510
- package/commands/debug.tmpl +3 -0
- package/commands/define-product.md +86 -509
- package/commands/dev-gen-test.md +120 -516
- package/commands/dev-run-test.md +120 -516
- package/commands/dev-smoke-test.md +86 -509
- package/commands/extend-prd.md +486 -0
- package/commands/extend-prd.tmpl +273 -0
- package/commands/fix-bug.md +152 -515
- package/commands/generate-architecture.md +94 -514
- package/commands/generate-architecture.tmpl +3 -0
- package/commands/generate-bdd.md +138 -519
- package/commands/generate-bdd.tmpl +18 -3
- package/commands/generate-code.md +156 -523
- package/commands/generate-code.tmpl +36 -7
- package/commands/generate-design-spec.md +86 -509
- package/commands/generate-prd.md +114 -509
- package/commands/generate-prd.tmpl +28 -0
- package/commands/generate-spec-manifest.md +86 -509
- package/commands/generate-tech-docs.md +86 -509
- package/commands/learn.md +172 -495
- package/commands/learn.tmpl +70 -3
- package/commands/map-testids.md +86 -509
- package/commands/propose-scenario.md +136 -508
- package/commands/propose-scenario.tmpl +52 -1
- package/commands/qc-analyze.md +86 -509
- package/commands/qc-design-test.md +87 -509
- package/commands/qc-design-test.tmpl +1 -0
- package/commands/qc-plan.md +86 -509
- package/commands/qc-report.md +86 -509
- package/commands/qc-review.md +86 -509
- package/commands/qc-run-test.md +133 -517
- package/commands/qc-run-test.tmpl +13 -1
- package/commands/refine-prd.md +99 -519
- package/commands/refine-prd.tmpl +3 -0
- package/commands/report-bug.md +86 -509
- package/commands/review-code.md +127 -513
- package/commands/review-code.tmpl +7 -3
- package/commands/review-context.md +96 -515
- package/commands/review-context.tmpl +6 -2
- package/commands/review-tech-docs.md +90 -510
- package/commands/review-tech-docs.tmpl +3 -0
- package/commands/setup-ai-first.md +166 -137
- package/commands/setup-ai-first.tmpl +72 -0
- package/commands/sync.md +86 -118
- package/commands/sync.tmpl +84 -16
- package/commands/update-framework.md +16 -102
- package/commands/update-framework.tmpl +14 -0
- package/commands/validate-traces.md +458 -531
- package/commands/validate-traces.tmpl +381 -31
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/README.md +20 -0
- package/core/commands/debug.md +123 -510
- package/core/commands/define-product.md +86 -509
- package/core/commands/dev-gen-test.md +120 -516
- package/core/commands/dev-run-test.md +120 -516
- package/core/commands/dev-smoke-test.md +86 -509
- package/core/commands/extend-prd.md +486 -0
- package/core/commands/fix-bug.md +152 -515
- package/core/commands/generate-architecture.md +94 -514
- package/core/commands/generate-bdd.md +138 -519
- package/core/commands/generate-code.md +156 -523
- package/core/commands/generate-design-spec.md +86 -509
- package/core/commands/generate-prd.md +114 -509
- package/core/commands/generate-spec-manifest.md +86 -509
- package/core/commands/generate-tech-docs.md +86 -509
- package/core/commands/learn.md +172 -495
- package/core/commands/map-testids.md +86 -509
- package/core/commands/propose-scenario.md +136 -508
- package/core/commands/qc-analyze.md +86 -509
- package/core/commands/qc-design-test.md +87 -509
- package/core/commands/qc-plan.md +86 -509
- package/core/commands/qc-report.md +86 -509
- package/core/commands/qc-review.md +86 -509
- package/core/commands/qc-run-test.md +133 -517
- package/core/commands/refine-prd.md +99 -519
- package/core/commands/report-bug.md +86 -509
- package/core/commands/review-code.md +127 -513
- package/core/commands/review-context.md +96 -515
- package/core/commands/review-tech-docs.md +90 -510
- package/core/commands/setup-ai-first.md +166 -137
- package/core/commands/sync.md +86 -118
- package/core/commands/update-framework.md +16 -102
- package/core/commands/validate-traces.md +458 -531
- package/core/hooks/data-guard.js +174 -83
- package/core/hooks/settings.json +2 -1
- package/core/rules/workflow.md +48 -4
- package/core/steps/capture-lesson.md +34 -1
- package/core/steps/context-loader.md +24 -3
- package/core/steps/gate.md +92 -35
- package/core/steps/report-footer.md +26 -2
- package/core/steps/trace-mirror.md +34 -7
- 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/01-getting-started/installation.md +18 -1
- package/docs/01-getting-started/what-is-sdd.md +4 -2
- package/docs/02-concepts/architecture.md +48 -5
- package/docs/02-concepts/pipeline-steps/02-specification.md +39 -3
- package/docs/02-concepts/pipeline-steps/04-bdd.md +24 -2
- package/docs/02-concepts/pipeline-steps/05-tech-docs.md +18 -1
- package/docs/02-concepts/pipeline-steps/06-code.md +35 -4
- package/docs/02-concepts/pipeline-steps/09-validate-traces.md +137 -12
- package/docs/02-concepts/pipeline-steps/10-feedback-loop.md +59 -3
- package/docs/02-concepts/roles-and-hitl.md +1 -1
- package/docs/02-concepts/traceability.md +183 -117
- package/docs/03-guides/architect.md +63 -0
- package/docs/03-guides/developer.md +20 -4
- package/docs/03-guides/product-owner.md +72 -68
- package/docs/03-guides/tester-qa.md +81 -70
- package/docs/04-reference/commands.md +134 -105
- package/docs/04-reference/configuration.md +146 -94
- package/docs/04-reference/model-selection.md +32 -19
- package/docs/04-reference/trace-schema.md +26 -9
- package/docs/explain/02-generate-prd.md +80 -78
- package/docs/explain/02b-extend-prd.md +125 -0
- package/docs/explain/03-refine-prd.md +86 -86
- package/docs/explain/04-review-context.md +18 -1
- package/docs/explain/06-generate-bdd.md +23 -0
- package/docs/explain/08-review-tech-docs.md +20 -5
- package/docs/explain/10-review-code.md +36 -2
- package/docs/explain/19-qc-run-test.md +87 -67
- package/docs/explain/21-validate-traces.md +75 -68
- package/docs/explain/23-fix-bug.md +19 -3
- package/docs/explain/26-propose-scenario.md +70 -63
- package/docs/explain/27-learn.md +5 -3
- package/docs/explain/README.md +135 -134
- package/hooks/data-guard.js +174 -83
- package/hooks/settings.json +2 -1
- package/package.json +53 -50
- package/rules/workflow.md +48 -4
- package/steps/capture-lesson.md +34 -1
- package/steps/context-loader.md +24 -3
- package/steps/gate.md +92 -35
- package/steps/report-footer.md +26 -2
- package/steps/trace-mirror.md +34 -7
- package/templates/README.md +24 -1
- package/templates/ci/trace-gate.yml +146 -0
- package/templates/feature.template +1 -1
- package/templates/hooks/pre-push +61 -0
- package/scripts/init.sh +0 -49
- package/scripts/upgrade.sh +0 -94
package/bin/self-check.js
CHANGED
|
@@ -8,16 +8,33 @@
|
|
|
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
|
-
*
|
|
12
|
-
* R1 field có read_by nhưng KHÔNG có producer
|
|
13
|
-
* R2 field có producer nhưng KHÔNG ai đọc → WARN (bắt được G15, fe_phase)
|
|
11
|
+
* 7 rule — áp cho tags · tsv_columns · aux_tables (_seams.tsv) · queues (feedback/*):
|
|
12
|
+
* R1 field/hàng đợi có read_by nhưng KHÔNG có producer → ERROR (bắt được G1; queue: G26)
|
|
13
|
+
* R2 field có producer nhưng KHÔNG ai đọc → WARN (bắt được G15, fe_phase, test_type)
|
|
14
14
|
* R3 actor được khai là producer/consumer mà file của
|
|
15
|
-
* nó KHÔNG nhắc tên field
|
|
15
|
+
* nó KHÔNG nhắc tên field/cột/path_key → ERROR (bắt được G5, G12, G13, G31, G32)
|
|
16
16
|
* R4 {paths.X} dùng trong lệnh mà X không có trong
|
|
17
17
|
* project-context.yaml, và ngược lại key chết → ERROR / WARN (bắt được G17)
|
|
18
|
-
* R5 pattern bị cấm xuất hiện trở lại → ERROR (bắt được G3, G8, G10, G18, G21)
|
|
18
|
+
* R5 pattern bị cấm xuất hiện trở lại → ERROR (bắt được G3, G8, G10, G18, G21, G11)
|
|
19
19
|
* R6 giá trị enum khai trong vocabulary mà không actor
|
|
20
20
|
* nào dùng → WARN (bắt được G19: integration vs integrated)
|
|
21
|
+
* R7 audit flag thiếu counter {flag}_count trong CẢ
|
|
22
|
+
* Step 7 LẪN khối `summary` của validate-traces → ERROR (bắt được G33)
|
|
23
|
+
* R8 bin/lint-trace.js tụt lại sau schema → ERROR (bắt được G38)
|
|
24
|
+
* R8a vocabulary không khai binding (neo vào đâu để kiểm sổ thật?)
|
|
25
|
+
* R8b binding đã khai mà lint-trace không đọc key đó
|
|
26
|
+
* R8c `n` của tsv_columns trùng/hổng — lint dựng header theo `n`, sai `n` = sai header
|
|
27
|
+
* R8d bảng phụ nằm ngoài phạm vi lint
|
|
28
|
+
*
|
|
29
|
+
* RANH GIỚI R1–R7 vs R8: R1–R7 canh CONTRACT (đọc commands/*.tmpl — "lệnh có gọi đúng tên
|
|
30
|
+
* 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,
|
|
31
|
+
* 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.
|
|
32
|
+
* bin/lint-trace.js canh DỮ LIỆU; R8 canh việc nó theo kịp contract.
|
|
33
|
+
*
|
|
34
|
+
* 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À
|
|
35
|
+
* cách G31 (@trace.service: 2 consumer khai, 0 nơi chứa) cùng G32 (design-spec: 0 cột/0 tag/
|
|
36
|
+
* 0 cờ) sống qua nhiều version với dấu ✅. Dấu ✅ chỉ có nghĩa "phần ĐÃ KHAI thì đúng".
|
|
37
|
+
* Giữ phạm vi đầy đủ: thêm tag/cột/bảng/hàng đợi mới thì khai NGAY, đừng để dồn.
|
|
21
38
|
*
|
|
22
39
|
* Chạy: node bin/self-check.js (exit 1 nếu có ERROR)
|
|
23
40
|
* node bin/self-check.js --warn-only (luôn exit 0)
|
|
@@ -126,6 +143,59 @@ function checkField(kind, name, def) {
|
|
|
126
143
|
for (const t of schema.tags) checkField('tag', t.name, t);
|
|
127
144
|
for (const c of schema.tsv_columns) checkField('cột TSV', c.name, c);
|
|
128
145
|
|
|
146
|
+
// Bảng phụ (_seams.tsv) — nguồn của 2 cờ 🔴 chặn PR nhưng nằm ngoài SoT tới G30.
|
|
147
|
+
// Chạy CÙNG checkField, không cần rule mới: mỗi cột là một field có producer/consumer.
|
|
148
|
+
for (const t of schema.aux_tables || []) {
|
|
149
|
+
checkField(`bảng phụ \`${t.name}\``, t.name, t); // file phải được cả 2 đầu nhắc tên
|
|
150
|
+
// `column_actors` = actor thao tác ở MỨC CỘT (ghi/parse từng field). Actor chỉ dùng bảng ở
|
|
151
|
+
// MỨC BẢNG (vd review-code chỉ kiểm "có dòng PENDING không") KHÔNG phải nhắc đủ tên cột —
|
|
152
|
+
// đòi thế là báo oan. Không khai `column_actors` → mặc định mọi actor.
|
|
153
|
+
const colActors = t.column_actors || [...(t.written_by || []), ...(t.read_by || [])];
|
|
154
|
+
for (const col of t.columns || []) {
|
|
155
|
+
for (const actor of new Set(colActors)) {
|
|
156
|
+
const text = actorText[actor];
|
|
157
|
+
if (text === undefined) continue;
|
|
158
|
+
if (!mentions(text, col)) {
|
|
159
|
+
err('R3', `cột \`${t.name}:${col}\`: schema khai \`${actor}\` đụng bảng này nhưng ${schema.actors[actor]} KHÔNG nhắc cột \`${col}\``,
|
|
160
|
+
`Hoặc lệnh thiếu implement, hoặc cột đã đổi tên. Bảng này là NGUỒN của SEAM_UNWIRED/STUB_UNRESOLVED —\n`
|
|
161
|
+
+ ` đổi tên cột mà không ai bắt được thì hai cờ 🔴 chặn PR sẽ mù trong im lặng.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Hàng đợi feedback — bắt ca "hàng đợi có producer nhưng KHÔNG ai drain" (hình dạng của G26).
|
|
168
|
+
//
|
|
169
|
+
// KHÔNG dùng chung mentions() với tag/cột: path key luôn xuất hiện dưới dạng `{paths.X}`,
|
|
170
|
+
// tức LUÔN có dấu `.` đứng trước — mà mentions() cố ý chặn ký tự đó (để `@trace.XXsc_version`
|
|
171
|
+
// không bị coi là đã nhắc `sc_version`). Dùng thẳng thì 100% queue báo lỗi oan.
|
|
172
|
+
// Ở đây chỉ cần chặn hậu tố: `bug_reports_dir` không được khớp `bug_reports_dirX`.
|
|
173
|
+
const mentionsKey = (text, key) =>
|
|
174
|
+
new RegExp(`\\b${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_])`).test(text);
|
|
175
|
+
|
|
176
|
+
for (const q of schema.queues || []) {
|
|
177
|
+
const producers = q.written_by || [];
|
|
178
|
+
const consumers = q.read_by || [];
|
|
179
|
+
const label = `hàng đợi \`${q.name}\``;
|
|
180
|
+
|
|
181
|
+
if (producers.length && !consumers.length) {
|
|
182
|
+
err('R1', `${label}: có producer nhưng KHÔNG ai đọc — file vào đó rồi nằm lại vĩnh viễn`,
|
|
183
|
+
`Đây là hình dạng của G26 (prd-change-requests: có producer, có storage, có commit,\n`
|
|
184
|
+
+ ` có mặt trong /sync — nhưng 0 consumer, và không gì báo).`);
|
|
185
|
+
}
|
|
186
|
+
if (consumers.length && !producers.length) {
|
|
187
|
+
err('R1', `${label}: có consumer nhưng KHÔNG ai ghi vào — hàng đợi chết?`);
|
|
188
|
+
}
|
|
189
|
+
for (const actor of new Set([...producers, ...consumers])) {
|
|
190
|
+
const text = actorText[actor];
|
|
191
|
+
if (text === undefined) continue;
|
|
192
|
+
if (!mentionsKey(text, q.path_key)) {
|
|
193
|
+
err('R3', `${label}: schema khai \`${actor}\` đụng hàng đợi này nhưng ${schema.actors[actor]} KHÔNG nhắc \`${q.path_key}\``,
|
|
194
|
+
`Hoặc lệnh thiếu implement, hoặc schema khai sai.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
129
199
|
// ── R4 — {paths.X} vs project-context.yaml ────────────────────────────────────
|
|
130
200
|
const cfgPath = path.join(ROOT, schema.path_keys.config_file);
|
|
131
201
|
if (!fs.existsSync(cfgPath)) {
|
|
@@ -202,9 +272,420 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
202
272
|
}
|
|
203
273
|
}
|
|
204
274
|
|
|
275
|
+
// ── R7 — mỗi audit flag phải có counter trong summary ─────────────────────────
|
|
276
|
+
// Vì sao tồn tại (G33): PRD_DRIFT / TECHDOC_DRIFT / FE_TECHDOC_DRIFT được TÍNH ở Step 4-5
|
|
277
|
+
// và CÓ mảng chi tiết trong `issues`, nhưng KHÔNG có counter trong `summary`. Dashboard đọc
|
|
278
|
+
// `summary` → ba cờ đó vô hình ở tầng tổng hợp. Bất đối xứng 7/10 còn tệ hơn thiếu cả 10:
|
|
279
|
+
// người viết dashboard thấy có counter cho các cờ khác nên tưởng `summary` là đủ.
|
|
280
|
+
//
|
|
281
|
+
// CỐ Ý KHÔNG CÓ ESCAPE HATCH (không `allow_no_counter`). Một luật có cửa thoát dùng được
|
|
282
|
+
// ngay là luật yếu — đó chính là cách G30 xảy ra (schema phủ 16/43 tag rồi dừng). Cờ nào
|
|
283
|
+
// "chỉ để nhắc, chưa phải lỗi" (SEAM_PENDING/STUB_PENDING) thì counter của nó vẫn có giá
|
|
284
|
+
// trị: PM đọc để xếp thứ tự gen UC. Thêm 2 dòng rẻ hơn nhiều so với một ngoại lệ vĩnh viễn.
|
|
285
|
+
// SIẾT (G30 lô 8): bản đầu chỉ hỏi "tên counter có xuất hiện Ở ĐÂU ĐÓ trong file không".
|
|
286
|
+
// Test G27 lộ ra lỗ: gỡ counter khỏi ĐÚNG hai chỗ luật đòi (Step 7 + `summary`) mà R7 vẫn PASS,
|
|
287
|
+
// vì tên còn sót trong khối report terminal. Tức nó bắt được ca "quên hoàn toàn" (ca thật của
|
|
288
|
+
// G33) nhưng KHÔNG bắt ca "có ở report mà quên ở summary" — mà đó CHÍNH LÀ ca làm dashboard mù.
|
|
289
|
+
// Giờ kiểm từng khối riêng.
|
|
290
|
+
{
|
|
291
|
+
const flagVocab = schema.vocabularies.audit_flags;
|
|
292
|
+
const vtName = 'validate-traces';
|
|
293
|
+
const vtText = actorText[vtName];
|
|
294
|
+
if (flagVocab && vtText !== undefined) {
|
|
295
|
+
/** Cắt lát văn bản giữa hai mốc; null nếu không tìm thấy mốc (đừng im lặng bỏ qua). */
|
|
296
|
+
const slice = (from, to) => {
|
|
297
|
+
const a = vtText.indexOf(from);
|
|
298
|
+
if (a < 0) return null;
|
|
299
|
+
const b = vtText.indexOf(to, a + from.length);
|
|
300
|
+
return vtText.slice(a, b < 0 ? undefined : b);
|
|
301
|
+
};
|
|
302
|
+
const zones = [
|
|
303
|
+
{ label: 'Step 7 (aggregate)', text: slice('### Step 7 —', '### Step 7b') },
|
|
304
|
+
{ label: '`summary` của trace-report.json', text: slice('"summary": {', '"prds"') },
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
for (const z of zones) {
|
|
308
|
+
if (z.text === null) {
|
|
309
|
+
warn('R7', `Không định vị được khối "${z.label}" trong ${schema.actors[vtName]} — R7 bỏ qua khối này`,
|
|
310
|
+
'Mốc cắt đã đổi? Cập nhật `slice()` trong self-check.js, đừng để R7 mù trong im lặng.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const missing = [];
|
|
315
|
+
for (const flag of flagVocab.values || []) {
|
|
316
|
+
const counter = `${flag.toLowerCase()}_count`;
|
|
317
|
+
const gone = zones.filter(z => z.text !== null && !mentions(z.text, counter));
|
|
318
|
+
if (gone.length) missing.push({ flag, counter, zones: gone.map(z => z.label) });
|
|
319
|
+
}
|
|
320
|
+
if (missing.length) {
|
|
321
|
+
err('R7', `${missing.length} audit flag thiếu counter trong ${schema.actors[vtName]}`,
|
|
322
|
+
missing.map(m => `${m.flag} → thiếu \`${m.counter}\` ở: ${m.zones.join(' + ')}`).join('\n ')
|
|
323
|
+
+ `\n Counter PHẢI có ở CẢ HAI khối. Chỉ có ở report terminal là KHÔNG đủ —`
|
|
324
|
+
+ `\n dashboard đọc \`summary\`, nên cờ sẽ vô hình ở tầng tổng hợp (G33).`);
|
|
325
|
+
}
|
|
326
|
+
} else if (!flagVocab) {
|
|
327
|
+
warn('R7', 'Không tìm thấy vocabulary `audit_flags` — R7 không chạy được');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── R8 — lint-trace phải theo kịp schema ──────────────────────────────────────
|
|
332
|
+
//
|
|
333
|
+
// Vì sao (G38): R1–R7 canh CONTRACT — chúng đọc file lệnh và kiểm "lệnh có gọi đúng tên
|
|
334
|
+
// 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 đó,
|
|
335
|
+
// 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
|
|
336
|
+
// dạy nó → checker tụt lại sau contract, im lặng, và 6 tháng sau ta có G38 lần hai.
|
|
337
|
+
// R8 là van đóng đường đó — cùng tinh thần R7: nợ không được phép tái sinh.
|
|
338
|
+
{
|
|
339
|
+
const LINT_REL = 'bin/lint-trace.js';
|
|
340
|
+
const lintAbs = path.join(ROOT, LINT_REL);
|
|
341
|
+
|
|
342
|
+
if (!fs.existsSync(lintAbs)) {
|
|
343
|
+
err('R8', `${LINT_REL} không tồn tại — không có gì kiểm DỮ LIỆU sổ trace`,
|
|
344
|
+
`Contract được canh bằng máy (R1–R7) nhưng sổ thật thì không. Đây chính là G38.`);
|
|
345
|
+
} else {
|
|
346
|
+
const lintText = fs.readFileSync(lintAbs, 'utf8');
|
|
347
|
+
|
|
348
|
+
// ── R8a — mọi vocabulary phải neo được vào một chỗ kiểm cụ thể ──
|
|
349
|
+
const BINDINGS = ['column', 'columns', 'aux_column', 'filename_segment'];
|
|
350
|
+
for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
351
|
+
if (vocab.startsWith('$') || !def || !def.values) continue;
|
|
352
|
+
const bound = BINDINGS.filter(b => def[b] !== undefined);
|
|
353
|
+
if (!bound.length && !def.$lint) {
|
|
354
|
+
err('R8', `vocabulary \`${vocab}\` không khai binding nào cho lint-trace`,
|
|
355
|
+
`Khai đúng một trong: ${BINDINGS.map(b => `\`${b}\``).join(' · ')} — hoặc \`$lint\` kèm LÝ DO\n`
|
|
356
|
+
+ ` nếu nó cố ý ngoài phạm vi (như audit_flags: sống trong trace-report.json, R7 canh).\n`
|
|
357
|
+
+ ` Không khai = ${lintText ? 'lint-trace' : 'checker'} không biết kiểm ở đâu, và enum đó chỉ là chữ trong schema.`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 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
|
|
361
|
+
// im lặng bỏ qua cả vocabulary đó (hình dạng G1: consumer trỏ vào chỗ trống).
|
|
362
|
+
const colNames = new Set(schema.tsv_columns.map(c => c.name));
|
|
363
|
+
for (const c of (def.columns || (def.column ? [def.column] : []))) {
|
|
364
|
+
if (!colNames.has(c)) {
|
|
365
|
+
err('R8', `vocabulary \`${vocab}\` neo vào cột \`${c}\` — cột này KHÔNG có trong tsv_columns`,
|
|
366
|
+
`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.`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (def.aux_column) {
|
|
370
|
+
const aux = (schema.aux_tables || []).find(t => t.name === def.aux_table);
|
|
371
|
+
if (!aux) {
|
|
372
|
+
err('R8', `vocabulary \`${vocab}\` khai \`aux_table: "${def.aux_table}"\` — không có bảng phụ nào tên đó`);
|
|
373
|
+
} else if (!aux.columns.includes(def.aux_column)) {
|
|
374
|
+
err('R8', `vocabulary \`${vocab}\` neo vào \`${def.aux_table}.${def.aux_column}\` — cột này không có trong bảng đó`,
|
|
375
|
+
`Cột hiện có: ${aux.columns.join(' · ')}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── R8b — kiểu binding đã khai thì lint-trace phải thực sự ĐỌC nó ──
|
|
380
|
+
for (const b of bound) {
|
|
381
|
+
if (!mentionsKey(lintText, b)) {
|
|
382
|
+
err('R8', `schema khai binding \`${b}\` (ở \`${vocab}\`) mà ${LINT_REL} KHÔNG đọc key đó`,
|
|
383
|
+
`Khai binding rồi mà checker không dùng = enum không được kiểm ở đâu cả.\n`
|
|
384
|
+
+ ` Dạy lint-trace đọc \`${b}\`, hoặc đổi sang một binding nó đã hiểu.`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── R8c — `n` phải là 1..N liền mạch, không trùng ──
|
|
390
|
+
//
|
|
391
|
+
// lint-trace dựng header bằng cách SORT theo `n` (thứ tự array KHÔNG phải thứ tự cột —
|
|
392
|
+
// 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
|
|
393
|
+
// header dẫn xuất sai ⇒ lint đối chiếu sổ thật với một contract sai. Kiểm cơ học.
|
|
394
|
+
const ns = schema.tsv_columns.map(c => c.n);
|
|
395
|
+
const dupes = ns.filter((n, i) => ns.indexOf(n) !== i);
|
|
396
|
+
if (dupes.length) {
|
|
397
|
+
err('R8', `tsv_columns có \`n\` trùng: ${[...new Set(dupes)].join(', ')}`,
|
|
398
|
+
`\`n\` là thứ tự cột authoritative — lint-trace sort theo nó để dựng header.\n`
|
|
399
|
+
+ ` Trùng ⇒ header dẫn xuất sai ⇒ lint đối chiếu sổ thật với contract sai.`);
|
|
400
|
+
}
|
|
401
|
+
const expected = Array.from({ length: schema.tsv_columns.length }, (_, i) => i + 1);
|
|
402
|
+
const missingN = expected.filter(n => !ns.includes(n));
|
|
403
|
+
if (missingN.length) {
|
|
404
|
+
err('R8', `tsv_columns hổng số thứ tự: thiếu n = ${missingN.join(', ')}`,
|
|
405
|
+
`${schema.tsv_columns.length} cột thì \`n\` phải là 1..${schema.tsv_columns.length} liền mạch.\n`
|
|
406
|
+
+ ` Bỏ một cột thì đánh số lại, đừng để lỗ — lint dựng header theo \`n\`.`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── R8d — bảng phụ cũng phải nằm trong phạm vi lint ──
|
|
410
|
+
for (const t of schema.aux_tables || []) {
|
|
411
|
+
if (!lintText.includes(t.name)) {
|
|
412
|
+
err('R8', `bảng phụ \`${t.name}\` không được ${LINT_REL} kiểm`,
|
|
413
|
+
`Nó là NGUỒN của SEAM_UNWIRED/STUB_UNRESOLVED (hai cờ 🔴 chặn PR).\n`
|
|
414
|
+
+ ` Header lệch ở đây thì hai cờ đó mù — đúng lý do bảng này được đưa vào SoT ở G30.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── R9 — cổng chặn PR phải neo đúng vào report ────────────────────────────────
|
|
421
|
+
//
|
|
422
|
+
// Vì sao (G39): gate đọc `summary[counter]`. Counter bị đổi tên mà gate không biết →
|
|
423
|
+
// `undefined > 0` là false → GATE PASS TRÊN CODE HỎNG, kèm dấu ✅. Đây là false-green
|
|
424
|
+
// 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
|
|
425
|
+
// PR hỏng được merge. gate-trace.js tự phòng bằng cách fail khi counter vắng mặt trong
|
|
426
|
+
// REPORT; R9 canh đầu còn lại — schema có neo đúng vào thứ validate-traces THỰC SỰ ghi.
|
|
427
|
+
{
|
|
428
|
+
const GATE_REL = 'bin/gate-trace.js';
|
|
429
|
+
const gateAbs = path.join(ROOT, GATE_REL);
|
|
430
|
+
const gate = schema.gate;
|
|
431
|
+
|
|
432
|
+
if (!gate) {
|
|
433
|
+
err('R9', 'schema thiếu khối `gate` — không khai được cờ nào chặn PR',
|
|
434
|
+
`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).`);
|
|
435
|
+
} else if (!fs.existsSync(gateAbs)) {
|
|
436
|
+
err('R9', `${GATE_REL} không tồn tại — cờ 🔴 không có gì chặn`,
|
|
437
|
+
`Khai blocking trong schema mà không có cổng đọc nó = vẫn honor-system.`);
|
|
438
|
+
} else {
|
|
439
|
+
const gateText = fs.readFileSync(gateAbs, 'utf8');
|
|
440
|
+
const vtName = 'validate-traces';
|
|
441
|
+
const vtText = actorText[vtName];
|
|
442
|
+
|
|
443
|
+
const slice = (from, to) => {
|
|
444
|
+
if (vtText === undefined) return null;
|
|
445
|
+
const a = vtText.indexOf(from);
|
|
446
|
+
if (a < 0) return null;
|
|
447
|
+
const b = vtText.indexOf(to, a + from.length);
|
|
448
|
+
return vtText.slice(a, b < 0 ? undefined : b);
|
|
449
|
+
};
|
|
450
|
+
const summaryZone = slice('"summary": {', '"prds"');
|
|
451
|
+
const issuesZone = slice('"issues": {', '"prds"');
|
|
452
|
+
|
|
453
|
+
if (summaryZone === null || issuesZone === null) {
|
|
454
|
+
warn('R9', `Không định vị được khối \`summary\`/\`issues\` trong ${schema.actors[vtName]} — R9 bỏ qua`,
|
|
455
|
+
'Mốc cắt đã đổi? Cập nhật `slice()` trong self-check.js, đừng để R9 mù trong im lặng.');
|
|
456
|
+
} else {
|
|
457
|
+
// (a) counter + issues_key phải CÓ THẬT trong report mà validate-traces sinh ra
|
|
458
|
+
for (const b of gate.blocking || []) {
|
|
459
|
+
if (!mentionsKey(summaryZone, b.counter)) {
|
|
460
|
+
err('R9', `\`gate.blocking\` chờ counter \`${b.counter}\` mà khối \`summary\` của ${schema.actors[vtName]} KHÔNG có`,
|
|
461
|
+
`Gate đọc summary[${b.counter}]. Không có ⇒ gate MÙ với cờ này.\n`
|
|
462
|
+
+ ` Hoặc counter đã đổi tên (sửa schema), hoặc validate-traces thiếu ghi nó.`);
|
|
463
|
+
}
|
|
464
|
+
if (!mentionsKey(issuesZone, b.issues_key)) {
|
|
465
|
+
err('R9', `\`gate.blocking\` chờ \`issues.${b.issues_key}\` mà khối \`issues\` KHÔNG có`,
|
|
466
|
+
`Không có ⇒ gate chặn được nhưng KHÔNG nêu được thủ phạm — người đọc CI thấy\n`
|
|
467
|
+
+ ` một con số và không biết sửa gì. Cờ chặn phải kèm đường ra.`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// (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
|
|
472
|
+
// quay về đọc một file có thể cũ, tức trở lại đúng thứ G39 định bịt.
|
|
473
|
+
for (const key of [...Object.keys((gate.reconcile || {}).hard || {}),
|
|
474
|
+
...Object.keys((gate.reconcile || {}).soft || {})]) {
|
|
475
|
+
if (!mentionsKey(summaryZone, key)) {
|
|
476
|
+
err('R9', `\`gate.reconcile\` dùng \`summary.${key}\` — khối \`summary\` không có key này`,
|
|
477
|
+
`Thiếu mốc đối chiếu ⇒ gate không phát hiện được report cũ.`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// (c) soft map trỏ vào giá trị status có thật
|
|
482
|
+
const statusVals = (schema.vocabularies.trace_status || {}).values || [];
|
|
483
|
+
for (const [key, val] of Object.entries((gate.reconcile || {}).soft || {})) {
|
|
484
|
+
if (!statusVals.includes(val)) {
|
|
485
|
+
err('R9', `\`gate.reconcile.soft.${key}\` trỏ tới status "${val}" — không thuộc vocabulary \`trace_status\``,
|
|
486
|
+
`Giá trị cho phép: ${statusVals.join(' | ')}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// (f) per_sc (G48) — mọi field dùng làm khoá/so sánh phải CÓ THẬT trong khối
|
|
491
|
+
// `scenarios[]` mà validate-traces sinh ra. Neo vào field không tồn tại thì
|
|
492
|
+
// 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 ✅.
|
|
493
|
+
const perSc = (gate.reconcile || {}).per_sc;
|
|
494
|
+
if (perSc) {
|
|
495
|
+
const scZone = slice('"scenarios": [', '"issues"');
|
|
496
|
+
if (scZone === null) {
|
|
497
|
+
warn('R9', `Không định vị được khối \`scenarios[]\` trong ${schema.actors[vtName]} — R9(f) bỏ qua`,
|
|
498
|
+
'Mốc cắt đã đổi? Cập nhật `slice()`, đừng để R9 mù trong im lặng.');
|
|
499
|
+
} else {
|
|
500
|
+
const needed = [...(perSc.key_fields || []), perSc.compare, perSc.orphaned_flag]
|
|
501
|
+
.filter(Boolean);
|
|
502
|
+
for (const f of needed) {
|
|
503
|
+
if (!mentionsKey(scZone, f)) {
|
|
504
|
+
err('R9', `\`gate.reconcile.per_sc\` dùng field \`${f}\` mà khối \`scenarios[]\` KHÔNG có`,
|
|
505
|
+
`Gate sẽ im lặng rơi về so tổng số ⇒ mất chính thứ G48 vừa thêm,\n`
|
|
506
|
+
+ ` mà report vẫn hiện ✅. Thêm field vào Step 8, hoặc sửa schema.`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (perSc.orphaned_status && !statusVals.includes(perSc.orphaned_status)) {
|
|
510
|
+
err('R9', `\`per_sc.orphaned_status\` = "${perSc.orphaned_status}" không thuộc \`trace_status\``,
|
|
511
|
+
`Giá trị cho phép: ${statusVals.join(' | ')}`);
|
|
512
|
+
}
|
|
513
|
+
// Khoá KHÔNG có platform = quay lại đúng ca G48 (sc_id nhập nhằng giữa platform).
|
|
514
|
+
if (!(perSc.key_fields || []).includes('platform')) {
|
|
515
|
+
err('R9', '`per_sc.key_fields` thiếu `platform` — sc_id một mình không định danh được scenario',
|
|
516
|
+
`validate-traces Step 2: "sc_id trùng số giữa các platform là 2 scenario khác nhau".\n`
|
|
517
|
+
+ ` Khoá thiếu platform ⇒ web và app đè nhau khi đối chiếu (G48).`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// (g) config_preconditions (tầng G0) — rule được NÂNG thành chặn phải TỒN TẠI THẬT
|
|
524
|
+
// trong lint-trace.js. Neo vào một rule id không có ⇒ `warns.filter(...)` không
|
|
525
|
+
// 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).
|
|
526
|
+
const precond = (gate.config_preconditions || {}).promote_lint_rules || [];
|
|
527
|
+
if (precond.length) {
|
|
528
|
+
const lintRuleIds = new Set(
|
|
529
|
+
(fs.readFileSync(gateAbs.replace(/gate-trace\.js$/, 'lint-trace.js'), 'utf8')
|
|
530
|
+
.match(/\b(?:err|warn)\('(T\d+)'/g) || []).map(m => /T\d+/.exec(m)[0]));
|
|
531
|
+
for (const p of precond) {
|
|
532
|
+
if (!lintRuleIds.has(p.rule)) {
|
|
533
|
+
err('R9', `\`gate.config_preconditions\` nâng rule \`${p.rule}\` mà bin/lint-trace.js KHÔNG phát ra rule đó`,
|
|
534
|
+
`Rule id đổi tên? Gate filter theo id, không khớp ⇒ G0 LUÔN XANH ⇒ cổng mù.\n`
|
|
535
|
+
+ ` Rule lint hiện có: ${[...lintRuleIds].sort().join(' · ')}`);
|
|
536
|
+
}
|
|
537
|
+
if (!p.why) {
|
|
538
|
+
err('R9', `\`config_preconditions\` rule \`${p.rule}\` thiếu \`why\``,
|
|
539
|
+
`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.`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (!mentionsKey(gateText, 'config_preconditions')) {
|
|
543
|
+
err('R9', `schema khai \`gate.config_preconditions\` mà ${GATE_REL} KHÔNG đọc nó`,
|
|
544
|
+
'Khai trong schema rồi hard-code lại trong code = hai nguồn-sự-thật, và chúng sẽ lệch.');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// (d) gate-trace.js phải thực sự ĐỌC khối gate, không hard-code danh sách cờ
|
|
549
|
+
for (const key of ['blocking', 'reconcile', 'report_path']) {
|
|
550
|
+
if (!mentionsKey(gateText, key)) {
|
|
551
|
+
err('R9', `schema khai \`gate.${key}\` mà ${GATE_REL} KHÔNG đọc nó`,
|
|
552
|
+
`Khai trong schema rồi hard-code lại trong code = hai nguồn-sự-thật, và chúng sẽ lệch.`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// (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
|
|
557
|
+
const blockingFlags = new Set((gate.blocking || [])
|
|
558
|
+
.map(b => b.counter.replace(/_count$/, '').toUpperCase()));
|
|
559
|
+
for (const f of blockingFlags) {
|
|
560
|
+
if (/_DRIFT$|_STALE_REF$|_PENDING$|_STALE_VS_/.test(f)) {
|
|
561
|
+
err('R9', `\`${f}\` nằm trong \`gate.blocking\` nhưng nó là cờ NỢ (🟠), không phải code hỏng (🔴)`,
|
|
562
|
+
`README §Traceability: 🟠 = "code chưa theo kịp spec", cố ý KHÔNG chặn.\n`
|
|
563
|
+
+ ` Chặn PR vì nợ ⇒ cổng đỏ liên tục ⇒ người ta tắt cổng ⇒ mất cả 🔴.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── R10 — file CI phải theo kịp danh sách tag làm audit hết tươi ──────────────
|
|
570
|
+
//
|
|
571
|
+
// Vì sao (nợ #4 sau Wave 1): job `require-fresh-audit` grep 7 tag trong diff để biết
|
|
572
|
+
// "PR có đổi thứ report đang khẳng định không". Danh sách đó nằm HAI chỗ: schema (nguồn)
|
|
573
|
+
// 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ì
|
|
574
|
+
// ⇒ job LUÔN XANH ⇒ cổng mù trong im lặng. Đúng hình dạng false-green của R9(a)/R9(g),
|
|
575
|
+
// chỉ ở một file khác loại.
|
|
576
|
+
//
|
|
577
|
+
// 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
|
|
578
|
+
// 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.
|
|
579
|
+
{
|
|
580
|
+
const inv = (schema.gate || {}).audit_invalidating_tags;
|
|
581
|
+
if (inv) {
|
|
582
|
+
const rel = inv.ci_template;
|
|
583
|
+
if (!rel) {
|
|
584
|
+
err('R10', '`gate.audit_invalidating_tags` thiếu `ci_template` — không biết canh file nào');
|
|
585
|
+
} else {
|
|
586
|
+
const abs = path.join(ROOT, rel);
|
|
587
|
+
if (!fs.existsSync(abs)) {
|
|
588
|
+
err('R10', `\`audit_invalidating_tags.ci_template\` trỏ tới ${rel} — file không tồn tại`,
|
|
589
|
+
'Template đổi tên/bị xoá? Không có nó thì lỗ "sửa code mà không audit lại" hở lại.');
|
|
590
|
+
} else {
|
|
591
|
+
const yml = fs.readFileSync(abs, 'utf8');
|
|
592
|
+
const declared = (inv.tags || []).map(t => t.tag);
|
|
593
|
+
if (!declared.length) {
|
|
594
|
+
err('R10', '`audit_invalidating_tags.tags` rỗng — job sẽ không grep gì và LUÔN XANH');
|
|
595
|
+
}
|
|
596
|
+
// YAML viết tag trong một alternation regex (`implements|verifies|…`), nên so theo
|
|
597
|
+
// phần sau `@trace.` thay vì so nguyên chuỗi có dấu chấm.
|
|
598
|
+
for (const full of declared) {
|
|
599
|
+
const bare = full.replace(/^@trace\./, '');
|
|
600
|
+
if (!new RegExp(`\\b${bare}\\b`).test(yml)) {
|
|
601
|
+
err('R10', `${rel} KHÔNG nhắc tag \`${full}\``,
|
|
602
|
+
`Job grep theo danh sách này. Thiếu một tag ⇒ đổi tag đó không bị đòi audit lại,\n`
|
|
603
|
+
+ ` và không gì báo. Tag phải canh: ${declared.join(' · ')}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Ngược lại: cờ mà tag khai là nguồn phải có thật trong vocabulary audit_flags,
|
|
607
|
+
// else danh sách này đang neo vào một cờ không tồn tại.
|
|
608
|
+
const flagVals = new Set(((schema.vocabularies.audit_flags || {}).values) || []);
|
|
609
|
+
const statusVals2 = new Set(((schema.vocabularies.trace_status || {}).values) || []);
|
|
610
|
+
for (const t of inv.tags || []) {
|
|
611
|
+
for (const f of t.flags || []) {
|
|
612
|
+
if (!flagVals.has(f) && !statusVals2.has(f)) {
|
|
613
|
+
err('R10', `tag \`${t.tag}\` khai sinh ra cờ \`${f}\` — cờ này không có trong vocabulary`,
|
|
614
|
+
`audit_flags: ${[...flagVals].join(' · ')}\n trace_status: ${[...statusVals2].join(' · ')}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── R11 — mức chặn CHECKPOINT phải được KHAI, không để agent tự suy ──────────
|
|
624
|
+
//
|
|
625
|
+
// 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
|
|
626
|
+
// 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ỏ
|
|
627
|
+
// qua được một cổng đáng ra KHÔNG BAO GIỜ được bỏ.
|
|
628
|
+
//
|
|
629
|
+
// Đâ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à
|
|
630
|
+
// 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.
|
|
631
|
+
{
|
|
632
|
+
const CL = (schema.gate || {}).checkpoint_levels;
|
|
633
|
+
if (CL) {
|
|
634
|
+
const marker = CL.label_marker || 'Checkpoint:';
|
|
635
|
+
const check = (list, levelWord, label) => {
|
|
636
|
+
for (const e of list || []) {
|
|
637
|
+
const rel = `commands/${e.cmd}.tmpl`;
|
|
638
|
+
const abs = path.join(ROOT, rel);
|
|
639
|
+
if (!fs.existsSync(abs)) {
|
|
640
|
+
err('R11', `\`checkpoint_levels.${label}\` khai lệnh \`${e.cmd}\` — ${rel} không tồn tại`,
|
|
641
|
+
'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.');
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
645
|
+
if (!text.includes(marker)) {
|
|
646
|
+
err('R11', `${rel} thiếu dòng khai mức checkpoint (\`${marker}\`)`,
|
|
647
|
+
`Schema xếp nó vào \`${label}\`. Không khai trong file thì agent phải TỰ SUY từ\n`
|
|
648
|
+
+ ` bảng ở gate Bước 3a — và suy sai theo hướng nhẹ hơn thì --yes bỏ qua được\n`
|
|
649
|
+
+ ` một cổng đáng ra không bao giờ được bỏ.`);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (!new RegExp(levelWord).test(text)) {
|
|
653
|
+
err('R11', `${rel} khai mức checkpoint KHÁC với schema (schema: \`${label}\`)`,
|
|
654
|
+
`File phải nói "${levelWord.replace(/\\/g, '')}". Hai nguồn nói khác nhau thì\n`
|
|
655
|
+
+ ` agent theo cái nào là tuỳ lúc — đúng hình dạng mâu thuẫn G41 vừa vá.`);
|
|
656
|
+
}
|
|
657
|
+
if (!e.why) {
|
|
658
|
+
err('R11', `\`checkpoint_levels.${label}\` mục \`${e.cmd}\` thiếu \`why\``,
|
|
659
|
+
'Một mức chặn không nói được VÌ SAO sẽ bị người sau coi là tuỳ ý và gỡ đi.');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
check(CL.hard, 'chặn CỨNG', 'hard');
|
|
664
|
+
check(CL.none, 'không chặn', 'none');
|
|
665
|
+
|
|
666
|
+
// Một lệnh không được vừa `hard` vừa `none`.
|
|
667
|
+
const hardSet = new Set((CL.hard || []).map(e => e.cmd));
|
|
668
|
+
for (const e of CL.none || []) {
|
|
669
|
+
if (hardSet.has(e.cmd)) {
|
|
670
|
+
err('R11', `lệnh \`${e.cmd}\` khai ở CẢ \`hard\` LẪN \`none\``,
|
|
671
|
+
'Mâu thuẫn trực tiếp — chọn một.');
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Mức `normal` cố ý KHÔNG liệt kê (nó là mặc định). Nếu ai thêm khoá đó vào schema,
|
|
675
|
+
// báo ngay: một danh sách phải bảo trì mãi là chỗ nợ sẽ tích lại.
|
|
676
|
+
if (CL.normal) {
|
|
677
|
+
err('R11', '`checkpoint_levels.normal` không nên tồn tại',
|
|
678
|
+
'`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'
|
|
679
|
+
+ ' mới mà quên thêm vào đó là sai lần nữa. Chỉ khai `hard` và `none`.');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
205
684
|
// ── report ────────────────────────────────────────────────────────────────────
|
|
206
685
|
const tagCount = schema.tags.length;
|
|
207
686
|
const colCount = schema.tsv_columns.length;
|
|
687
|
+
const auxCount = (schema.aux_tables || []).length;
|
|
688
|
+
const queueCount = (schema.queues || []).length;
|
|
208
689
|
console.log('');
|
|
209
690
|
console.log('Self-check (bin/trace-schema.json) ...');
|
|
210
691
|
console.log('');
|
|
@@ -219,7 +700,19 @@ show(errors, '❌');
|
|
|
219
700
|
show(warns, '⚠️ ');
|
|
220
701
|
|
|
221
702
|
if (!errors.length && !warns.length) {
|
|
222
|
-
|
|
703
|
+
// Đếm bằng cách LOẠI key `$…` (metadata/comment), không phải `- 1`: schema có nhiều hơn
|
|
704
|
+
// một key `$` kể từ khi thêm `$lint_binding_comment`, nên hằng số cũ sẽ đếm lố.
|
|
705
|
+
const vocabCount = Object.keys(schema.vocabularies).filter(k => !k.startsWith('$')).length;
|
|
706
|
+
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`);
|
|
707
|
+
// ĐẾM rule từ chính lint-trace.js, không hard-code: một con số phát biểu ở hai chỗ rồi
|
|
708
|
+
// 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.
|
|
709
|
+
let lintRules = '?';
|
|
710
|
+
try {
|
|
711
|
+
const txt = fs.readFileSync(path.join(ROOT, 'bin/lint-trace.js'), 'utf8');
|
|
712
|
+
lintRules = new Set((txt.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
|
|
713
|
+
.map(m => /T\d+/.exec(m)[0])).size;
|
|
714
|
+
} catch {}
|
|
715
|
+
console.log(` + sổ trace thật được canh bởi bin/lint-trace.js (${lintRules} rule) — R8 giữ nó theo kịp schema`);
|
|
223
716
|
}
|
|
224
717
|
console.log('');
|
|
225
718
|
|