@educa-corp/sdd-framework 0.9.5 → 0.9.6

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.
Files changed (55) hide show
  1. package/bin/build.js +11 -1
  2. package/bin/lint-trace.js +397 -28
  3. package/bin/self-check.js +183 -12
  4. package/bin/trace-schema.json +2656 -1981
  5. package/core/FRAMEWORK_VERSION +1 -1
  6. package/core/commands/dev-gen-test.md +62 -0
  7. package/core/commands/generate-code.md +1 -1
  8. package/core/commands/generate-tech-docs.md +3 -3
  9. package/core/commands/map-testids.md +88 -11
  10. package/core/commands/qc-analyze.md +509 -425
  11. package/core/commands/qc-design-test.md +475 -247
  12. package/core/commands/qc-plan.md +134 -93
  13. package/core/commands/qc-review.md +216 -131
  14. package/core/commands/qc-run-test.md +346 -231
  15. package/core/commands/validate-traces.md +17 -2
  16. package/core/rules/workflow.md +40 -0
  17. package/core/skills/qc/qa-analyst/DOC_GAP.template.md +9 -1
  18. package/core/skills/qc/qa-designer/shared/duplicate-check-procedure.md +33 -5
  19. package/core/skills/qc/qa-designer/shared/tc-metadata-format.md +24 -0
  20. package/core/skills/qc/qa-planner/test-plan.md +7 -0
  21. package/core/skills/qc/qa-runner/functional/gui-feature.md +1 -1
  22. package/core/skills/qc/qa-runner/functional/gui-screen.md +1 -1
  23. package/core/steps/qc-scope.md +67 -11
  24. package/core/steps/qc-stamp.md +142 -0
  25. package/core/steps/report-footer.md +13 -5
  26. package/core/templates/tech-design.template.md +3 -3
  27. package/docs/01-getting-started/quickstart.md +4 -3
  28. package/docs/02-concepts/architecture.md +14 -0
  29. package/docs/02-concepts/glossary.md +8 -0
  30. package/docs/02-concepts/overview.md +3 -2
  31. package/docs/02-concepts/pipeline-steps/04-bdd.md +1 -1
  32. package/docs/02-concepts/pipeline-steps/05-tech-docs.md +21 -5
  33. package/docs/02-concepts/pipeline-steps/06-code.md +12 -2
  34. package/docs/02-concepts/pipeline-steps/08-qc-automation.md +60 -12
  35. package/docs/02-concepts/pipeline-steps/README.md +4 -3
  36. package/docs/02-concepts/traceability.md +2 -2
  37. package/docs/03-guides/architect.md +2 -2
  38. package/docs/03-guides/developer.md +5 -2
  39. package/docs/03-guides/tester-qa.md +17 -5
  40. package/docs/04-reference/commands.md +6 -3
  41. package/docs/04-reference/trace-schema.md +1 -1
  42. package/docs/explain/07-generate-tech-docs.md +5 -3
  43. package/docs/explain/08-review-tech-docs.md +15 -3
  44. package/docs/explain/09-generate-code.md +30 -4
  45. package/docs/explain/10-review-code.md +1 -1
  46. package/docs/explain/11-map-testids.md +72 -70
  47. package/docs/explain/12-dev-gen-test.md +1 -1
  48. package/docs/explain/15-qc-analyze.md +14 -2
  49. package/docs/explain/16-qc-plan.md +5 -1
  50. package/docs/explain/17-qc-design-test.md +26 -3
  51. package/docs/explain/18-qc-review.md +6 -2
  52. package/docs/explain/19-qc-run-test.md +29 -6
  53. package/docs/explain/20-qc-report.md +5 -2
  54. package/docs/explain/README.md +4 -1
  55. package/package.json +1 -1
package/bin/self-check.js CHANGED
@@ -431,22 +431,69 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
431
431
  // schema đó chỉ là chữ. Đây là nửa "máy canh" của luật "khai tường minh + để máy canh" —
432
432
  // và hợp đồng test-id là chỗ luật đó bị thiếu lâu nhất (3 lệnh đọc, 2 lệnh ghi, 0 nơi kiểm).
433
433
  {
434
- const TC = schema.testid_contract;
435
- if (TC && Array.isArray(TC.lint_rules)) {
436
- const emits = new Set((lintText.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
437
- .map(m => /T\d+/.exec(m)[0]));
438
- for (const r of TC.lint_rules) {
439
- if (!r.why) {
440
- err('R8', `\`testid_contract.lint_rules\` mục \`${r.rule}\` thiếu \`why\``,
441
- 'Một rule không nói được SAO sẽ bị người sau coi tuỳ ý gỡ đi.');
434
+ const emits = new Set((lintText.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
435
+ .map(m => /T\d+/.exec(m)[0]));
436
+
437
+ // Gom MỌI chỗ trong schema khai một lint rule cho hợp đồng test-id.
438
+ // `testid_fourth_leg.lint_rule` vào đây G76: khối đó khai `T19` và — trước bản sửa này —
439
+ // KHÔNG chỗ nào ép nó tồn tại. Gỡ `warn('T19')` khỏi lint-trace vẫn build xanh, tức là
440
+ // khối schema mới thêm lại đúng khuôn B ("luật ĐÚNG, viết RÕ, KHÔNG AI CANH") mà nó sinh
441
+ // ra để chống. Một allowlist đọc-từ-một-khoá-cứng chỉ phủ đúng khoá đó cùng bài học R18.
442
+ const declaredRules = [
443
+ ...((schema.testid_contract || {}).lint_rules || [])
444
+ .map(r => ({ ...r, at: 'testid_contract.lint_rules' })),
445
+ ...((schema.testid_fourth_leg || {}).lint_rule
446
+ ? [{ ...schema.testid_fourth_leg.lint_rule, at: 'testid_fourth_leg.lint_rule' }]
447
+ : []),
448
+ ];
449
+
450
+ // ── R8f — qc_artifact_stamp: mỗi field phải khai `probe` + `artifact`, và T20 phải tồn tại ──
451
+ //
452
+ // G85: `fields.bdd_version` khai nguồn "| **Version** | của header .feature" — cú pháp hợp lệ,
453
+ // mô tả rõ ràng, và KHÔNG THỂ ĐÚNG (.feature là Gherkin, không có bảng). Một rule chỉ canh
454
+ // "mô tả có tồn tại" sẽ cho nó qua. Nên phần canh chia hai tầng, đúng khuôn self-check ↔ lint:
455
+ // R8f (đây) — canh CONTRACT: field có khai probe/artifact hợp lệ không
456
+ // T20 (lint-trace) — canh DỮ LIỆU : probe đó có TÌM ĐƯỢC ở artifact thật không
457
+ {
458
+ const QS = schema.qc_artifact_stamp;
459
+ if (QS && QS.fields) {
460
+ const KINDS = new Set(['prd', 'feature', 'tech-design', 'design-spec']);
461
+ for (const [name, f] of Object.entries(QS.fields)) {
462
+ if (!f || typeof f !== 'object' || !f.probe) {
463
+ err('R8', `\`qc_artifact_stamp.fields.${name}\` thiếu \`probe\``,
464
+ 'Không có probe thì không ai kiểm được mô tả nguồn có TÌM ĐƯỢC ở artifact thật\n'
465
+ + ' hay không — đúng khe mà G85 lọt qua.');
466
+ continue;
467
+ }
468
+ try { new RegExp(f.probe); }
469
+ catch { err('R8', `\`qc_artifact_stamp.fields.${name}.probe\` không phải regex hợp lệ`,
470
+ `Giá trị: ${f.probe}`); }
471
+ if (!KINDS.has(f.artifact)) {
472
+ err('R8', `\`qc_artifact_stamp.fields.${name}.artifact\` không hợp lệ: ${f.artifact}`,
473
+ `Phải là một trong: ${[...KINDS].join(' · ')}. Sai loại thì T20 soi nhầm tập file\n`
474
+ + ' và báo sạch trên một thứ chưa từng được kiểm.');
475
+ }
442
476
  }
443
- if (!emits.has(r.rule)) {
444
- err('R8', `schema khai \`testid_contract\` rule \`${r.rule}\` ${LINT_REL} KHÔNG phát ra`,
445
- 'Khai rule rồi mà checker không có nó = hợp đồng test-id không được kiểm ở đâu cả,\n'
446
- + ' trong khi bảng §4.5.6 3 lệnh đọc 2 lệnh ghi.');
477
+ const lr = QS.lint_rule;
478
+ if (lr && !new Set((lintText.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
479
+ .map(m => /T\d+/.exec(m)[0])).has(lr.rule)) {
480
+ err('R8', `schema khai \`qc_artifact_stamp.lint_rule\` rule \`${lr.rule}\` ${LINT_REL} KHÔNG phát ra`,
481
+ 'Khai rule rồi mà checker không có nó = mô tả nguồn không được kiểm ở đâu cả.');
447
482
  }
448
483
  }
449
484
  }
485
+
486
+ for (const r of declaredRules) {
487
+ if (!r.why) {
488
+ err('R8', `\`${r.at}\` mục \`${r.rule}\` thiếu \`why\``,
489
+ 'Một rule không nói được VÌ SAO sẽ bị người sau coi là tuỳ ý và gỡ đi.');
490
+ }
491
+ if (!emits.has(r.rule)) {
492
+ err('R8', `schema khai \`${r.at}\` rule \`${r.rule}\` mà ${LINT_REL} KHÔNG phát ra`,
493
+ 'Khai rule rồi mà checker không có nó = hợp đồng test-id không được kiểm ở đâu cả,\n'
494
+ + ' trong khi bảng §4.5.6 có 3 lệnh đọc và 2 lệnh ghi.');
495
+ }
496
+ }
450
497
  }
451
498
 
452
499
  // ── R8c — `n` phải là 1..N liền mạch, không trùng ──
@@ -1382,6 +1429,130 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
1382
1429
  }
1383
1430
  }
1384
1431
 
1432
+ // ── R17 — cờ bỏ qua điều kiện chỉ được mang MỘT tên: --force ──────────────────
1433
+ //
1434
+ // Vì sao: trước `gate.bypass_flags`, mỗi chỗ bypass đặt một tên riêng — `--force`
1435
+ // (generate-code), `--include-draft` (qc-scope), và exec-d0-b5 còn đang đề xuất
1436
+ // `--no-testid-contract` "theo đúng tinh thần --include-draft". Cái thứ ba chưa kịp sinh ra
1437
+ // đã thấy nó sẽ là cái thứ ba. Luật gom về một tên đã viết; đây là phần MÁY CANH.
1438
+ //
1439
+ // USE-CHECK, KHÔNG PHẢI MENTION-CHECK — cùng bài học R12. Một rule grep thô sẽ báo oan mỗi
1440
+ // lần file NHẮC TÊN một cờ cũ trong câu giải thích, mà rules/workflow.md đang cố ý nhắc
1441
+ // `--include-draft` bốn lần để giải thích tên đã bỏ. Nên chỉ quét DÒNG ĐỊNH NGHĨA cờ (hàng
1442
+ // bảng mở đầu bằng một cờ trong backtick), và BỎ QUA mọi dòng blockquote — nơi chứa toàn bộ
1443
+ // phần biện minh.
1444
+ {
1445
+ const BF = (schema.gate || {}).bypass_flags;
1446
+ if (BF) {
1447
+ const canonical = BF.canonical_name || '--force';
1448
+
1449
+ // Cờ KHÔNG phải bypass — chúng chọn chế độ/phạm vi, không bỏ qua điều kiện nào.
1450
+ const NON_BYPASS = new Set([
1451
+ '--yes', '--force', '--resume', '--fix', '--all', '--api', '--atomic-max', '--phase',
1452
+ '--from-code', '--uc', '--prd', '--domain', '--full', '--remote', '--init', '--apply',
1453
+ '--retire', '--review', '--interview', '--from', '--comment', '--code', '--trace',
1454
+ '--specs', '--flag', '--lint-trace', '--migrate-specs', '--warn-only',
1455
+ '--realign', '--realign-prd-version', '--realign-techdoc-revision', '--reconcile-code',
1456
+ ]);
1457
+ // Hình dạng một cờ BYPASS mới.
1458
+ const BYPASS_SHAPE = /^--(no|skip|ignore|include|allow|accept|bypass|override)-/;
1459
+
1460
+ for (const cmd of Object.keys(schema.actors || {})) {
1461
+ const rel = schema.actors[cmd];
1462
+ if (!/^commands\//.test(rel)) continue;
1463
+ const abs = path.join(ROOT, rel);
1464
+ if (!fs.existsSync(abs)) continue;
1465
+
1466
+ for (const line of fs.readFileSync(abs, 'utf8').split(/\r?\n/)) {
1467
+ if (/^\s*>/.test(line)) continue; // blockquote = phần giải thích
1468
+ const m = /^\s*\|\s*`(--[a-z0-9-]+)`/.exec(line); // hàng bảng định nghĩa cờ
1469
+ if (!m) continue;
1470
+ const flag = m[1];
1471
+ if (NON_BYPASS.has(flag) || !BYPASS_SHAPE.test(flag)) continue;
1472
+ err('R17', `${rel} định nghĩa cờ bypass riêng \`${flag}\``,
1473
+ `Mọi chỗ "biết mà vẫn chạy" dùng CHUNG một tên: \`${canonical}\`\n`
1474
+ + ` (schema → gate.bypass_flags · rules/workflow.md §Cờ bỏ qua điều kiện).\n`
1475
+ + ` Mỗi lệnh một tên riêng thì người dùng gặp cổng chặn, không nhớ lệnh này dùng\n`
1476
+ + ` từ nào, rồi đi SỬA SPEC cho hợp lệ giả thay vì khai mình đang chạy sớm.`);
1477
+ }
1478
+ }
1479
+
1480
+ for (const e of BF.declared_by || []) {
1481
+ if (!e.bypasses) {
1482
+ err('R17', `\`bypass_flags.declared_by\` mục \`${e.cmd}\` thiếu \`bypasses\``,
1483
+ 'Điều khoản (1): phạm vi khai TỪNG LỆNH, không bao giờ bao trùm. Không nói rõ nó bỏ\n'
1484
+ + ' qua ĐÚNG điều kiện nào thì `--force` thành "ghi đè tất cả".');
1485
+ }
1486
+ const rel = (schema.actors || {})[e.cmd] || `steps/${e.cmd}.md`;
1487
+ const abs = path.join(ROOT, rel);
1488
+ if (fs.existsSync(abs) && !fs.readFileSync(abs, 'utf8').includes(canonical)) {
1489
+ err('R17', `\`bypass_flags.declared_by\` khai \`${e.cmd}\` nhưng ${rel} không nhắc \`${canonical}\``,
1490
+ 'Khai một producer không tồn tại thì luật trông như đã được áp, trong khi không ai áp.');
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+
1496
+ // ── R18 — lệnh ghi artifact phải KHAI, và "có người nhập tay" thì phải `hard` ──
1497
+ //
1498
+ // Vì sao: `gate.checkpoint_levels` là ALLOWLIST với mặc định `normal` (mức LỎNG NHẤT), và
1499
+ // 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
1500
+ // lặp nào chạm tới. Nên "quên khai" = "mở cửa", và nó đã xảy ra BỐN lần: G67 · G77 · G78 · G79.
1501
+ //
1502
+ // 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
+ // thấy được cái ĐÃ KHAI, mà cái CHƯA KHAI mới là chỗ lỗi sống.
1504
+ {
1505
+ const AW = schema.artifact_writers;
1506
+ const CL = (schema.gate || {}).checkpoint_levels;
1507
+ if (AW && CL) {
1508
+ const hardSet = new Set((CL.hard || []).map(e => e.cmd));
1509
+ const enrolled = AW.enrolled || {};
1510
+ const pending = (AW.pending_enrollment || {}).cmds || [];
1511
+
1512
+ for (const cmd of Object.keys(enrolled)) {
1513
+ const e = enrolled[cmd];
1514
+ const rel = `commands/${cmd}.tmpl`;
1515
+
1516
+ if (!fs.existsSync(path.join(ROOT, rel))) {
1517
+ err('R18', `\`artifact_writers.enrolled\` khai \`${cmd}\` — ${rel} không tồn tại`,
1518
+ 'Lệnh bị đổi tên/bỏ? Khai một lệnh không có nghĩa là registry đang phủ nó.');
1519
+ continue;
1520
+ }
1521
+ if (!Array.isArray(e.writes) || !e.writes.length) {
1522
+ err('R18', `\`artifact_writers.enrolled.${cmd}\` thiếu \`writes\``,
1523
+ '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".');
1524
+ }
1525
+ if (typeof e.has_human_content !== 'boolean') {
1526
+ err('R18', `\`artifact_writers.enrolled.${cmd}\` thiếu \`has_human_content\` (boolean)`,
1527
+ 'Đây là CÂU HỎI mà bốn lần trước không ai hỏi: "file này có phần người nhập tay\n'
1528
+ + ' không?". Bỏ trống là quay lại đúng mặc định lỏng mà rule này sinh ra để đảo.');
1529
+ }
1530
+ // `why` BẮT BUỘC cả khi false — đó chính là G79.
1531
+ if (!e.why) {
1532
+ err('R18', `\`artifact_writers.enrolled.${cmd}\` thiếu \`why\``,
1533
+ 'Bắt buộc CẢ KHI `has_human_content: false`. Ghi đè an toàn mà không ai khai là an\n'
1534
+ + ' 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
+ }
1536
+ if (e.has_human_content === true && !hardSet.has(cmd)) {
1537
+ err('R18', `\`${cmd}\` ghi artifact CÓ phần người nhập tay nhưng KHÔNG ở \`checkpoint_levels.hard\``,
1538
+ `Khai: "${String(e.why || '').slice(0, 110)}…"\n`
1539
+ + ` Ghi đè thứ người nhập tay mà chỉ ở mức \`normal\` thì \`--yes\` xoá nó TRONG IM LẶNG.\n`
1540
+ + ` Sửa: thêm \`${cmd}\` vào gate.checkpoint_levels.hard + viết §luật chạy lại trong lệnh.`);
1541
+ }
1542
+ }
1543
+
1544
+ // Chiều ngược: lệnh đã ở `hard` VÌ LÝ DO GHI ĐÈ phải có mặt trong registry.
1545
+ for (const h of CL.hard || []) {
1546
+ if (!/ghi đè/i.test(String(h.why || ''))) continue; // `hard` vì lý do khác (vd --resume)
1547
+ if (!enrolled[h.cmd] && !pending.includes(h.cmd)) {
1548
+ err('R18', `\`${h.cmd}\` ở \`checkpoint_levels.hard\` vì ghi đè, nhưng không có trong \`artifact_writers\``,
1549
+ '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'
1550
+ + ' Thêm vào `enrolled` (đã rà) hoặc `pending_enrollment` (chưa rà, nợ được KHAI).');
1551
+ }
1552
+ }
1553
+ }
1554
+ }
1555
+
1385
1556
  // ── report ────────────────────────────────────────────────────────────────────
1386
1557
  const tagCount = schema.tags.length;
1387
1558
  const colCount = schema.tsv_columns.length;