@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.
- package/bin/build.js +11 -1
- package/bin/lint-trace.js +397 -28
- package/bin/self-check.js +183 -12
- package/bin/trace-schema.json +2656 -1981
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/commands/dev-gen-test.md +62 -0
- package/core/commands/generate-code.md +1 -1
- package/core/commands/generate-tech-docs.md +3 -3
- package/core/commands/map-testids.md +88 -11
- package/core/commands/qc-analyze.md +509 -425
- package/core/commands/qc-design-test.md +475 -247
- package/core/commands/qc-plan.md +134 -93
- package/core/commands/qc-review.md +216 -131
- package/core/commands/qc-run-test.md +346 -231
- package/core/commands/validate-traces.md +17 -2
- package/core/rules/workflow.md +40 -0
- package/core/skills/qc/qa-analyst/DOC_GAP.template.md +9 -1
- package/core/skills/qc/qa-designer/shared/duplicate-check-procedure.md +33 -5
- package/core/skills/qc/qa-designer/shared/tc-metadata-format.md +24 -0
- package/core/skills/qc/qa-planner/test-plan.md +7 -0
- package/core/skills/qc/qa-runner/functional/gui-feature.md +1 -1
- package/core/skills/qc/qa-runner/functional/gui-screen.md +1 -1
- package/core/steps/qc-scope.md +67 -11
- package/core/steps/qc-stamp.md +142 -0
- package/core/steps/report-footer.md +13 -5
- package/core/templates/tech-design.template.md +3 -3
- package/docs/01-getting-started/quickstart.md +4 -3
- package/docs/02-concepts/architecture.md +14 -0
- package/docs/02-concepts/glossary.md +8 -0
- package/docs/02-concepts/overview.md +3 -2
- package/docs/02-concepts/pipeline-steps/04-bdd.md +1 -1
- package/docs/02-concepts/pipeline-steps/05-tech-docs.md +21 -5
- package/docs/02-concepts/pipeline-steps/06-code.md +12 -2
- package/docs/02-concepts/pipeline-steps/08-qc-automation.md +60 -12
- package/docs/02-concepts/pipeline-steps/README.md +4 -3
- package/docs/02-concepts/traceability.md +2 -2
- package/docs/03-guides/architect.md +2 -2
- package/docs/03-guides/developer.md +5 -2
- package/docs/03-guides/tester-qa.md +17 -5
- package/docs/04-reference/commands.md +6 -3
- package/docs/04-reference/trace-schema.md +1 -1
- package/docs/explain/07-generate-tech-docs.md +5 -3
- package/docs/explain/08-review-tech-docs.md +15 -3
- package/docs/explain/09-generate-code.md +30 -4
- package/docs/explain/10-review-code.md +1 -1
- package/docs/explain/11-map-testids.md +72 -70
- package/docs/explain/12-dev-gen-test.md +1 -1
- package/docs/explain/15-qc-analyze.md +14 -2
- package/docs/explain/16-qc-plan.md +5 -1
- package/docs/explain/17-qc-design-test.md +26 -3
- package/docs/explain/18-qc-review.md +6 -2
- package/docs/explain/19-qc-run-test.md +29 -6
- package/docs/explain/20-qc-report.md +5 -2
- package/docs/explain/README.md +4 -1
- 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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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}\` mà ${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;
|