From 1bd55c3f2d218f6033547213bc019a0896b03a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=E1=BA=A1nh?= Date: Wed, 23 Sep 2026 06:49:44 +0700 Subject: [PATCH] =?UTF-8?q?chore(kit):=20l=E1=BB=9Bp=20vendored=20l=C3=AAn?= =?UTF-8?q?=202.18.1=20(9=20=E2=86=92=2015=20t=E1=BB=87p),=20GI=E1=BB=AE?= =?UTF-8?q?=20fork=20stale-scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin scope project: acceptance-gate + feature-loop 2.16.0 → 2.18.1. Lớp CI theo INIT-CI-COPY-LIST của kit 2.18.1 (15 tệp). Sáu tệp vendor lần đầu (nhan-canh-gay, nguong-o-co-hoi, out-of-contract, product-map, trang-thai-ho-so, khong-can-nguoi); hai tệp lib/scripts đổi được chép nguyên bản; sáu tệp còn lại đã trùng khít. Không có `lib/out-of-contract.js` cũ để xoá. Tệp cổng là fork của kho (`feature_scope`) nên đi đường gộp BA CHIỀU — nền kit 11e98c47 (bản 2.17.0 mà lần nâng trước đã gộp vào; tám tệp còn lại trùng khít nền này), của-ta là fork, của-họ là gói 2.18.1. Gộp sạch, 0 xung đột; +78 dòng, các hunk trùng đúng từng dòng với diff nền→2.18.1 của kit; `feature_scope` còn nguyên bốn chỗ. Ba chỗ đi kèm, đều theo lệ sẵn có của kho cho tệp thước vendored: - `_acceptance/config.yaml`: sáu tệp mới vào `t1_skip_globs`. Thiếu dòng này chính PR nâng thước đỏ ở luật T1-escape (đo: exit 1, 1 VIOLATION). - `biome.json`: sáu tệp mới vào danh sách loại trừ. Thiếu thì `lint:check` đỏ vì định dạng/luật của mã kit. - `skills/acceptance/references/opportunity-template.md`: `product-map.mjs` 2.18.1 đọc khuôn này; vắng nó bản vendored thoát 2. Tệp NẰM NGOÀI danh sách 15 của kit. PRODUCT-MAP.md vẽ lại bằng product-map 2.18.1: đúng một dòng — `normalize-text-vi` mang nhãn «đã nghỉ, giữ sử liệu». Đo TRƯỚC↔SAU trên cây này: `pre-merge-check --base origin/main` exit 0 → exit 0, từng dòng giống hệt (chỉ khác số đếm «suppressed»); với `--recheck-all` cũng giống hệt. Không hồ sơ nào đổi phán quyết. Co-Authored-By: Claude Opus 5.5 --- PRODUCT-MAP.md | 2 +- _acceptance/config.yaml | 9 + biome.json | 6 + lib/nguong-o-co-hoi.cjs | 91 ++++ lib/nhan-canh-gay.cjs | 150 ++++++ lib/out-of-contract.cjs | 164 +++++++ lib/workspace-record.cjs | 104 ++++- scripts/khong-can-nguoi.mjs | 152 ++++++ scripts/pre-merge-check.sh | 78 ++++ scripts/product-map.mjs | 433 ++++++++++++++++++ scripts/recheck-evidence.cjs | 38 ++ scripts/trang-thai-ho-so.cjs | 83 ++++ .../references/opportunity-template.md | 136 ++++++ 13 files changed, 1444 insertions(+), 2 deletions(-) create mode 100644 lib/nguong-o-co-hoi.cjs create mode 100644 lib/nhan-canh-gay.cjs create mode 100644 lib/out-of-contract.cjs create mode 100644 scripts/khong-can-nguoi.mjs create mode 100644 scripts/product-map.mjs create mode 100644 scripts/trang-thai-ho-so.cjs create mode 100644 skills/acceptance/references/opportunity-template.md diff --git a/PRODUCT-MAP.md b/PRODUCT-MAP.md index 44a3e935..757e7a8f 100644 --- a/PRODUCT-MAP.md +++ b/PRODUCT-MAP.md @@ -53,7 +53,7 @@ flowchart TD - Local CPU plugins — ffmpeg and pyscenedetect off Modal (`local-cpu-plugins`) - Measurement harness — Whisper-vi WER, blind TTS rating, per-node COGS (`measure-harness`) - Mở hoá B01 — hạ cánh nhánh đổi định danh kho (ba README, NOTICE, SECURITY, CONTRIBUTING, CLAUDE.md, .github, docker-compose trỏ ảnh của fork, tắt trigger tag desktop-release) (`mo-hoa-b01`) — đã giao — không đo, khai ở Cổng Đáng -- slot đọc số/giá/ngày thành chữ tiếng Việt, bắt buộc đứng trước TTS (Phase 1.3) (`normalize-text-vi`) +- slot đọc số/giá/ngày thành chữ tiếng Việt, bắt buộc đứng trước TTS (Phase 1.3) (`normalize-text-vi`) — đã giao — đã nghỉ, giữ sử liệu - Ô đo chạy 0 ca thử mà vẫn báo đạt — hàng rào ở chốt CI (`o-do-chay-0-ca-van-xanh`) - Plugin directory prefix — accept oneflow-*, keep tongflow-* installable (`oneflow-plugin-prefix`) - Per-plugin origin in the official manifest (`per-plugin-origin`) diff --git a/_acceptance/config.yaml b/_acceptance/config.yaml index ed520541..f47e39dd 100644 --- a/_acceptance/config.yaml +++ b/_acceptance/config.yaml @@ -851,6 +851,15 @@ risk_tiers: # workspace-record: vendor LẦN ĐẦU ở lượt này. Trước đây oneflow thiếu hẳn # file này nên nhánh đọc workspace của cổng chưa từng chạy. - "lib/workspace-record.cjs" + # Kit 2.18.x: sáu tệp vendor LẦN ĐẦU (lớp CI 9 → 15 theo INIT-CI-COPY-LIST). + # Cùng lớp thước với các dòng trên — thiếu miễn trừ thì chính PR nâng thước + # đỏ ở luật T1-escape (đo 23/09 trên chore/kit-2-18-1). + - "lib/nhan-canh-gay.cjs" + - "lib/nguong-o-co-hoi.cjs" + - "lib/out-of-contract.cjs" + - "scripts/product-map.mjs" + - "scripts/trang-thai-ho-so.cjs" + - "scripts/khong-can-nguoi.mjs" # Lint configuration only — it constrains how source is written, never what # the product does at runtime, so a change here cannot invalidate evidence. - "biome.json" diff --git a/biome.json b/biome.json index 80b56b8a..adad0610 100644 --- a/biome.json +++ b/biome.json @@ -35,6 +35,12 @@ "!lib/workspace-record.cjs", "!lib/eval-yaml.cjs", "!lib/lop-nhin-thay.cjs", + "!lib/nhan-canh-gay.cjs", + "!lib/nguong-o-co-hoi.cjs", + "!lib/out-of-contract.cjs", + "!scripts/product-map.mjs", + "!scripts/trang-thai-ho-so.cjs", + "!scripts/khong-can-nguoi.mjs", "!_acceptance" ] }, diff --git a/lib/nguong-o-co-hoi.cjs b/lib/nguong-o-co-hoi.cjs new file mode 100644 index 00000000..d3f8007e --- /dev/null +++ b/lib/nguong-o-co-hoi.cjs @@ -0,0 +1,91 @@ +// nguong-o-co-hoi.cjs — LUẬT DUY NHẤT trả lời «ô ngưỡng của hồ sơ cơ hội đang ở đâu». +// +// Vì sao một chỗ: bộ quét, thẻ quyết định và ca đo đều phải phân loại cùng một ô. Vòng 1 +// của hồ sơ ra-co-ten viết luật này BA LẦN (start-scan · gate-card · ca đo) và bản thứ ba +// đã lệch ngay khi ra đời: nó chỉ đòi «mọi bullet ĐANG CÓ đều đã điền», không đòi ĐỦ nhãn +// của khuôn — nên một ô thiếu hẳn hai bullet bị hai bên kết luận trái nhau. Cùng doctrine +// với lib/workspace-record.cjs: ai thêm bên đọc thứ ba thì GỌI hàm này, đừng chép luật. +// +// Hàm THUẦN: nhận nội dung đã đọc, không tự đọc file — bên gọi quyết định đọc ở đâu. +'use strict'; +const { section } = require('./md-section.cjs'); + +const UAT_THRESHOLD_HEADING = 'Ngưỡng chết / ngưỡng UAT'; +const PLACEHOLDER_RE = /^(…|\.\.\.)?$/; +const bulletOf = l => { const m = l.match(/^\s*[-*]\s+([^:]+):(.*)$/); return m ? { label: m[1].trim(), value: m[2].trim() } : null; }; +// «Chưa điền» nhận diện RỘNG hơn bulletOf: dấu đầu dòng TUỲ CHỌN. Ô cơ hội viết tay có dòng +// ngưỡng không gạch đầu dòng; đòi gạch thì placeholder lọt thành «đã khai» — thẻ Cổng Phạm vi +// mất cờ vàng và đòi thêm `## Đường đo` cho ô chưa có thước nào, trong khi thresholdState (đi +// qua bulletOf) vẫn gọi «chưa chốt»: hai bộ đọc nói hai chuyện về CÙNG hồ sơ, đúng lớp lỗi +// mà lib này sinh ra để giết (S4-r8). bulletOf giữ CHẶT — nhãn của nó round-trip với khuôn. +const CHUA_DIEN_RE = /^[-*]?\s*[^:]*:\s*(…|\.\.\.)?\s*$/; +const chuaDien = l => CHUA_DIEN_RE.test(String(l).trim()); + +// Rút chuỗi máy-đọc từ khối marker của khuôn. FAIL-CLOSED: khuôn mất khối thì NÉM, không +// trả null. Một răng chặn tắt im lặng vì thiếu khuôn là chiều hỏng tệ nhất cho một cổng. +function prefixFromTemplate(tplText, marker) { + const m = String(tplText).match(new RegExp(`<<<${marker} -->\\n([\\s\\S]*?) +// "type":"revisit","stage":"gate2","at":"","decision":" — (): ghi hạn chế rồi ship","impact":"","serves":[""] +// GD{"Cổng Đáng"}`, + ` GD --> B["Sắp mở vòng
${n('sap-mo')}"]`, + ` GD --> XL["Xếp lại sau
${n('xep-lai')}"]`, + ` GD --> DB["Đã bác từ khám phá
${n('da-bac')}"]`, + ` B --> CD["Chờ duyệt phạm vi
${n('cho-duyet')}"] --> GP{"Cổng Phạm vi"}`, + ` GP --> DL["Đang làm
${n('dang-dung')}"] --> GB{"Cổng Bằng chứng"}`, + ` GB --> DG["Đã giao
${n('da-ship')}"]`, + ` GB --> CN["Chờ phiên nghiệm thu
${n('cho-nghiem-thu')}"] --> GG{"Cổng Giá trị"}`, + ` GG --> NT["Đã nghiệm thu giá trị
${n('da-nghiem-thu')}"]`, + ]; + // Ô hỏng KHÔNG nằm trong mạch — nó là cờ. Chỉ hiện khi có, và hiện thì phải + // đập vào mắt chứ không nấp cuối danh sách. + if (count.hong) lines.push(` HS["Hồ sơ hỏng
${dem(count.hong)}"]`); + lines.push('```'); + // Nhãn cổng đứng MỘT MÌNH trong hình là chữ người đọc lần đầu không suy ra + // được — "Đáng" nghĩa là gì? Mỗi cổng nói CÂU HỎI nó hỏi, ngay dưới hình + // (luật N6 + bảng tên cổng trong CONTEXT.md). Thẻ /start đã giải nghĩa bốn mã + // này ngay lần đầu hiện; bản đồ là mặt người thứ hai nên cũng phải tự đủ. + lines.push('', + '> **Bốn cổng người** — mỗi cổng là một câu hỏi chỉ người trả lời được:', + '> **Cổng Đáng** việc này có đáng làm không · **Cổng Phạm vi** bộ tiêu chí', + '> đã đủ và đúng chưa · **Cổng Bằng chứng** đã làm đúng thứ đã hứa chưa ·', + '> **Cổng Giá trị** thứ đã giao có ăn thua không.'); + return lines; +} +const UAT_KET_CUC = { + release: 'giao rộng (release)', + iterate: 'lặp thêm (iterate)', + kill: 'dừng (kill)', +}; + +// Doc file KHONG phai ho so workspace (chinh PRODUCT-MAP.md khi so noi dung, +// va cac de xuat trong .out-of-scope/). Ho so workspace di qua readRecord cua +// luat chung — phan biet ENOENT voi loi khac, khong nuot chung mot ro. +const readPlain = p => { try { return readFileSync(p, 'utf8'); } catch { return null; } }; +const fm = (txt, key) => (txt == null ? null : frontmatterField(txt, key)); +const low = v => (v == null ? null : v.toLowerCase()); + +// Cạnh chỉ hiện khi hồ sơ THẬT có khai — vắng thì im, không placeholder. +// (Write-side `epic:` thuộc vòng khám phá; đây chỉ là bên đọc.) +function edges(cTxt, oTxt) { + const pick = k => fm(cTxt, k) || fm(oTxt, k) || ''; + const out = []; + const epic = pick('epic'); if (epic) out.push(`epic: ${epic}`); + const sup = pick('supersedes'); if (sup) out.push(`thay thế: ${sup}`); + const rel = pick('relates'); if (rel) out.push(`liên quan: ${rel}`); + return out.length ? ' · ' + out.join(' · ') : ''; +} + +function classify(dir, slug) { + // Đọc LƯỜI và phân biệt lỗi, đúng thứ tự bộ quét đi: mở file nào là câu hỏi + // của luật chung. Đọc cả ba vô điều kiện thì (a) lỗi quyền ở một hồ sơ trạng + // thái hiện tại không dùng lại quyết định ô của slug, và (b) `read` cũ nuốt + // mọi lỗi thành "vắng" nên một contract.md mất quyền đọc bị xếp xuống đường + // opportunity — bản đồ nói "Sắp mở vòng" trong khi bộ quét nói hỏng (S4-r14). + const cR = readRecord(path.join(dir, 'contract.md')); + if (cR.err) return { key: 'hong', slug, file: 'contract.md', reason: ioReason(cR.err) }; + const cTxt = cR.t; + const uR = readRecord(path.join(dir, 'uat-session.md')); + const oR = readRecord(path.join(dir, 'opportunity.md')); + // Sổ quyết định — chỉ để hỏi vị từ NGHỈ; lỗi đọc KHÔNG quyết định ô của slug + // (sổ không phải hồ sơ điều hướng), nên nuốt về null như mọi bộ đọc sổ. + const ledgerTxt = readRecord(path.join(dir, 'decisions.jsonl')).t; + // Báo cáo đọc KHÔNG điều kiện CHỈ để hỏi vị từ nghỉ: `usesEvidence` loại + // `signed-off` khỏi tập tiêu thụ, mà hồ sơ đã ký lại đúng là hồ sơ duy nhất + // được quyền nghỉ sau khi thu phạm vi — dùng `eR.t` ở đây là mù với ca chính. + const nghiReport = readRecord(path.join(dir, 'evidence-report.md')).t; + + // HAI câu hỏi khác nhau, đừng trộn: + // PHÂN Ô — hồ sơ nào được quyền quyết định ô của slug, và lỗi của hồ sơ nào + // làm slug thành hỏng. Câu này do luật chung trả lời (consumedTexts), và + // lỗi ở hồ sơ KHÔNG được tiêu thụ thì không quyết định gì. + // HIỂN THỊ — tên việc và các cạnh (epic/thay thế/liên quan) lấy ở đâu. Câu + // này đọc CẢ BA, luôn luôn: `epic:` được khai lúc khám phá, tức nằm trong + // opportunity.md — mà opportunity chỉ được TIÊU THỤ khi chưa có hợp đồng + // hoặc đã ký. S4-r14 tôi gộp hai câu này làm một, nên suốt cả pha dựng + // (draft→verified) bản đồ mất tên việc lẫn mọi cạnh của hồ sơ khám phá + // (S4-r15 dựng lại được: `- Ban do gia tri (\`x\`) · epic: EP-1` thành + // `- \`x\``). Lỗi đọc ở hồ sơ không tiêu thụ chỉ làm mất phần hiển thị + // của nó, không làm hỏng slug. + if (usesUat(cTxt) && uR.err) + return { key: 'hong', slug, file: 'uat-session.md', reason: ioReason(uR.err) }; + if (usesOpportunity(cTxt, usesUat(cTxt) ? uR.t : null) && oR.err) + return { key: 'hong', slug, file: 'opportunity.md', reason: ioReason(oR.err) }; + // evidence-report.md tiêu thụ theo LUẬT CHUNG (implemented/verified) — trước + // đây bản đồ không đọc file này lần nào, nên mọi luật về nó sống riêng ở bộ + // quét và hai bên trôi nhau đúng trục đó (AC-1 workspace-reader-unification). + const eR = usesEvidence(cTxt) ? readRecord(path.join(dir, 'evidence-report.md')) : { t: null, err: null }; + if (usesEvidence(cTxt) && eR.err) + return { key: 'hong', slug, file: 'evidence-report.md', reason: ioReason(eR.err) }; + const uTxt = uR.t, oTxt = oR.t; + // `feature:` hay mở đầu bằng chính slug (" — mô tả"); dòng bản đồ đã + // in slug rồi nên để nguyên là đọc thành tiếng máy vọng lại hai lần. + const rawName = fm(cTxt, 'feature') || fm(oTxt, 'feature') || fm(uTxt, 'feature') || slug; + const name = rawName.startsWith(slug + ' — ') ? rawName.slice(slug.length + 3) : rawName; + const edge = edges(cTxt, oTxt); + // uat-session.md CHỈ được TIÊU THỤ khi hợp đồng đã ký — một phiên nghiệm thu + // nằm cạnh hợp đồng còn draft là hồ sơ chưa tới lượt, lỗi của nó không được + // quyết định ô của slug. Doctrine này của start-scan-hardening (học qua 4 + // round), và bộ quét vào phiên áp cùng luật — hai bên đọc phải tiêu thụ CÙNG + // tập hồ sơ thì mới có nghĩa khi so kết luận (case P123). + // ĐIỀU KIỆN TIÊU THỤ do LUẬT CHUNG trả lời — chép lại điều kiện ở đây là + // đúng cách hai reader trôi khỏi nhau suốt r12/r13 dù bảng enum đã gom. + const texts = consumedTexts({ contract: cTxt, opportunity: oTxt, uat: uTxt, evidence: eR.t }); + + // Lượt 1 — luật chung: hồ sơ đọc được không? (file có mà frontmatter hỏng, + // field bắt buộc rỗng, giá trị ngoài enum — tất cả là hỏng, không cái nào + // được rơi vào khoảng trống rồi hiện ở một ô bình thường.) Kèm luật + // khai-xong-mà-thiếu-file: verified mà vắng evidence-report là hỏng. + const problem = recordProblem(texts) || missingArtifact(texts) || conflictProblem(texts); + if (problem) return { key: 'hong', slug, file: problem.file, reason: problem.reason }; + + // Lượt 2 — xếp ô, tra từ artifact muộn nhất về sớm nhất. + const { status, stage, decision, verdict } = navValues(texts); + if (verdict) return { key: 'da-nghiem-thu', slug, name, edge, note: UAT_KET_CUC[verdict] }; + + // Khoá trạng thái trước, ô bản đồ suy từ nó qua BUCKET_OF — thay vì tự map + // status sang ô. Bucket vẫn THÔ như cũ (approved/implemented/verified gộp một + // nhãn) vì phép chiếu gom chúng lại, nên bản đồ vẫn đứng yên giữa hai lần + // đóng cổng người và `--check` không đỏ oan giữa vòng. + const o = k => ({ key: BUCKET_OF[k], slug, name, edge }); + // Hồ sơ NGHỈ — HỎI cùng vị từ với bộ quét (lib/workspace-record.cjs). Bản đồ + // tự suy ô từ status là bộ đọc thứ hai của cùng một sự thật, và nó đã trôi: + // hợp đồng chưa thông Cổng 2 mang dòng nghỉ thì bộ quét xếp «đã đóng có hồ sơ» + // còn bản đồ xếp «đang làm» — hai NHÓM khác nhau cho cùng một hồ sơ (S4-r1 t3). + // So ĐÚNG LOẠI như ba bộ đọc kia. Bản trước dùng giá trị trả về như đúng/sai, + // mà vị từ còn trả `dong-so-thieu` và `chua-ky` — hai hình dạng KHÔNG phải + // nghỉ — nên một dòng viết thiếu vẫn được bản đồ xếp vào ô nghỉ (lượt chấm 2). + if (status && hoSoNghi({ ledgerText: ledgerTxt, reportText: nghiReport, + contractAtRoot: true, suLieuContract: false })?.kieu === 'dong-so') { + const kNghi = DA_THONG_CONG_2.includes(status) ? 'da-nghi' : 'da-dong-ho-so'; + return { ...o(kNghi), note: chu(kNghi).nhan }; + } + // Trạng thái thứ bảy (ADR 0020 Đ8): reality đã chấm — ô cuối, cùng khoá với bộ quét. + if (status && DA_DONG_THUC_TE && DA_DONG_THUC_TE.includes(status)) return { ...o('da-cham-thuc-te'), note: chu('da-cham-thuc-te').nhan }; + if (status) { + if (DA_THONG_CONG_2.includes(status)) { + // LỜI KHAI PHẢI CÓ VẬT — hỏi TRƯỚC mọi nhánh rẽ, đúng vị trí start-scan đặt nó + // (scripts/start-scan.mjs, ngay sau khối DA_THONG_CONG_2). Đặt chốt này BÊN TRONG + // nhánh `status === 'machine-cleared'` ở cuối là vô dụng: hồ sơ đường A thoát ra ở + // `if (duongA) return …` TRƯỚC đó, nên bản đồ lại tin thẳng frontmatter trong khi bộ + // quét gọi HỎNG — đúng lớp lỗi vừa định giết, chỉ đổi chỗ (S4-r12 [0][1]). + if (status === 'machine-cleared' && !khongCanNguoi(cTxt, texts['evidence-report.md'] || '', + { findings: readRecord(path.join(dir, 'review-findings.md')).t ?? null, ledger: ledgerTxt ?? null })) { + return { key: 'hong', slug, file: 'evidence-report.md', reason: 'status machine-cleared nhưng bằng chứng KHÔNG đạt sáu điều kiện xanh-sạch — hồ sơ tự khai «máy đã thông» mà không có vật' }; + } + // Đường A (cơ hội quyết build/iterate) còn một cổng người nữa: phiên + // nghiệm thu. Đường B/C/E ship thẳng — không dựng phiên giả cho chúng. + const duongA = decision === 'build' || decision === 'iterate'; + // Ô ngưỡng khai «không đo được» thì vòng ĐÓNG ngay tại đây: không có phiên nghiệm + // thu để chờ. Nhánh này phải có Ở CẢ HAI bộ đọc — bộ quét có mà bản đồ không là hai + // bên nói hai chuyện về cùng hồ sơ, đúng lớp lib/workspace-record.cjs dựng ra để giết + // (finding S4-r2, đo trên chính hồ sơ duong-do). Luật hỏi LIB, không chép. + // FAIL-CLOSED như hai bộ đọc anh em (start-scan bail, gate-card exit 2): khuôn hỏng + // thì CHẾT TO kèm tên khuôn. Nuốt lỗi ở đây là bản đồ im lặng xếp sai ô trong khi + // ba bộ đọc kia đỏ — hai bên nói hai chuyện, và `--check` mất luôn căn cứ miễn trừ + // t1 của chính bản đồ (finding S4-r4). + if (duongA && oTxt) { + const ngBD = thresholdStateOrDie(oTxt); + if (ngBD === 'khong-do-duoc') return { ...o('da-giao-khong-do'), note: chu('da-giao-khong-do').nhan }; + } + if (duongA) return o('cho-cong-gia-tri'); + // Ô kết của làn V có TÊN RIÊNG — bản đồ vẫn gom theo giai đoạn (cùng ô «Đã + // giao»), nhưng chữ đi kèm phải phân biệt máy-thông với hồ sơ người ký. + if (status === 'machine-cleared') { + const vMo = (frontmatterField(cTxt, 'veto_state') || '').trim().toLowerCase() === 'mo'; + const k = vMo ? 'da-giao-may-thong-veto-mo' : 'da-giao-may-thong-xanh-sach'; + return { ...o(k), note: chu(k).nhan }; + } + return o('da-giao'); + } + if (status === 'draft') return o('cho-cong-pham-vi'); + return o('dang-viet-code'); + } + + if (stage === 'archived') return { ...o('da-dong-ho-so'), note: chu('da-dong-ho-so').nhan }; + if (stage !== 'decided' || !decision) return o('y-can-nhac'); + if (decision === 'build' || decision === 'iterate') return o('sap-mo-vong'); + if (decision === 'park') return o('xep-lai'); + return o('da-bac'); +} + +export function renderProductMap(root) { + const acc = path.join(root, '_acceptance'); + const buckets = Object.fromEntries(SECTIONS.map(([k]) => [k, []])); + + if (existsSync(acc)) { + for (const entry of readdirSync(acc, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const item = classify(path.join(acc, entry.name), entry.name); + buckets[item.key].push(item); + } + } + + const oos = path.join(root, '.out-of-scope'); + if (existsSync(oos)) { + for (const f of readdirSync(oos)) { + if (!f.endsWith('.md')) continue; + const txt = readPlain(path.join(oos, f)) || ''; + const title = (txt.split('\n').find(l => l.startsWith('# ')) || '# ' + f).slice(2).trim(); + buckets['ngoai-pham-vi'].push({ slug: f.replace(/\.md$/, ''), name: title, file: f }); + } + } + + // Không timestamp, sort theo slug: hai lần chạy phải cho hai bản giống hệt, + // nếu không thì `--check` không so được bằng đẳng thức. + const lines = [ + '# Bản đồ sản phẩm', + '', + '> Bản đồ vẽ lại từ hồ sơ của xưởng mỗi lần một người ký một cổng — đừng sửa tay.', + '> (đọc từ thư mục `_acceptance/` và `.out-of-scope/`)', + '', + ...mermaidBlock(Object.fromEntries(SECTIONS.map(([k]) => [k, buckets[k].length]))), + '', + ]; + for (const [key, title] of SECTIONS) { + const items = buckets[key].sort((a, b) => a.slug.localeCompare(b.slug)); + if (!items.length) continue; + lines.push(`## ${title}`, ''); + for (const it of items) { + // Tên việc đứng trước, slug xuống cuối dòng trong ngoặc: mã là thứ để + // TRA CỨU, không phải nội dung (N3). Không in đậm — tên việc thừa hưởng + // từ hồ sơ có thể dài cả câu, bôi đậm cả câu thì dòng nặng và hết quét + // được. Hình ở đầu file mới là thứ dẫn mắt. + if (key === 'hong') lines.push(`- \`${it.slug}\` — không đọc được hồ sơ (\`${it.file}\`): ${it.reason}`); + else if (key === 'ngoai-pham-vi') lines.push(`- ${it.name} (\`.out-of-scope/${it.file}\`)`); + else if (it.name === it.slug) lines.push(`- \`${it.slug}\`${it.note ? ` — ${it.note}` : ''}${it.edge}`); + else lines.push(`- ${it.name} (\`${it.slug}\`)${it.note ? ` — ${it.note}` : ''}${it.edge}`); + } + lines.push(''); + } + return lines.join('\n'); +} + +// ─── CLI ─── +// So bằng realpath, KHÔNG bằng path.resolve: loader ESM đã giải symlink cho +// import.meta.url trong khi argv[1] giữ nguyên đường người gõ. Repo nằm dưới +// một symlink (/tmp, /var/folders trên macOS, home mount…) thì hai chuỗi khác +// nhau, khối CLI im lặng không chạy, và `--check` exit 0 mà chẳng kiểm gì — +// đúng dạng false-green. Case P119 chạy trong bản sao dưới /var/folders bắt +// được điều này. +const isMain = (() => { + if (!process.argv[1]) return false; + const same = (a, b) => { try { return realpathSync(a) === realpathSync(b); } catch { return false; } }; + return same(process.argv[1], __filename) || + path.resolve(process.argv[1]) === path.resolve(__filename); +})(); +if (isMain) { + const args = process.argv.slice(2); + // Chốt mode + thứ tự tham số, theo đúng khuôn kit đã dựng cho chính lớp lỗi + // này (pre-merge-check v1.22.1): một lỗi gõ (`--chek`) không được biến lệnh + // KIỂM thành lệnh GHI rồi báo thành công — nó xoá luôn cái drift vừa tiêm. + // Và `--root` không có giá trị thì `--check` bị nuốt làm đường dẫn, script + // soi một thư mục không tồn tại rồi exit 0 mà chẳng kiểm gì. ADR 0007 lấy + // `--check` làm cổng độc lập DUY NHẤT biện minh cho miễn trừ t1 của bản đồ, + // nên `--check` fail-open là cả miễn trừ mất căn cứ. + for (let i = 0; i < args.length; i++) { + if (args[i] === '--check') continue; + if (args[i] === '--root') { + const v = args[i + 1]; + if (!v || v.startsWith('--')) { + console.error('product-map: `--root` cần một đường dẫn ngay sau nó — nhận được ' + + (v ? `\`${v}\`` : '(trống)')); + process.exit(2); + } + i++; + continue; + } + console.error(`product-map: tham số lạ ${args[i]} — chỉ nhận \`--root \` và \`--check\``); + process.exit(2); + } + const rootIx = args.indexOf('--root'); + const root = path.resolve(rootIx >= 0 && args[rootIx + 1] ? args[rootIx + 1] : '.'); + const mapPath = path.join(root, 'PRODUCT-MAP.md'); + const check = args.includes('--check'); + // Gợi ý lệnh phải chạy được ở CHÍNH repo đang đỏ, nên suy từ vị trí script + // thật: tự host → `scripts/product-map.mjs`; repo tiêu thụ → đường dẫn + // plugin. Ghim một đường dẫn self-host vào đây là đưa consumer một lệnh + // exit 127 ngay lúc họ cần nó nhất. + const rel = path.relative(root, __filename); + const hint = rel && !rel.startsWith('..') ? rel : __filename; + + // AC-5: đường dẫn sai phải CHẾT TO (exit 2), không rơi xuống "chưa dựng + // cổng" rồi exit 0 — mode kiểm phân biệt "chưa init" với "gõ nhầm đường". + if (!existsSync(root)) { + console.error(`product-map: --root trỏ đường dẫn không tồn tại: ${root}`); + process.exit(2); + } + if (!statSync(root).isDirectory()) { + console.error(`product-map: --root trỏ vào thứ không phải thư mục: ${root}`); + process.exit(2); + } + if (!existsSync(path.join(root, '_acceptance', 'config.yaml'))) { + console.log('Repo chưa dựng cổng nghiệm thu — chưa có gì để vẽ bản đồ.'); + process.exit(0); + } + const rendered = renderProductMap(root); + if (!check) { + writeFileSync(mapPath, rendered); + console.log(mapPath); + process.exit(0); + } + if (!existsSync(mapPath)) { + // Phân biệt "repo CHƯA TỪNG dựng bản đồ" (đường đọc-cũ, hợp lệ) với "đã có + // rồi MẤT". File được git theo dõi mà biến khỏi cây làm việc là một lần + // XOÁ — và vì bản đồ nằm trong t1_skip_globs, một PR chỉ xoá nó vừa bỏ qua + // cổng nghiệm thu vừa xanh ở CI nếu đây cũng exit 0. Một vật máy sinh biến + // mất phải làm cổng ĐỎ, không phải làm cổng im. + // Hỏi INDEX thôi là fail-OPEN ở đúng hình dạng CI gặp: một PR đã COMMIT + // việc xoá thì `actions/checkout` ra cây không còn file trong index, + // `ls-files` thất bại, và cổng in "repo chưa dựng bản đồ" rồi exit 0 — + // tức PR xoá bản đồ vừa bỏ qua cổng nghiệm thu vừa xanh CI (S4-r14, dựng + // lại được). Nên hỏi thêm LỊCH SỬ: file từng bị xoá trong quá khứ cũng là + // "đã có rồi mất". Chỉ repo chưa từng có bản đồ mới đi đường đọc-cũ. + // Ba tín hiệu (config daBat · git index · git history) nay sống MỘT chỗ + // trong lib.mapTracked — bộ quét vào phiên hỏi cùng hàm đó, hết cảnh hai + // bên suy tracked từ hai tín hiệu khác nhau (S4-r1 vòng này). Toàn bộ + // rationale từng dòng tín hiệu ghi tại lib/workspace-record.cjs. + const cfgTxt = (() => { try { return readFileSync(path.join(root, '_acceptance', 'config.yaml'), 'utf8'); } catch { return ''; } })(); + const daTheoDoi = mapTracked(root, cfgTxt); + // Nhãn rút từ BẢNG NHÃN CHUNG (lib) — cùng chữ với thẻ /start (AC-3/AC-7). + const state = mapState({ exists: false, tracked: daTheoDoi }); + if (state === 'da-xoa') { + console.error(`${MAP_LABELS[state]} — khôi phục, hoặc vẽ lại: node ${hint} --root .`); + process.exit(1); + } + console.log(`${MAP_LABELS[state]} — PRODUCT-MAP.md chưa có; bật thì nó tự sinh ở lần đóng cổng người kế.`); + process.exit(0); + } + if (readPlain(mapPath) === rendered) { + console.log('PRODUCT-MAP.md khớp hồ sơ xưởng.'); + process.exit(0); + } + console.error(`PRODUCT-MAP.md lệch với hồ sơ xưởng — chạy: node ${hint} --root .`); + process.exit(1); +} diff --git a/scripts/recheck-evidence.cjs b/scripts/recheck-evidence.cjs index 718ed0e0..55d54c60 100644 --- a/scripts/recheck-evidence.cjs +++ b/scripts/recheck-evidence.cjs @@ -40,6 +40,44 @@ try { process.exit(2); } +// ── Trạng thái thứ bảy: REALITY ĐÃ CHẤM (hồ sơ nhan-trang-thai-va-reality, AC-10) ────── +// Hợp đồng cùng thư mục khai `da-cham-boi-thuc-te` → CÙNG hàm checkThucTe với lưới +// trước-merge. Dòng hợp lệ → NOTE, không chấm bằng chứng (reality là cuối); thiếu vế / +// bản dựng lạ / thước đổi sau dòng quan sát → đỏ; dòng vắng hoặc đã mở lại → luật cũ. +{ + const dir = path.dirname(path.resolve(reportPath)); + let st = ''; + try { st = String(core.frontmatterField(fs.readFileSync(path.join(dir, 'contract.md'), 'utf8'), 'status') || '').trim().toLowerCase(); } catch { st = ''; } + let wr = null; + try { wr = require(path.join(__dirname, '..', 'lib', 'workspace-record.cjs')); } catch { wr = null; } + if (wr && typeof wr.checkThucTe === 'function' && Array.isArray(wr.DA_DONG_THUC_TE) && wr.DA_DONG_THUC_TE.includes(st)) { + const r = wr.checkThucTe(path.dirname(path.dirname(dir)), path.basename(dir)); + if (r.code === 0) { process.stderr.write(`recheck-evidence: NOTE ${reportPath} — đã chấm bởi thực tế: ${r.out.replace(/^OK /, '')}\n`); process.exit(0); } + if (r.code !== 2) { process.stderr.write(`recheck-evidence: ${reportPath} — đã chấm bởi thực tế nhưng ${r.out}\n`); process.exit(1); } + } +} + +// ── Ký trên CẠNH GÃY có tên (hồ sơ nhan-trang-thai-va-reality, AC-8) ──────────── +// Báo cáo BLOCKED mang chữ ký người là bằng chứng hợp lệ CHỈ khi thẻ đã mở ô ký trên cạnh gãy +// (bàn đo / hệ thống chết đã thử lại) và MỖI mục chặn có dòng sổ `revisit` đúng khuôn. MỘT +// vị từ với lưới trước-merge và thẻ: lib/nhan-canh-gay.cjs. Lib vắng → đi tiếp như cũ (bản +// lib cũ hơn luật; lưới trước-merge vẫn đóng BLOCKED). +{ + const vm = payload.match(/^verdict:\s*([^\s#]+)/m); + const verdict = vm ? vm[1].replace(/["']/g, '').toUpperCase() : ''; + let ncg = null; + try { ncg = require(path.join(__dirname, '..', 'lib', 'nhan-canh-gay.cjs')); } catch { ncg = null; } + const ky = core.frontmatterField ? String(core.frontmatterField(payload, 'human_signoff') || '').trim() : ''; + if (ncg && verdict === 'BLOCKED' && ky) { + const dir = path.dirname(path.resolve(reportPath)); + const r = ncg.checkHoSo(path.dirname(path.dirname(dir)), path.basename(dir)); + if (r.code === 0) { process.stderr.write(`recheck-evidence: NOTE ${reportPath} — ký trên cạnh gãy có tên: ${r.out.replace(/^OK /, '')}\n`); process.exit(0); } + if (r.code === 1) { process.stderr.write(`recheck-evidence: ${reportPath} — ký trên cạnh gãy thiếu dòng sổ revisit cho ${r.out.replace(/^THIEU /, '')}\n`); process.exit(1); } + process.stderr.write(`recheck-evidence: ${reportPath} — báo cáo BLOCKED mang chữ ký nhưng không phải cạnh gãy mở ký được (${r.out.replace(/^KHONG /, '')})\n`); + process.exit(1); + } +} + // Only a PASS-family verdict carries an evidence bar to re-check. if (!core.determineEnforce(payload)) process.exit(0); diff --git a/scripts/trang-thai-ho-so.cjs b/scripts/trang-thai-ho-so.cjs new file mode 100644 index 00000000..f0a69bd3 --- /dev/null +++ b/scripts/trang-thai-ho-so.cjs @@ -0,0 +1,83 @@ +// trang-thai-ho-so.cjs — MỘT bảng chữ cho mọi bộ đọc mặt người. +// +// Vì sao ở scripts/ chứ không lib/: `lib/**` là `risk_tiers.t3_paths` vì nó là +// LÕI CƯỠNG CHẾ — bug ở đó sinh màu xanh giả trên mọi repo tiêu thụ. Một bảng +// chữ hỏng chỉ cho CHỮ SAI trên thẻ, không cho MÀU XANH GIẢ. Đặt vào lib/ nâng +// hồ sơ này lên T3 = thêm một chốt kế hoạch + một chữ ký bắt buộc, cho đúng cái +// hồ sơ sinh ra để giảm số lượt gọi người (ledger d-20260823T012029Z-27499). +// Tiền lệ cùng lý do: scripts/khong-can-nguoi.mjs. +// +// KHÔNG trùng vai `MAP_LABELS` trong lib/workspace-record.cjs: bảng kia là +// trạng thái của FILE bản đồ, bảng này là trạng thái của HỒ SƠ — hai trục khác +// nhau, không có gì phải giữ đồng bộ. +// +// File này THUẦN DỮ LIỆU: không đọc file, không phán trạng thái. Bộ PHÂN Ô duy +// nhất là scripts/start-scan.mjs; ai cần trạng thái thì HỎI NÓ, đừng dựng lại — +// dựng lại là bản thứ ba của lớp lỗi lan-v-khong-phai-cho-ky đã trả giá. +'use strict'; + +const TRANG_THAI = { + 'y-can-nhac': { nhan: 'đang cân nhắc', viecKe: 'người: điền ngưỡng thì mới có gì để ký' }, + 'cho-cong-dang': { nhan: 'chờ chữ ký — Cổng Đáng', viecKe: 'người: quyết có làm việc này không' }, + 'sap-mo-vong': { nhan: 'sắp mở vòng', viecKe: 'máy: chốt thiết kế và bộ tiêu chí' }, + 'xep-lai': { nhan: 'xếp lại sau', viecKe: 'không ai — đã quyết để lại' }, + 'da-bac': { nhan: 'đã bác từ khám phá', viecKe: 'không ai — đã quyết không làm' }, + 'cho-cong-pham-vi': { nhan: 'chờ chữ ký — Cổng Phạm vi', viecKe: 'người: duyệt bộ tiêu chí trước khi code' }, + 'dang-lap-ke-hoach': { nhan: 'đang lập kế hoạch', viecKe: 'máy: lập kế hoạch thi công' }, + 'dang-viet-code': { nhan: 'đang viết code', viecKe: 'máy: viết code theo kế hoạch' }, + 'vat-da-o-nhanh-goc': { nhan: 'vật đã nằm trong nhánh gốc — hồ sơ còn treo ở «đã duyệt»', + viecKe: 'người: chọn một lối — đóng theo quan sát (ghi quyết định, không chấm lại) hoặc chấm lại (đưa về «code xong», chạy nghiệm thu máy)' }, + 'cho-nghiem-thu-may': { nhan: 'code xong, chưa ai chấm', viecKe: 'máy: chạy vòng nghiệm thu' }, + 'dang-sua-theo-bang-chung': { nhan: 'đang sửa theo bằng chứng', viecKe: 'máy: sửa rồi chấm vòng mới' }, + 'nghiem-thu-bi-chan': { nhan: 'nghiệm thu bị chặn', viecKe: 'máy: khắc phục nguyên nhân kẹt rồi chạy lại' }, + 'cho-cong-bang-chung': { nhan: 'chờ chữ ký — Cổng Bằng chứng', viecKe: 'người: đọc bằng chứng rồi ký' }, + 'may-di-tiep-veto-mo': { nhan: 'máy đi tiếp — cửa veto còn mở', viecKe: 'người: veto lúc nào cũng được, cửa không có hạn' }, + 'may-di-tiep-xanh-sach': { nhan: 'máy đi tiếp — bằng chứng xanh-sạch', viecKe: 'không ai — người đã duyệt hoặc miễn Cổng Phạm vi' }, + 'da-giao': { nhan: 'đã giao', viecKe: 'không ai — vòng đã đóng' }, + 'da-giao-may-thong-veto-mo': { nhan: 'đã giao — máy thông, cửa veto còn mở', viecKe: 'người: veto lúc nào cũng được, cửa không có hạn' }, + 'da-giao-may-thong-xanh-sach': { nhan: 'đã giao — máy thông, bằng chứng xanh-sạch', viecKe: 'không ai — người đã duyệt hoặc miễn Cổng Phạm vi' }, + 'da-giao-khong-do': { nhan: 'đã giao — không đo, khai ở Cổng Đáng', viecKe: 'không ai — vòng đóng theo lời khai, không có phiên nghiệm thu' }, + 'da-dong-ho-so': { nhan: 'đã đóng có hồ sơ', viecKe: 'không ai — đã quyết dừng, hồ sơ giữ lại' }, + 'da-nghi': { nhan: 'đã giao — đã nghỉ, giữ sử liệu', + viecKe: 'không ai — đã quyết cho nghỉ, chữ ký giữ làm sử liệu' }, + // Trạng thái thứ bảy (ADR 0020 Đ8): reality đã chấm — người ghi bản dựng prod + ngày + tên. + 'da-cham-thuc-te': { nhan: 'đã chấm bởi thực tế', viecKe: 'không ai — reality đã chấm' }, + 'cho-cong-gia-tri': { nhan: 'chờ chữ ký — Cổng Giá trị', viecKe: 'người: xem số thật từ phiên nghiệm thu rồi quyết' }, + 'da-nghiem-thu-release': { nhan: 'đã nghiệm thu — giao rộng', viecKe: 'không ai — vòng đã đóng' }, + 'da-nghiem-thu-iterate': { nhan: 'đã nghiệm thu — lặp thêm', viecKe: 'người: mở vòng kế' }, + 'da-nghiem-thu-kill': { nhan: 'đã nghiệm thu — dừng', viecKe: 'không ai — đã quyết dừng' }, + 'ho-so-hong': { nhan: 'hồ sơ đọc không được', viecKe: 'người: sửa hồ sơ rồi quét lại' }, +}; + +// Phép chiếu NHIỀU-VỀ-MỘT sang ô của bản đồ sản phẩm. Bản đồ cố ý KHÔNG mang vị +// từ (đã quyết; known-limit lan-v-khong-phai-cho-ky) — nó gom theo GIAI ĐOẠN. +// Chiếu ở đây là cách hai bên hết trôi mà bản đồ vẫn giữ nguyên vai cũ: đổi +// đường ĐI TỚI ô, không đổi TÊN ô. +const BUCKET_OF = { + 'y-can-nhac': 'can-nhac', 'cho-cong-dang': 'can-nhac', + 'sap-mo-vong': 'sap-mo', 'xep-lai': 'xep-lai', + 'da-bac': 'da-bac', 'cho-cong-pham-vi': 'cho-duyet', + 'dang-lap-ke-hoach': 'dang-dung', 'dang-viet-code': 'dang-dung', + 'vat-da-o-nhanh-goc': 'dang-dung', + 'cho-nghiem-thu-may': 'dang-dung', 'dang-sua-theo-bang-chung': 'dang-dung', + 'nghiem-thu-bi-chan': 'dang-dung', 'cho-cong-bang-chung': 'dang-dung', + 'may-di-tiep-veto-mo': 'dang-dung', 'may-di-tiep-xanh-sach': 'dang-dung', + 'da-giao': 'da-ship', 'cho-cong-gia-tri': 'cho-nghiem-thu', + 'da-giao-may-thong-veto-mo': 'da-ship', 'da-giao-may-thong-xanh-sach': 'da-ship', + 'da-giao-khong-do': 'da-ship', 'da-dong-ho-so': 'da-bac', + 'da-nghi': 'da-ship', 'da-cham-thuc-te': 'da-ship', + 'da-nghiem-thu-release': 'da-nghiem-thu', 'da-nghiem-thu-iterate': 'da-nghiem-thu', + 'da-nghiem-thu-kill': 'da-nghiem-thu', 'ho-so-hong': 'hong', +}; + +const STATE_KEYS = Object.keys(TRANG_THAI); + +// Khoá lạ CHẾT TO: trả một mặc định câm ở đây nghĩa là một trạng thái mới lọt ra +// mặt người dưới TÊN CỦA TRẠNG THÁI KHÁC — đúng lớp lỗi bảng này sinh ra để chặn. +function chu(key) { + const c = TRANG_THAI[key]; + if (!c) throw new Error(`trang-thai-ho-so: khoá không có trong bảng: ${key}`); + return c; +} + +module.exports = { TRANG_THAI, BUCKET_OF, STATE_KEYS, chu }; diff --git a/skills/acceptance/references/opportunity-template.md b/skills/acceptance/references/opportunity-template.md new file mode 100644 index 00000000..bca9e978 --- /dev/null +++ b/skills/acceptance/references/opportunity-template.md @@ -0,0 +1,136 @@ +# Opportunity — khuôn D1b (điền xong mới tới red-team D2; Cổng 0 ký trên file này) + +> Bản mẫu rút từ vòng V1 thật (trang-tu-van-v2, artifact-platform, 27-28/07/2026) +> — mọi section dưới đây đều đã được dùng thật ít nhất một lần trong vòng đó. +> Vị trí khi dùng: `_acceptance//opportunity.md` của repo sản phẩm. +> Frontmatter là phần MÁY ĐỌC (card/funnel Cổng 0 sẽ đọc) — giữ nguyên tên key, +> chỉ thay giá trị. Xoá các dòng hướng dẫn `>` khi dùng thật. + +**ĐỪNG chép hai dòng chú thích `` và hàng rào ```` ```yaml ```` quanh +frontmatter.** Chúng ở đó để test rút được đúng khuôn máy-đọc (case P115 cho nó +đi vòng qua chính bộ đọc mà các cổng dùng); `opportunity.md` thật phải BẮT ĐẦU +ngay ở dòng `---`. Chép cả hàng rào thì dòng đầu file là ```` ```yaml ````, mọi +bên đọc coi đây là hồ sơ hỏng và slug rơi xuống mục "Hồ sơ hỏng". + + +```yaml +--- +schema_version: 1 +slug: {slug} +feature: {feature} +owner: {owner} +stage: {stage} # discovery | decided | archived +decision: {decision} # build | iterate | park | kill — người ký Cổng 0 điền +decided_by: {decided_by} +decided_at: {decided_at} # ISO UTC +prototype: + base_commit: {base_commit} # điểm cắt nhánh proto khỏi nhánh chính — guard diffBase khi keep + disposition: {disposition} # keep | archive +--- +``` + + +## Vấn đề & ai gặp + +> Ai đang đau, đau ở khâu nào của vòng, bằng chứng thực địa nào (ngày + nguồn). +> Nêu cả vật liệu nền (spec/PRD/gói thiết kế cũ) — nhưng vật liệu NGOÀI repo thì +> khai tiếp ở section "Nguồn ngoài & phạm vi kế thừa", không vào nguyên khối ở đây. + +## Giả định chốt sinh tử + +> Xếp hạng, re-rank sau red-team D2 (giữ vết re-rank). Trạng thái cập nhật khi +> có phép thử; ưu tiên phép thử KHÔNG cần dựng (D2.5) trước, rẻ → đắt. + +| # | Giả định | Nếu sai thì | Phép thử rẻ nhất | Trạng thái | +|---|---|---|---|---| +| 1 | … | … | … | Chưa thử | + +## Ngưỡng chết / ngưỡng UAT + +> Mô hình đo-tại-UAT (quyết 28/07): ngưỡng KHAI tại Cổng 0, ĐO tại phiên UAT +> trên sản phẩm thật sau flag; prototype chỉ hội tụ ý định, không gánh phép đo. +> Nếu phép đo đổi giữa chừng: ghi `[SUPERSEDED — ]` tường +> minh và GIỮ bản gốc bên dưới làm sử liệu — không xoá ngưỡng đã khai. +> Hai đồng hồ của prototype: giờ-dựng (quá timebox = đang build sản phẩm) và +> giờ-chờ-tín-hiệu (số không về sau timebox tự nó là tín hiệu). + +> Máy ĐƯỢC đề xuất ngưỡng khi kết buổi khai thác — mỗi bullet mang tiền tố `[đề xuất]` +> ngay sau dấu `:`; người ký Cổng Đáng gỡ tiền tố = chốt. Ý còn mờ thật thì giữ `…`. +> Vòng KHÔNG có người dùng cuối (việc nội bộ của bộ công cụ) → thay các bullet bằng MỘT +> dòng `Không đo được — `, khai TẠI Cổng Đáng; viết trần hay dạng bullet +> (`- Không đo được — `) đều được nhận — «Không đo được:» (hai chấm) thì KHÔNG. +> Khai sau khi đã làm xong là mọi kết +> quả tự biện minh — đó là đường lách, không phải lối ra. +> Hai tiền tố là CHUỖI MÁY ĐỌC, bản gốc ở mục «Ký hiệu máy đọc» cuối file. + +- Câu hỏi phép đo trả lời: … +- Kết quả nào là SỐNG: … +- Kết quả nào là CHẾT: … +- Timebox: … + +## Kết quả prototype + +> Dựng trong/ngoài timebox · verify local · số so ngưỡng (hoặc lý do KHÔNG ĐO, +> ghi tường minh — không đo mà im lặng là gian). + +## Nguồn ngoài & phạm vi kế thừa + +> Lưới B1 (retro V1): vật liệu ngoài vào không phân loại là mắt xích đầu của +> chuỗi drift 7 bước. Liệt kê TỪNG món vật liệu ngoài repo (gói spec, prototype +> cũ, design system khác, code mẫu, tài liệu đối thủ…), phân loại từng món: +> - **triết-lý/logic** — engine, luật nghiệp vụ, ngưỡng, thuật toán: kế thừa được. +> - **ngôn-ngữ-thiết-kế/hình-thái** — layout, DNA thị giác, khuôn tương tác, +> giọng component: mặc định **KHÔNG** kế thừa — chuẩn của repo tiêu thụ THẮNG. +> Muốn kế thừa hình thái: khai đích danh vào bảng + người ký tại Cổng 0. +> Luật răng: không phân loại = chưa đủ điều kiện ký Cổng 0. + +| Món vật liệu | Nguồn (đường dẫn/tên gói) | Phân loại | Kế thừa? | Người ký | +|---|---|---|---|---| +| … | … | triết-lý/logic hoặc ngôn-ngữ-thiết-kế/hình-thái | có/không | (chỉ bắt buộc khi kế thừa hình thái) | + +## Cổng 0 + +> Hai câu hỏi, hai key frontmatter: số phận CƠ HỘI → `decision` +> (build/iterate/park/kill) · số phận CODE prototype → `prototype.disposition` +> (keep/archive). Ký = điền `decided_by`/`decided_at`. +> Ngưỡng UAT chốt CÙNG LÚC ký (chép từ section Ngưỡng, tinh chỉnh lần cuối). +> `disposition: keep` → BẮT BUỘC điền Bảng nợ kế thừa + 3 guard (diffBase = +> `prototype.base_commit` · baseline bắt buộc · branch cắt từ nhánh chính). + +- **decision = …** Căn cứ: … +- **disposition = …** Căn cứ: … +- **Ngưỡng UAT chốt cùng lúc ký:** … + +## Thước đo thành công → ứng viên criterion + +> Đo-sau-ship. Mỗi thước phải truy được thành AC của contract ở S1 (DP-1 đo +> đúng tỷ lệ này); thước không truy được = thước trang trí. + +- … + +## Bảng nợ kế thừa (CHỈ khi disposition: keep) + +| Path | Giữ / Dựng lại | Chạm t3_paths? | Ghi chú | +|---|---|---|---| + +## Out of scope từ khám phá + +> ≥2 bullet. Nhánh-đã-bác ghi kèm lý do 1 dòng — để khỏi bàn lại; những bullet +> này chép sang contract ở S1, mất bullet nào phải có giải trình. + +- … +- … + +## Ký hiệu máy đọc (đừng xoá, đừng chép vào hồ sơ thật) + +> Hai chuỗi dưới đây là bản gốc DUY NHẤT; bộ quét, thẻ, `/approve` và phiên nghiệm thu rút +> từ đây lúc chạy. Đổi chuỗi ở một phía là phép đo round-trip đỏ. Mục này nằm NGOÀI mọi +> section dữ liệu của ô — đặt nó trong section Ngưỡng thì khuôn trắng trông như đã khai. + + +[đề xuất] + + + +Không đo được — +