@educa-corp/sdd-framework 0.9.6 → 0.9.8
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/lint-trace.js +4 -4
- package/bin/qc-base-map.json +13 -11
- package/bin/self-check.js +497 -16
- package/bin/trace-schema.json +3226 -2656
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/commands/amend-prd.md +7 -1
- package/core/commands/debug.md +8 -2
- package/core/commands/define-product.md +38 -1
- package/core/commands/dev-gen-test.md +9 -3
- package/core/commands/dev-run-test.md +8 -2
- package/core/commands/dev-smoke-test.md +7 -1
- package/core/commands/extend-prd.md +7 -1
- package/core/commands/fix-bug.md +11 -5
- package/core/commands/generate-architecture.md +9 -1
- package/core/commands/generate-bdd.md +45 -5
- package/core/commands/generate-code.md +43 -4
- package/core/commands/generate-design-spec.md +7 -1
- package/core/commands/generate-prd.md +9 -1
- package/core/commands/generate-spec-manifest.md +7 -1
- package/core/commands/generate-tech-docs.md +41 -1
- package/core/commands/learn.md +7 -1
- package/core/commands/map-testids.md +11 -5
- package/core/commands/propose-scenario.md +7 -1
- package/core/commands/qc-analyze.md +12 -6
- package/core/commands/qc-automation-assess.md +356 -0
- package/core/commands/qc-design-script.md +430 -0
- package/core/commands/qc-design-test.md +98 -20
- package/core/commands/qc-plan.md +9 -3
- package/core/commands/qc-report.md +92 -77
- package/core/commands/qc-review-script.md +342 -0
- package/core/commands/{qc-review.md → qc-review-testcase.md} +86 -54
- package/core/commands/qc-run-manualtest.md +401 -0
- package/core/commands/qc-run-script.md +421 -0
- package/core/commands/refine-prd.md +7 -1
- package/core/commands/report-bug.md +9 -3
- package/core/commands/review-code.md +9 -3
- package/core/commands/review-context.md +11 -3
- package/core/commands/review-tech-docs.md +11 -3
- package/core/commands/setup-ai-first.md +7 -1
- package/core/commands/validate-traces.md +10 -4
- package/core/modules/qc-playwright-ts/module.yaml +13 -0
- package/core/modules/qc-playwright-ts/stack-profile.yaml +99 -0
- package/core/modules/qc-wdio-appium/module.yaml +20 -0
- package/core/modules/qc-wdio-appium/stack-profile.yaml +107 -0
- package/core/rules/workflow.md +2 -2
- package/core/skills/qc/_shared/self-review-principles.md +2 -2
- package/core/skills/qc/qa-analyst/DOC_GAP.template.md +1 -1
- package/core/skills/qc/qa-analyst/data-flow.md +1 -1
- package/core/skills/qc/qa-analyst/spec-issue-reporter.md +1 -1
- package/core/skills/qc/qa-automation-assess/matrix.md +123 -0
- package/core/skills/qc/qa-designer/e2e/journey.md +1 -1
- package/core/skills/qc/qa-designer/exploratory/explore-to-functional.md +1 -1
- package/core/skills/qc/{qa-runner → qa-designer}/exploratory/session.md +8 -2
- package/core/skills/qc/qa-designer/functional/api.md +2 -2
- package/core/skills/qc/qa-designer/functional/gui-feature.md +1 -1
- package/core/skills/qc/qa-designer/functional/gui-screen.md +1 -1
- package/core/skills/qc/qa-designer/functional/job.md +128 -0
- package/core/skills/qc/qa-designer/integration/api.md +2 -2
- package/core/skills/qc/qa-designer/integration/db.md +2 -2
- package/core/skills/qc/qa-designer/integration/gui.md +1 -1
- package/core/skills/qc/qa-designer/integration/{kafka.md → queue.md} +21 -5
- package/core/skills/qc/qa-designer/non-functional.md +1 -1
- package/core/skills/qc/qa-designer/shared/skill-decision-tree.md +17 -0
- package/core/skills/qc/qa-designer/shared/tc-metadata-format.md +28 -6
- package/core/skills/qc/qa-reviewer/script/_shared/review-rules.md +121 -0
- package/core/skills/qc/qa-reviewer/script/api/auth.md +49 -0
- package/core/skills/qc/qa-reviewer/script/api/endpoint.md +89 -0
- package/core/skills/qc/qa-reviewer/script/api/security.md +46 -0
- package/core/skills/qc/qa-reviewer/script/exploratory.md +3 -3
- package/core/skills/qc/qa-reviewer/script/mobile/e2e.md +41 -0
- package/core/skills/qc/qa-reviewer/script/mobile/functional.md +90 -0
- package/core/skills/qc/qa-reviewer/script/mobile/integration.md +41 -0
- package/core/skills/qc/qa-reviewer/script/mobile/non-functional.md +43 -0
- package/core/skills/qc/qa-reviewer/script/web/e2e.md +46 -0
- package/core/skills/qc/qa-reviewer/script/web/functional.md +111 -0
- package/core/skills/qc/qa-reviewer/script/web/integration.md +46 -0
- package/core/skills/qc/qa-reviewer/script/web/non-functional.md +49 -0
- package/core/skills/qc/qa-reviewer/shared/read-doc-gap-inputs.md +1 -1
- package/core/skills/qc/qa-reviewer/shared/review-file-template.md +29 -10
- package/core/skills/qc/qa-reviewer/test-case/e2e.md +2 -2
- package/core/skills/qc/qa-reviewer/test-case/exploratory.md +1 -1
- package/core/skills/qc/qa-reviewer/test-case/functional.md +2 -2
- package/core/skills/qc/qa-reviewer/test-case/integration.md +2 -2
- package/core/skills/qc/qa-reviewer/test-case/non-functional.md +2 -2
- package/core/skills/qc/qa-script-designer/_shared/api-conventions.md +94 -0
- package/core/skills/qc/qa-script-designer/_shared/file-naming-and-folders.md +109 -0
- package/core/skills/qc/qa-script-designer/_shared/mobile-conventions.md +196 -0
- package/core/skills/qc/qa-script-designer/_shared/web-conventions.md +257 -0
- package/core/skills/qc/qa-script-designer/api/auth.md +43 -0
- package/core/skills/qc/qa-script-designer/api/endpoint.md +61 -0
- package/core/skills/qc/qa-script-designer/api/security.md +41 -0
- package/core/skills/qc/qa-script-designer/mobile/e2e.md +35 -0
- package/core/skills/qc/qa-script-designer/mobile/functional/feature.md +32 -0
- package/core/skills/qc/qa-script-designer/mobile/functional/screen.md +42 -0
- package/core/skills/qc/qa-script-designer/mobile/integration.md +39 -0
- package/core/skills/qc/qa-script-designer/mobile/non-functional.md +39 -0
- package/core/skills/qc/qa-script-designer/web/e2e.md +36 -0
- package/core/skills/qc/qa-script-designer/web/functional/api.md +39 -0
- package/core/skills/qc/qa-script-designer/web/functional/gui-feature.md +34 -0
- package/core/skills/qc/qa-script-designer/web/functional/gui-screen.md +42 -0
- package/core/skills/qc/qa-script-designer/web/integration.md +43 -0
- package/core/skills/qc/qa-script-designer/web/non-functional.md +42 -0
- package/core/skills/qc/qa-script-runner/mobile/run.md +38 -0
- package/core/skills/qc/qa-script-runner/report.md +41 -0
- package/core/skills/qc/qa-script-runner/web/run.md +48 -0
- package/core/steps/context-loader.md +1 -1
- package/core/steps/gate.md +7 -1
- package/core/steps/qc-scope.md +45 -2
- package/core/steps/qc-stamp.md +4 -4
- package/core/steps/report-footer.md +10 -9
- package/docs/02-concepts/pipeline-steps/07-dev-selftest.md +1 -1
- package/docs/02-concepts/pipeline-steps/08-qc-automation.md +13 -12
- package/docs/02-concepts/pipeline-steps/10-feedback-loop.md +3 -3
- package/docs/02-concepts/traceability.md +1 -1
- package/docs/03-guides/developer.md +1 -1
- package/docs/03-guides/tester-qa.md +40 -11
- package/docs/04-reference/commands.md +4 -2
- package/docs/04-reference/modules.md +2 -1
- package/docs/04-reference/trace-schema.md +4 -4
- package/docs/explain/17-qc-design-test.md +5 -5
- package/docs/explain/18-qc-review.md +42 -20
- package/docs/explain/19-qc-run-test.md +13 -10
- package/docs/explain/20-qc-report.md +3 -3
- package/docs/explain/23-fix-bug.md +2 -2
- package/docs/explain/README.md +2 -2
- package/docs/plans/qc-surgery/01-checklist.md +86 -21
- package/docs/plans/qc-surgery/PLAN_v2.md +295 -0
- package/docs/plans/qc-surgery/exec-S-ap-stack-typescript.md +420 -0
- package/docs/plans/qc-surgery/exec-S0-guard-cam-stack-cu.md +400 -0
- package/docs/plans/qc-surgery/exec-S1-hai-module-thay-qc-playwright.md +267 -0
- package/docs/plans/qc-surgery/exec-S2-qa-runner-thanh-script-designer-runner.md +340 -0
- package/docs/plans/qc-surgery/exec-S3-viet-lai-tieu-chi-review-script.md +322 -0
- package/docs/plans/qc-surgery/exec-S5-an-theo-don-dau-vet-stack-cu.md +292 -0
- package/package.json +1 -1
- package/core/commands/qc-run-test.md +0 -561
- package/core/modules/qc-playwright/stack-profile.yaml +0 -66
- package/core/skills/qc/qa-reviewer/script/e2e.md +0 -95
- package/core/skills/qc/qa-reviewer/script/functional.md +0 -109
- package/core/skills/qc/qa-reviewer/script/integration.md +0 -99
- package/core/skills/qc/qa-reviewer/script/non-functional.md +0 -134
- package/core/skills/qc/qa-runner/e2e.md +0 -49
- package/core/skills/qc/qa-runner/functional/api.md +0 -35
- package/core/skills/qc/qa-runner/functional/gui-feature.md +0 -57
- package/core/skills/qc/qa-runner/functional/gui-screen.md +0 -61
- package/core/skills/qc/qa-runner/integration.md +0 -47
- package/core/skills/qc/qa-runner/non-functional.md +0 -49
- package/core/skills/qc/qa-runner/report/report.md +0 -37
package/bin/self-check.js
CHANGED
|
@@ -103,8 +103,26 @@ function allSourceFiles() {
|
|
|
103
103
|
push(path.join(ROOT, 'templates'), () => true);
|
|
104
104
|
push(path.join(ROOT, 'rules'), f => f.endsWith('.md'));
|
|
105
105
|
push(path.join(ROOT, 'modules'), f => f.endsWith('.yaml') || f.endsWith('.md'));
|
|
106
|
+
// skills/ vào scan set từ Bước S · S0 (2026-09-17). Trước đó R4/R5/R6 mù với toàn bộ
|
|
107
|
+
// skills/ — và 21 trong 27 file mang stack QC cũ nằm đúng ở đó, nên R5 không canh được
|
|
108
|
+
// việc một stack đã bỏ quay trở lại. Đo trước khi mở: 11 pattern đang có × 64 file
|
|
109
|
+
// skills/**/*.md = 0 hit, tức mở scan set không làm đỏ thứ gì đang xanh.
|
|
110
|
+
push(path.join(ROOT, 'skills'), f => f.endsWith('.md'));
|
|
106
111
|
return out;
|
|
107
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Glob tối giản cho `forbidden_patterns[].scope`. Hỗ trợ `*` (không qua `/`) và `**`
|
|
116
|
+
* (qua mọi cấp) — đủ cho ba dạng đang dùng và cố ý KHÔNG hơn: một glob engine đầy đủ ở
|
|
117
|
+
* đây là thứ không ai đọc lại, và scope sai thì rule im lặng chứ không kêu lên.
|
|
118
|
+
*/
|
|
119
|
+
function globMatch(glob, rel) {
|
|
120
|
+
const re = glob.split('/').map(seg =>
|
|
121
|
+
seg === '**' ? '.*'
|
|
122
|
+
: seg.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
|
|
123
|
+
).join('/').replace(/\/\.\*$/, '(?:/.*)?');
|
|
124
|
+
return new RegExp(`^${re}$`).test(rel);
|
|
125
|
+
}
|
|
108
126
|
const sources = allSourceFiles().map(p => ({
|
|
109
127
|
rel: path.relative(ROOT, p).replace(/\\/g, '/'),
|
|
110
128
|
text: fs.readFileSync(p, 'utf8'),
|
|
@@ -281,16 +299,43 @@ for (const fp of schema.forbidden_patterns) {
|
|
|
281
299
|
// `except` = file được phép chứa pattern vì nó đang TRÍCH DẪN pattern trong một cảnh
|
|
282
300
|
// báo ("đừng glob X"). Mỗi entry phải kèm lý do trong schema — không phải ignore mù.
|
|
283
301
|
const except = new Set(Object.keys(fp.except || {}));
|
|
284
|
-
|
|
302
|
+
// `scope` (thêm ở Bước S · S0) = mảng glob giới hạn vùng file rule soi tới. KHÔNG khai
|
|
303
|
+
// scope = toàn scan set, nên mọi entry có từ trước giữ nguyên hành vi. Vì sao cần: `pytest`
|
|
304
|
+
// bị cấm trong phạm vi QC nhưng HỢP LỆ ở lane DEV — modules/context-engineering là module
|
|
305
|
+
// Python thật, dev-gen-test mặc định sinh test Python. Cấm toàn cục là 4 dương tính giả,
|
|
306
|
+
// và dương tính giả đẻ ra except bừa cho tới khi R5 rỗng ruột mà vẫn xanh.
|
|
307
|
+
const inScope = s => !fp.scope || fp.scope.some(g => globMatch(g, s.rel));
|
|
308
|
+
const hits = sources.filter(s => inScope(s) && s.text.includes(fp.pattern) && !except.has(s.rel));
|
|
285
309
|
if (hits.length) {
|
|
286
310
|
err('R5', `Pattern bị cấm quay lại: \`${fp.pattern}\` (${fp.gap})`,
|
|
287
311
|
`${fp.reason}\n Thấy ở: ${hits.map(h => h.rel).join(', ')}`);
|
|
288
312
|
}
|
|
289
|
-
// Cảnh báo nếu một except trở nên vô nghĩa — cảnh báo đã bị xoá thì except cũng nên
|
|
313
|
+
// Cảnh báo nếu một except trở nên vô nghĩa — cảnh báo đã bị xoá thì except cũng nên bỏ.
|
|
314
|
+
// Nằm ngoài `scope` cũng là vô nghĩa: file đó không bao giờ bị soi, giữ except chỉ làm
|
|
315
|
+
// người đọc tưởng nó đang che một thứ có thật.
|
|
290
316
|
for (const f of except) {
|
|
291
317
|
const src = sources.find(s => s.rel === f);
|
|
292
|
-
if (!src || !src.text.includes(fp.pattern)) {
|
|
293
|
-
warn('R5', `\`${fp.pattern}\`: except \`${f}\` không còn chứa pattern — bỏ except này khỏi schema`);
|
|
318
|
+
if (!src || !src.text.includes(fp.pattern) || !inScope(src)) {
|
|
319
|
+
warn('R5', `\`${fp.pattern}\`: except \`${f}\` không còn chứa pattern (hoặc ngoài scope) — bỏ except này khỏi schema`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Một `scope` glob khớp 0 file thì rule **trông như** đang canh một vùng, thực ra canh không
|
|
325
|
+
// khí — đúng kiểu rỗng ruột mà cảnh báo `except` chết ở trên sinh ra để chống, chỉ ở đầu kia.
|
|
326
|
+
// Đã suýt xảy ra thật ở S0: scope trỏ `qa-runner/**` (thư mục S2 sắp XOÁ) và không trỏ
|
|
327
|
+
// `qa-script-designer/**` (thư mục S2 sắp TẠO) → sau S2 đèn xanh vì RỖNG, không vì SẠCH.
|
|
328
|
+
// Chỉ WARN, không ERROR: trỏ vào thư mục chưa tạo là hợp lệ trong lúc đang chuyển stack.
|
|
329
|
+
// Gộp theo glob, không theo entry — 6 entry dùng chung một scope thì cảnh báo một lần.
|
|
330
|
+
{
|
|
331
|
+
const seen = new Set();
|
|
332
|
+
for (const fp of schema.forbidden_patterns) {
|
|
333
|
+
for (const g of (fp.scope || [])) {
|
|
334
|
+
if (seen.has(g)) continue;
|
|
335
|
+
seen.add(g);
|
|
336
|
+
if (!sources.some(s => globMatch(g, s.rel))) {
|
|
337
|
+
warn('R5', `scope \`${g}\` khớp 0 file — thư mục chưa tồn tại, hoặc đã bị xoá mà glob còn ở lại`);
|
|
338
|
+
}
|
|
294
339
|
}
|
|
295
340
|
}
|
|
296
341
|
}
|
|
@@ -1096,7 +1141,7 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1096
1141
|
// ── R14 — quyền khẳng định `pass`: chủ cột phải ĐỌC status, và T12 phải tồn tại ─
|
|
1097
1142
|
//
|
|
1098
1143
|
// Vì sao cần rule riêng (G55): schema khai cột 21 `status` có `read_by` gồm dev-run-test và
|
|
1099
|
-
// qc-run-
|
|
1144
|
+
// qc-run-script — và R3 xác nhận ✅ vì cả hai file CÓ nhắc `status`… trong đúng câu khai rằng
|
|
1100
1145
|
// chúng "trực giao" với nó, tức cố ý KHÔNG dùng. Số lần đọc thật: 0.
|
|
1101
1146
|
//
|
|
1102
1147
|
// Đâ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
|
|
@@ -1118,10 +1163,15 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1118
1163
|
}
|
|
1119
1164
|
|
|
1120
1165
|
for (const g of PAG.guards || []) {
|
|
1121
|
-
|
|
1166
|
+
// `owner` là CHUỖI khi cột có một chủ, MẢNG khi có nhiều (F2 · Đợt 2 · b2: cột
|
|
1167
|
+
// `qc_status` có HAI chủ — /qc-run-script và /qc-run-manualtest). Chuẩn hoá về mảng
|
|
1168
|
+
// thay vì khai hai entry guard cho cùng một cột: hai entry làm `blocked_when_status`
|
|
1169
|
+
// và `downgrade_to` bị chép đôi, và hai nơi nói một chuyện thì một nơi sẽ lệch (R19).
|
|
1170
|
+
for (const owner of (Array.isArray(g.owner) ? g.owner : [g.owner])) {
|
|
1171
|
+
const rel = `commands/${owner}.tmpl`;
|
|
1122
1172
|
const abs = path.join(ROOT, rel);
|
|
1123
1173
|
if (!fs.existsSync(abs)) {
|
|
1124
|
-
err('R14', `\`positive_assertion_guards\` khai chủ cột \`${g.column}\` là \`${
|
|
1174
|
+
err('R14', `\`positive_assertion_guards\` khai chủ cột \`${g.column}\` là \`${owner}\` — ${rel} không tồn tại`);
|
|
1125
1175
|
continue;
|
|
1126
1176
|
}
|
|
1127
1177
|
const text = fs.readFileSync(abs, 'utf8');
|
|
@@ -1143,6 +1193,7 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1143
1193
|
'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'
|
|
1144
1194
|
+ ' giữ nguyên giá trị cũ — tức guard không có tác dụng.');
|
|
1145
1195
|
}
|
|
1196
|
+
}
|
|
1146
1197
|
// (c) `fail` KHÔNG được nằm trong positive_values. Chặn tin xấu là biến một guard
|
|
1147
1198
|
// 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.
|
|
1148
1199
|
if ((g.positive_values || []).includes('fail')) {
|
|
@@ -1187,7 +1238,7 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1187
1238
|
//
|
|
1188
1239
|
// R3 hỏi "file có nhắc tên field không". Đó là câu SAI cho những field mà trả lời sai
|
|
1189
1240
|
// gây hại im lặng. Ca thật: schema khai cột `status` có read_by gồm dev-run-test và
|
|
1190
|
-
// qc-run-
|
|
1241
|
+
// qc-run-script; R3 xác nhận ✅ vì cả hai file CÓ nhắc `status` — trong đúng câu khai rằng
|
|
1191
1242
|
// chúng "trực giao" với nó, tức CỐ Ý KHÔNG dùng. Số lần đọc thật: 0. Đó là G55, và R3
|
|
1192
1243
|
// xác nhận contract bằng đúng câu văn đang phủ nhận contract.
|
|
1193
1244
|
//
|
|
@@ -1493,7 +1544,7 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1493
1544
|
}
|
|
1494
1545
|
}
|
|
1495
1546
|
|
|
1496
|
-
// ── R18 — lệnh ghi artifact phải KHAI, và
|
|
1547
|
+
// ── R18 — lệnh ghi artifact phải KHAI, và cái ĐÈ MẤT KHÔNG DỰNG LẠI ĐƯỢC thì phải `hard` ──
|
|
1497
1548
|
//
|
|
1498
1549
|
// Vì sao: `gate.checkpoint_levels` là ALLOWLIST với mặc định `normal` (mức LỎNG NHẤT), và
|
|
1499
1550
|
// R11 duyệt TỪ SCHEMA RA FILE — nó chỉ kiểm lệnh ĐÃ liệt kê. Lệnh chưa khai thì không vòng
|
|
@@ -1501,26 +1552,107 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1501
1552
|
//
|
|
1502
1553
|
// R18 duyệt CHIỀU NGƯỢC LẠI. Đó là toàn bộ giá trị của nó: một checker duyệt allowlist chỉ
|
|
1503
1554
|
// thấy được cái ĐÃ KHAI, mà cái CHƯA KHAI mới là chỗ lỗi sống.
|
|
1555
|
+
//
|
|
1556
|
+
// HAI LUẬT ÉP `hard`, KHÔNG PHẢI MỘT:
|
|
1557
|
+
// (A) `has_human_content: true` — cấp LỆNH
|
|
1558
|
+
// (B) artifact `mode !== 'append'` + `regenerable: false` + `when: 'always'` — cấp ARTIFACT
|
|
1559
|
+
//
|
|
1560
|
+
// Luật (B) sinh ra vì (A) không phủ được SỔ TÍCH LUỸ: `trace-history.jsonl` KHÔNG ai gõ tay
|
|
1561
|
+
// vào — nên (A) trả lời "false", ĐÚNG CHỮ — trong khi chính /validate-traces gọi nó là
|
|
1562
|
+
// AUTHORITATIVE, không regenerate được. Đè lên là mất vĩnh viễn.
|
|
1563
|
+
//
|
|
1564
|
+
// VÌ SAO `mode !== 'append'` CHỨ KHÔNG PHẢI `mode === 'overwrite'`:
|
|
1565
|
+
// `merge` KHÔNG an toàn. /amend-prd là merge, và nó tự khai là "thao tác ghi DUY NHẤT trong
|
|
1566
|
+
// framework được phép làm output KHÔNG phải superset của bản cũ". Chỉ `append` an toàn vô
|
|
1567
|
+
// điều kiện. Gộp `merge` vào diện miễn là mở lại đúng cánh cửa vừa đóng.
|
|
1568
|
+
//
|
|
1569
|
+
// VÌ SAO (B) THÊM ĐIỀU KIỆN `when === 'always'` (D4):
|
|
1570
|
+
// Hành vi ghi thuộc về CHẾ ĐỘ, không thuộc về lệnh. NĂM lệnh tự khai "read-only" và cả năm
|
|
1571
|
+
// đều có ghi (/review-context · /review-tech-docs · /validate-traces · /review-code · /debug).
|
|
1572
|
+
// Ghi có điều kiện thì người dùng đã phải gõ THÊM một cờ hoặc trả lời Y — bản thân việc đó
|
|
1573
|
+
// đã là một cổng. Ép `hard` cho cả lệnh vì một nhánh hiếm là phạt nhầm chế độ đọc:
|
|
1574
|
+
// /validate-traces chạy hàng ngày ở chế độ đọc, và dựng cổng chặn lên nó vì nhánh
|
|
1575
|
+
// `--reconcile-code` sẽ dạy người ta thêm `--yes` vào script — rồi `--yes` đó nằm lại vĩnh
|
|
1576
|
+
// viễn, kể cả cho ngày họ chạy `--reconcile-code`.
|
|
1577
|
+
//
|
|
1578
|
+
// ĐỔI LẠI, ĐIỀU KIỆN PHẢI KIỂM CHỨNG ĐƯỢC — nếu không thì `when` thành cửa thoát cho mọi
|
|
1579
|
+
// lệnh lười khai. `when` là tên cờ ⇒ tmpl PHẢI nhắc đúng cờ đó; `when: 'confirm'` ⇒ tmpl
|
|
1580
|
+
// PHẢI có câu hỏi (Y/N). Khai một điều kiện không tồn tại thì luật trông như đã được áp
|
|
1581
|
+
// trong khi không ai áp — đúng bài học R17.
|
|
1504
1582
|
{
|
|
1505
1583
|
const AW = schema.artifact_writers;
|
|
1506
1584
|
const CL = (schema.gate || {}).checkpoint_levels;
|
|
1507
1585
|
if (AW && CL) {
|
|
1508
|
-
const hardSet = new Set((CL.hard || []).map(e => e.cmd));
|
|
1586
|
+
const hardSet = new Set((CL.hard || []).filter(e => e.cmd).map(e => e.cmd));
|
|
1509
1587
|
const enrolled = AW.enrolled || {};
|
|
1510
1588
|
const pending = (AW.pending_enrollment || {}).cmds || [];
|
|
1589
|
+
// Từ vựng đọc TỪ SCHEMA, không hard-code — thêm giá trị mới mà quên dạy checker là đúng
|
|
1590
|
+
// kiểu lỗi R18 sinh ra để bắt.
|
|
1591
|
+
const MODES = Object.keys(AW.write_modes || {}).filter(k => k !== '$comment');
|
|
1592
|
+
// Mode được miễn luật (B) — ĐỌC TỪ SCHEMA, không hard-code (xem artifact_writers.safe_modes).
|
|
1593
|
+
const SAFE = new Set(((AW.safe_modes || {}).modes) || ['append']);
|
|
1594
|
+
const ALWAYS = 'always';
|
|
1595
|
+
const CONFIRM = 'confirm';
|
|
1596
|
+
const isFlag = w => /^--[a-z0-9][a-z0-9-]*$/.test(w);
|
|
1597
|
+
const validWhen = w => w === ALWAYS || w === CONFIRM || isFlag(w);
|
|
1511
1598
|
|
|
1512
1599
|
for (const cmd of Object.keys(enrolled)) {
|
|
1513
1600
|
const e = enrolled[cmd];
|
|
1514
1601
|
const rel = `commands/${cmd}.tmpl`;
|
|
1602
|
+
const abs = path.join(ROOT, rel);
|
|
1515
1603
|
|
|
1516
|
-
if (!fs.existsSync(
|
|
1604
|
+
if (!fs.existsSync(abs)) {
|
|
1517
1605
|
err('R18', `\`artifact_writers.enrolled\` khai \`${cmd}\` — ${rel} không tồn tại`,
|
|
1518
1606
|
'Lệnh bị đổi tên/bỏ? Khai một lệnh không có nghĩa là registry đang phủ nó.');
|
|
1519
1607
|
continue;
|
|
1520
1608
|
}
|
|
1609
|
+
const tmpl = fs.readFileSync(abs, 'utf8');
|
|
1610
|
+
|
|
1521
1611
|
if (!Array.isArray(e.writes) || !e.writes.length) {
|
|
1522
1612
|
err('R18', `\`artifact_writers.enrolled.${cmd}\` thiếu \`writes\``,
|
|
1523
1613
|
'Không liệt kê ghi cái gì thì không ai kiểm được "cái đó có người nhập tay không".');
|
|
1614
|
+
} else {
|
|
1615
|
+
e.writes.forEach((w, i) => {
|
|
1616
|
+
const at = `artifact_writers.enrolled.${cmd}.writes[${i}]`;
|
|
1617
|
+
if (typeof w !== 'object' || w === null || Array.isArray(w)) {
|
|
1618
|
+
err('R18', `\`${at}\` phải là object, không phải chuỗi`,
|
|
1619
|
+
'Khuôn cũ (list chuỗi) không chứa được \`mode\`/\`regenerable\`/\`when\`. Một lệnh đụng\n'
|
|
1620
|
+
+ ' nhiều artifact khác tính chất thì cờ phẳng ở cấp lệnh luôn nói dối một nửa.');
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (!w.path) {
|
|
1624
|
+
err('R18', `\`${at}\` thiếu \`path\``,
|
|
1625
|
+
'Không có tên thì không đối chiếu ngược được với tmpl.');
|
|
1626
|
+
}
|
|
1627
|
+
if (!MODES.includes(w.mode)) {
|
|
1628
|
+
err('R18', `\`${at}\` có \`mode\` không hợp lệ: ${JSON.stringify(w.mode)}`,
|
|
1629
|
+
`Phải là một trong: ${MODES.join(' · ')} — định nghĩa ở \`artifact_writers.write_modes\`.`);
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof w.regenerable !== 'boolean') {
|
|
1632
|
+
err('R18', `\`${at}\` thiếu \`regenerable\` (boolean)`,
|
|
1633
|
+
'Câu hỏi mà \`has_human_content\` KHÔNG hỏi được: "đè mất thì dựng lại được không?".\n'
|
|
1634
|
+
+ ' Sổ tích luỹ không ai gõ tay vào, nên \`has_human_content\` trả lời false — đúng\n'
|
|
1635
|
+
+ ' chữ, và để lọt đúng thứ mà mất là mất vĩnh viễn.');
|
|
1636
|
+
}
|
|
1637
|
+
if (!validWhen(w.when)) {
|
|
1638
|
+
err('R18', `\`${at}\` thiếu/sai \`when\`: ${JSON.stringify(w.when)}`,
|
|
1639
|
+
'Phải là \`always\`, \`confirm\`, hoặc một tên cờ dạng \`--ten-co\`. Hành vi ghi thuộc\n'
|
|
1640
|
+
+ ' về CHẾ ĐỘ, không thuộc về lệnh — năm lệnh đã khai "read-only" trong khi cả năm\n'
|
|
1641
|
+
+ ' đều có ghi, chính vì nhãn đặt theo chế độ mặc định rồi không ai sửa.');
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
// Điều kiện phải KIỂM CHỨNG ĐƯỢC, nếu không `when` thành cửa thoát.
|
|
1645
|
+
if (isFlag(w.when) && !tmpl.includes(w.when)) {
|
|
1646
|
+
err('R18', `\`${at}\` khai \`when: "${w.when}"\` nhưng ${rel} KHÔNG nhắc cờ đó`,
|
|
1647
|
+
'Khai một điều kiện không tồn tại thì luật trông như đã được áp trong khi không ai\n'
|
|
1648
|
+
+ ' áp — đúng bài học R17. Và nó miễn luôn artifact này khỏi luật (B).');
|
|
1649
|
+
}
|
|
1650
|
+
if (w.when === CONFIRM && !/\(Y\/N\)/.test(tmpl)) {
|
|
1651
|
+
err('R18', `\`${at}\` khai \`when: "confirm"\` nhưng ${rel} không có câu hỏi \`(Y/N)\``,
|
|
1652
|
+
'`confirm` nghĩa là NGƯỜI đã bấm Y — đó là cái thay cho CHECKPOINT. Không tìm thấy\n'
|
|
1653
|
+
+ ' câu hỏi nào thì thao tác ghi đang chạy thẳng, mà vẫn được miễn luật (B).');
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1524
1656
|
}
|
|
1525
1657
|
if (typeof e.has_human_content !== 'boolean') {
|
|
1526
1658
|
err('R18', `\`artifact_writers.enrolled.${cmd}\` thiếu \`has_human_content\` (boolean)`,
|
|
@@ -1533,17 +1665,70 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1533
1665
|
'Bắt buộc CẢ KHI `has_human_content: false`. Ghi đè an toàn mà không ai khai là an\n'
|
|
1534
1666
|
+ ' toàn thì một ngoại lệ CÓ CHỦ Ý trông y hệt một chỗ sót — và sẽ bị copy đi (G79).');
|
|
1535
1667
|
}
|
|
1668
|
+
|
|
1669
|
+
// ── Luật (B) — cấp ARTIFACT, chỉ cho thao tác ghi VÔ ĐIỀU KIỆN ──
|
|
1670
|
+
const doomed = (Array.isArray(e.writes) ? e.writes : [])
|
|
1671
|
+
.filter(w => w && typeof w === 'object'
|
|
1672
|
+
&& !SAFE.has(w.mode) && w.regenerable === false && w.when === ALWAYS);
|
|
1673
|
+
if (doomed.length && !hardSet.has(cmd)) {
|
|
1674
|
+
const w = doomed[0];
|
|
1675
|
+
err('R18', `\`${cmd}\` ghi \`${w.mode}\` lên artifact KHÔNG dựng lại được ở MỌI lần chạy, nhưng KHÔNG ở \`checkpoint_levels.hard\``,
|
|
1676
|
+
`Artifact: ${w.path}\n`
|
|
1677
|
+
+ ` Khai: "${String(w.note || e.why || '').slice(0, 110)}…"\n`
|
|
1678
|
+
+ ' Không ai gõ tay vào nó thì `has_human_content` vẫn là false — đúng chữ. Nhưng\n'
|
|
1679
|
+
+ ` \`${w.mode}\` lên thứ không dựng lại được là mất VĨNH VIỄN, và \`--yes\` ở mức\n`
|
|
1680
|
+
+ ' `normal` làm việc đó TRONG IM LẶNG.\n'
|
|
1681
|
+
+ ` Sửa: thêm \`${cmd}\` vào gate.checkpoint_levels.hard · HOẶC \`mode: "append"\` nếu\n`
|
|
1682
|
+
+ ' thao tác thật sự chỉ thêm bản ghi mới · HOẶC `when` là cờ/confirm nếu nó CÓ điều kiện.');
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// ── Luật (A) — cấp LỆNH ──
|
|
1536
1686
|
if (e.has_human_content === true && !hardSet.has(cmd)) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1687
|
+
// `has_human_content` là cờ CẤP LỆNH. Khi một lệnh ghi nhiều artifact mà chỉ MỘT trong
|
|
1688
|
+
// số đó có phần người nhập tay, cờ cấp lệnh không nói được điều đó — và luật (A) sẽ ép
|
|
1689
|
+
// `hard` vì một tiền đề SAI. Gặp thật ở /fix-bug (N3): 4 artifact, chỉ `{lessons_path}`
|
|
1690
|
+
// có phần người nhập tay, và nó nằm sau `(Y/N)`.
|
|
1691
|
+
// Nên artifact ĐƯỢC PHÉP tự khai `human_content: true`; khi có khai, luật (A) chỉ xét
|
|
1692
|
+
// đúng những artifact đó. Không khai thì giữ hành vi cũ (xét mọi artifact) — an toàn hơn.
|
|
1693
|
+
// Miễn cho mode AN TOÀN, y như luật (B). Gặp thật ở /qc-run-manualtest (Đợt 2 · b2):
|
|
1694
|
+
// MANUAL_EVIDENCE.md CÓ phần người nhập tay và ghi ở MỌI lần chạy — nhưng `append`,
|
|
1695
|
+
// nên không lần ghi nào làm mất dòng nào. Ép `hard` ở đó là chặn một thao tác không
|
|
1696
|
+
// mất gì, tức đúng loại ma sát vô ích mà `unconditional_hard_budget` sinh ra để chống.
|
|
1697
|
+
const flagged = (e.writes || []).filter(w => w && w.human_content === true);
|
|
1698
|
+
const scope = (flagged.length ? flagged : (e.writes || [])).filter(w => w && !SAFE.has(w.mode));
|
|
1699
|
+
const conditional = scope.every(w => w && w.when && w.when !== ALWAYS);
|
|
1700
|
+
if (!conditional) {
|
|
1701
|
+
err('R18', `\`${cmd}\` ghi artifact CÓ phần người nhập tay nhưng KHÔNG ở \`checkpoint_levels.hard\``,
|
|
1702
|
+
`Khai: "${String(e.why || '').slice(0, 110)}…"\n`
|
|
1703
|
+
+ ` Ghi đè thứ người nhập tay mà chỉ ở mức \`normal\` thì \`--yes\` xoá nó TRONG IM LẶNG.\n`
|
|
1704
|
+
+ ` Sửa: thêm \`${cmd}\` vào gate.checkpoint_levels.hard + viết §luật chạy lại trong lệnh.`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// `non_writers` — lệnh đã rà và kết luận KHÔNG ghi gì. Một lệnh vắng mặt ở cả ba danh sách
|
|
1710
|
+
// trông y hệt một lệnh bị quên; ghi ra đây biến "không có gì" thành kết luận kiểm chứng được.
|
|
1711
|
+
const NW = AW.non_writers || {};
|
|
1712
|
+
for (const c of NW.cmds || []) {
|
|
1713
|
+
if (!fs.existsSync(path.join(ROOT, `commands/${c}.tmpl`))) {
|
|
1714
|
+
err('R18', `\`non_writers\` khai \`${c}\` — commands/${c}.tmpl không tồn tại`,
|
|
1715
|
+
'Khai một lệnh không có thật làm danh sách trông đã phủ rộng hơn thực tế.');
|
|
1716
|
+
}
|
|
1717
|
+
if (enrolled[c]) {
|
|
1718
|
+
err('R18', `\`${c}\` ở CẢ \`non_writers\` LẪN \`enrolled\``,
|
|
1719
|
+
'Mâu thuẫn trực tiếp: vừa khai không ghi gì, vừa liệt kê nó ghi cái gì. Chọn một.');
|
|
1720
|
+
}
|
|
1721
|
+
if (!(NW.why || {})[c]) {
|
|
1722
|
+
err('R18', `\`non_writers\` khai \`${c}\` nhưng thiếu lý do trong \`why\``,
|
|
1723
|
+
'"Không ghi gì" phải là một KẾT LUẬN kiểm chứng được, không phải một khoảng trống —\n'
|
|
1724
|
+
+ ' cùng lập luận đã bắt `why` phải khai cả khi `has_human_content: false` (G79).');
|
|
1541
1725
|
}
|
|
1542
1726
|
}
|
|
1543
1727
|
|
|
1544
1728
|
// Chiều ngược: lệnh đã ở `hard` VÌ LÝ DO GHI ĐÈ phải có mặt trong registry.
|
|
1545
1729
|
for (const h of CL.hard || []) {
|
|
1546
|
-
if (
|
|
1730
|
+
if (!h.cmd) continue; // mục $comment
|
|
1731
|
+
if (!/ghi đè/i.test(String(h.why || ''))) continue; // `hard` vì lý do khác (vd --resume)
|
|
1547
1732
|
if (!enrolled[h.cmd] && !pending.includes(h.cmd)) {
|
|
1548
1733
|
err('R18', `\`${h.cmd}\` ở \`checkpoint_levels.hard\` vì ghi đè, nhưng không có trong \`artifact_writers\``,
|
|
1549
1734
|
'Hai nơi cùng nói về một chuyện mà chỉ một nơi biết — nơi kia sẽ lệch.\n'
|
|
@@ -1553,6 +1738,295 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
1553
1738
|
}
|
|
1554
1739
|
}
|
|
1555
1740
|
|
|
1741
|
+
// ── R19 — bảng mức gate CHÉP TAY trong steps/ và rules/ phải khớp schema ─────
|
|
1742
|
+
//
|
|
1743
|
+
// Vì sao: cùng một danh sách "lệnh nào chặn, lệnh nào không" tồn tại ở BA nơi, xếp thành
|
|
1744
|
+
// một chuỗi "nguồn" mà KHÔNG AI THỰC THI:
|
|
1745
|
+
// rules/workflow.md — tự nhận "đây chỉ là bản tóm tắt, gate là nguồn"
|
|
1746
|
+
// steps/gate.md — tự nhận "Nguồn máy đọc: bin/trace-schema.json"
|
|
1747
|
+
// bin/trace-schema.json — nguồn thật
|
|
1748
|
+
// Mỗi bản trỏ sang bản sau nói "kia mới là nguồn", và cả hai vẫn CHÉP CỨNG tên lệnh. Một
|
|
1749
|
+
// con trỏ tới nguồn KHÔNG ngăn được bản sao lệch — nó chỉ làm người sửa yên tâm rằng có ai
|
|
1750
|
+
// đó đang canh.
|
|
1751
|
+
//
|
|
1752
|
+
// Đã lệch thật (2026-09-16): /review-context và /review-tech-docs chuyển `none` → `hard`,
|
|
1753
|
+
// schema + commands/*.tmpl + README đều sửa, build XANH — mà hai bảng kia vẫn liệt chúng ở
|
|
1754
|
+
// "Không chặn". R11 canh `commands/*.tmpl` ↔ schema nên người sửa tin là "đã có máy canh
|
|
1755
|
+
// mức gate"; phạm vi nó canh hẹp hơn thế. BIẾT CÓ MÁY CANH KHÔNG BẰNG BIẾT MÁY CANH ĐẾN ĐÂU.
|
|
1756
|
+
//
|
|
1757
|
+
// QUÉT THEO DÒNG, KHÔNG THEO FILE. Mục đích là GIỮ được ví dụ cụ thể (thứ làm bảng dễ đọc)
|
|
1758
|
+
// mà vẫn buộc ví dụ phải ĐÚNG. Cấm hẳn việc nêu tên sẽ làm bảng khó hiểu hơn, và rồi người
|
|
1759
|
+
// ta chép lại ở chỗ khác — xa hơn, khó canh hơn.
|
|
1760
|
+
//
|
|
1761
|
+
// KHÔNG quét `skills/`: ở đó "read-only" nói về ARTIFACT ĐANG XEM (vd /review-code không
|
|
1762
|
+
// sửa code nó review), không nói về mức CHECKPOINT. Hai nghĩa khác nhau của cùng một chữ.
|
|
1763
|
+
{
|
|
1764
|
+
const CL = (schema.gate || {}).checkpoint_levels;
|
|
1765
|
+
if (CL) {
|
|
1766
|
+
const hardSet = new Set((CL.hard || []).filter(e => e.cmd).map(e => e.cmd));
|
|
1767
|
+
const noneSet = new Set((CL.none || []).filter(e => e.cmd).map(e => e.cmd));
|
|
1768
|
+
const realLevel = c => hardSet.has(c) ? 'hard' : (noneSet.has(c) ? 'none' : 'normal');
|
|
1769
|
+
|
|
1770
|
+
// Từ-mức ↔ khoá schema. `normal` là MẶC ĐỊNH (không liệt kê trong schema — xem R11).
|
|
1771
|
+
const ROW = [
|
|
1772
|
+
[/^\|\s*\*\*Không chặn\*\*/i, 'none'],
|
|
1773
|
+
[/^\|\s*\*\*Chặn thường\*\*/i, 'normal'],
|
|
1774
|
+
[/^\|\s*\*\*Chặn CỨNG\*\*/i, 'hard'],
|
|
1775
|
+
];
|
|
1776
|
+
|
|
1777
|
+
for (const dir of ['steps', 'rules']) {
|
|
1778
|
+
const abs = path.join(ROOT, dir);
|
|
1779
|
+
if (!fs.existsSync(abs)) continue;
|
|
1780
|
+
for (const f of fs.readdirSync(abs).filter(x => x.endsWith('.md'))) {
|
|
1781
|
+
const rel = `${dir}/${f}`;
|
|
1782
|
+
const lines = fs.readFileSync(path.join(abs, f), 'utf8').split(/\r?\n/);
|
|
1783
|
+
lines.forEach((line, i) => {
|
|
1784
|
+
const hit = ROW.find(([re]) => re.test(line));
|
|
1785
|
+
if (!hit) return; // không phải hàng bảng mức
|
|
1786
|
+
const declared = hit[1];
|
|
1787
|
+
// Lookbehind chặn báo oan: "sinh/sửa artifact" từng bị cắt ra thành lệnh `/s`.
|
|
1788
|
+
const names = [...line.matchAll(/(?<!\p{L})\/([a-z][a-z0-9-]{2,})(?![\p{L}\d-])/gu)].map(m => m[1]);
|
|
1789
|
+
for (const c of new Set(names)) {
|
|
1790
|
+
if (!fs.existsSync(path.join(ROOT, 'commands', `${c}.tmpl`))) {
|
|
1791
|
+
err('R19', `${rel}:${i + 1} nêu \`/${c}\` — commands/${c}.tmpl không tồn tại`,
|
|
1792
|
+
'Một ví dụ trỏ vào lệnh không có thật làm cả hàng mất giá trị: người đọc không\n'
|
|
1793
|
+
+ ' phân biệt được đâu là tên cũ chưa dọn, đâu là tên đang sai.');
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
const real = realLevel(c);
|
|
1797
|
+
if (real !== declared) {
|
|
1798
|
+
err('R19', `${rel}:${i + 1} xếp \`/${c}\` vào mức \`${declared}\`, schema nói \`${real}\``,
|
|
1799
|
+
'Hai file này được nạp vào MỌI phiên chạy. gate.md tự viết: "Hai file cùng được\n'
|
|
1800
|
+
+ ' nạp vào mọi lệnh mà nói ngược nhau; agent theo cái nào là TUỲ LÚC".\n'
|
|
1801
|
+
+ ` Sửa hàng bảng cho khớp \`gate.checkpoint_levels\`, đừng sửa schema cho khớp bảng.`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Chiều ngược: lệnh ở `hard`/`none` không được nằm nhầm hàng ở bất kỳ bảng nào — đã phủ
|
|
1809
|
+
// bởi vòng trên. Nhưng một lệnh `none` mà KHÔNG bảng nào nêu thì người đọc không có ví
|
|
1810
|
+
// dụ nào — đó là mất mát tài liệu, không phải lỗi contract, nên chỉ cảnh báo.
|
|
1811
|
+
const shown = new Set();
|
|
1812
|
+
for (const dir of ['steps', 'rules']) {
|
|
1813
|
+
const abs = path.join(ROOT, dir);
|
|
1814
|
+
if (!fs.existsSync(abs)) continue;
|
|
1815
|
+
for (const f of fs.readdirSync(abs).filter(x => x.endsWith('.md'))) {
|
|
1816
|
+
const txt = fs.readFileSync(path.join(abs, f), 'utf8');
|
|
1817
|
+
for (const line of txt.split(/\r?\n/)) {
|
|
1818
|
+
if (!ROW.some(([re]) => re.test(line))) continue;
|
|
1819
|
+
for (const m of line.matchAll(/(?<!\p{L})\/([a-z][a-z0-9-]{2,})(?![\p{L}\d-])/gu)) shown.add(m[1]);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const missing = [...noneSet].filter(c => !shown.has(c));
|
|
1824
|
+
if (missing.length) {
|
|
1825
|
+
warn('R19', `mức \`none\` có lệnh không bảng nào nêu làm ví dụ: ${missing.join(', ')}`,
|
|
1826
|
+
'Không phải lỗi contract — nhưng `none` là mức LỎNG NHẤT và cũng là mức dễ bị copy\n'
|
|
1827
|
+
+ ' nhầm nhất. Một ví dụ trong bảng rẻ hơn một lần copy sai.');
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// ── R20 — mỗi cổng CỨNG phải khai ĐIỀU KIỆN nó nổ, và số cổng vô điều kiện có HẠN MỨC ──
|
|
1833
|
+
//
|
|
1834
|
+
// Vì sao: `hard` là mức `--yes` KHÔNG bỏ qua được. Khi nó trở nên phiền, người dùng không
|
|
1835
|
+
// thêm `--yes` — họ GỠ LỆNH KHỎI `hard`. Và lúc đó mất luôn những cổng đáng giữ. Một cơ chế
|
|
1836
|
+
// cổng chặn quá dày không làm hệ thống an toàn hơn rồi chậm hơn; nó làm hệ thống KÉM an toàn
|
|
1837
|
+
// hơn, vì nó tiêu huỷ chính cơ chế nó thuộc về. gate.md Bước 3b tồn tại vì đúng rủi ro đó:
|
|
1838
|
+
// "cổng luôn in ra một bảng giống hệt nhau … nên Y thành phản xạ và cổng hỏng âm thầm".
|
|
1839
|
+
//
|
|
1840
|
+
// ĐO 2026-09-16 — 12 lệnh ở `hard`, nhưng chỉ 3 trong đó nổ ở MỌI lần chạy:
|
|
1841
|
+
// 7 lệnh khai "chỉ áp khi file đã tồn tại" → lần chạy đầu đi thẳng
|
|
1842
|
+
// 2 lệnh CÓ điều kiện y hệt nhưng KHÔNG khai (generate-prd · generate-architecture)
|
|
1843
|
+
// 3 lệnh không có lần chạy vô hại (extend/amend/refine-prd — chỉ chạy trên PRD đã duyệt)
|
|
1844
|
+
// Con số gây lo lắng là 12; con số ĐÚNG là 3. R20 làm con số 3 đó đo được và giữ được.
|
|
1845
|
+
//
|
|
1846
|
+
// BẮT KHAI `null` KÈM LÝ DO: "không có điều kiện" và "quên nghĩ tới điều kiện" trông y hệt
|
|
1847
|
+
// nhau — đúng lập luận đã bắt `why` phải khai cả khi `false` (G79).
|
|
1848
|
+
//
|
|
1849
|
+
// ĐIỀU KIỆN PHẢI CÓ Ở CẢ HAI NƠI: khai trong schema mà tmpl không nói thì agent đọc tmpl sẽ
|
|
1850
|
+
// chặn mọi lần — hình dạng G41, hai nguồn nói khác nhau.
|
|
1851
|
+
{
|
|
1852
|
+
const CL = (schema.gate || {}).checkpoint_levels;
|
|
1853
|
+
if (CL) {
|
|
1854
|
+
const B = CL.unconditional_hard_budget;
|
|
1855
|
+
// Khuôn câu thu hẹp trong tmpl. Chấp nhận cả `*Mức cứng chỉ áp …*` lẫn
|
|
1856
|
+
// `*Mức cứng **chỉ áp** …*` — hai biến thể đang cùng tồn tại.
|
|
1857
|
+
const NARROW_LINE = /Mức cứng\s*(?:\*\*)?\s*chỉ áp/i;
|
|
1858
|
+
const unconditional = [];
|
|
1859
|
+
|
|
1860
|
+
for (const e of CL.hard || []) {
|
|
1861
|
+
if (!e.cmd) continue;
|
|
1862
|
+
const rel = `commands/${e.cmd}.tmpl`;
|
|
1863
|
+
|
|
1864
|
+
if (!('narrowing' in e)) {
|
|
1865
|
+
err('R20', `\`checkpoint_levels.hard\` mục \`${e.cmd}\` thiếu \`narrowing\``,
|
|
1866
|
+
'Phải khai điều kiện cổng nổ — hoặc `null` nếu KHÔNG có lần chạy vô hại.\n'
|
|
1867
|
+
+ ' Bỏ trống thì "cố ý không có điều kiện" trông y hệt "quên nghĩ tới điều kiện",\n'
|
|
1868
|
+
+ ' và người sau sẽ copy chỗ sót đi (G79).');
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (e.narrowing === null) {
|
|
1873
|
+
unconditional.push(e.cmd);
|
|
1874
|
+
if (!e.narrowing_why) {
|
|
1875
|
+
err('R20', `\`${e.cmd}\` khai \`narrowing: null\` nhưng thiếu \`narrowing_why\``,
|
|
1876
|
+
'Cổng nổ ở MỌI lần chạy là thứ đắt nhất trong cơ chế này. Nói ra vì sao nó ĐÚNG,\n'
|
|
1877
|
+
+ ' nếu không người sau sẽ tưởng là chỗ sót rồi thêm điều kiện cho "đỡ phiền".');
|
|
1878
|
+
}
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (typeof e.narrowing !== 'string' || !e.narrowing.trim()) {
|
|
1883
|
+
err('R20', `\`${e.cmd}\`.\`narrowing\` phải là chuỗi điều kiện hoặc \`null\``,
|
|
1884
|
+
`Thấy: ${JSON.stringify(e.narrowing)}`);
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const abs = path.join(ROOT, rel);
|
|
1889
|
+
if (fs.existsSync(abs) && !NARROW_LINE.test(fs.readFileSync(abs, 'utf8'))) {
|
|
1890
|
+
err('R20', `\`${e.cmd}\` khai \`narrowing\` trong schema nhưng ${rel} KHÔNG có câu thu hẹp`,
|
|
1891
|
+
`Khai: "${e.narrowing.slice(0, 95)}…"\n`
|
|
1892
|
+
+ ' Agent đọc TMPL, không đọc schema. Thiếu câu đó thì nó chặn MỌI lần chạy —\n'
|
|
1893
|
+
+ ' đúng thứ điều kiện này sinh ra để tránh. Thêm một dòng ngay dưới nhãn\n'
|
|
1894
|
+
+ ' Checkpoint theo khuôn: *Mức cứng chỉ áp khi …*');
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// ── Hạn mức — bánh cóc cho số cổng nổ vô điều kiện ──
|
|
1899
|
+
if (!B || typeof B.max !== 'number') {
|
|
1900
|
+
err('R20', '`checkpoint_levels.unconditional_hard_budget.max` chưa khai',
|
|
1901
|
+
'Với mọi cơ chế mà "thêm một cái nữa" luôn hợp lý từng lần, hạn mức phải nằm TRONG\n'
|
|
1902
|
+
+ ' contract để việc vượt nó là một lần sửa NHÌN THẤY trong diff, không phải một\n'
|
|
1903
|
+
+ ' hệ quả cộng dồn.');
|
|
1904
|
+
} else if (unconditional.length > B.max) {
|
|
1905
|
+
err('R20', `${unconditional.length} cổng cứng nổ VÔ ĐIỀU KIỆN, vượt hạn mức ${B.max}`,
|
|
1906
|
+
`Đang vượt: ${unconditional.join(', ')}\n`
|
|
1907
|
+
+ ' `--yes` KHÔNG bỏ qua được `hard`. Nên khi `hard` quá phiền, người ta không thêm\n'
|
|
1908
|
+
+ ' `--yes` — họ GỠ LỆNH KHỎI `hard`, và mất luôn cổng đáng giữ.\n'
|
|
1909
|
+
+ ' Sửa: thêm `narrowing` cho một lệnh, HOẶC nâng `max` — nâng là một QUYẾT ĐỊNH,\n'
|
|
1910
|
+
+ ' phải kèm lý do trong `why` của khối ngân sách.');
|
|
1911
|
+
} else if (Array.isArray(B.current)) {
|
|
1912
|
+
const a = [...unconditional].sort().join(',');
|
|
1913
|
+
const b = [...B.current].sort().join(',');
|
|
1914
|
+
if (a !== b) {
|
|
1915
|
+
err('R20', '`unconditional_hard_budget.current` lệch với thực tế',
|
|
1916
|
+
`khai: ${b || '(rỗng)'}\n thật: ${a || '(rỗng)'}\n`
|
|
1917
|
+
+ ' Danh sách chép tay cạnh một con số sẽ lệch — đúng bài học R19. Cập nhật nó.');
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
if (B && typeof B.max === 'number' && !B.why) {
|
|
1922
|
+
err('R20', '`unconditional_hard_budget` thiếu `why`',
|
|
1923
|
+
'Một con số không nói được vì sao sẽ bị người sau nâng lên cho tiện.');
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// ── R21 — mọi `checked_by[].cmd` phải trỏ vào một lệnh CÓ THẬT ───────────────
|
|
1929
|
+
//
|
|
1930
|
+
// Vì sao: nhiều khối contract khai "ai canh điều kiện này" bằng một danh sách `checked_by`.
|
|
1931
|
+
// Khi một lệnh bị ĐỔI TÊN hoặc TÁCH ĐÔI, danh sách đó lệch mà KHÔNG máy nào bắt — và hậu quả
|
|
1932
|
+
// tệ hơn một tham chiếu treo thường: contract vẫn khai điều kiện đó "đã được canh", trong khi
|
|
1933
|
+
// không lệnh nào còn canh nó. Người đọc contract thấy một cái lưới; cái lưới không có ở đó.
|
|
1934
|
+
//
|
|
1935
|
+
// GẶP THẬT (Đợt 2 · b1 · 2026-09-16): tách /qc-review → /qc-review-testcase + /qc-review-script.
|
|
1936
|
+
// `testid_fourth_leg.checked_by` — CHÂN THỨ TƯ của hợp đồng test-id (G64/G76) — trỏ vào tên cũ.
|
|
1937
|
+
// Bản kế hoạch của bước đó khai "tách không đụng schema, grep → 0"; grep thật ra 3. Nếu tin bản
|
|
1938
|
+
// kế hoạch, chân thứ tư mất người canh và build vẫn XANH.
|
|
1939
|
+
//
|
|
1940
|
+
// Đây là hình dạng đã gặp ở R17 ("khai một producer không tồn tại thì luật trông như đã được áp,
|
|
1941
|
+
// trong khi không ai áp") — R21 là cùng luật đó, áp cho `checked_by` thay vì `declared_by`.
|
|
1942
|
+
//
|
|
1943
|
+
// QUÉT ĐỆ QUY, không liệt kê tên khối: hôm nay có 2 khối (`testid_fourth_leg`, `qc_artifact_stamp`);
|
|
1944
|
+
// khối thứ ba thêm vào mà quên dạy checker là đúng kiểu lỗi rule này sinh ra để bắt.
|
|
1945
|
+
{
|
|
1946
|
+
const seen = [];
|
|
1947
|
+
(function walk(node, at) {
|
|
1948
|
+
if (!node || typeof node !== 'object') return;
|
|
1949
|
+
for (const k of Object.keys(node)) {
|
|
1950
|
+
const v = node[k];
|
|
1951
|
+
if (k === 'checked_by' && Array.isArray(v)) seen.push([at + '.' + k, v]);
|
|
1952
|
+
if (v && typeof v === 'object') walk(v, at + '.' + k);
|
|
1953
|
+
}
|
|
1954
|
+
})(schema, '');
|
|
1955
|
+
|
|
1956
|
+
if (!seen.length) {
|
|
1957
|
+
warn('R21', 'không tìm thấy khối `checked_by` nào',
|
|
1958
|
+
'Rule này sinh ra để canh chúng. Không còn khối nào thì hoặc contract đã đổi hình,\n'
|
|
1959
|
+
+ ' hoặc rule đang quét sai chỗ — cả hai đều đáng nhìn lại.');
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
for (const [at, list] of seen) {
|
|
1963
|
+
list.forEach((e, i) => {
|
|
1964
|
+
if (!e || !e.cmd) {
|
|
1965
|
+
err('R21', `\`${at}[${i}]\` thiếu \`cmd\``,
|
|
1966
|
+
'Một mục "ai canh" không nói được AI canh thì nó không khai gì cả.');
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
const rel = `commands/${e.cmd}.tmpl`;
|
|
1970
|
+
if (!fs.existsSync(path.join(ROOT, rel))) {
|
|
1971
|
+
err('R21', `\`${at}\` khai \`${e.cmd}\` — ${rel} không tồn tại`,
|
|
1972
|
+
'Lệnh bị đổi tên hoặc TÁCH ĐÔI? Contract vẫn khai điều kiện này "đã được canh", nhưng\n'
|
|
1973
|
+
+ ' không lệnh nào còn canh nó — tệ hơn một lỗ hổng ai cũng biết là chưa canh.\n'
|
|
1974
|
+
+ ' Sửa: trỏ sang lệnh THỪA HƯỞNG vai đó, đừng chỉ xoá mục.');
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// ── R22 — mọi artifact được khai phải có ÍT NHẤT MỘT lệnh TẠO ra nó ──────────
|
|
1981
|
+
//
|
|
1982
|
+
// Vì sao: `artifact_writers` trả lời "ai ghi gì", nhưng GHI không đồng nghĩa với TẠO. Một lệnh
|
|
1983
|
+
// khai `mode: merge` là nói "tôi sửa một phần của file đã có" — nó KHÔNG hứa file đó tồn tại.
|
|
1984
|
+
// Nếu MỌI lệnh ghi một artifact đều chỉ `merge`, thì **không lệnh nào sinh ra nó**, và cả chuỗi
|
|
1985
|
+
// trạm phía sau đứng chờ một file không bao giờ xuất hiện. Build vẫn xanh suốt.
|
|
1986
|
+
//
|
|
1987
|
+
// GẶP THẬT, HAI CA (Đợt 2 · b3 · 2026-09-16):
|
|
1988
|
+
// · AUTOMATION_ASSESSMENT.md — /qc-design-script khai ghi CỘT `Script file` của nó (merge),
|
|
1989
|
+
// và /qc-run-manualtest đọc cột `Automatable` từ nó. Không lệnh nào TẠO. Ba lệnh viết ở b2
|
|
1990
|
+
// cùng trỏ vào một file không ai sinh, và không rule nào kêu.
|
|
1991
|
+
// · _seams.tsv — /generate-code tự nói "tạo file + header nếu chưa có", nhưng KHÔNG khai
|
|
1992
|
+
// artifact đó; chỉ /validate-traces khai (merge, "nếu tồn tại"). Sổ này rơi hẳn khỏi registry.
|
|
1993
|
+
//
|
|
1994
|
+
// `creates: true` LÀ ĐƯỜNG KHAI CHÍNH XÁC, KHÔNG PHẢI ĐƯỜNG TRỐN. Nhiều lệnh vừa tạo-khi-vắng
|
|
1995
|
+
// vừa merge-khi-có (file findings của /refine-prd · /review-context · /review-tech-docs). Ép
|
|
1996
|
+
// chúng khai `overwrite` là nói dối — chế độ delta của chúng GIỮ `status` của reviewer. Nên
|
|
1997
|
+
// mode vẫn `merge`, và `creates: true` nói thêm "và tôi sinh nó ra nếu chưa có".
|
|
1998
|
+
{
|
|
1999
|
+
const AW = schema.artifact_writers;
|
|
2000
|
+
if (AW && AW.enrolled) {
|
|
2001
|
+
// Khoá nhận dạng artifact: tên file trong `path`. Cùng tên = cùng artifact, bất kể
|
|
2002
|
+
// placeholder thư mục khác nhau ({qc_artifact_dir} vs {refinement_dir}).
|
|
2003
|
+
const KEY = /([A-Z][A-Z0-9_]{3,}(?:_?<[A-Z]+>)?\.[A-Za-z.]+|[a-z][a-z-]*\.(?:tsv|jsonl|json|yaml|yml))/;
|
|
2004
|
+
const seen = new Map(); // tên file → [{cmd, mode, creates}]
|
|
2005
|
+
|
|
2006
|
+
for (const [cmd, e] of Object.entries(AW.enrolled)) {
|
|
2007
|
+
for (const w of e.writes || []) {
|
|
2008
|
+
if (!w || typeof w !== 'object') continue;
|
|
2009
|
+
const m = KEY.exec(String(w.path || ''));
|
|
2010
|
+
if (!m) continue; // artifact mô tả bằng lời (vd "Page Object") — không khoá được
|
|
2011
|
+
if (!seen.has(m[1])) seen.set(m[1], []);
|
|
2012
|
+
seen.get(m[1]).push({ cmd, mode: w.mode, creates: w.creates === true });
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const CREATING = new Set(['overwrite', 'create', 'append']);
|
|
2017
|
+
for (const [file, writers] of seen) {
|
|
2018
|
+
const creators = writers.filter(w => CREATING.has(w.mode) || w.creates);
|
|
2019
|
+
if (creators.length) continue;
|
|
2020
|
+
err('R22', `\`${file}\` không lệnh nào TẠO — mọi lệnh ghi nó đều \`merge\``,
|
|
2021
|
+
`Đang ghi: ${writers.map(w => w.cmd + ':' + w.mode).join(' · ')}\n`
|
|
2022
|
+
+ ' `merge` nói "tôi sửa một phần của file ĐÃ CÓ" — nó không hứa file đó tồn tại. Cả\n'
|
|
2023
|
+
+ ' chuỗi trạm sau sẽ đứng chờ một file không bao giờ xuất hiện, và build vẫn xanh.\n'
|
|
2024
|
+
+ ' Sửa: khai lệnh TẠO nó (`overwrite`/`create`/`append`), HOẶC thêm `creates: true`\n'
|
|
2025
|
+
+ ' vào entry của lệnh vừa-tạo-khi-vắng vừa-merge-khi-có.');
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1556
2030
|
// ── report ────────────────────────────────────────────────────────────────────
|
|
1557
2031
|
const tagCount = schema.tags.length;
|
|
1558
2032
|
const colCount = schema.tsv_columns.length;
|
|
@@ -1576,6 +2050,13 @@ if (!errors.length && !warns.length) {
|
|
|
1576
2050
|
// một key `$` kể từ khi thêm `$lint_binding_comment`, nên hằng số cũ sẽ đếm lố.
|
|
1577
2051
|
const vocabCount = Object.keys(schema.vocabularies).filter(k => !k.startsWith('$')).length;
|
|
1578
2052
|
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`);
|
|
2053
|
+
{
|
|
2054
|
+
const _CL = (schema.gate || {}).checkpoint_levels || {};
|
|
2055
|
+
const _hard = (_CL.hard || []).filter(e => e.cmd);
|
|
2056
|
+
const _u = _hard.filter(e => e.narrowing === null);
|
|
2057
|
+
const _max = ((_CL.unconditional_hard_budget || {}).max);
|
|
2058
|
+
console.log(` + ${_hard.length} cổng CỨNG, trong đó ${_u.length} nổ VÔ ĐIỀU KIỆN` + (typeof _max === 'number' ? ` (hạn mức ${_max})` : '') + ` — ${_hard.length - _u.length} cổng còn lại chỉ nổ khi đã có thứ để mất`);
|
|
2059
|
+
}
|
|
1579
2060
|
// ĐẾM rule từ chính lint-trace.js, không hard-code: một con số phát biểu ở hai chỗ rồi
|
|
1580
2061
|
// 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.
|
|
1581
2062
|
let lintRules = '?';
|