@educa-corp/sdd-framework 0.6.0 → 0.7.1
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/gate-trace.js +25 -2
- package/bin/index.js +32 -5
- package/bin/lint-trace.js +41 -0
- package/bin/self-check.js +430 -3
- package/bin/trace-schema.json +418 -31
- package/core/FRAMEWORK_VERSION +1 -1
- package/{commands/extend-prd.md → core/commands/amend-prd.md} +206 -173
- package/core/commands/dev-run-test.md +48 -10
- package/core/commands/extend-prd.md +39 -12
- package/core/commands/generate-bdd.md +52 -10
- package/core/commands/generate-code.md +35 -2
- package/core/commands/generate-tech-docs.md +36 -4
- package/core/commands/map-testids.md +1 -1
- package/core/commands/qc-run-test.md +29 -3
- package/core/commands/refine-prd.md +13 -2
- package/core/commands/review-context.md +43 -8
- package/core/commands/sync.md +105 -1
- package/core/commands/validate-traces.md +289 -16
- package/core/rules/workflow.md +34 -0
- package/core/steps/context-loader.md +27 -6
- package/core/templates/feature.template +1 -1
- package/core/templates/project-context.yaml +3 -3
- package/core/templates/tech-design.template.md +2 -2
- package/docs/02-concepts/architecture.md +37 -1
- package/docs/02-concepts/overview.md +1 -1
- package/docs/02-concepts/pipeline-steps/02-specification.md +13 -7
- package/docs/02-concepts/pipeline-steps/07-dev-selftest.md +2 -0
- package/docs/02-concepts/pipeline-steps/08-qc-automation.md +1 -0
- package/docs/02-concepts/pipeline-steps/09-validate-traces.md +34 -3
- package/docs/02-concepts/pipeline-steps/10-feedback-loop.md +10 -1
- package/docs/02-concepts/traceability.md +187 -183
- package/docs/03-guides/architect.md +13 -4
- package/docs/03-guides/developer.md +1 -0
- package/docs/03-guides/product-owner.md +89 -72
- package/docs/03-guides/tester-qa.md +81 -81
- package/docs/04-reference/commands.md +148 -134
- package/docs/04-reference/trace-schema.md +45 -1
- package/docs/explain/02b-extend-prd.md +1 -1
- package/docs/explain/02c-amend-prd.md +152 -0
- package/docs/explain/06-generate-bdd.md +1 -1
- package/docs/explain/13-dev-run-test.md +15 -1
- package/docs/explain/19-qc-run-test.md +91 -87
- package/docs/explain/21-validate-traces.md +79 -75
- package/docs/explain/28-sync.md +25 -0
- package/docs/explain/README.md +136 -135
- package/package.json +1 -8
- package/commands/debug.md +0 -529
- package/commands/debug.tmpl +0 -260
- package/commands/define-product.md +0 -438
- package/commands/define-product.tmpl +0 -225
- package/commands/dev-gen-test.md +0 -700
- package/commands/dev-gen-test.tmpl +0 -490
- package/commands/dev-run-test.md +0 -435
- package/commands/dev-run-test.tmpl +0 -225
- package/commands/dev-smoke-test.md +0 -374
- package/commands/dev-smoke-test.tmpl +0 -217
- package/commands/extend-prd.tmpl +0 -273
- package/commands/fix-bug.md +0 -519
- package/commands/fix-bug.tmpl +0 -197
- package/commands/generate-architecture.md +0 -354
- package/commands/generate-architecture.tmpl +0 -197
- package/commands/generate-bdd.md +0 -923
- package/commands/generate-bdd.tmpl +0 -590
- package/commands/generate-code.md +0 -859
- package/commands/generate-code.tmpl +0 -649
- package/commands/generate-design-spec.md +0 -737
- package/commands/generate-design-spec.tmpl +0 -524
- package/commands/generate-prd.md +0 -722
- package/commands/generate-prd.tmpl +0 -226
- package/commands/generate-spec-manifest.md +0 -321
- package/commands/generate-spec-manifest.tmpl +0 -164
- package/commands/generate-tech-docs.md +0 -920
- package/commands/generate-tech-docs.tmpl +0 -273
- package/commands/learn.md +0 -399
- package/commands/learn.tmpl +0 -130
- package/commands/map-testids.md +0 -238
- package/commands/map-testids.tmpl +0 -81
- package/commands/propose-scenario.md +0 -359
- package/commands/propose-scenario.tmpl +0 -202
- package/commands/qc-analyze.md +0 -269
- package/commands/qc-analyze.tmpl +0 -112
- package/commands/qc-design-test.md +0 -226
- package/commands/qc-design-test.tmpl +0 -69
- package/commands/qc-plan.md +0 -206
- package/commands/qc-plan.tmpl +0 -49
- package/commands/qc-report.md +0 -217
- package/commands/qc-report.tmpl +0 -60
- package/commands/qc-review.md +0 -210
- package/commands/qc-review.tmpl +0 -53
- package/commands/qc-run-test.md +0 -326
- package/commands/qc-run-test.tmpl +0 -116
- package/commands/refine-prd.md +0 -653
- package/commands/refine-prd.tmpl +0 -281
- package/commands/report-bug.md +0 -305
- package/commands/report-bug.tmpl +0 -148
- package/commands/review-code.md +0 -415
- package/commands/review-code.tmpl +0 -146
- package/commands/review-context.md +0 -902
- package/commands/review-context.tmpl +0 -530
- package/commands/review-tech-docs.md +0 -561
- package/commands/review-tech-docs.tmpl +0 -404
- package/commands/setup-ai-first.md +0 -602
- package/commands/setup-ai-first.tmpl +0 -450
- package/commands/sync.md +0 -430
- package/commands/sync.tmpl +0 -429
- package/commands/update-framework.md +0 -203
- package/commands/update-framework.tmpl +0 -202
- package/commands/validate-traces.md +0 -1077
- package/commands/validate-traces.tmpl +0 -920
- package/hooks/data-guard.js +0 -232
- package/hooks/settings.json +0 -19
- 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 -99
- 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 -112
- package/steps/context-loader.md +0 -406
- package/steps/gate.md +0 -151
- package/steps/report-footer.md +0 -125
- 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 -70
- package/templates/architecture.template.md +0 -394
- package/templates/ci/trace-gate.yml +0 -146
- package/templates/design-spec.template.md +0 -217
- package/templates/feature.template +0 -123
- package/templates/hooks/pre-push +0 -61
- 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/gate-trace.js
CHANGED
|
@@ -241,9 +241,32 @@ if (report && !NO_RECONCILE) {
|
|
|
241
241
|
const domain = report.domain || 'all';
|
|
242
242
|
const summary = report.summary || {};
|
|
243
243
|
|
|
244
|
-
// (a) Report phủ một phần repo → cờ 🔴 ở
|
|
244
|
+
// (a) Report phủ một phần repo → cờ 🔴 ở phần chưa audit hoàn toàn vô hình với gate.
|
|
245
245
|
// Cho qua ở đây là cấp một tờ giấy xanh cho thứ chưa ai xem.
|
|
246
|
-
|
|
246
|
+
//
|
|
247
|
+
// G57 — LUẬT TUYỆT ĐỐI, không phụ thuộc trên đĩa có bao nhiêu domain.
|
|
248
|
+
// Bản cũ hỏi "còn domain NÀO KHÁC không": `if (others.length) fail`. Trong repo MỘT domain,
|
|
249
|
+
// `others` là RỖNG ⇒ KHÔNG fail ⇒ một report hẹp-theo-PRD (hoặc theo-UC) được nhận là "toàn
|
|
250
|
+
// bộ". Lúc chưa có cờ scope thì vô hại; nó BẬT NGAY khi scoping thành khả dụng — tức bản fix
|
|
251
|
+
// G57 sẽ tự tay mở đúng cái false-green mà chú thích ngay trên cảnh báo.
|
|
252
|
+
// Nên: scope.kind !== 'all' ⇒ FAIL. Hết.
|
|
253
|
+
const scope = report.scope;
|
|
254
|
+
if (scope && typeof scope === 'object') {
|
|
255
|
+
if (scope.kind !== 'all') {
|
|
256
|
+
const what = scope.value && scope.value !== 'all'
|
|
257
|
+
? `${scope.kind}="${scope.value}"` : String(scope.kind);
|
|
258
|
+
fail('G2', `report chỉ phủ ${what} — đây KHÔNG phải một audit đầy đủ`,
|
|
259
|
+
['Cờ 🔴 ngoài phạm vi đó hoàn toàn vô hình với gate.',
|
|
260
|
+
'Gate cho qua lúc này là khẳng định sạch cho thứ chưa ai xem.',
|
|
261
|
+
'Chạy /validate-traces KHÔNG kèm --domain/--prd/--uc, rồi commit report.'].join('\n '));
|
|
262
|
+
}
|
|
263
|
+
} else if (domain !== 'all') {
|
|
264
|
+
// TƯƠNG THÍCH NGƯỢC: report sinh trước G57 không có `scope`. Rơi về logic cũ theo `domain`
|
|
265
|
+
// và nói rõ kiểm đang ở mức YẾU — KHÔNG fail vì thiếu field. Người dùng chưa chạy lại
|
|
266
|
+
// /validate-traces không phải là lỗi của họ. Cùng khuôn G48 đã dùng cho `platform`.
|
|
267
|
+
warn('G2', 'report không có field `scope` (sinh trước G57) — kiểm độ phủ đang ở mức YẾU',
|
|
268
|
+
'Chạy lại /validate-traces để report mang `scope`; chỉ khi đó gate mới chặn được mọi\n'
|
|
269
|
+
+ ' loại audit phiến diện, kể cả hẹp-theo-PRD trong repo một domain.');
|
|
247
270
|
const others = domainsOnDisk(TRACE_DIRS).filter(d => d !== domain);
|
|
248
271
|
if (others.length) {
|
|
249
272
|
fail('G2', `report chỉ phủ domain "${domain}" — còn ${others.length} domain có sổ mà chưa được audit`,
|
package/bin/index.js
CHANGED
|
@@ -201,21 +201,48 @@ try {
|
|
|
201
201
|
corePrebuilt = fs.readFileSync(coreVersionFile, 'utf8').trim() === VERSION;
|
|
202
202
|
} catch { /* không có file = chưa build */ }
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
// `hasSources` LÀ phép phân biệt dev-checkout vs bản cài từ npm — đặt tên MỘT LẦN.
|
|
205
|
+
//
|
|
206
|
+
// Vì sao gọi tên nó (GAPS-v4 G59): trước đó cùng một `existsSync(commands/*.tmpl)` được viết
|
|
207
|
+
// ở hai chỗ với hai ý khác nhau, và `package.json → files` thì SHIP `commands/` — nên phép thử
|
|
208
|
+
// LUÔN đúng và consumer nhìn thấy một lời nhắc mà chú thích của chính nó nói là
|
|
209
|
+
// "consumer không cần biết bước này tồn tại". Đã kiểm bằng cách pack rồi chạy tarball thật.
|
|
210
|
+
// Giờ `files` chỉ còn bin/ core/ scripts/ docs/, nên `hasSources` phân biệt đúng.
|
|
211
|
+
const hasSources = fs.existsSync(path.join(ROOT, 'commands', 'generate-code.tmpl'));
|
|
212
|
+
|
|
213
|
+
if (!corePrebuilt && hasSources && fs.existsSync(buildScript)) {
|
|
214
|
+
// DEV CHECKOUT, core/ vắng hoặc lệch version → dựng lại từ nguồn.
|
|
205
215
|
try {
|
|
206
216
|
require(buildScript);
|
|
207
217
|
} catch (err) {
|
|
208
218
|
console.error('Build step failed:', err.message);
|
|
209
219
|
process.exit(1);
|
|
210
220
|
}
|
|
211
|
-
} else if (corePrebuilt
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
221
|
+
} else if (!corePrebuilt) {
|
|
222
|
+
// BẢN CÀI TỪ NPM mà core/ vắng hoặc lệch version — KHÔNG có nguồn để dựng lại.
|
|
223
|
+
//
|
|
224
|
+
// Trước G59, nhánh này rơi vào `require(buildScript)` và build.js chết với một ENOENT trên
|
|
225
|
+
// `commands/` — thông báo không nói được vấn đề thật. Cố build ở đây còn tệ hơn: G43 đã nêu
|
|
226
|
+
// rõ là nó ghi vào THƯ MỤC PACKAGE (npx cache / node_modules global), có thể read-only, và
|
|
227
|
+
// hai `--init` song song sẽ đua nhau. Nên: dừng với một câu nói đúng việc cần làm.
|
|
228
|
+
console.error('');
|
|
229
|
+
console.error(`❌ Bản cài này thiếu core/ dựng sẵn cho v${VERSION}.`);
|
|
230
|
+
console.error(' Package đã tải về bị thiếu/hỏng — KHÔNG thể dựng lại vì tarball không mang');
|
|
231
|
+
console.error(' commands/*.tmpl (có chủ ý: nguồn chỉ có trong dev checkout).');
|
|
232
|
+
console.error('');
|
|
233
|
+
console.error(' Xử: npx clear-npx-cache (hoặc xoá thủ công cache npx) rồi chạy lại');
|
|
234
|
+
console.error(` npm i -g @educa-corp/sdd-framework@${VERSION} nếu dùng bản global`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
} else if (hasSources) {
|
|
237
|
+
// corePrebuilt && hasSources → DEV CHECKOUT, core/ đã khớp version.
|
|
238
|
+
// Cần lời nhắc này vì trước G43 thì `--init` LUÔN build, nên "sửa .tmpl rồi --init" từng có
|
|
239
|
+
// tác dụng. Giờ không — và một thay đổi lặng lẽ như thế sẽ tốn của ai đó nửa tiếng.
|
|
215
240
|
console.log('');
|
|
216
241
|
console.log(`ℹ️ core/ đã build sẵn (v${VERSION}) — bỏ qua bước build.`);
|
|
217
242
|
console.log(' Vừa sửa commands/*.tmpl · steps/ · templates/ ? Chạy `npm run build` trước.');
|
|
218
243
|
}
|
|
244
|
+
// corePrebuilt && !hasSources → bản cài từ npm, mọi thứ đúng chỗ. IM LẶNG — đây là đường của
|
|
245
|
+
// consumer, và họ không cần biết bước build tồn tại.
|
|
219
246
|
|
|
220
247
|
// ── --init mode: install to .agent/ + create .claude/commands/ shortcuts ─────
|
|
221
248
|
if (isInit) {
|
package/bin/lint-trace.js
CHANGED
|
@@ -95,6 +95,20 @@ function buildEnumBindings() {
|
|
|
95
95
|
}
|
|
96
96
|
const ENUMS = buildEnumBindings();
|
|
97
97
|
|
|
98
|
+
// ── T12 — quyền khẳng định `pass` (G55) ───────────────────────────────────────
|
|
99
|
+
//
|
|
100
|
+
// T1–T8 kiểm HÌNH DẠNG TỪNG Ô (đủ cột · giá trị trong vocabulary · ngày parse được). Không
|
|
101
|
+
// rule nào kiểm NHẤT QUÁN GIỮA CÁC Ô — nên `status=DRIFT` + `dev_selftest=pass` là một sổ
|
|
102
|
+
// HOÀN TOÀN HỢP LỆ với cả 11 rule cũ. T12 là rule đầu tiên nhìn nhiều ô cùng lúc.
|
|
103
|
+
//
|
|
104
|
+
// Vì sao cần dù /dev-run-test đã có guard: guard đó chặn NGUỒN. T12 bắt TRẠNG THÁI, bất kể
|
|
105
|
+
// ai ghi ra — sổ sửa tay, sổ sinh bởi version framework cũ, hay một lệnh tương lai quên luật.
|
|
106
|
+
// Hai tầng độc lập; đây là tầng chạy được trong CI mà không cần LLM.
|
|
107
|
+
//
|
|
108
|
+
// Đọc từ schema, KHÔNG hard-code: self-check R14 canh việc rule id ở đây khớp
|
|
109
|
+
// `positive_assertion_guards.lint_rule`.
|
|
110
|
+
const PAG = schema.positive_assertion_guards || null;
|
|
111
|
+
|
|
98
112
|
// Ô "chưa biết" — framework dùng `—` (em dash) làm placeholder ở mọi cột.
|
|
99
113
|
// Rỗng cũng chấp nhận: TSV cũ có cột vắng, và union merge có thể để ô trống.
|
|
100
114
|
const UNKNOWN = new Set(['—', '-', '', 'n/a', 'N/A']);
|
|
@@ -296,6 +310,33 @@ function lintBook(file) {
|
|
|
296
310
|
`Định dạng: YYYY-MM-DD hoặc ISO-8601 đầy đủ (hoặc — nếu chưa biết)`);
|
|
297
311
|
}
|
|
298
312
|
}
|
|
313
|
+
|
|
314
|
+
// T12 — row không được vừa `status` ∈ {DRIFT, ORPHANED} vừa mang một giá trị KHẲNG ĐỊNH
|
|
315
|
+
// ở cột nghiệm thu. `fail`/`skip` KHÔNG bị chặn: chúng không phải lời khẳng định, và
|
|
316
|
+
// chặn chúng là biến guard chống-báo-cáo-sai thành guard che-tin-xấu.
|
|
317
|
+
if (PAG) {
|
|
318
|
+
const statusIdx = header.indexOf('status');
|
|
319
|
+
const st = statusIdx === -1 ? '' : String(f[statusIdx] || '').trim();
|
|
320
|
+
if (PAG.blocked_when_status.includes(st)) {
|
|
321
|
+
for (const g of PAG.guards) {
|
|
322
|
+
const gi = header.indexOf(g.column);
|
|
323
|
+
if (gi === -1) continue; // sổ cũ vắng cột ở đuôi — T1 lo việc đó
|
|
324
|
+
const val = String(f[gi] || '').trim();
|
|
325
|
+
if (!g.positive_values.includes(val)) continue;
|
|
326
|
+
warn('T12', at,
|
|
327
|
+
`status = "${st}" nhưng ${g.column} = "${val}"`,
|
|
328
|
+
`${g.why}
|
|
329
|
+
` +
|
|
330
|
+
` "${val}" không mang nghĩa "test đã chạy xanh" — nó mang nghĩa "scenario này
|
|
331
|
+
` +
|
|
332
|
+
` đã được nghiệm thu theo spec HIỆN TẠI". Trên row ${st} thì nghĩa đó SAI.
|
|
333
|
+
` +
|
|
334
|
+
` Hạ ${g.column} → ${g.downgrade_to} (và ${g.companion} → ${g.companion_downgrade}),
|
|
335
|
+
` +
|
|
336
|
+
` hoặc xử ${st} rồi chạy lại /${g.owner}.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
299
340
|
}
|
|
300
341
|
}
|
|
301
342
|
|
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
|
|
@@ -25,6 +26,23 @@
|
|
|
25
26
|
* R8b binding đã khai mà lint-trace không đọc key đó
|
|
26
27
|
* R8c `n` của tsv_columns trùng/hổng — lint dựng header theo `n`, sai `n` = sai header
|
|
27
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ụ.
|
|
28
46
|
*
|
|
29
47
|
* RANH GIỚI R1–R7 vs R8: R1–R7 canh CONTRACT (đọc commands/*.tmpl — "lệnh có gọi đúng tên
|
|
30
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,
|
|
@@ -110,6 +128,26 @@ function mentions(text, name) {
|
|
|
110
128
|
return new RegExp(`${before}${esc}(?![A-Za-z0-9_])`).test(text);
|
|
111
129
|
}
|
|
112
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
|
+
|
|
113
151
|
// ── R1 + R2 + R3 — producer / consumer / prose khớp ───────────────────────────
|
|
114
152
|
function checkField(kind, name, def) {
|
|
115
153
|
const producers = [...(def.written_by || []), ...(def.bumped_by || [])];
|
|
@@ -448,7 +486,12 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
448
486
|
return vtText.slice(a, b < 0 ? undefined : b);
|
|
449
487
|
};
|
|
450
488
|
const summaryZone = slice('"summary": {', '"prds"');
|
|
451
|
-
|
|
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 }');
|
|
452
495
|
|
|
453
496
|
if (summaryZone === null || issuesZone === null) {
|
|
454
497
|
warn('R9', `Không định vị được khối \`summary\`/\`issues\` trong ${schema.actors[vtName]} — R9 bỏ qua`,
|
|
@@ -487,6 +530,76 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
487
530
|
}
|
|
488
531
|
}
|
|
489
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
|
+
}
|
|
490
603
|
// (f) per_sc (G48) — mọi field dùng làm khoá/so sánh phải CÓ THẬT trong khối
|
|
491
604
|
// `scenarios[]` mà validate-traces sinh ra. Neo vào field không tồn tại thì
|
|
492
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 ✅.
|
|
@@ -557,7 +670,10 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
557
670
|
const blockingFlags = new Set((gate.blocking || [])
|
|
558
671
|
.map(b => b.counter.replace(/_count$/, '').toUpperCase()));
|
|
559
672
|
for (const f of blockingFlags) {
|
|
560
|
-
|
|
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)) {
|
|
561
677
|
err('R9', `\`${f}\` nằm trong \`gate.blocking\` nhưng nó là cờ NỢ (🟠), không phải code hỏng (🔴)`,
|
|
562
678
|
`README §Traceability: 🟠 = "code chưa theo kịp spec", cố ý KHÔNG chặn.\n`
|
|
563
679
|
+ ` Chặn PR vì nợ ⇒ cổng đỏ liên tục ⇒ người ta tắt cổng ⇒ mất cả 🔴.`);
|
|
@@ -681,6 +797,317 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
|
|
|
681
797
|
}
|
|
682
798
|
}
|
|
683
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
|
+
|
|
684
1111
|
// ── report ────────────────────────────────────────────────────────────────────
|
|
685
1112
|
const tagCount = schema.tags.length;
|
|
686
1113
|
const colCount = schema.tsv_columns.length;
|