KB-749C rev 3

S176-INVESTIGATE K2-SUPPLEMENT — KS.1+KS.2+KS.3 done

51 min read Revision 3
s176investigatek2supplementwhy-analysisphaseC

title: "S176-INVESTIGATE K2-SUPPLEMENT — 6 mục bổ sung + WHY analysis" sprint: S176 phase: INVESTIGATE block: K2-SUPPLEMENT date: 2026-04-13 status: in_progress tags: ['s176','investigate','k2','supplement','why-analysis','phaseC']

S176-INVESTIGATE · K2-SUPPLEMENT — 6 mục bổ sung

Mỗi mục: Mục tiêu → Phương pháp → Kết quả → WHY → TD đề xuất → Câu hỏi mở. Triết lý: "Mỗi lỗi tìm thấy = cơ hội học gốc rễ". Chế độ CHỈ ĐỌC. Mask mọi secret.


KS.1 — DDL events thời MySQL (cửa sổ 96 ngày H2)

Mục tiêu

Khai thác cửa sổ directus_activity 2025-12-07 → 2026-03-13 (mốc S115 migration MySQL→PG, K0) để trả lời:

  • AI sửa schema thời MySQL?
  • Đi qua đường nào (UI/script/curl/python)?
  • Có ai bypass directus_activity (DDL không trace) không?

Phương pháp

SSH → docker exec postgres psql -U directus -d directus → query directus_activity join directus_users. Cửa sổ: timestamp BETWEEN '2025-12-07' AND '2026-03-13'. Lọc collection IN ('directus_collections','directus_fields','directus_relations').

Kết quả

1. Tổng quan 96 ngày

Collection Events
directus_collections 261
directus_fields 1823
directus_relations 0
Tổng 2084 DDL events

2. Phân theo user × action

Collection Action User Count
directus_fields create admin@example.com 1255
directus_fields update admin@example.com 544
directus_collections update admin@example.com 130
directus_collections create admin@example.com 122
directus_fields delete admin@example.com 24
directus_collections delete admin@example.com 9

100% DDL events trong 96 ngày H2 thực hiện bởi 1 user duy nhất: admin@example.com. Vi phạm AP-03 (--admin) ở quy mô 2084 lần.

3. Phân theo user agent (con đường thực tế)

User Agent Count Tỉ lệ Phân loại
Python-urllib/3.11 872 41.8% Script Python (DOT/migration)
node 653 31.3% Script Node.js (Nuxt/DOT TS)
curl/8.7.1 404 19.4% Bash/dot-bin scripts
(empty) 106 5.1% Internal Directus calls
python-httpx/0.28.1 24 1.2% Modern Python lib
cloud-run-proxy node 14 0.7% Cloud Run dev tunnel
Mozilla/5.0 ... Chrome/143 9 0.4% Browser UI thật
cloud-run-proxy curl 2 0.1% Tunnel

99.6% DDL events là script/bot, chỉ 0.4% (9 events) qua browser UI thật.

4. Phân theo IP / origin

  • 2 nhóm nguồn:
    • 127.0.0.1 (localhost — Directus container internal hoặc DOT chạy trên cùng host): 647 events
    • External IPs (14.177.x, 14.162.x, 1.55.x, 59.153.x, 118.68.x, IPv6 2001:ee0:4141:* và 2405:4802:1baa:*): 1437 events — đều residential VN (ISP của owner).
  • origin field: 2075/2084 (99.6%) là rỗng, chỉ 9 events có origin https://directus-test-pfne2mqwja-as.a.run.app (Cloud Run thử nghiệm).

5. Spike days (timeline H2)

Day Events Đặc điểm Giả thuyết
2025-12-07 16 127.0.0.1 Bootstrap Directus + DOT đầu tiên
2025-12-10 15
2025-12-16 186 IPv6 operator Đợt khai báo collection lớn
2026-01-19→2026-02-01 195 cộng dồn mix IP Sprint S128-S135
2026-02-14 631 127.0.0.1, 05:32:58 → 05:48:20 (16 phút) Bulk script run: 490 create + 33 update fields trong 16 phút → ~2 fields/giây = chạy automated, không phải người
2026-03-06 698 14.177.x (chính), 59.153.x, 1.55.x meta_catalog collection được tạo lúc 07:03:53 + 09:22:11→09:22:32 bulk update 23 collections / 21 giây. Sprint S140 (chuẩn bị migration)
2026-03-10 41 14.162.x TRÙNG mốc system_issues + registry_changelog (KS.2 sẽ verify)
2026-03-12 3 Cuối ngày trước migration

6. Truy collection được tạo ngày 2026-03-06

Mẫu 15 events đầu (07:03 + 09:22):

07:03:53 create meta_catalog          14.177.128.73
09:22:11 update billing                14.177.128.73
09:22:13 update block_button_groups    14.177.128.73
09:22:15 update block_buttons          14.177.128.73
09:22:16 update block_library          14.177.128.73
09:22:17 update checkpoint_instances   14.177.128.73
09:22:19 update checkpoint_rules       14.177.128.73
... (23 collection updated trong 21 giây)

→ Pattern bulk script touching 23 collections in 21s — đây là đặc trưng của một DOT script đi qua Directus API (loop qua tất cả collections + PATCH meta).

WHY (cơ hội học gốc rễ)

WHY-1 — Tại sao 100% DDL thời MySQL chỉ qua 1 user admin@example.com?

  • Gốc rễ: Đến mốc DOT 100% (2026-03-30, K0), hệ thống chưa có tách quyền agent user vs admin user. Mọi script đều dùng DIRECTUS_TOKEN của admin. Cái này đã được giải quyết ở S148 với policy ai-agent (xuất hiện ở .claude/settings.local.json permissions list mà ta thấy ở K0), nhưng 2084 events H2 đã commit trước khi tách quyền → không thể truy "DOT nào sửa schema nào" một cách phân biệt vì user log đều là admin.
  • Bài học: SSOT về "ai sửa cái gì" yêu cầu phân quyền theo agent role TRƯỚC khi cho ghi, không phải sau. Hiện tại đã có ai-agent policy nhưng dữ liệu lịch sử bị "ngộ độc admin".

WHY-2 — Tại sao 99.6% DDL là bot mà chỉ 0.4% UI?

  • Gốc rễ: Triết lý "DOT 100%" được áp dụng (ngay cả thời MySQL) — schema không tạo bằng UI mà bằng dot-schema-*-ensure scripts. Đây là điểm tích cực: tự động hoá tốt.
  • Hệ luỵ: Khi DDL = script, người không thấy schema thay đổi qua diff UI. Cần mechanism khác (migration log, schema snapshot diff). dot-schema-snapshot (DOT-075) tồn tại nhưng chưa rõ chạy có đều không.

WHY-3 — Tại sao directus_relations có 0 events trong khi directus_fields có 1823?

  • Gốc rễ: Directus quản lý relation qua directus_fields với type=many-to-one/one-to-many, không tạo row riêng trong directus_relations qua API admin schema endpoints. directus_relations row được tạo ngầm khi Directus build relation từ field meta.
  • Bài học: Audit relation phải đi qua directus_fields (special_field=m2o/o2m) chứ không qua directus_relations.

WHY-4 — Tại sao có cửa sổ audit cho DDL Directus nhưng KHÔNG cho DDL trực tiếp PG?

  • Gốc rễ — đây là lỗ hổng lớn nhất KS.1: directus_activity chỉ log những thao tác đi qua Directus API. Mọi DDL chạy trực tiếp qua psql (CI auto-apply sql/*.sql ở K2.7, dot-pg-*-ensure ở K1.2.2, dot-schema-taxonomy-pg-apply) KHÔNG xuất hiện trong directus_activity.
  • Ví dụ: nếu s133_measurement_framework.sql được apply trên VPS qua CI ngày 2026-02-14, sẽ không có row directus_activity cho CREATE TABLE measurement_loginvisible audit gap.
  • Hệ luỵ: Cửa sổ 128 ngày directus_activity chỉ cover một phần DDL. Phần còn lại — DDL qua psql trực tiếp — mù tịt, không có log nào (PG WAL archive OFF — K0).
  • Bài học gốc: Để SSOT về DDL, phải có DDL event log ở tầng PG (event trigger pg_event_trigger_ddl_commands()) — không thể tin tầng Directus. Hoặc REVOKE quyền DDL khỏi user directus để buộc mọi DDL đi qua Directus admin API.

WHY-5 — Tại sao spike 2026-03-06 (698 events) tạo meta_catalog?

  • Gốc rễ: Đây là sprint chuẩn bị migration S115. meta_catalog là collection trung tâm cho registry counting (Điều 26). Operator (IP 14.177.128.73 = home VN) chạy DOT bulk tạo + cập nhật registry trước khi cắt đường MySQL.
  • Không có gì sai ở event này — nhưng nó là smoking gun cho thấy mọi sprint preparation đều là 1-shot bulk, chứ không phải continuous deployment. Rủi ro: nếu script fail giữa chừng, không rollback được — dữ liệu lệch.

TD đề xuất

TD-S176-DDL-MYSQL-ERA-AUDIT (mới)

  • Triệu chứng: directus_activity chỉ log DDL qua Directus API. DDL qua psql (CI auto-apply, dot-schema--ensure, dot-pg-) không có log. Không thể phân biệt "DOT nào tạo bảng nào" thời H2.
  • Gốc: User PG directus có CREATE/ALTER/DROP. Không có PG event trigger cho ddl_command_end. WAL archive OFF (K0).
  • Fix đề xuất:
    1. Tạo PG event trigger ghi DDL log vào bảng ddl_audit_log (hoặc reuse system_issues với issue_class='ddl_audit'),
    2. Hoặc REVOKE CREATE/ALTER/DROP khỏi directus user, giao cho 1 service role chuyên DDL (định hướng Phương án C: gateway).
  • WHY-cần-fix: Không có audit DDL = không thể truy gốc khi schema drift. K2-K3 phải đoán bằng grep code.

TD-S176-ADMIN-MONOPOLY-LEGACY (mới)

  • Triệu chứng: 100% DDL events H2 (2084 lần) ghi user = admin@example.com. Không phân biệt được agent/script/người.
  • Gốc: Trước S148, hệ thống chỉ có 1 admin token. Mọi DOT chia sẻ chung. Hiện đã có ai-agent policy.
  • Fix đề xuất:
    1. Audit hiện tại còn DOT nào dùng admin token không (K3 verify),
    2. Backfill/note: trong báo cáo registry/audit, nhãn rõ "events <2026-03-30 không có user phân biệt" để không mislead phân tích.
  • WHY-cần-fix: NT12 yêu cầu DOT cặp verify, nhưng nếu user log của writer vs verifier đều là admin thì verify mất ý nghĩa (cùng credential).

Câu hỏi mở (chuyển K3-K4)

  1. PG có event trigger nào hiện đang chạy không? K3 query pg_event_trigger.
  2. dot-schema-snapshot (DOT-075) có chạy đều không? K1 thấy last_executed=bulk-set, không tin được. Có snapshot đối chiếu với spike 2026-03-06 không?
  3. 14.177.128.73 và các IP khác có map với người/máy nào? 1 owner hay nhiều agent? (mostly cosmetic — không cần fix, nhưng giúp audit forensic)
  4. s133_*.sql, s134_*.sql, s146_*.sql chạy thực sự ngày nào trên VPS? (K4 sẽ check marker file)
  5. ai-agent policy được kích hoạt từ sprint nào — trước hay sau spike 2026-02-14? (cần cross-check với directus_policies history)
  • Backward: K0 (cửa sổ 128 ngày activity), K1 (DOT DDL writers), K2 (CI auto-apply pattern).
  • Forward: KS.2 (verify mốc 2026-03-10 — đã có hint từ KS.1: 41 events ngày này từ IP 14.162.130.216), K3 (PG event trigger + permissions), K4 (markers .sql-applied).

Trạng thái KS.1

  • DONE. Đã duyệt → KS.2 dưới đây.
  • 0 thao tác ghi. Toàn bộ là SELECT PG.

KS.2 — Verify mốc 2026-03-10 (system_issues + registry_changelog)

Mục tiêu

K0 phát hiện cả system_issues lẫn registry_changelog đều có row đầu tiên ngày 2026-03-10 (3 ngày trước migration MySQL→PG). Cần trả lời: bị reset/truncate hay được tạo mới? Ai chạy? Có alert/approve không?

Phương pháp

  • KS.2.1 — Trace 41 DDL events ngày 2026-03-10 từ IP 14.162.130.216 (hint vàng KS.1) trên directus_activity.
  • KS.2.2 — Grep toàn repo: CREATE/TRUNCATE/DROP TABLE tên 2 collection.
  • KS.2.3git log các script + PR liên quan ngày 2026-03-08 → 2026-03-12.
  • KS.2.4 — VPS file mtime: /opt/incomex/deploys/web-test/.sql-applied/, /opt/incomex/dot/bin/dot-schema-*ensure.
  • KS.2.5\d schema hiện tại + MIN/MAX/COUNT cho mỗi bảng (PG đọc trực tiếp qua SSH).

Kết quả

KS.2.1 — 41 DDL events (collections+fields) từ IP 14.162.130.216

Ngoài 41 DDL events, IP này có 1675 events tổng ngày 2026-03-10 (login+create+update+run nhiều collection). Lọc ra DDL events thì có 2 cụm thời gian:

Cụm A — registry_changelog (03:17:35 → 03:18:12 UTC = 10:17 → 10:18 +0700):

03:17:35  create  directus_collections  registry_changelog        ← LẦN ĐẦU TẠO
03:17:35  create  directus_fields       1226 (field cho registry_changelog)
03:17:38..03:18:12  create  directus_fields  1227..1239 (13 fields)
03:25:35..03:25:40  update  directus_fields  1227, 1232, 1233, 1229 (chỉnh sửa)

14 fields tạo trong 37 giây, sau đó 4 fields được update 7 phút sau (chỉnh tweak schema).

Cụm B — system_issues (05:06:30 → 05:07:07 UTC = 12:06 → 12:07 +0700):

05:06:30  create  directus_collections  system_issues             ← LẦN ĐẦU TẠO
05:06:30  create  directus_fields       1240 (field đầu tiên)
05:06:35..05:07:07  create  directus_fields  1241..1253 (13 fields)
05:08:21..05:08:27  update  directus_fields  1241, 1242, 1246, 1247, 1250 (5 chỉnh sửa)

14 fields tạo trong 37 giây, sau đó 5 fields được update ~1 phút sau.

Toolchain: user_agent=curl/8.7.1 cho mọi lệnh DDL → script bash gọi Directus REST API.

Sau cụm B (09:08:04 UTC): thêm 1 field cuối (id 1254) — bổ sung muộn.

Tổng 30+ DDL events thuộc về TẠO MỚI 2 collection, KHÔNG phải reset.

KS.2.2 — Grep CREATE/TRUNCATE/DROP toàn repo

Pattern Match
CREATE TABLE.*system_issues 0 trong sql/, scripts/, infra/ (vì collection được tạo qua Directus API, không qua raw SQL)
CREATE TABLE.*registry_changelog 0 (idem)
TRUNCATE.*(system_issues|registry_changelog) 0
DROP TABLE.*(system_issues|registry_changelog) 0
DELETE FROM.*system_issues 1: sql/s162c_cleanup_archived_orphan_issues.sql (chỉ DELETE archived orphan rows, không phải truncate)

KHÔNG có script TRUNCATE/DROP nào. Chỉ có 1 cleanup DELETE archived rows (cleanup hậu kỳ, không phải reset 2026-03-10).

KS.2.3 — git log quanh ngày 2026-03-10

Commit tạo dot-schema-registry-changelog-ensure (git log --diff-filter=A):

848bc9c  2026-03-10 10:30:58 +0700  Nguyễn Minh Huyên
         feat: S108-WATCHDOG — registry changelog + watchdog flows + activity UI
f66fb29  2026-03-10 10:41:19 +0700  feat: S108-WATCHDOG ... (#471)   ← merge

Commit tạo dot-schema-system-issues-ensure (git log --diff-filter=A):

33cf77f  2026-03-10 12:23:24 +0700  Nguyễn Minh Huyên
         feat: S108-SELFHEAL — layer integrity audit + system issues + fix counting
edf7fe7  2026-03-10 12:30:06 +0700  feat: S108-SELFHEAL ... (#473)   ← merge

→ Hai PR riêng biệt cùng ngày 2026-03-10, cùng author, cùng sprint S108, lệch nhau ~2 giờ.

Header script trong repo (xác nhận tự khai):

# dot-schema-registry-changelog-ensure
# v1.0.0 (2026-03-10): Initial version for Watchdog System
#   - registry_changelog: tracks all entity changes for watchdog system
#   - 13 fields: code, timestamp, entity_type, entity_code, entity_name, ...
#   - Idempotent: checks existence before create
#   - Based on dot-schema-entity-dependencies-ensure pattern
# dot-schema-system-issues-ensure
# v1.0.0 (2026-03-10): Initial version for System Issues tracking
#   - system_issues: tracks integrity issues detected by audits
#   - 13 fields: code, title, description, entity_type, entity_code, ...
#   - Idempotent: checks existence before create
#   - Based on dot-schema-registry-changelog-ensure pattern

Header CHANGELOG khẳng định "Initial version" 2026-03-10 cho cả hai. KHÔNG có version trước. KHÔNG có entry "RESET data".

KS.2.4 — VPS file mtime + markers

.sql-applied/ markers (30 file):

0047c_antigravity_proposal.sql.done    Mar 24 11:11
s133_fix_rerun.sql.done                Mar 24 11:11
s133_measurement_framework.sql.done    Mar 24 11:11
s134_pg_grants.sql.done                Mar 24 ...
... (30 markers, oldest mtime Mar 24 11:11)

→ Marker system chỉ xuất hiện từ 2026-03-24 (~14 ngày sau 2026-03-10). Trước đó CI auto-apply chưa có cơ chế marker → các SQL chạy thủ công hoặc chạy lại mỗi deploy.

Script dot-schema-*ensure trên VPS (/opt/incomex/dot/bin/):

2026-04-02 16:35:50  dot-schema-system-issues-ensure
2026-04-02 16:35:50  dot-schema-registry-changelog-ensure

→ Script được rsync lên VPS 2026-04-02 (lần sync gần nhất). Nhưng đã chạy trên cloud Directus từ máy operator ngày 2026-03-10 (qua curl). Tức là operator chạy script từ local trước, push code lên git/VPS sau.

6 file SQL CHƯA có marker (bonus K4):

s175_law_dedupe_p1.sql               ← UPDATE knowledge_documents (K2 phát hiện)
s175_source_id_current_unique_p2.sql
0048_link_policies_mysql.sql         ← K2 hypothesis MySQL syntax sẽ fail
kb_protection_phase2_cron_checks.sql
kb_protection_phase2_full_history.sql
kb_protection_phase2_verify.sql
vps_pg_schema_incomex_metadata_kb_20260405.sql  ← pg_dump snapshot

0048_link_policies_mysql.sql không có marker trong khi 30 file khác có → CHƯA TỪNG được CI apply thành công, hoặc chạy fail. Cần K4 verify chi tiết.

KS.2.5 — Schema + dữ liệu hiện tại

system_issues (24 cột, sample):

id integer not null DEFAULT nextval('system_issues_id_seq')
code, title, description, entity_type, entity_code, issue_type, severity,
source, detected_at timestamp DEFAULT CURRENT_TIMESTAMP,
status, resolved_at, resolved_by, resolution,
violation_hash, source_system, issue_class, business_logic_hash, run_id,
first_seen_at, last_seen_at, occurrence_count integer DEFAULT 1, ...

Counts hiện tại (2026-04-13):

Bảng First row Latest Count
system_issues 2026-03-10 05:37:30 UTC = 12:37 +0700 2026-04-12 01:30:03 1069
registry_changelog 2026-03-10 03:28:07 UTC = 10:28 +0700 2026-04-13 10:01:33 20732

Đối chiếu với DDL events:

  • registry_changelog: collection tạo lúc 03:17:35 UTC → row đầu tiên 03:28:07 UTC → gap ~10 phút (operator tự test ghi 1 row sau khi schema xong).
  • system_issues: collection tạo lúc 05:06:30 UTC → row đầu tiên 05:37:30 UTC → gap ~31 phút.

Dữ liệu liên tục từ ngày tạo cho đến hiện tại — không có khoảng đứt → không bị reset/truncate sau khi tạo.

KẾT LUẬN KS.2 — Giả thuyết K0 BỊ BÁC

K0 hypothesis: "2 bảng log bắt đầu 2026-03-10, trùng mốc đáng nghi với migration 2026-03-13 → có thể bị reset hoặc TRUNCATE".

KS.2 verdict: KHÔNG. Cả 2 collection được TẠO MỚI HOÀN TOÀN ngày 2026-03-10, là sản phẩm của 2 PR cùng sprint S108 (#471 WATCHDOG + #473 SELFHEAL):

  • Trước 2026-03-10: 2 collection KHÔNG TỒN TẠI (không có CREATE TABLE history nào sớm hơn, không có row data nào sớm hơn, header script tự khai "Initial version 2026-03-10").
  • Tại 2026-03-10 sáng: operator chạy 2 ensure script qua curl → Directus tạo collection + 14 fields mỗi bảng, mất tổng cộng ~2 phút active work.
  • Operator commit + merge PR ~13 phút sau khi chạy script.
  • Migration MySQL→PG ngày 2026-03-13 cuốn theo 2 bảng vừa mới sinh + dữ liệu kế thừa (vì pg_dump/restore mang theo content).
  • Mốc trùng nhau là tình cờ — cùng sprint S108 chuẩn bị infrastructure cho watchdog + self-healing trước khi cắt MySQL.

WHY (cơ hội học)

WHY-1 — Vì sao K0 đoán "có thể bị reset"?

  • Gốc: MIN(detected_at) của một bảng = ngày tạo bảng = mốc tự nhiên. K0 thấy 2 bảng cùng MIN ngày → suy đoán "trùng mốc đáng nghi". Nhưng trùng mốc cũng có thể nghĩa là cùng sinh ra 1 ngày, không phải bị reset.
  • Bài học: Trước khi suy diễn "reset", phải kiểm tra "có version trước không" (git log diff-filter=A) + "có row sớm hơn không". KS.2 khắc phục bằng git log --diff-filter=A — đây là tiêu chuẩn forensic gốc trước khi đoán.

WHY-2 — Vì sao 2 collection log quan trọng (system_issues, registry_changelog) KHÔNG có trước 2026-03-10?

  • Gốc: Trước S108, hệ thống chưa có "watchdog" và "self-healing" như khái niệm chính thức. Triết lý NT5 "Tự phát hiện, tự sửa" được codify ở S108 + S148 (HP v4.4.0). Đây là hạ tầng tự phát hiện được ban hành quá muộn — sau khi đã có 100k+ row directus_activity mà không có cơ chế phát hiện vi phạm.
  • Bài học: Hệ thống "tự phát hiện sai" cần được dựng từ ngày đầu chứ không phải retrofit. 2026-03-10 là ngày Incomex chính thức có self-healing layer — nhưng thiết kế quá muộn → mọi vi phạm trước đó không có log nào ghi lại. Window 4 tháng tối tăm = không thể truy cứu pattern lỗi cũ.

WHY-3 — Vì sao S108 chia làm 2 PR (#471, #473) cách nhau 2 giờ?

  • Gốc: PR #471 chỉ là watchdog (ghi changelog). PR #473 thêm self-healing (issues + counter fix). Operator viết tuần tự, không gộp 1 PR lớn — best practice phát triển.
  • Bài học: Không có gì sai. Pattern "schema-ensure script kèm PR" là chuẩn. Nhưng hệ luỵ là: mỗi sprint có nhiều PR rải rác → khó có 1 báo cáo "ngày X tạo những bảng nào" trừ khi grep git log như KS.2 đã làm.

WHY-4 — Vì sao operator chạy script TRƯỚC khi commit (10:17 vs 10:30)?

  • Gốc: Workflow "test trên cloud Directus → nếu xanh → commit → merge". Cho phép thử nghiệm nhanh. Nhưng: tạo cửa sổ 13 phút mà production schema có collection chưa có trong git. Nếu operator quên commit, schema drift ngầm.
  • Bài học: Hiện tại đã có DOT-067 dot-schema-diff và DOT-075 dot-schema-snapshot, lý thuyết phải catch drift này. Nhưng K1 phát hiện last_executed=bulk-set cho hầu hết DOT → không tin được DOT diff/snapshot có chạy đều. Cần K4 verify.

WHY-5 — Tại sao 2 collection được tạo qua Directus API (curl) thay vì raw SQL?

  • Gốc: Đây là điểm tích cực. Ensure script đi qua Directus API → có vết trong directus_activity → KS.2 truy được. Nếu ensure script đi qua psql trực tiếp như dot-schema-taxonomy-pg-apply (K1), KS.2 sẽ rơi vào INVISIBLE AUDIT GAP của KS.1 và không thể truy được.
  • Bài học gốc: Tuyến đường L1 qua Directus API là tuyến forensic khả thi duy nhất hiện nay. Bất kỳ DOT nào đi thẳng PG = ngày sau truy không ra. Đây là argument mạnh cho Phương án C (Single Writer Gateway): ép tất cả qua 1 cổng duy nhất để có audit trail.

TD đề xuất

TD-S176-2026-03-10-RESET (closed — không có reset)

  • Verdict: KHÔNG có reset. Hai bảng được tạo mới hoàn toàn ngày 2026-03-10 trong S108 sprint. K0 hypothesis bị bác.
  • Không cần TD cho mốc 2026-03-10. Đóng giả thuyết.
  • Reuse câu hỏi: K0 nên cập nhật, đổi nhãn từ "đáng nghi" thành "ngày sinh hợp lệ S108-WATCHDOG/SELFHEAL".

TD-S176-SELF-HEALING-LATE (mới — bài học gốc rễ)

  • Triệu chứng: System self-healing layer (system_issues, registry_changelog, watchdog flows) chỉ tồn tại từ 2026-03-10. Trước đó 4 tháng directus_activity đã ghi 100k events nhưng KHÔNG có cơ chế phát hiện vi phạm.
  • Gốc: NT5 "Tự phát hiện, tự sửa" codify quá muộn (HP v4.4.0 ban hành 2026-03-30, hạ tầng dựng 2026-03-10).
  • Fix đề xuất:
    1. Backfill system_issues cho 4 tháng "tối" — chạy lại các checker (DOT-110 coverage-inspector, DOT-098 dependency-scan) trên dữ liệu lịch sử để ghi vi phạm ngược.
    2. Hoặc chấp nhận: chỉ kiểm tra forward, không backfill — và ghi nhận "data ≤ 2026-03-10 không qua self-healing layer".
  • WHY-cần-fix: Vi phạm hiện nay có thể là di sản từ 4 tháng tối. Nếu không backfill, không bao giờ biết.

TD-S176-RUN-BEFORE-COMMIT (mới — workflow risk)

  • Triệu chứng: Operator chạy script trên production trước khi commit code (gap 10-13 phút). Tạo cửa sổ schema drift nếu quên commit.
  • Gốc: Workflow "test on cloud first, commit if works" — fast iteration. Không có DOT enforce "code-first".
  • Fix đề xuất:
    1. DOT pre-commit hook: chạy dot-schema-snapshot ngay sau khi script chạy → buộc snapshot record commit cùng với script.
    2. Hoặc daily DOT diff: dot-schema-diff chạy cron 1 lần/ngày, alert nếu PG schema lệch git.
  • WHY-cần-fix: Window drift nhỏ nhưng tích luỹ. Cần K4 verify dot-schema-snapshotdot-schema-diff có chạy thực không.

Câu hỏi mở (chuyển KS.3-K3-K4)

  1. Backfill self-healing data cho cửa sổ 2025-12-07 → 2026-03-10 có khả thi không? Cần dot-orphan-scanner, dot-coverage-inspector chạy trên snapshot cũ — nhưng K0 đã nói WAL OFF và directus_activity không lưu schema state, chỉ events.
  2. dot-schema-diffdot-schema-snapshot (DOT-067, DOT-075) thực sự chạy bao lâu một lần? last_executed=2026-03-31 bulk-set không tin được. → KS.6 sẽ verify.
  3. 6 file SQL chưa có marker (s175_*, 0048_link_policies_mysql.sql, kb_protection_phase2_*, vps_pg_schema_*): tại sao chưa apply? K4 verify từng file.
  4. 0048_link_policies_mysql.sql không có marker xác nhận K2 hypothesis "MySQL syntax sẽ fail trên PG" — nhưng cần verify file thực sự đã thử apply hay chưa được scheduling. K4 grep CI logs.
  5. Cửa sổ 4 tháng "tối" trước self-healing: có vi phạm nào trượt qua không? Kiểm tra directus_revisions cũ có data lạ không.
  • Backward: KS.1 (cửa sổ DDL events H2 + IP 14.162.130.216 hint).
  • Forward:
    • KS.3 sẽ cross-check 15 script unregistered K1 với git log (giống pattern KS.2.3) để xác định di tích vs cố tình.
    • KS.5 sẽ check dot-collection-health race risk vs S175 fix — workflow run-before-commit có thể tăng race risk.
    • K4 sẽ verify markers chi tiết + 5 cron không có installer.

Trạng thái KS.2

  • DONE. Đã duyệt → KS.3 dưới đây.
  • 0 thao tác ghi. SELECT PG + git log --diff-filter=A + grep + stat SSH + \d schema.

KS.3 — Đối chiếu 15 script unregistered K1 (forensic + classify + WHY)

Mục tiêu

K1 §6 liệt kê 15 script trong dot/bin/ không match dot_tools qua join file_path:

  • 2 bypass thuần (không metadata): dot-cron-matrix-setup, dot-script-lint
  • 3 cụm doc: dot-doc-generate, dot-doc-partition, dot-doc-render
  • 10 cụm nrm: dot-nrm-{amend, binding, config, discover, enact, impact, lifecycle, retire, sync, verify}

KS.3 phân loại mỗi script theo 3 nhãn (a) cố tình bypass / (b) metadata catch-up gap / (c) di tích chết, và trả 5 WHY.

Phương pháp

  • KS.3.1wc -l + head -1 shebang trong repo; stat -c '%y' trên VPS.
  • KS.3.2git log --all --diff-filter=A --follow cho commit đầu tiên + PR + sprint.
  • KS.3.3 — Grep caller: crontab/production.crontab, infra/cron/, .github/workflows/, dot/bin/ (call lẫn nhau), scripts/. Xác nhận bằng crontab -l trên VPS.
  • KS.3.4 — Query dot_tools tìm record uppercase semantic: DOT_NRM_*, DOT_DOC_*, DOT_SCRIPT_LINT, DOT_CRON_MATRIX*.
  • KS.3.5 — Classify + 5 WHY.

KS.3.1 — Inventory repo + VPS

Script Lines Shebang VPS mtime
dot-cron-matrix-setup 41 bash 2026-04-04 04:51:19
dot-script-lint 53 bash 2026-04-04 04:52:47
dot-doc-generate 30 bash 2026-04-04 04:39:16
dot-doc-partition 29 bash 2026-04-04 04:39:16
dot-doc-render 21 bash 2026-04-04 04:39:16
dot-nrm-amend 31 bash 2026-04-04 04:39:16
dot-nrm-binding 25 bash 2026-04-04 04:39:16
dot-nrm-config 29 bash 2026-04-04 04:39:16
dot-nrm-discover 31 bash 2026-04-04 04:39:16
dot-nrm-enact 29 bash 2026-04-04 04:39:16
dot-nrm-impact 36 bash 2026-04-04 04:39:16
dot-nrm-lifecycle 36 bash 2026-04-04 04:39:16
dot-nrm-retire 26 bash 2026-04-04 04:39:16
dot-nrm-sync 31 bash 2026-04-04 04:39:16
dot-nrm-verify 46 bash 2026-04-04 04:39:16

Quan sát:

  1. Tất cả 15 file tồn tại cả repo lẫn VPS. Kích thước nhỏ (21-53 dòng — placeholder/lean pattern).
  2. 13 file (dot-nrm-* + dot-doc-*) cùng timestamp VPS 2026-04-04 04:39:16 (chính xác tới giây) → rsync 1 batch.
  3. 2 file (dot-cron-matrix-setup, dot-script-lint) rsync 13 phút sau (04:51-04:52) — batch riêng.

KS.3.2 — Forensic git log (first commit)

Script First commit Date Author Sprint / PR
dot-cron-matrix-setup cff4d81 / 84a262a 2026-04-01 09:37:29 +0700 Nguyễn Minh Huyên S150-P1: PG Matrix Foundation — pivot_matrix() + 4 DOT tools + cron (#659)
dot-script-lint (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên sync: copy 35 VPS-only files to macbook (Step 2A)
dot-doc-generate (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-doc-partition (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-doc-render (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-amend (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-binding (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-config (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-discover (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-enact (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-impact (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-lifecycle (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-retire (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-sync (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem
dot-nrm-verify (sync commit) 2026-04-05 12:29:44 +0700 Nguyễn Minh Huyên idem

🔴 PHÁT HIỆN SMOKING GUN — commit message:

sync: copy 35 VPS-only files to macbook (Step 2A)

14/15 script KHÔNG có first commit "tạo mới" — chúng là "sync" retroactive copy ngược từ VPS về macbook/repo 2026-04-05. Nghĩa là:

  1. 14 script tồn tại trên VPS trước 2026-04-05 (ngày sync).
  2. Chúng được tạo trực tiếp trên VPS (qua ssh/scp/manual edit) hoặc đã sync từ một nơi khác.
  3. Không có dấu vết git log --diff-filter=A cho bản gốc — commit đầu tiên KHÔNG phải "add" mà là "đồng bộ ngược".
  4. Tên commit tự thừa nhận: "VPS-only files".

Đây là pattern §0-AY kinh điển: writer tồn tại trên production (chạy cron thực sự) mà git, dot_tools, code review, PR gate đều không biết cho đến khi có người "dọn dẹp" kéo về.

Với dot-cron-matrix-setup: ngược lại — có commit "tạo mới" hợp lệ S150-P1 (#659) ngày 2026-04-01. Nó được tạo đúng chuẩn (qua PR), nhưng operator quên đăng ký dot_tools row cho bản thân installer (đã đăng ký 4 DOT matrix khác: DOT-312..315 nhưng không có DOT cho chính installer).

KS.3.3 — Cross-check caller thực tế

Grep repo + crontab -l trên VPS:

Script Repo caller VPS crontab -l Status
dot-cron-matrix-setup NONE NONE One-shot installer (chạy thủ công lần duy nhất để install cron con)
dot-script-lint crontab/production.crontab 30 3 * * 0 dot-script-lint --local LIVE cron weekly Sun 03:30
dot-doc-generate NONE NONE Pre-flight checkpoint (inspect source)
dot-doc-partition crontab/production.crontab 0 3 1 1,4,7,10 * dot-doc-partition LIVE cron quarterly
dot-doc-render NONE NONE Pre-flight checkpoint
dot-nrm-amend NONE NONE On-demand (meta trigger_type=on-demand)
dot-nrm-binding NONE NONE On-demand
dot-nrm-config NONE NONE On-demand
dot-nrm-discover crontab/production.crontab 0 4 * * 0 dot-nrm-discover LIVE cron weekly Sun 04:00
dot-nrm-enact NONE NONE On-demand
dot-nrm-impact NONE NONE On-demand (tier-A checker, paired=amend)
dot-nrm-lifecycle crontab/production.crontab 0 6 * * * dot-nrm-lifecycle LIVE cron daily 06:00
dot-nrm-retire NONE NONE On-demand
dot-nrm-sync crontab/production.crontab 30 5 * * * dot-nrm-sync LIVE cron daily 05:30
dot-nrm-verify crontab/production.crontab 0 5 * * * dot-nrm-verify LIVE cron daily 05:00

Tổng kết caller:

  • 6 script LIVE trên production cron (doc-partition, nrm-discover, nrm-lifecycle, nrm-sync, nrm-verify, script-lint)
  • 8 script không có cron, không có caller (6 dot-nrm-* trigger on-demand + doc-generate + doc-render)
  • 1 script one-shot installer (dot-cron-matrix-setup)

Quan trọng: 6 script LIVE đang ghi PG thực sự mỗi ngày/tuần/quý mà dot_tools.file_path = ∅. Cron owner KHÔNG join được với DOT entry qua file_path → §0-AY thất thủ.

KS.3.4 — Metadata dot_tools cross-check

Query dot_tools lấy record tương ứng uppercase semantic:

Script Code trong dot_tools tier domain paired_dot file_path trigger Khớp script?
dot-cron-matrix-setup (NONE) Không có metadata
dot-script-lint DOT_SCRIPT_LINT A monitoring.integrity cron ✓ có meta, thiếu fp + paired
dot-doc-generate DOT_DOC_GENERATE B normative DOT_DOC_PARTITION on-demand ✓ có meta, thiếu fp
dot-doc-partition DOT_DOC_PARTITION A normative DOT_DOC_GENERATE cron ✓ có meta, thiếu fp
dot-doc-render DOT_DOC_RENDER B normative DOT_DOC_PARTITION on-demand ✓ có meta, thiếu fp
dot-nrm-amend DOT_NRM_AMEND B normative.enact DOT_NRM_IMPACT on-demand ✓ có meta, thiếu fp
dot-nrm-binding DOT_NRM_BINDING B normative DOT_NRM_VERIFY on-demand
dot-nrm-config DOT_NRM_CONFIG B normative DOT_NRM_VERIFY on-demand
dot-nrm-discover DOT_NRM_DISCOVER A normative DOT_NRM_DRAFT cron
dot-nrm-enact DOT_NRM_ENACT B normative.enact DOT_NRM_VERIFY on-demand
dot-nrm-impact DOT_NRM_IMPACT A normative DOT_NRM_AMEND on-demand
dot-nrm-lifecycle DOT_NRM_LIFECYCLE A normative DOT_NRM_RETIRE cron
dot-nrm-retire DOT_NRM_RETIRE B normative DOT_NRM_LIFECYCLE on-demand
dot-nrm-sync DOT_NRM_SYNC A normative DOT_NRM_ENACT cron
dot-nrm-verify DOT_NRM_VERIFY A normative DOT_NRM_DRAFT cron

Quan sát:

  1. 14/15 có metadata tương ứng (tier, domain, paired_dot, trigger_type đều khai đầy đủ).
  2. 15/15 file_path=∅. Không một record nào link script bằng file_path.
  3. 1/15 hoàn toàn không có metadatadot-cron-matrix-setup.
  4. Cụm nrm có pairing NT12 đầy đủ trong metadata (amend↔impact, discover↔draft, lifecycle↔retire, sync↔enact, verify↔draft, binding/config/enact↔verify).
  5. DOT_NRM_DRAFTfile_path=opt/incomex/dot/bin/dot-nrm-draft (là script KHÁC, không nằm trong 15 unregistered — vì K1 đã match nó qua basename). Điều này chứng tỏ: operator BIẾT cách set file_path, nhưng quên set cho 14 script nrm/doc còn lại.
  6. DOT_NRM_AMEND ghi domain normative.enact nhưng dot-nrm-amend là action "amend" — label drift: metadata đặt theo plan, script là ad-hoc.

KS.3.5 — Classification + 5 WHY analysis

Classification tổng hợp

Script Nhãn Lý do
dot-cron-matrix-setup (c) di tích hợp lệ (one-shot installer đã chạy xong) Commit hợp lệ S150-P1; không có metadata; nhưng nhiệm vụ đã hoàn thành — installer chỉ cần chạy 1 lần. Không chạy tiếp = đúng thiết kế. Cần đăng ký metadata dạng "one-shot installer" để trace.
dot-script-lint (b) metadata catch-up + đang chạy LIVE cron, có meta DOT_SCRIPT_LINT, thiếu fp + paired. Ironic: linter chống hardcode nhưng bản thân là direct PG writer.
dot-doc-generate (b) metadata catch-up + placeholder Pre-flight check, chưa có writer thật (chờ docxtemplater deploy).
dot-doc-partition (b) metadata catch-up + đang chạy LIVE cron quarterly, exit early nếu document_instances chưa tồn tại.
dot-doc-render (b) metadata catch-up + placeholder Pre-flight check (chờ Carbone CE deploy).
dot-nrm-{discover,lifecycle,sync,verify} (b) metadata catch-up + đang chạy (4) LIVE cron daily/weekly, ghi PG trực tiếp qua run_pg.
dot-nrm-{amend,binding,config,enact,impact,retire} (b) metadata catch-up + dormant (6) On-demand, chạy khi agent gọi. Không chạy cron.

Tổng phân loại:

  • (a) cố tình bypass: 0/15 (0%)
  • (b) metadata catch-up gap: 14/15 (93.3%) — chia thành:
    • b1 đang chạy cron: 6
    • b2 placeholder chờ deps: 2
    • b3 on-demand dormant: 6
  • (c) di tích: 1/15 (6.7%) — chỉ dot-cron-matrix-setup là installer đã hoàn thành

Kết luận: Không có script nào là "cố tình bypass" theo nghĩa xấu. Vấn đề chủ đạo là metadata catch-up gap — operator đã đăng ký metadata với tier/domain/paired_dot/trigger_type chuẩn chỉnh, nhưng quên set file_path khi tạo script. Script chạy trên VPS rồi mới được kéo về repo ngày 2026-04-05 qua "Step 2A sync".

WHY-A — dot-cron-matrix-setup + dot-script-lint: cố tình bỏ đăng ký?

Bằng chứng cứng:

  • dot-cron-matrix-setup: commit S150-P1 #659 hợp lệ, cùng PR với 4 DOT khác (DOT-312..315 matrix-{declare,update,retire,health}). 4 DOT đồng cấp được đăng ký, 1 installer không.
  • dot-script-lint: có DOT_SCRIPT_LINT tier-A trong dot_tools với trigger_type=cron, chỉ thiếu file_path.

Gốc rễ:

  1. dot-cron-matrix-setup không cố tình bỏ — nó là installer phụ trợ cho nhóm pivot (giống dot-cron-pivot-setup DOT-308). Operator đăng ký 4 DOT "matrix-verb" nhưng coi installer như build tool, không phải "DOT thật sự". Asymmetry trong định nghĩa "DOT là gì": writer action → yes; installer one-shot → ??
  2. dot-script-lint ironic self-exemption: script chống hardcode lại dùng pattern run_pg trực tiếp. Operator nhận thức được (code có comment hardcode_violation class) nhưng đặt mình ngoài vòng tự check.

Self-aware mâu thuẫn dot-script-lint:

  • Script grep 3 pattern hardcode:
    1. PG_CONTAINER="[a-z] (không env default)
    2. -U directus, -U incomex (direct user)
    3. docker exec postgres\b (hardcoded container)
  • Nhưng chính dot-script-lint dùng PG_CONTAINER="${PG_CONTAINER:-postgres}" (pass Pattern 1 qua :-) và docker exec -i ${PG_CONTAINER} (pass Pattern 3 qua ${}). Script lách chính luật mình detect bằng cách dùng env var wrapper thay vì giá trị literal.
  • Nhưng semantically vẫn là direct PG writer — ghi system_issues qua run_pg psql, không qua Directus API.

WHY-A kết luận: Không ai cố tình bypass. cron-matrix-setup là oversight (installer không được coi là DOT), script-lint là self-aware lỗ hổng (linter tự exempt). Cả hai đều là bài học về định nghĩa DOT chưa đủ chặt.

WHY-B — Cụm dot-nrm-*: tại sao tier-A metadata có file_path=∅ mà tier-B script vẫn chạy?

Bằng chứng cứng:

  • 10 dot-nrm-* scripts: 100% metadata đầy đủ trừ file_path.
  • DOT_NRM_DRAFT (không nằm trong 15 unregistered) có file_path=opt/incomex/dot/bin/dot-nrm-draft → operator BIẾT cách set fp.
  • Commit "sync: copy 35 VPS-only files to macbook (Step 2A)" 2026-04-05 → các script sinh trực tiếp trên VPS, không qua PR/CR từ ban đầu.

Gốc rễ — sequence nhiều khả năng:

  1. Sprint nào đó (có thể S155-S159, S148 KG) ban hành khái niệm normative registry.
  2. Agent (operator) tạo record metadata DOT_NRM_* trong dot_tools trước — qua curl POST items/dot_tools — để định nghĩa DOT cặp và paired_dot ma trận.
  3. Script thực tế viết sau — trực tiếp trên VPS (vì VPS = SSOT mã nguồn theo OR v7.57 §VI: "Agent SSH → sửa VPS → git commit → cron push GH 2×/ngày").
  4. Khi viết script trên VPS, không có pipeline tự động update dot_tools.file_path = '...'skipped forever.
  5. Ngày 2026-04-05, operator "Step 2A" chạy sync pull 35 file về macbook để đưa vào git — nhưng vẫn chưa backfill dot_tools.file_path.

Có phải decision hay bug?

  • Không phải decision — metadata khai rõ ý muốn trace (paired_dot, trigger_type set đầy đủ). Nếu cố tình exempt, sẽ để file_path=NULL + thêm field exempted_reason='normative-external'.
  • Là bug quy trình: VPS-first workflow không có khâu dot-register-self / dot-metadata-link sau khi tạo script mới trên VPS.
  • Đ33 §13 E3 (ngoại lệ normative) — chưa verify E3 có thật sự tồn tại không (không query KB được). Nhưng dù có E3, các script nrm/doc vẫn là writer thực sự → vẫn cần audit, không nên dùng E3 để bypass §0-AY.

WHY-B kết luận: Gap là workflow bug, không phải decision. Thiếu một bước "register file_path after VPS-create".

WHY-C — Cụm dot-doc-*: tương tự nrm?

Bằng chứng:

  • 3 script: dot-doc-{generate,partition,render}.
  • Metadata đầy đủ: tier A/B, paired trong cụm (generate ↔ partition, render → partition).
  • dot-doc-generatedot-doc-renderPRE-FLIGHT CHECKPOINT (script 30/21 dòng, không phải renderer thật):
    • dot-doc-generate check require('docxtemplater') + có template enacted?
    • dot-doc-render check Carbone CE presence?
    • Cả 2 ghi system_issues với issue_class='dependency_blocked' và exit 1 → là placeholder đợi dependency.
  • dot-doc-partition = quarterly cron, check document_instances exist, exit 0 nếu table rỗng → live cron but quick-exit.

Gốc rễ:

  • Pattern giống nrm: metadata trước, script sau. Nhưng bản chất script còn yếu hơn — 2/3 là placeholder chờ dependency bên ngoài (docxtemplater, Carbone CE).
  • Cụm doc được đăng ký trong sprint thiết kế KG/Normative (S155-S159) để chiếm chỗ trước khi có engine render.
  • Vừa là metadata catch-up gap + vừa là placeholder chưa có writer thật.

WHY-C kết luận: Cùng pattern (b), cộng thêm vấn đề "DOT đăng ký trước implementation" — khai tier/paired nhưng chưa có logic. Đúng NT "Sẵn sàng thay đổi" theo nghĩa khai kế hoạch trước, nhưng vi phạm NT12 vì "checker tier-A" trỏ tới "writer tier-B" đều là placeholder → pair không verify được gì.

WHY-D — Pattern chung: tại sao §0-AY không catch?

Câu hỏi gốc: §0-AY = "Writer không đăng ký dot_tools = vô hình, CẤM". Vì sao 15 script sống sót qua mọi gate?

Bằng chứng:

  • K1 phát hiện cả 15 bằng cách join trên file_path, không phải trên code.
  • Join bằng file_path chỉ work cho các script có file_path set — 212/272 record có fp. 60 record file_path=∅ không thể join.
  • DOT cảnh sát hiện có:
    • dot-orphan-scan (DOT-095, tier-A) — scan birth.orphan
    • dot-orphan-scanner (DOT-115, tier-A) — scan birth.orphan
    • dot-dependency-scan (DOT-098, tier-A) — scan relations
    • dot-coverage-inspector (DOT-110, tier-A) — scan monitoring.integrity
    • dot-dot-coverage (DOT-COVERAGE, tier-A) — scan monitoring.dot
    • dot-dot-health (DOT-HEALTH-DOT, tier-A) — scan monitoring.dot
  • Không thấy DOT nào tên dot-register-orphan hoặc dot-metadata-file-link-check — tức là không có cảnh sát "script trên disk vs dot_tools.file_path".

Gốc rễ:

  • §0-AY định nghĩa theo "writer không có trong dot_tools" nhưng registry có thể có entry rỗng fp → entry "giả" thoả mãn §0-AY literal.
  • dot-dot-coveragedot-dot-health check dot coverage nhưng không filter "entries có file_path NULL mà script thật chạy cron".
  • Cảnh sát thiếu chính xác: chỉ kiểm "có trong registry không" mà không kiểm "link đúng không".
  • Không có CI guard: GitHub Actions không chạy grep dot/bin/dot-* vs dot_tools.file_path pre-merge.

WHY-D kết luận: §0-AY bị vô hiệu hoá bởi metadata shells (row tồn tại nhưng file_path rỗng). Cần một DOT tier-A mới: dot-file-path-orphan-check — cross ref script disk vs dot_tools.file_path, báo cáo file_path=NULL cho mọi metadata trỏ tới executable thật.

Bằng chứng — quét lại K1 §2 metadata distribution:

  • K1 phát hiện 60/272 record file_path=∅ (22%).
  • 14/60 đã giải thích ở KS.3 (nrm × 10 + doc × 3 + script-lint × 1 = 14). 46 record còn lại file_path=∅ chưa biết.
  • Trong phần tier-A K1 đã liệt kê: DOT-312, DOT-313, DOT-314 (tier A, paired=DOT-315) đều fp=∅cụm matrix pivot cùng pattern (metadata 4 DOT, chỉ DOT-315 có file_path — hoặc cả 4 cũng thiếu).

Let me verify:

  • DOT-315 = dot-matrix-health với file_path=bin/dot/dot-matrix-health.ts (K1 có thấy).
  • DOT-312, DOT-313, DOT-314 = dot-matrix-declare/update/retire, theo KS.3.4 metadata query: file_path=∅.

Pattern metadata-trước-link-sau lặp lại ít nhất 4 cụm:

  1. Cụm NRM (10): dot-nrm-*
  2. Cụm DOC (3): dot-doc-*
  3. Cụm MATRIX (3): DOT-312..314 (dot-matrix-declare/update/retire — cần K4 verify)
  4. Cụm KG (~20+): K1 cho thấy DOT_KG_* tier-A với file_path=None — chưa kiểm tra từng cái.

Dự đoán con số cuối (chưa verify): nếu 46 record còn lại đa phần là KG + một số matrix + một số MISC, thì pattern phổ biến trong ~60/272 = 22% dot_tools record — gần 1/4 registry bị "rỗng file_path".

Gốc rễ hệ thống:

  • Workflow VPS-first (OR §VI) tối ưu cho speed: sửa thẳng VPS, push git sau. Nhưng không có hook sau dot-auth/curl items/dot_tools để force set file_path.
  • Luật DOT mới (Đ35 v5.0 S155) yêu cầu DOT registry complete, nhưng triển khai không kèm migration tool để backfill legacy records.
  • DOT 100% ban hành 2026-03-30 nhưng không có "registry sanity check" trước khi ban hành → registry đã dirty từ trước.

WHY-E kết luận: Pattern lặp lại ≥4 cụm, ảnh hưởng ~22% registry. Đây không phải bug riêng lẻ mà là hệ quả cấu trúc: registry không có trường bắt buộc file_path NOT NULL và không có CI gate kiểm exists(file_path). Cần fix kiến trúc chứ không phải vá từng script.

TD đề xuất (phần áp đảo = (b) metadata catch-up)

TD-S176-METADATA-CATCHUP (mới — áp đảo 14/15 case)

  • Triệu chứng: 14/15 unregistered script thực ra CÓ metadata DOT_* trong dot_tools với tier/domain/paired/trigger đầy đủ, nhưng file_path=NULL. Cross-ref "script disk vs registry" bằng file_path fail. Join by file_path cho ra 15 false-positive "unregistered" khi thực tế là metadata-link gap.
  • Gốc:
    1. Workflow VPS-first (OR §VI): sửa thẳng VPS, push git sau → không có pipeline force set file_path tại thời điểm tạo script.
    2. Không có CI gate kiểm dot_tools.file_path IS NOT NULL cho mọi record có code DOT-xxx hoặc DOT_xxx liên kết với script thực.
    3. §0-AY định nghĩa "không register" nhưng để registry có shell rỗng fp → check bị lách bằng "khai code trước, link sau".
    4. Không có DOT cảnh sát đối chiếu ls dot/bin/dot-* vs SELECT file_path FROM dot_tools WHERE file_path IS NOT NULL.
  • Fix đề xuất:
    1. Hạ tầng chặn hơn dặn dò (Câu tuyên ngôn #2): thêm PG constraint CHECK (file_path IS NOT NULL OR status='placeholder') trên dot_tools → không thể khai "đăng ký" nếu không link file.
    2. DOT mới dot-file-path-orphan-check (tier A, cron daily): cross-ref disk vs registry, ghi system_issues mỗi file file_path IS NULL AND code LIKE 'DOT_%' có script thật trên disk.
    3. CI pre-merge hook: grep dot/bin/dot-* mới + check tương ứng có row với file_path set trong dot_tools chưa. Fail PR nếu thiếu.
    4. Backfill script (one-shot L2): tự động set file_path cho 46 record có code match basename script. Sau khi xong, retire script.
  • WHY-cần-fix: Registry không tin được = K3-K4 phải chạy nhiều DOT cảnh sát đặc biệt. Nếu fix gốc, mọi audit sau này chỉ cần join 1 lần.

TD-S176-VPS-FIRST-NO-HOOK (mới — gốc rễ workflow)

  • Triệu chứng: 14/15 script có first commit là sync: copy 35 VPS-only files to macbook (Step 2A) ngày 2026-04-05 → script chạy trên VPS nhiều ngày/tuần trước khi xuất hiện trong git.
  • Gốc: OR §VI quy định "Agent SSH → sửa VPS → git commit → cron push GH 2×/ngày". Cron chạy push nhưng không có khâu validate "mọi dot- có metadata đầy đủ"* trước khi push.
  • Fix đề xuất:
    1. Cron push (2×/ngày) thêm bước gọi dot-file-path-orphan-check trước khi git push → alert nếu có script orphan.
    2. Hoặc: VPS pre-commit hook .git/hooks/pre-commit trong repo VPS-clone của web-test kiểm tra orphan.
  • WHY-cần-fix: Dead window giữa "script live trên VPS" và "git biết" = không ai check được quality gate trong window đó.

TD-S176-LINTER-SELF-EXEMPT (mới — self-awareness gap)

  • Triệu chứng: dot-script-lint (DOT_SCRIPT_LINT tier-A monitoring.integrity) detect 3 pattern hardcode nhưng bản thân dùng direct PG write (run_pg psql docker exec).
  • Gốc: Linter detect theo REGEX string pattern, không detect semantic (direct-PG-write vs via-Directus). ${VAR:-default} bypass pattern 1 literal match.
  • Fix đề xuất:
    1. Mở rộng pattern detection: grep run_pg\|psql.*-U.*-d\|docker exec.*postgres psql kể cả khi dùng env var wrapping.
    2. Hoặc: chuyển dot-script-lint sang ghi system_issues qua Directus API (không qua psql).
  • WHY-cần-fix: Linter tự exempt chính là mâu thuẫn nội tại — tự không tuân luật mình áp đặt = mất uy tín rule enforcement.

TD-S176-INSTALLER-NOT-DOT (mới — định nghĩa DOT chưa đủ chặt)

  • Triệu chứng: dot-cron-matrix-setup là installer one-shot, có commit S150-P1 #659 hợp lệ, nhưng không có row trong dot_tools. dot-cron-pivot-setup cùng loại CÓ đăng ký (DOT-308). Asymmetry.
  • Gốc: Định nghĩa "DOT là gì" mơ hồ: writer/checker thì rõ, nhưng installer/bootstrap/one-shot thì ??
  • Fix đề xuất:
    1. Thêm kind vào dot_tools (writer, checker, installer, bootstrap, one-shot) → installer one-shot được coi là DOT nhưng với vòng đời đặc biệt.
    2. Hoặc: bắt buộc mọi script trong dot/bin/ đều phải có row, ngay cả one-shot — coverage_status = one_shot_completed.
  • WHY-cần-fix: Nếu không định nghĩa rõ, nhiều installer khác sẽ lặp lại pattern này.

Câu hỏi mở (chuyển KS.4, KS.5, KS.6, K3, K4)

  1. KS.4 — DOT MA: 60 record file_path=∅ + những record có file_path nhưng script không tồn tại (chiều ngược) — dự đoán ≥5 record có file_path đã lỗi thời.
  2. KS.5 — dot-collection-health: trong 15 này không có collection-health (có trong K1 SELECT-then-INSERT list). Pattern 14 unregistered dùng run_pg/ON CONFLICT cơ bản. Cần verify: trong 9 S175 writer, có bao nhiêu writer dùng SELECT-then-INSERT thực sự? KS.5 sẽ giao điểm.
  3. KS.6 — Cron installer mapping: KS.3.3 xác nhận 6 cron LIVE cho nrm/doc/script-lint. Ai cài 6 cron này? Không có DOT installer registered. Nếu operator cài thủ công qua crontab -e, đây là đơn bypass cron management.
  4. K3: 14.177.128.73, 14.162.130.216, 127.0.0.1 cho DDL events — 14.162.130.216 (hint KS.1/KS.2) tạo 2 collection 2026-03-10; còn IP 14.177.128.73 là ai? Cần K3 verify qua directus_users metadata.
  5. K4 — .sql-applied/ markers: 6 SQL file chưa marker + cụm này cần check từng file trên disk còn tồn tại không.
  6. E3 Đ33 ngoại lệ normative: có thật không? Nếu có, phải tra vào constitution/law để xác minh WHY-B gap có hợp pháp không (mặc dù gap vẫn là bug workflow).
  • Backward: KS.2 (pattern git log --diff-filter=A đã thành công + hint IP forensic).
  • Forward:
    • KS.4 dùng cùng pattern check chiều ngược: SELECT file_path FROM dot_tools WHERE file_path IS NOT NULL → SSH check file tồn tại không → ra bảng DOT MA.
    • KS.5 quan tâm cụm nrm/doc nếu có ai dùng SELECT-then-INSERT thay vì ON CONFLICT.
    • KS.6 verify 6 cron nrm/doc/script-lint ai cài — installer DOT missing hay manual?
    • K3 check PG user dùng cho run_pg() trong 14 script (tất cả dùng PG_USER:-directus).
    • K4 check placeholder dot-doc-generate, dot-doc-render có phải dead code hay đang chờ dep thực sự.

Trạng thái KS.3

  • KS.3 DONE — chờ duyệt KS.4.
  • 0 thao tác ghi. Toàn bộ là SELECT Directus, git log, grep, crontab -l, stat SSH.