Chuyển tới nội dung chính

Mức hoàn thiện — tính năng, bằng chứng và độ phủ Tầng 1

Tài liệu này trả lời một câu hỏi: tính năng nào của Signet đã xong thật, căn cứ vào đâu. Mỗi tính năng trong đặc tả (dac-ta-san-pham.md §4) và kế hoạch (du-thi/ke-hoach-phat-trien.md) được xếp một mức, kèm bằng chứng kiểm được trong repo: tệp test và số hàm test, bước CI, lệnh trình diễn, hạn chế đã biết. Mục 3 đối chiếu với 7 cụm dịch vụ Tầng 1 mà đề liệt kê (du-thi/olp-2026-de-thi.md §2.1), kể cả những năng lực chưa có. Việc sắp làm nằm ở ROADMAP.md.

Tài liệu lập ngày 23/10/2026 trên nhánh develop, các dòng sửa theo thay đổi về sau; số đếm tĩnh ở mục 1 đếm lại ngày 31/10/2026 trên develop 23bfadb. Số hàm test của từng tệp ở mục 2 luôn khớp mã, có test kiểm. Tổng ở mục 1 đổi theo khi thêm, bớt test — mục 4 có lệnh để đếm lại. Đường dẫn trong bảng tính từ gốc repo. Cột CI ghi job, bước có trong .github/workflows/ci.yml; lượt chạy gần nhất xanh hay đỏ thì xem trang Actions, tài liệu này không chép lại.

Các mức​

MứcĐiều kiện
Hoàn thiệnĐủ cả bốn: (1) có kiểm tự động khẳng định đúng sai — test đơn vị, test trên CSDL thật, hoặc kiểm đầu-cuối có điều kiện dừng (make smoke, make e2e, make demo-*); (2) kiểm đó chạy trong CI trên mọi pull request; (3) trình diễn được bằng một lệnh: make demo-*, kịch bản e2e trên giao diện, hoặc — với phần lõi không có giao diện — lệnh make của chính tính năng hay bước smoke gọi qua cổng APISIX; (4) không còn hạn chế đã ghi nào chặn luồng chính
Dùng đượcChạy đầu-cuối và có kiểm tự động trong CI, nhưng thiếu một trong (1)–(3), hoặc còn hạn chế đã ghi buộc người dùng làm ngoài giao diện (gọi API, chạy lệnh, sửa mã)
Minh họaCó mã và có kiểm trong CI, nhưng chủ đích chỉ để chứng minh năng lực: thiết bị hay dữ liệu giả lập, không có giá trị pháp lý, hoặc bản rút gọn so với kế hoạch. Dùng thật phải làm thêm
Một phầnMới có một phần phạm vi kế hoạch mô tả; phần còn lại chưa có mã
Chưa làmChưa có mã trong repo

Cột "Test tự động" ghi số hàm test đếm tĩnh của từng tệp (cách đếm ở mục 1). Thêm hay bớt hàm test thì sửa số trong bảng ở cùng pull request: scripts/tests/test_muc_hoan_thien.py (chạy trong make test) đối mọi "<thư mục> tệp N" với tệp thật, sai thì báo dòng, số ghi và số thật (#285). "test_x_*.py N" là tổng mọi tệp khớp mẫu, trừ tệp nêu trong "(trừ …)" ngay sau số. Số chỉ một phần tệp viết "tệp (N hàm về …)" để test bỏ qua. Một tệp có thể phục vụ nhiều tính năng; bảng gán tệp cho tính năng chính của nó, chỗ dùng lại ghi "đã tính ở …". Viết tắt thư mục test: svc-files = services/svc-files/tests/, svc-workflow = services/svc-workflow/tests/, db = db/tests/, scripts = scripts/tests/, office = apps/office/tests/, ca-nhan = apps/ca-nhan/tests/, bang-tin = apps/bang-tin/tests/, nghi-phep = apps/nghi-phep/tests/, sinh = apps/sinh/tests/, e2e = tests/e2e/; mã nguồn svc_files/… = services/svc-files/src/svc_files/…, svc_workflow/… = services/svc-workflow/src/svc_workflow/…. "smoke" là một kiểm của scripts/smoke.py.

1. Số test​

Đếm tĩnh ngày 31/10/2026 trên develop 23bfadb (sau bản v1.4.0), bằng các lệnh ở mục 4. Số chạy vẫn lấy từ log của lần CI 40552387498 trên e32dd1d (27/10/2026, xanh hết), chưa đếm lại: cây mã lúc đó ít test hơn và chưa có Đổi, bù lịch giảng.

PhầnThư mụcCách đếm tĩnhĐếm tĩnh (23bfadb)Chạy trong CI (lần 40552387498, 27/10)Job CI
svc-filesservices/svc-files/tests/grep -rhoE '^\s*(async )?def test_'1 043 hàm1 691 đạt, 9 bỏ quaunit (make test)
svc-workflowservices/svc-workflow/tests/như trên506 hàm963 đạtunit (make test)
Lệnh vận hành, demoscripts/tests/như trên (phương thức unittest)754 hàm596 chạy, 594 đạt, 2 bỏ quaunit (make test)
Giao diện Văn phòng điện tửapps/office/tests/grep -hE '^\s*(test|it)\('259 lời gọi262 đạtunit (make test)
Không gian cá nhân P.A.R.Aapps/ca-nhan/tests/như trên20 lời gọi20 đạtunit (make test)
Bảng tin đơn vịapps/bang-tin/tests/như trên; def test_ cho tệp Python14 lời gọi + 15 hàm14 + 15 đạtunit (make test)
Đơn nghỉ phép (#301)apps/nghi-phep/tests/như trên33 lời gọi + 12 hàm111 + 12 đạtunit (make test)
Đổi, bù lịch giảng (#482)apps/lich-giang/tests/như trên32 lời gọi + 12 hàmchưa có, ra sau lần chạy nàyunit (make test)
Trang tài liệu API (#357)apps/tai-lieu-api/tests/def test_15 hàm15 đạtunit (make test)
CSDL + Hasura thậtdb/tests/grep -rhoE '^\s*(async )?def test_'349 hàm583 đạtdb (MOC_IT=1)
Trình duyệt thật (Playwright)tests/e2e/grep -hE '^test\('43 test trong 28 tệp *.spec.ts49 đạtchay-thu, phần e2e, bước make e2e
Smoke qua cổng APISIXscripts/smoke.pygrep -cE '^\s*ok\('103 kiểm99 kiểm đạtchay-thu, phần luong-chinh, bước make smoke

Tổng đếm tĩnh (23bfadb, 31/10): 3 107 test (1 043 + 506 + 754 + 259 + 20 + 14 + 15 + 33 + 12 + 32 + 12 + 15 + 349 + 43), cộng 103 kiểm của smoke; lúc chạy trong CI (lần 40552387498, 27/10): 4 329 test đạt (1 691 + 963 + 594 + 262 + 20 + 14 + 15 + 111 + 12 + 15 + 583 + 49 — scripts tính 594 vì 2 test bỏ qua), cộng 99 kiểm smoke. Trên cùng cây mã, số lúc chạy lớn hơn số đếm tĩnh vì pytest.mark.parametrize và test sinh trong vòng lặp nhân một hàm thành nhiều ca; smoke đạt ít hơn số ok( vì vài kiểm nằm trong nhánh if, chỉ chạy khi đủ điều kiện. Test của scripts chỉ bỏ qua có điều kiện (skipIf, skipUnless: thiếu công cụ, thư viện hay thư mục). 9 test bỏ qua của svc-files là integration/test_live.py (chỉ chạy khi đặt MOC_IT=1 và khóa JWT thử — job unit không đặt, nên 9 test này không chạy ở đâu tự động). Không gán cho tính năng nào: svc-files test_hasura.py 20 (máy khách Hasura dùng chung), integration/test_live.py 9, office format.test.ts 6, paging.test.ts 4.

2. Ma trận tính năng​

2.1. Lõi Tầng 1 (Phase 1)​

MãChức năngMứcTest tự độngĐầu-cuối trong CI · trình diễnHạn chế đã biết
F1.1SSO + MFA: Keycloak, vai trò, tenant_id và department_id trong tokenHoàn thiệnsvc-files test_auth_config.py 26 (kiểm token Keycloak, biến môi trường của dịch vụ); scripts test_dang_nhap.py 4smoke: 5 tài khoản đăng nhập qua cổng, văn thư phải nhập TOTP, luồng mật khẩu của moc-cli và admin-cli cũng đòi TOTP; e2e: mọi kịch bản đăng nhập qua Keycloak kèm TOTP (tests/e2e/helpers.ts)Realm chỉ nạp ở lần khởi động đầu: sửa platform/keycloak/realm/moc-realm.json sau đó phải make reset hoặc sửa trong trang quản trị Keycloak. Xác thực hai bước chỉ có TOTP
F1.2Cổng API zero-trust: APISIX, danh sách trắng đường dẫn, kiểm JWT ở biên, giới hạn tốc độ, X-Request-IdHoàn thiệnscripts test_mang_ung_dung.py 8 (container ứng dụng chỉ ở mạng apps cùng APISIX, không service lõi nào khác ở đó; cổng hook 9081 có trong node_listen, không publish, route hook chỉ khớp ở cổng đó); route khai báo trong platform/apisix/apisix.yaml, kiểm bằng smokesmoke: GraphQL không token → 401; đường quản trị Hasura, Keycloak, Flowable bị chặn; khóa quản trị Hasura gửi từ ngoài bị gỡ; đăng nhập quá giới hạn theo IP, GraphQL quá giới hạn theo phiên → 429; tải tấn công bị WAF chặn ở cổng → 403 (F3.15); bước CI make demo-cai-ung-dung bước 5: container app-bang-tin chỉ mở được kết nối tới apisix:9080, POST /su-kien/bang-tin ở cổng công khai → 404WAF ở cổng (F3.15) mới ở mức Dùng được: chữ lạ ở các ô chữ ngắn (Cơ quan ban hành, Nơi nhận…) có thể bị chặn nhầm, khi đó người quản trị thêm loại trừ trong apisix.yaml
F1.3Dữ liệu có cấu trúc: PostgreSQL 16 + Hasura v2 CE, GraphQL tự sinh, quyền theo vai trò và đơn vị tới từng dòng, migrations và metadata tự ápHoàn thiệndb test_schema.py 15, test_permissions.py 23job db; smoke: văn thư Trường Kinh tế không thấy văn bản Trường CNTT & TT, không tải được tệp (404); văn thư không ghi thẳng được bảng văn bảnsvc-files, svc-workflow giữ khóa quản trị Hasura (lấy từ két): quyền cấp dòng không giới hạn chính hai dịch vụ này
F1.4Quy trình BPMN: Flowable REST 8, Hasura Actions → svc-workflow, callback, job đối soátHoàn thiệnsvc-workflow test_workflow.py 32, test_app.py 8, test_so_do.py 9; office processDiagram.test.ts 8smoke: vào sổ khởi chạy quy trình → chỉ đạo → phân công → báo xong, dòng thời gian đủ 5 bước từ tiếp nhận; e2e van-ban-den.spec.ts (cả mục Sơ đồ quy trình: bước chỉ đạo đã qua, bước phân công đang chờ), van-ban-di.spec.tsĐơn vị gắn vào quy trình bằng biến tenantId và nhóm ứng viên t:<đơn vị>:…, không dùng tenant của Flowable. Lệch giữa Flowable và CSDL chỉ được sửa ở lượt đối soát kế tiếp (MOC_RECONCILE_INTERVAL_S)
F1.5Dữ liệu phi cấu trúc: SeaweedFS, tải lên và tải xuống chỉ qua svc-filesHoàn thiệnsvc-files test_upload.py 18, test_download.py 10smoke: văn thư tải công văn lên, quét sạch; đơn vị khác tải tệp → 404Một bucket chung, đơn vị tách bằng tiền tố tenant/<id>/ — có chủ ý, lý do ở docs/mo-hinh-du-lieu.md
F1.6Trạm kiểm dịch: ClamAV quét trước khi ghiHoàn thiệnsvc-files test_clamav.py 8smoke: EICAR → 422, không lưu, lần chặn vào sổ kiểm toán; e2e kiem-dich.spec.ts; make demo-kiem-dichClamAV chiếm khoảng 0,9 GiB RAM (docker stats trên bản 1.3.0; README, mục Yêu cầu hệ thống), lên 1,9 GiB trong khoảng 10 giây lúc nạp lại chữ ký (#288)
F1.7Tìm kiếm toàn văn: Meilisearch qua /api/search, lọc đơn vị ở máy chủHoàn thiệnsvc-files test_search.py 34e2e van-ban-den.spec.ts: tìm lại bằng chữ không dấuXem F2.7
F1.8Cài ra ngoài thư mục mã nguồn: make install PREFIX=…Hoàn thiện— (không có test đơn vị cho scripts/install.sh)bước CI "Cài ra ngoài thư mục mã nguồn và chạy từ đó (make install)": cài sang thư mục tạm, dựng lại từ đó, chạy smoke—
F1.9Lên mạng: HTTPS sau proxy (MOC_TRUSTED_PROXY), Cloudflare Tunnel tùy chọnHoàn thiệnscripts test_duong_ham.py 13 (kiểm chữ cấu hình tunnel, route Keycloak sau proxy, MOC_TRUSTED_PROXY=auto chỉ tin mạng moc), test_kiem_ban_online.py 24 (site giả)Job .github/workflows/ban-online.yml kiểm bản online https://signet.htechcs.com hằng ngày và ngay sau mỗi lần phát hành, bản dev theo develop https://signet-dev.htechcs.com hằng ngày (đúng commit đầu develop) (scripts/kiem_ban_online.py: chứng chỉ, HTTP sang HTTPS, /api/status đúng thẻ phát hành mới nhất, URL của Keycloak sau proxy, header bảo mật); diễn tập tay 22/10/2026 trên máy phát triển (CHANGELOG [1.0.0]); phần CI https trên mọi pull request (#261): .env như docs/trien-khai.md bước 3, proxy TLS nginx với chứng thư tự ký như bước 5, rồi kiem_ban_online.py, smoke trọn qua https://, nhật ký APISIX ghi IP của máy gọi; với MOC_TRUSTED_PROXY=auto, X-Forwarded-For giả từ container ở mạng apps bị bỏ (#211); hướng dẫn docs/trien-khai.mdMáy chạy bản online tắt, hoặc bản online chưa nâng lên thẻ mới (quá 60 phút sau khi phát hành, hay tới lượt mỗi sáng), thì job đỏ. CI dùng nginx với chứng thư tự ký thay Caddy và Let's Encrypt, không kiểm Cloudflare Tunnel
F1.10Tài liệu API xuất tự động (docs/api/), kèm trang API REST đọc được sinh từ OpenAPI (rest-*.md)Hoàn thiệnscripts test_api_docs.py 8 (trang REST trong git khớp OpenAPI trong git; gói dòng, ô bảng, kiểu dữ liệu)bước CI "Tài liệu API trong docs/api/ khớp mã (make api-docs)": scripts/api_docs.py --check so tài liệu với hệ thống vừa dựng—
F1.11Ứng dụng sinh từ bảng: Studio, postgres-meta của Supabase và PostgREST trên schema sinh, khung SQL (cột chuẩn lấy từ token, RLS theo đơn vị, sổ kiểm toán, chặn lệnh gỡ bảo vệ), trang /sinh dựng biểu mẫu từ OpenAPI (QĐ-014); báo cáo khai báo bằng dữ liệu (sinh.bao_cao, hàm gộp của PostgREST, #587); mẫu in lưu thành dữ liệu, svc-files in ra PDF (sinh.mau_in, #586)Dùng đượcdb test_sinh.py 15; scripts test_cong_rest.py 3; sinh openapi.test.js 7, i18n.test.js 7, test_server.py 6, bao-cao.test.js 9, in.test.js 3; svc-files test_sinh_print.py 11phần e2e của chay-thu chạy scripts/kiem_sinh.py (#568) trên hệ thống dựng thật, WAF bật: bảng tạo qua postgres-meta có ngay trên /rest/v1, không token 401, cột chuẩn theo token, hai đơn vị không thấy, không sửa, xóa được dòng của nhau, lệnh gỡ bảo vệ bị hủy, PATCH, DELETE không bộ lọc bị cổng chặn 403, không dòng nào đổi (#588), đếm, tổng, trung bình theo nhóm chỉ gồm dòng của đơn vị (#589), in ra PDF, đơn vị khác 404 (#594); trình diễn: màn "Ngoài 4 nguyên lý" của demo-script.md, làm tay, chưa có make demo-*Profile studio tắt mặc định; chỉ người vận hành nền tảng dùng Studio, qua đường hầm SSH (Studio là chủ bảng, thấy dòng mọi đơn vị); bảng tạo trong Studio không nằm trong git; chưa có quy trình duyệt, tệp đính kèm, thông báo; trang /sinh chưa xóa dòng, chưa phân trang, lọc, tìm; RLS là cơ chế mới, chỉ bảo đảm trên /rest/v1; chặn PATCH, DELETE không bộ lọc chỉ có khi MOC_WAF=On; báo cáo một cột nhóm, một phép tính, chưa xuất tệp; mẫu in là văn bản thuần, chỉ bảng có khóa chính id số nguyên (them-ung-dung.md mục 8.5–8.7)

2.2. Văn phòng điện tử — Must (Phase 2)​

MãChức năngMứcTest tự độngĐầu-cuối trong CI · trình diễnHạn chế đã biết
F2.1Đăng nhập SSO; tài khoản dùng thử giamkhao (chuyên viên, không TOTP)Hoàn thiệnscripts test_seed_mau.py 4 (giám khảo có sẵn việc); svc-workflow test_directory.py 6 (danh bạ đồng bộ từ Keycloak)e2e giam-khao.spec.ts; smoke: chuyên viên đúng mật khẩu là vàoapps/office không có trang hồ sơ cá nhân riêng
F2.2Tiếp nhận văn bản đến: Poka-yoke 3 lớp theo schemas/van-ban-den.json, chặn vào sổ trùngHoàn thiệnoffice forms.test.ts 6; svc-workflow test_intake.py 17; db test_intake.py 5 — ba lớp chạy chung bộ ca thử schemas/van-ban-den.cases.jsonsmoke: lỗi trả về từng trường, chặn tiếp nhận trùng, ràng buộc CHECK chặn cả ghi bằng quyền quản trị; e2e van-ban-den.spec.ts: form sai bị chặn trước khi gửiJSON Schema dùng chung mới có cho form tiếp nhận văn bản đến
F2.3Vào sổ, chỉ đạo (chủ trì / phối hợp), phân công, xử lý — BPMN van-ban-den; nhắc hạn, báo lãnh đạoHoàn thiệnsvc-workflow test_sla.py 15 (luồng: test_workflow.py, đã tính ở F1.4); db test_registration.py 11smoke: đủ các bước; CI đặt MOC_SLA_DURATION=PT20S → nhắc người làm rồi báo lãnh đạo; e2e van-ban-den.spec.tsThời hạn mỗi bước đếm theo đồng hồ (PT24H), không trừ ngày nghỉ
F2.4Văn bản đi: soạn, duyệt, ký, cấp số, phát hành PDF có mã QR và SHA-256 — BPMN van-ban-diHoàn thiệnsvc-files test_outgoing.py 28; svc-workflow test_outgoing.py 28; db test_outgoing.py 15smoke: soạn → trình → duyệt → ký → phát hành, mã QR, SHA-256 khớp CSDL; e2e van-ban-di.spec.ts (cả trả lại, trình lại)Một khuôn thể thức viết trong mã (services/svc-files/src/svc_files/render.py); quản trị đơn vị chưa tự sửa mẫu được
F2.5Hộp việc + thông báo realtime từng người (Centrifugo)Hoàn thiệnsvc-workflow test_notify.py 14, test_subscriptions.py 16; db test_notifications.py 6smoke: token giả bị đóng kết nối, tin việc mới tới qua WebSocket, thông báo lưu đúng người; e2e thong-bao.spec.ts (hai trình duyệt cùng lúc)Kênh ngoài (email, webhook, SMS, Zalo) tắt mặc định, xem ⑦ ở mục 3; SMS, Zalo chỉ tới người tự bật cuối chuông
F2.6Dòng thời gian văn bản (office.document_events)Hoàn thiệnCác test CSDL đọc office.document_events: db test_registration.py, test_outgoing.py, test_dispatch.py… (đã tính ở F2.3, F2.4, F3.12)smoke: dòng thời gian đủ 5 bước từ tiếp nhận (sự kiện office.document.received, #381); e2e lien-thong.spec.ts kiểm dòng thời gian bên nhận—
F2.7Tìm theo số ký hiệu, trích yếu, cơ quan ban hành và nội dung PDF: lớp chữ, bản scan qua OCR (#253)Hoàn thiệnsvc-files test_pdftext.py 22, test_texts.py 17 (test_search.py đã tính ở F1.7); db test_file_texts.py 7smoke: tìm ra công văn theo chữ chỉ có trong tệp PDF; bản scan 3 trang tìm ra theo chữ chỉ có trong ảnh, chữ OCR giữ ranh giới trang; tệp văn bản hạn chế, kể cả bản scan, không bị đọc chữ; e2e van-ban-den.spec.tsBản scan chỉ vào chỉ mục chữ OCR của MOC_TEXT_OCR_PAGES trang đầu (mặc định 3), có thể sai chữ, sai dấu; OCR chạy lần lượt từng tệp, nhiều bản scan cùng lúc thì chữ PDF mới phải chờ; OCR hỏng vì máy (quá giờ lúc máy bận) 5 lượt liền thì bản scan đó giữ empty, không tự đọc lại. Lớp chữ của văn bản đã nộp lưu rời chỉ mục (F3.8)
F2.8Sổ đăng ký văn bản đến / đi theo năm, in PDF theo Phụ lục IV NĐ 30/2020Hoàn thiệnsvc-files test_registers.py 39smoke: sổ liền số, lần in vào sổ kiểm toán, chuyên viên → 403; e2e so-dang-ky.spec.ts—
F2.9Giao diện tiếng Việt / tiếng AnhDùng đượcoffice: một test trong answer.test.ts kiểm vi.json, en.json cùng khóa, cùng tham số (1573 khóa mỗi bên); i18n.test.ts 4 (quét mọi tệp app/: khóa t('…') có thật, phần viết thẳng của khóa ghép có thật, không chữ tiếng Việt viết cứng trong template và script); ca-nhan i18n.test.js 7, bang-tin i18n.test.js 7 (#306: vi và en cùng khóa, cùng tham số; không chữ tiếng Việt viết cứng trong public/*.js; chữ tĩnh của index.html khớp bảng dịch; ngôn ngữ lưu chung khóa moc.locale với Văn phòng điện tử); nghi-phep i18n.test.js 9 (#398: cùng các kiểm đó, cộng form dựng từ forms/*.json hiện nhãn, lựa chọn, gợi ý, câu báo lỗi bằng tiếng Anh, dựng lại khi đổi ngôn ngữ vẫn giữ chữ đang gõ); svc-files test_registers.py (6 hàm về sổ đăng ký tiếng Anh: hai bảng chữ cùng khóa, cùng số cột; chỉ chữ cố định được dịch, chữ người dùng nhập giữ nguyên; mọi loại văn bản nạp sẵn có tên tiếng Anh; ?lang=en qua API, ngôn ngữ khác → 422); svc-files test_outgoing.py (6 hàm về văn bản đi tiếng Anh: hai bảng chữ cùng khóa; ngày kiểu Anh, địa danh giữ nguyên; quốc hiệu, số, nơi nhận, khối xác thực bằng tiếng Anh, chữ người soạn gõ giữ nguyên; công văn "Re:", "To:"; dự thảo "DRAFT"; đọc cột language, giá trị lạ về tiếng Việt); db test_outgoing.py (1 hàm: mặc định vi, người soạn đổi được tới lúc trình, fr bị chặn)job unit; e2e tieng-anh.spec.ts (3 test): bấm nút đổi ngôn ngữ trong apps/office/app/layouts/ → menu, danh sách văn bản đến và đi, soạn và lưu dự thảo (ngôn ngữ văn bản English, tải được bản xem trước PDF), trang văn bản (dòng thời gian, tab Lịch sử) bằng tiếng Anh, tải lại vẫn giữ (cả thẻ <html lang>), bấm "Tiếng Việt" thì về lại; trang công khai (trạng thái hệ thống, cổng xác thực) đổi được không cần đăng nhập; Không gian cá nhân bấm "English" → thư mục, tiêu đề bằng tiếng Anh, tải lại vẫn giữ, Bảng tin và Văn phòng điện tử mở ra cũng tiếng Anh, về lại tiếng Việt ở Văn phòng điện tử thì Không gian cá nhân về theoVăn bản đi tiếng Anh (#398) chỉ dịch phần cố định: tên đơn vị, cơ quan chủ quản, địa danh, chức danh người ký in như đã khai (thường là tiếng Việt), tên loại do đơn vị tự thêm giữ tên đã khai; cổng xác thực vẫn hiện tên loại tiếng Việt
F2.10Dữ liệu mẫu make seed, tạo qua API như người dùng thật; dữ liệu công khai (#424): bản chụp thông tư Bộ GD&ĐT từ Công báo trong seed/du-lieu-mo/, nạp qua API nạp lô (?open_data=: tệp đối chiếu manifest trên máy chủ, lượt nạp có phả hệ OpenLineage, #445), /api/status báo tuổi dữ liệuHoàn thiệnscripts test_seed_mau.py (đã tính ở F2.1), test_du_lieu_mo.py 18 (bản chụp khớp manifest và khuôn hợp đồng; sai băm, thiếu tệp, tên tệp lạ là hỏng; quá hạn là cũ; lấy lỗi giữ bản cũ; đọc trang Công báo; lô sinh ra qua bộ đọc của svc-files); svc-files test_open_data.py 9bước CI "Nạp dữ liệu mẫu (make seed)" trước smoke và e2e (cả bốn thông tư qua API nạp lô); e2e giam-khao.spec.ts dựa vào dữ liệu seed2 đơn vị, 18 văn bản đến và 7 văn bản đi giả lập (danh sách CNTT, KT, OUT_CNTT trong scripts/seed_documents.py) cộng 4 thông tư thật; kế hoạch là ~50 văn bản lấy từ công văn đã công bố. Danh sách thông tư chọn tay, lấy mới bằng tay ở máy của đội
F2.11Cộng đồngMột phần—README.en.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, mẫu issue .github/ISSUE_TEMPLATE/Repo chưa ghi đơn vị chạy thử nào; bài giới thiệu không kiểm được từ mã nguồn
F2.12Sổ kiểm toán chuỗi băm, neo đầu chuỗi ra bucket khóa ghi, make auditHoàn thiệndb test_audit.py 23; svc-files test_anchors.py 28; scripts test_moc_neo.py 34smoke: moc audit verify khớp mốc neo, kiểm chữ ký mốc chép ra tệp, mốc neo không xóa được (S3 Object Lock); bước CI sao lưu → xóa → khôi phục → make audit; e2e so-kiem-toan.spec.ts (2 test)Mốc neo nằm trên cùng máy chủ (bucket lạnh của SeaweedFS); bản ngoài máy chủ có chữ ký của két: moc audit anchor --publish đẩy lên kho git công khai (timer mẫu moc-anchor-publish), nhưng kho công khai, khóa triển khai do đơn vị tự tạo
F2.13Cổng xác thực công khai: tra mã hoặc quét QR; thả PDF, trình duyệt tự tính SHA-256Hoàn thiệnsvc-files test_outgoing.py (đã tính ở F2.4); office verify.test.ts 7smoke: tra mã in cạnh QR ra đúng văn bản, giới hạn theo IP; tệp nguyên vẹn khớp, sửa một byte không khớp; e2e van-ban-di.spec.ts (người ngoài, không đăng nhập)Là trang /xac-thuc trong apps/office (layout public), không phải ứng dụng apps/verify riêng như đặc tả §5

2.3. Should (Phase 3)​

MãChức năngMứcTest tự độngĐầu-cuối trong CI · trình diễnHạn chế đã biết
F3.1(iv) Màn kiểm dịchHoàn thiệndb test_audit.py (dòng file.rejected), svc-files test_clamav.py (đã tính ở F2.12, F1.6)smoke: EICAR bị chặn, lãnh đạo thấy lần chặn trong sổ kiểm toán; e2e kiem-dich.spec.ts; make demo-kiem-dichmake demo-kiem-dich không phải bước CI — smoke và e2e đã phủ cùng luồng
F3.2(iv) Két bí mật OpenBao: static seal, AppRole, TransitHoàn thiệnsvc-files test_openbao.py 9; svc-workflow test_openbao.py 9bước CI make demo-ket-bi-mat; smoke: két đã mở, dịch vụ không có bí mật trong biến môi trường, AppRole không đọc chéo.env vẫn là nguồn bí mật, khóa mở két nằm trong .env; PostgreSQL, Keycloak, Flowable, Hasura, SeaweedFS, Meilisearch, Centrifugo (và Parseable, Scorpio, restic khi bật profile) vẫn đọc bí mật từ biến môi trường; két chưa có TLS (docs/van-hanh.md, mục Két bí mật)
F3.3(iv) GitOps + tự phục hồi: scripts/reconcile.py, systemd timer hoặc cronHoàn thiệnscripts test_gitops_ky.py 4 (chỉ áp commit đã ký), test_gitops_profile.py 3 (profile thêm của bản cài)bước CI make demo-tu-phuc-hoi và "GitOps — reconciler áp commit mới, sửa lệch cấu hình"; smoke: một lượt reconciler không có gì phải sửaCompose + reconciler, không Argo CD; áp một commit là cài lại toàn bộ, Signet dừng khoảng một phút; image cũ không tự xóa; .env không do git quản
F3.4Sao lưu, khôi phục (restic)Hoàn thiệndb test_backup.py 3bước CI: hai lượt sao lưu, xóa volume CSDL, tệp và két, khôi phục, smoke, make auditKho sao lưu nằm trên cùng máy chủ, chép ra ngoài là việc của người vận hành (docs/trien-khai.md); lịch sử đo NGSI-LD không vào bản sao lưu
F3.5Tab "Lịch sử" trên trang văn bản, đọc từ sổ kiểm toánHoàn thiệndb test_document_history.py 3e2e van-ban-den.spec.ts (tab Lịch sử), giao-viec.spec.tsTrường nội dung chỉ báo "đã đổi", không hiện chữ trước và sau
F3.7Văn bản hạn chế: mã hóa từng tệp, giải mã trong bộ nhớ, dấu tên người xemHoàn thiệnsvc-files test_sealed.py 34; svc-workflow test_restricted.py 16; db test_restricted.py 14bước CI make demo-han-che; smoke 8 kiểm; e2e han-che.spec.tsChỉ nhận PDF; không dùng cho bí mật nhà nước; ai có quyền Docker trên máy chủ đọc được két (docs/van-hanh.md)
F3.8(i) Lập hồ sơ, nộp lưu nóng → lạnh: Parquet + tệp + manifest có băm, khóa ghi; phòng đọc xem tệp; phân tích trên dữ liệu lạnh: DuckDB đếm văn bản đã nộp lưu theo năm, loại, phòng trên các gói Parquet, hiện ở trang Điều hành (#304)Hoàn thiệnsvc-files test_archive.py 19, test_reading_room.py 22, test_archive_stats.py 17; db test_case_files.py 8; office caseFiles.test.ts 10, archiveStats.test.ts 5bước CI make demo-nong-lanh (có xem tệp trong phòng đọc; bước 5 đọc lại gói vừa nộp lưu qua GET /api/case-files/archive-stats: số đếm tăng đúng số văn bản của hồ sơ, chuyên viên bị 403, phả hệ ghi lần đọc); smoke: nộp lưu qua POST /api/case-files/{id}/archive (chuyên viên bị 403), gói trên bucket lạnh, cổng xác thực vẫn "khớp", xem tệp có dấu người xem + dòng sổ kiểm toán; e2e ho-so.spec.ts (chuyên viên lập, đóng; tổ trưởng nộp lưu; văn thư xem tệp; hết văn bản thì đỏ, không bỏ qua)Nộp lưu trên giao diện chạy đồng bộ trong một yêu cầu (cổng chờ 60 giây), chưa đo với hồ sơ lớn; quá 60 giây thì cổng trả 504 trong khi svc-files có thể vẫn nộp lưu xong — tải lại trang để xem trạng thái hồ sơ — job nền mỗi giờ vẫn là đường chính cho hồ sơ đóng quá hạn. Phòng đọc chỉ xem được tệp PDF; tệp khác chỉ thấy tên, băm. Bản xem có dấu người xem nhưng người xem vẫn lưu lại được (như F3.7). Phân tích kho lạnh đọc lại mọi gói mỗi khi có hồ sơ mới nộp lưu (giữa hai lần thì dùng kết quả lưu đệm), chưa đo với hàng nghìn gói; chỉ lãnh đạo đơn vị xem, như các biểu đồ nóng
F3.9(iii) Phả hệ OpenLineage + kiểm chất lượng bảng Parquet; phả hệ báo cáo tổng hợp (biểu đồ ← khung nhìn ← bảng, tính từ catalog PostgreSQL) và trang xem đồ thị; cờ chất lượng số liệu (Đạt / Cảnh báo / Trượt): 5 luật tính trong CSDL trên khung nhìn báo cáo, cờ nộp lưu theo assertion đã ghiHoàn thiệnsvc-files test_archive.py (6 test phả hệ, đã tính ở F3.8); db test_lineage.py 2, test_report_lineage.py 6; office lineage.test.ts 14 (khai báo biểu đồ → khung nhìn khớp truy vấn thật của trang Điều hành, cả hai chiều, dựng và xếp đồ thị, lượt nạp nguồn công khai, xuất OpenLineage); scripts test_pha_he.py 4; db test_report_checks.py 7 (từng luật với số biết trước, cảnh báo, giả lập view hỏng, cột cá nhân qua catalog kể cả view lồng, quyền, khớp khai báo của office); office quality.test.ts 8 (cờ của biểu đồ, assertion nộp lưu lấy nguyên văn, cờ cạnh đúng từng nút Nguồn số liệu, i18n)bước CI make demo-pha-he (bước 5: phả hệ báo cáo); smoke 5 kiểm (2 của cờ chất lượng: không luật nào Trượt trên dữ liệu luồng chính, văn thư không đọc được); e2e pha-he.spec.ts (nút "Nguồn số liệu" → trang Phả hệ; cờ cạnh từng biểu đồ, mục kiểm chất lượng)Chỉ lượt nộp lưu và lượt nạp nguồn công khai (#445) phát sự kiện phả hệ, trang chỉ vẽ lượt gần nhất (lượt nạp: gần nhất mỗi nguồn); chưa dựng Marquez — tệp OpenLineage xuất ở trang (JobEvent cho khung nhìn, biểu đồ) chưa thử nạp vào Marquez; phả hệ báo cáo mức bảng, chưa mức cột; view gọi hàm SQL đọc bảng thì catalog không ghi cạnh đó (hiện không view v_ nào làm vậy, test chặn); cạnh biểu đồ → khung nhìn vẫn khai trong mã giao diện; cờ chất lượng: luật cột cá nhân theo tên cột (cột đặt tên lạ không bắt được, khóa người dùng user_id, created_by không tính), status_total, unique_keys hôm nay đạt theo cấu trúc (ràng buộc, GROUP BY — luật chặn hồi quy), ngưỡng 0 cố định, không lưu lịch sử cờ, không báo chủ động khi một luật chuyển Trượt; nộp lưu chỉ có cờ của lượt gần nhất và lượt bị dừng gần nhất
F3.10(ii) IT/OT rút gọn: cảm biến → NGSI-LD (Scorpio) → dự thảo "Báo cáo sự cố"; sơ đồ cảm biến kho (/cam-bien, SVG) với bốn cờ chất lượng dữ liệu OTMinh họasvc-workflow test_ot.py 23, test_ot_map.py 24 (cờ mất tín hiệu, ngoài dải, trùng observedAt, observedAt ở tương lai; chỉ đọc tenant của người gọi; broker tắt, lỗi → unavailable không 5xx; quyền vai trò khớp actions.yaml; kết quả khớp kiểu GraphQL; sensor.py --room, --late, --frozen-clock, --clock-ahead); office otMap.test.ts 9bước CI make demo-it-ot (profile iot; bước 7: năm cảm biến kho ra đúng cờ, đơn vị khác không thấy, quản trị đơn vị không gọi được Action)Cảm biến giả lập (services/ot-sensor/sensor.py); broker không xác thực thiết bị; broker gửi mỗi thông báo một lần; lịch sử đo không vào bản sao lưu (docs/van-hanh.md, mục Hội tụ IT/OT). Sơ đồ mặt bằng mẫu cố định trong mã giao diện, chung cho mọi đơn vị; dải đo −55…125 °C cố định, không theo từng cảm biến; cờ chỉ trên sơ đồ — số đo vượt ngưỡng vẫn dựng dự thảo kể cả khi ngoài dải; tối đa 100 cảm biến mỗi đơn vị
F3.11[D] Dashboard điều hành (ECharts, bốn view số đếm qua Hasura; thêm phần văn bản đã nộp lưu đọc từ kho lạnh)Hoàn thiệndb test_dashboard.py 6; phả hệ và cờ chất lượng từng biểu đồ đã tính ở F3.9; phần kho lạnh đã tính ở F3.8smoke 2 kiểm (số chia khớp văn bản lãnh đạo đọc được, văn thư không đọc được); e2e dieu-hanh.spec.ts, pha-he.spec.ts (nguồn số liệu, cờ chất lượng của biểu đồ)Bốn view cố định (office.v_*); muốn báo cáo mới phải viết view mới, và khai biểu đồ mới ở apps/office/app/utils/lineage.ts (test đỏ nếu quên); cờ chất lượng là một truy vấn thứ hai đọc lại bốn view, hỏi sau khi biểu đồ có số (không làm chậm biểu đồ, cờ hiện sau): đo trên PostgreSQL 16 thử 2 CPU, đơn vị 2 000 văn bản đến — số liệu 19 ms, cờ 36 ms; 20 000 — số liệu 158–520 ms, cờ 167–313 ms (dao động giữa các lần; phần lớn là v_processing_time có sẵn)
F3.12Liên thông văn bản giữa các đơn vị; thêm đơn vị bằng một lệnhHoàn thiệnsvc-files test_dispatch.py 12; svc-workflow test_dispatch.py 28; db test_dispatch.py 16; scripts test_tenants.py 32; office dispatch.test.ts 4bước CI make demo-lien-thong và "Thêm đơn vị bằng một lệnh (moc tenant create, check)"; smoke: mỗi đơn vị một Organization, công văn tới đơn vị nhận; e2e lien-thong.spec.tsChưa gửi lại được dòng liên thông failed; danh sách đơn vị nhận hiện cho mọi người dùng; văn bản hạn chế không liên thông (chủ ý)
F3.13Giám sát: Prometheus + Grafana, cảnh báo (#255) gửi ra chuông qua Alertmanager (#284), bảng trạng thái thành phần (#274)Dùng đượcluật cảnh báo platform/monitoring/kiem-tra/canh-bao.test.yml 8 ca (promtool test rules, job build), cấu hình Alertmanager (amtool check-config, job build); svc-files test_status.py (4 hàm cho GET /metrics); svc-workflow test_alerts.py 10; office notifications.test.ts 5 (câu, đường dẫn của cảnh báo, của flow Node-RED #366; chuông hiện cho quản trị đơn vị)smoke 4 kiểm: Prometheus đọc được chỉ số cổng API, Grafana có bảng điều khiển nạp sẵn; Prometheus nạp và tính được 8 luật cảnh báo; Prometheus nối Alertmanager; mỗi thành phần đang chạy của /api/status có đúng một dòng moc_component_up = 1, /metrics qua cổng → 404. Bước CI "Cảnh báo vận hành ra chuông (make demo-canh-bao)" (chay-thu, phần luong-chinh): cảnh báo giả tới chuông của quản trị cả ba đơn vị, hết cảnh báo thêm một thông báo, token sai → 401, svc-files không tới được API của AlertmanagerThu chỉ số của APISIX và trạng thái từng thành phần (platform/monitoring/prometheus.yml), chưa có chỉ số bên trong từng dịch vụ (số yêu cầu, độ trễ của svc-files, svc-workflow). Lên Hoàn thiện khi chọn người nhận, tắt tạm một cảnh báo (silence) làm được trên giao diện và lỗi gửi cảnh báo có cảnh báo riêng. Hiện cảnh báo tới mọi quản trị đơn vị; silence phải gọi API của Alertmanager (amtool trong container); PostgreSQL, Hasura, svc-workflow gián đoạn thì cảnh báo về chính sự cố đó chưa vào chuông được (Alertmanager gửi lại khi chúng chạy lại); gửi lỗi (token sai) chỉ thấy trong log của Alertmanager. Trợ lý AI không có cảnh báo (Ollama không trả lời thì báo Tắt, như chưa bật)
F3.14Log tập trung (Vector, Parseable)Dùng đượcsvc-workflow test_request_id.py 10; scripts test_log_tap_trung.py 35; platform/logs/vector.test.yaml 43 ca (vector test)bước CI make demo-log-tap-trung: một mã qua APISIX → svc-workflow → Hasura, svc-files; câu SQL lỗi, lần đăng nhập sai, câu tìm gọi thẳng Meilisearch vào kho đã che; Vector chạy lại thì dòng đầu tiên của dịch vụ đang im vẫn tới kho (#226)Profile logs tắt mặc định; thu 9 dịch vụ (APISIX, hai ứng dụng web, svc-files, svc-workflow tự che; Hasura, Keycloak, PostgreSQL, Meilisearch che bằng luật riêng của Vector); cổng fluent không xác thực (chỉ 127.0.0.1); Vector dùng tài khoản quản trị Parseable; Parseable chạy root (bỏ mọi capability), luôn mở CORS cho cdn.parseable.com; che lần hai không bắt số viết tách nhóm không có từ khóa đứng trước, họ tên, lời báo lỗi của hàm SQL tự viết, giá trị Meilisearch trích không trong dấu `; bật, tắt phải tạo lại 9 container (CSDL, Hasura, Keycloak, tìm kiếm gián đoạn vài giây); log không vào bản sao lưu (docs/van-hanh.md, mục Log tập trung)
F3.15WAF ở cổng: Coraza + OWASP CRS (plugin proxy-wasm của APISIX)Dùng đượcscripts test_waf.py 27 (cấu hình: image ghim digest và băm, pha rewrite, WAF thấy thân yêu cầu, chặn mặc định, MOC_WAF sai bị từ chối ở make up, ở reconciler GitOps trước khi dừng Signet và lúc khởi động, loại trừ gỡ theo id có lý do và chỉ trên khóa giao diện còn gửi, token Keycloak chỉ mất đúng các luật bắt nhầm, chặn JSON lồng sâu); svc-files test_upload.py, test_download.py (tên tệp có ' " ; = : giữ nguyên, tải về an toàn)smoke 8 kiểm: SQLi, XSS, duyệt thư mục, SQLi trong biến GraphQL không token, tải lên .php → 403 có X-Request-Id, không lộ luật; dòng chặn của Coraza mang đúng request_id; JSON lồng 100 tầng → 400, cổng vẫn trả lời; chữ thật qua: GraphQL có nháy, gạch, _is_null; tải lên tên có ' ; = :; dự thảo có đề mục I./II./III.; khối ký, "File …?", http://<IP>, "03 file (…)" ở nội dung và ô Tìm kiếm; token ngẫu nhiên của Keycloak (cookie phiên, mã PKCE, code). Smoke và e2e chạy qua WAF (MOC_WAF=On từ .env.example) trên mọi pull requestLên Hoàn thiện khi chữ nghiệp vụ ở các ô nhập liệu (Cơ quan ban hành, Nơi nhận…) không còn cần người quản trị thêm loại trừ trong apisix.yaml: loại trừ theo ô, có test. Dò chặn nhầm trên 7 389 yêu cầu dựng từ dữ liệu mẫu: 0 với chữ của dữ liệu mẫu, 28 với câu tự viết đặt vào ô không dành cho chúng (Cơ quan ban hành, Nơi nhận) — gặp chữ thật bị chặn thì người quản trị thêm loại trừ trong apisix.yaml. Token ngẫu nhiên của Keycloak, OIDC (#140): trước khi loại trừ khoảng 1/2 500 giá trị mỗi loại bị chặn, sau 0/50 968 trong lượt đo. Coraza 3.3.3 — bản mới nhất có dạng WebAssembly — còn bốn cách lẩn WAF mức medium; hai lỗ mức high có biện pháp ở cổng (platform/apisix/README.md, mục WAF). Nội dung tệp không qua WAF (ClamAV soi). Cổng tốn thêm ~0,4–0,8 GB RAM

F3.6 bỏ trống: cổng xác thực đã lên Must thành F2.13.

2.4. Could (Phase 4)​

MãChức năngMứcTest tự độngĐầu-cuối trong CI · trình diễnHạn chế đã biết
F4.1Đọc văn bản đến: lớp chữ PDF hoặc OCR Tesseract, quy tắc theo bố cục NĐ 30, điền sẵn formHoàn thiệnsvc-files test_docread_*.py 142 (api 43, rules 35, ocr 22, summarize 16, pdflayout 13, layout 10, samples 3); db test_intake_suggestions.py 20; office suggest.test.ts 13bước CI make demo-doc-van-ban, make do-doc-van-ban (đỏ khi PDF có chữ dưới ngưỡng); smoke 3 kiểm; e2e doc-van-ban.spec.tsĐo trên 22 văn bản mẫu hư cấu (số ký hiệu đúng 20/22, ngày ban hành 22/22 — CHANGELOG [1.0.0]), chưa đo trên công văn thật; CI không bật mô hình ngôn ngữ, trích yếu theo quy tắc
F4.2Trợ lý hỏi đáp: câu trích nguyên văn kèm "số ký hiệu · tr. n", lọc quyền ở bước truy xuấtHoàn thiệnsvc-files test_assist_*.py 298 (trừ test_assist_similar.py); db test_assistant.py 15; scripts test_hoi_dap.py 28; office answer.test.ts 31, chatHistory.test.ts 6bước CI make demo-hoi-dap, make do-hoi-dap (đỏ khi recall@3 dưới 80 %, ra nguồn bị cấm, câu đi sai đường hoặc câu âm được trả lời, #448); smoke 3 kiểm; e2e hoi-dap.spec.tsCI chỉ chạy chế độ extract (câu trích), không bật mô hình viết câu trả lời; bộ đo 54 câu hỏi (19 câu tra cứu văn bản, 3 câu trên thông tư nguồn A, 22 câu theo loại của bộ định tuyến, 10 câu đường dữ liệu) trên 6 PDF mẫu hư cấu và 4 thông tư nguồn A
F4.3Tác tử gợi ý giao việc: nhánh song song từ lúc vào sổ, lãnh đạo quyếtHoàn thiệnsvc-workflow test_advisor.py 33, test_suggest.py 22, test_ngay_nghi.py 8; svc-files test_assist_similar.py 26; db test_assignment_suggestions.py 17; scripts test_giao_viec.py 24, test_nguon_ngay_nghi_le.py 10; office assignSuggest.test.ts 13bước CI make demo-giao-viec, make do-giao-viec (đỏ khi top-1 dưới 60 %); smoke 5 kiểm; e2e giao-viec.spec.tsTop-1 29/29 đo trên lịch sử giả lập chia cụm gọn, chỉ 2 đơn vị — cao hơn thực tế (CHANGELOG [1.0.0]); hạn gợi ý tránh ngày lễ của MOC_HOLIDAYS và lịch nghỉ chính thức của nguồn công khai B (Tết, nghỉ bù, làm bù; tới hết 2027, #427) — năm chưa công bố thì người vận hành lấy mới hoặc thêm vào MOC_HOLIDAYS (services/svc-workflow/README.md)
F4.4Che thông tin cá nhân trong nhật ký và trong chữ gửi mô hìnhHoàn thiệnsvc-files test_pii*.py 40 (gồm đo trên kho mẫu và 18 đột biến mã); svc-workflow test_pii_copy.py 3, test_pii_logging.py 2; scripts test_che_pii.py 23bước CI make demo-che-pii; make do-che-pii chạy sẵn trong make test (job unit)Chưa che họ tên, địa chỉ, số dính chữ (trừ di động 10 số sau ĐT, SĐT, DĐ, Tel, Phone: #58), CMND và số tài khoản không có từ khóa đi trước; nhật ký Hasura, PostgreSQL, Keycloak, Meilisearch không qua bộ che (CHANGELOG [1.0.0], mục Bảo mật)
F4.5Ký số PAdES bản phát hành bằng PKI nội bộ trong két — chỉ minh họaMinh họasvc-files test_signing.py 29; db test_file_signatures.py 14; scripts test_ky_so.py 22; office verify.test.ts (đã tính ở F2.13)bước CI make demo-ky-so; smoke 5 kiểm (bỏ qua khi két chưa có PKI); e2e van-ban-di.spec.ts kiểm khối chữ ký ở cổng xác thựcKhông có giá trị pháp lý như chữ ký số công cộng; không HSM; ai chiếm được svc-files ký được bằng khóa của đơn vị; két dựng trước F4.5 phải nâng cấp tay để ký
F4.6Thu góp ý, đo số liệu "sau" theo đặc tả §8Dùng đượcdb test_feedback.py 25, test_success_metrics.py 8; office feedback.test.ts 11, metrics.test.ts 10; scripts test_so_lieu_sau.py 16job db; bước CI make do-so-lieu-sau; trang Số đo /so-do của lãnh đạo đơn vị, nút Góp ý và trang /gop-yCột "trước" của cả bốn chỉ số chưa đo — chưa có đơn vị chạy thử; số trên bản demo tính trên dữ liệu mẫu (seed đi hết các bước trong vài giây); thời gian tìm lại văn bản cũ là độ trễ hệ thống, không phải thời gian người tìm; số bước chuyển tay chỉ so sánh định tính; chưa có ≥ 5 người ngoài đội góp ý như kế hoạch; góp ý không hiện người gửi nhưng không ẩn danh (giờ gửi chính xác, khuôn màn hình có thể lộ vai trò); nội dung chỉ được che email, dãy số dài bằng luật SQL theo mẫu (#265: không che họ tên, địa chỉ, số dính chữ; đơn giản hơn bộ che của F4.4) (docs/so-lieu-sau.md)
F4.7Tự chẩn đoán mức trưởng thành H-P-D-I cho lãnh đạo: 12 câu × 5 mức, radar ECharts, lệch nền, không gian nên làm trước, tỷ trọng và hình dạng theo giáo trình DX-OS mục 2.4 (bốn nguyên mẫu), cảnh báo "rác vào, rác ra" dẫn tới cờ chất lượng số liệu, bước tiếp theo trỏ trang Signet có thật mà người xem mở được, bằng chứng đo được từ các view đã có, lưu lịch sử theo đơn vịDùng đượcoffice hpdi.test.ts 21; db test_maturity.py 18job db; e2e tu-chan-doan.spec.ts (chay-thu, phần e2e); trang /tu-chan-doan của lãnh đạo đơn vị, lãnh đạo phòngBộ câu do đội tự viết, chưa thử với đơn vị thật, không phải bộ chỉ số chính thức, không quy đổi sang DTI; tỷ trọng H-P-D-I chỉ ước lượng từ mức tự đánh giá, hệ số thực chứng lấy bằng 1; một người trả lời cho cả đơn vị; tỷ lệ văn bản đi ký số và các số đo khác của [H] chưa có nguồn lãnh đạo đọc được nên không hiện; lãnh đạo phòng không lưu, không thấy bằng chứng (docs/tu-chan-doan.md §6)

2.5. Ngoài danh sách F — kiểm chất lượng của dự án​

ViệcBằng chứng
Giấy phépjob license "Giấy phép (PoF #2)": scripts/license-header.sh check, có LICENSE, NOTICE, không bundle mã dự án khác; khai REUSE ở REUSE.toml
Dựng từ mã nguồnjob build "Dựng từ mã nguồn (PoF #4)": docker compose config cho từng profile, cú pháp và quyền chạy của script, actionlint
Tài liệu bắt buộcjob docs "Tài liệu (PoF #6)"; scripts test_tai_lieu_ci.py 7 (job unit): job, phần chạy thử, bước CI mà tài liệu trỏ tới làm bằng chứng có thật trong .github/workflows/; test_muc_hoan_thien.py 9: số hàm test của từng tệp trong bảng này khớp mã
Bảo mật chuỗi cung ứngjob security: gitleaks trên lịch sử commit, OSV-Scanner trên tệp khóa phụ thuộc
Quy trình pull request.github/workflows/pr.yml "Quy trình nhánh và tiêu đề PR"; scripts test_quy_trinh.py 23; ruleset .github/rulesets/develop.json đòi đủ 8 kiểm tra (7 job của CI + kiểm quy trình PR)
Phát hành.github/workflows/release.yml "Phát hành gói nguồn"; scripts test_phat_hanh.py 32
Quy trình của ứng dụng là dịch vụ của lõi (#301, mục workflow của manifest, them-ung-dung.md mục 1.10) — mức Dùng đượcKiểm BPMN theo danh sách trắng, form JSON Schema, triển khai một deployment theo băm nội dung, gỡ = tạm dừng; API /api/workflow/<ứng dụng>/… chỉ nhận token của client app-<ứng dụng>. svc-workflow test_app_*.py 107 (api 37, deploy 22, bpmn 13, forms 27 — chạy mọi ca của apps/*/forms/*.cases.json, luật form lúc cài app_form_rules.cases.json và tệp mẫu của trình dựng form, core 8 — hộp việc Văn phòng không lẫn việc ứng dụng); scripts test_ung_dung_quy_trinh.py 25. Trên Flowable thật: bước CI "Quy trình của ứng dụng qua lõi (make demo-quy-trinh)" (chay-thu, phần luong-chinh; ứng dụng mẫu Đơn nghỉ phép), xanh ở lần CI 40369675895 trên develop fe0a37c (26/10/2026). Dùng được, chưa Hoàn thiện: quy trình chỉ cài, gỡ cùng ứng dụng bằng dòng lệnh moc app install, moc app remove. Chỉ quy trình duyệt: không timer, bước tự động, quy trình con; điều kiện chỉ so chuỗi; form chưa có tệp đính kèm (#325); gỡ không xóa lịch sử khỏi Flowable
Cắm ứng dụng bằng một lệnh (moc app install, them-ung-dung.md mục 1) — mức Dùng đượcManifest apps/<thư mục>/app.yaml theo scripts/app-manifest.schema.json; client Keycloak, route APISIX, container, schema + migration bằng vai trò CSDL riêng của ứng dụng (không quyền gì trên lõi; đối tượng đọc lõi hay chạy bằng quyền của Hasura bị từ chối trước khi commit), bảng và quyền Hasura (điều kiện trong quyền không đọc được dữ liệu lõi), Event Trigger — không sửa tệp lõi nào, trạng thái trong .moc-apps/; gỡ giữ dữ liệu. Ứng dụng thứ ba apps/bang-tin (Bảng tin đơn vị). Trang Danh mục ứng dụng /ung-dung của Văn phòng điện tử (mọi vai trò; băm manifest, ứng dụng đã gỡ chỉ lãnh đạo, quản trị đơn vị; khung nhìn hẹp core.app_catalog). scripts test_ung_dung.py 49, test_mang_ung_dung.py 8, test_yaml_nho.py 6; bang-tin node --test 5, unittest 15; db test_app_registry.py 13 (44 ca), test_app_catalog.py 6; office apps.test.ts 10; e2e ung-dung.spec.ts; bước CI make demo-cai-ung-dung (chay-thu, phần luong-chinh; bước 4, 6, 7 kiểm cả danh mục). Một container một trang mỗi ứng dụng, không route API riêng, không Action; cài = tin mã của ứng dụng (mã không bị kiểm; container chỉ ở mạng apps, tới lõi duy nhất qua APISIX, các ứng dụng tới được nhau); .moc-apps/ không vào moc backup; cài, gỡ chỉ bằng dòng lệnh (trang danh mục chỉ để xem, ghi "Đã cài" — không dò sức khỏe container của ứng dụng)
Tách lõi khỏi Văn phòng điện tử (architecture.md §4.7, bước 1–2, #302) — mức Dùng đượcMOC_OFFICE_ENABLED=false (mặc định bật): svc-files, svc-workflow vẫn lên, không gắn route, Action, BPMN, vòng nền nào của Văn phòng; svc-files test_office_flag.py 10, svc-workflow test_office_toggle.py 3. Trên hệ thống chạy: make demo-tat-van-phong, bước CI "Tắt Văn phòng điện tử, lõi vẫn chạy (make demo-tat-van-phong)" — dựng lại hai dịch vụ với cờ tắt, kiểm /api/verify 404 và Action myTasks lỗi, smoke.py --chi-loi và make demo-ca-nhan đạt, rồi dựng lại như cũ. Chưa làm: bước 3–5 (CSDL, vai trò, gói riêng)
Ứng dụng mẫu có quy trình riêng: Đơn nghỉ phép (apps/nghi-phep, #301, them-ung-dung.md mục 7) — mức Minh họaBPMN (chỉ sự kiện, việc người làm, cổng rẽ) và hai form JSON Schema khai trong manifest, chạy trên động cơ quy trình của lõi qua API chung; container chỉ phục vụ trang tĩnh. Trang dựng form từ chính hai tệp JSON Schema đó (apps/nghi-phep/public/schema-form.js, không thư viện, #312), kiểm theo cùng schema mà svc-workflow kiểm lại. nghi-phep schema-form.test.js 14, don.test.js 11, test_server.py 6, test_bpmn.py 6; scripts test_nghi_phep.py 10 (phần thuần của kịch bản); job chay-thu, phần luong-chinh, bước "Quy trình của ứng dụng qua lõi (make demo-quy-trinh)", xanh ở lần CI 40369675895 nêu ở hàng trên. Minh họa: ứng dụng viết để chứng minh lõi chạy được quy trình của ứng dụng khác, bản rút gọn của một đơn nghỉ phép thật. Cần phần lõi của #301 (mục workflow của manifest, API /api/workflow/<ứng dụng>/…); một bước duyệt, không hủy đơn đã gửi
Wiki nội bộ DokuWiki (#367, profile wiki tắt mặc định, van-hanh.md mục Wiki nội bộ) — mức Dùng đượcMỗi đơn vị một không gian don-vi:<mã> chỉ người của đơn vị đọc, sửa; đăng nhập chỉ qua Keycloak (client bí mật wiki, nhóm = tenant_id), không tài khoản quản trị; volume vào moc backup. scripts test_wiki.py 25 (danh sách quyền xét như DokuWiki: hai đơn vị tách nhau, không gian bỏ lại không ai đọc, nhóm mặc định không ghi; client Keycloak; cấu hình compose, APISIX, WAF, Makefile, sao lưu); office apps.test.ts (thẻ Wiki chỉ khi wiki trả lời). Bước CI "Wiki nội bộ theo đơn vị (make wiki-up, make demo-wiki)" (chay-thu, phần sao-luu): người phòng CNTT viết trang, văn thư phòng KT không đọc, không sửa được, sơ đồ trang không lộ trang, người chưa đăng nhập về Keycloak; sau khôi phục bản sao lưu trang vẫn còn (demo_wiki.py --sau-khoi-phuc). Chưa có không gian chung cả trường; đơn vị mới cần chạy lại moc wiki sync; trang wiki không vào tìm kiếm của Văn phòng

2.6. Tóm tắt​

45 tính năng (F1.1–F4.7, không tính F3.6):

MứcSốTính năng
Hoàn thiện35F1.1–F1.10; F2.1–F2.8, F2.10, F2.12, F2.13; F3.1–F3.5, F3.7–F3.9, F3.11, F3.12; F4.1–F4.4
Dùng được7F1.11; F2.9, F3.13, F3.14, F3.15, F4.6, F4.7
Minh họa2F3.10, F4.5
Một phần1F2.11

3. Độ phủ 7 cụm dịch vụ Tầng 1​

Đề liệt kê năng lực của từng cụm (du-thi/olp-2026-de-thi.md §2.1). Chỉ 4 dịch vụ là bắt buộc (định danh, cổng API, dữ liệu, quy trình); phủ thêm là điểm cộng. Bảng tách những năng lực đề viết chung một ô ("log truy vết phân tán + kiểm toán bất biến", "chữ ký số / mã hóa") thành hai dòng. Mức đánh trên đúng năng lực đề nêu, nên có thể thấp hơn mức của tính năng tương ứng ở mục 2: tính năng có thể xong đúng phạm vi kế hoạch mà vẫn chưa phủ hết năng lực của cụm.

CụmNăng lực đề nêuThành phầnMứcGhi chú
① Định danh, bảo mậtSSO, MFAKeycloak, platform/keycloak/realm/moc-realm.jsonHoàn thiệnF1.1
①Két quản lý vòng đời khóa và bí mậtOpenBao, platform/openbao/; svc_files/openbao.py, svc_workflow/openbao.pyDùng đượcF3.2. secret_id của dịch vụ cấp mới mỗi lần khởi động; chưa có quy trình xoay khóa transit, xoay bí mật; .env vẫn là nguồn bí mật
①Trạm kiểm dịch tệpClamAV, services/svc-files/src/svc_files/clamav.pyHoàn thiệnF1.6, F3.1
①Mã hóaTransit của OpenBao + AES-GCM, services/svc-files/src/svc_files/sealed.pyHoàn thiệnF3.7
①Chữ ký số (PKI, HSM)PKI nội bộ trong OpenBao + pyHanko, services/svc-files/src/svc_files/signing.pyMinh họaF4.5. Không HSM — đặc tả §4.4 xếp HSM, chữ ký số công cộng vào "Không làm"
② Đa thuê ước, biên mạngAPI Gateway zero-trustAPISIX, platform/apisix/Hoàn thiệnF1.2; WAF Coraza + OWASP CRS ở cổng (F3.15, Dùng được); ứng dụng tới lõi duy nhất qua cổng: container ứng dụng chỉ ở mạng apps cùng APISIX (test_mang_ung_dung.py, make demo-cai-ung-dung bước 5)
②Rà soát và che PIIservices/svc-files/src/svc_files/pii.py (bản chép ở svc-workflow)Hoàn thiệnF4.4; các giới hạn ở mục 2.4
②Đa thuê ước cách ly dữ liệutenant_id + quyền Hasura (db/metadata/), Keycloak Organizations, scripts/tenants.pyHoàn thiệnF1.3, F3.12; một realm, một bucket chia theo tiền tố
②Dịch thuật, bản địa hóa đa ngôn ngữapps/office/app/locales/Một phầnF2.9: bản địa hóa giao diện Việt / Anh; lõi không có dịch vụ dịch thuật nào cho ứng dụng dùng chung
③ Dữ liệu có cấu trúcOLTP toàn vẹnPostgreSQL 16, db/migrations/default/ (CHECK, khóa ngoại kèm tenant_id, trigger)Hoàn thiệnF1.3
③Tự sinh API từ CSDL, phân quyền tới cấp dòngHasura v2 CE, db/metadata/; PostgREST cho bảng tạo trong Studio (schema sinh, RLS của khung, QĐ-014)Hoàn thiệnF1.3; db test_permissions.py; F1.11
③Phiên bản và lịch sử biến độngcore.audit_log, view office.document_historyHoàn thiệnF2.12, F3.5; mức dòng thay đổi, trường nội dung dài chỉ báo "đã đổi"
③Đóng băng số liệu chuỗi thời gianLịch sử đo trong Scorpio (NGSI-LD temporal API, đọc ở make demo-it-ot); bảng office.archived_document_factsMột phầnChưa có kho chuỗi thời gian riêng, chưa chụp số liệu theo kỳ; archived_document_facts chỉ giữ số liệu dashboard của văn bản đã nộp lưu
③Lớp ngữ nghĩa báo cáoBảng sinh: báo cáo khai báo bằng dữ liệu (sinh.bao_cao: bảng nguồn, cột nhóm, phép tính, kiểu hiện), trang /sinh gọi hàm gộp của PostgREST (#587); Văn phòng: bốn view office.v_* qua Hasura (db/migrations/default/1792281601200_dashboard/)Một phầnF1.11, F3.11. Bảng sinh: chiều là một cột enum hay ngày, chỉ số là đếm, tổng, trung bình; thêm báo cáo không viết mã, RLS lọc theo đơn vị trước khi gộp. Chưa có định nghĩa chỉ số dùng chung giữa các báo cáo; báo cáo của Văn phòng vẫn là view viết tay
④ Dữ liệu phi cấu trúcLưu trữ đối tượngSeaweedFS, services/svc-files/src/svc_files/storage.pyHoàn thiệnF1.5
④Quản lý hồ sơ, văn bản cho quy trình phê duyệtapps/office + svc-workflow + FlowableHoàn thiệnF2.2–F2.4, F3.8 (lập hồ sơ, nộp lưu, phòng đọc trên giao diện)
④Chỉ mục toàn vănMeilisearch, svc_files/search.py, svc_files/texts.pyHoàn thiệnF2.7; bản scan vào chỉ mục bằng chữ OCR của 3 trang đầu (#253)
④Trộn dữ liệu sinh biểu mẫu, báo cáo độngBảng sinh: mẫu in lưu thành dữ liệu (sinh.mau_in), svc-files trộn một dòng vào mẫu rồi dựng PDF bằng WeasyPrint (svc_files/sinh_print.py, GET /api/sinh/<bảng>/<id>/pdf, #586); báo cáo khai báo bằng dữ liệu (sinh.bao_cao, #587). Văn phòng: WeasyPrint với svc_files/render.py (văn bản đi), svc_files/registers.py (sổ đăng ký); dự thảo "Báo cáo sự cố" (svc_workflow/ot.py)Dùng đượcF1.11. Bảng sinh: đổi mẫu, thêm báo cáo không sửa mã; mẫu là văn bản thuần (chỗ trống {{cột}}, tiêu đề, đoạn), giá trị thoát HTML, WeasyPrint chỉ tải data:. Khuôn văn bản đi, sổ đăng ký của Văn phòng vẫn viết trong mã
⑤ Quy trình, tự động hóaBPMN quản lý trạng thái luồng phê duyệtFlowable 8, services/svc-workflow/processes/*.bpmn20.xml (có DI); sơ đồ trên trang văn bản: svc_workflow/so_do.py, Action documentProcess, apps/office/app/components/ProcessDiagram.vueHoàn thiệnF1.4, F2.3, F2.4. Người đọc được văn bản xem được quy trình BPMN của chính văn bản đó ở mục Sơ đồ quy trình: bước đã qua, bước đang chờ, nhánh đã đi, kèm danh sách bước dạng chữ; SVG tự vẽ từ DI, không bpmn-js (#603; QĐ-009). Ứng dụng cắm thêm dùng chung động cơ này qua API /api/workflow/<ứng dụng>/… (#301; mục 2.5, mức Dùng được), chưa có sơ đồ
⑤Thực thi kịch bản serverlessNode-RED 5.0.8 (profile tu-dong, tắt mặc định), platform/tu-dong/, scripts/tu_dong.pyMột phầnNgười vận hành nền tảng (vai trò operator của client Keycloak tu-dong) soạn, triển khai flow ở /tu-dong/ không sửa mã dịch vụ; không node exec, không cài node từ trình soạn, chỉ tới lõi qua APISIX (#366, van-hanh.md mục Tự động hóa bằng Node-RED, bước CI make demo-tu-dong). Flow gọi API của lõi bằng robot của đơn vị (moc tu-dong robot add: chỉ đọc văn bản không hạn chế, báo người trong đơn vị qua Action notifyFromFlow); hai flow mẫu: văn bản sắp hạn lúc 07:00, văn bản Hỏa tốc vào sổ. Chưa có endpoint webhook và bước BPMN gọi flow; việc nền của lõi vẫn là vòng lặp trong svc-files, svc-workflow
⑤API nạp dữ liệu khối lớn theo lôPOST /api/imports/incoming của svc-files (svc_files/imports/), bảng core.import_jobs, core.import_rowsDùng đượcVăn bản đến: tệp NDJSON/CSV + zip tệp đính kèm, xử lý nền theo mẻ; mỗi dòng qua đúng ClamAV và Action receiveIncoming thay mặt văn thư, lỗi từng dòng, gửi lại không nhân đôi, chạy tiếp sau khi svc-files khởi động lại. svc-files test_imports_*.py 111; db test_import_jobs.py 18; bước CI make demo-nap-lo. Chưa có màn "Nhập lô" trên giao diện — gọi API; mới có một loại lô (văn bản đến); zip tệp đính kèm mất khi svc-files khởi động lại (dòng còn cần tệp báo archive_lost)
⑥ Vận hành, giám sát, kiểm toánLog truy vết phân tánAPISIX gắn X-Request-Id (platform/apisix/apisix.yaml) và ghi nó ở cuối dòng log truy cập; svc-files, svc-workflow ghi mã này vào nhật ký, chuyển tiếp sang Hasura và vào core.audit_log.request_id; log tập trung (F3.14, profile logs) tra theo mã: make logs-tra ID=…. Truy vết (#364, profile trace): APISIX lấy mẫu MOC_TRACE_RATIO yêu cầu, svc-files, svc-workflow ghi span tiếp theo traceparent (tracing.py), Jaeger 2.22 lưu trong RAM và hiện thời gian từng chặng; tìm theo thẻ x-request-idDùng đượcTắt mặc định (cả log tập trung và truy vết). Span che hai lớp: dịch vụ gửi theo danh sách trắng, bỏ chuỗi truy vấn, URL ngoài chỉ còn tên máy, lỗi chỉ còn tên loại; Jaeger bỏ http.target của APISIX. Chưa có span của Hasura (bản CE), Flowable, Keycloak, PostgreSQL; trace mất khi Jaeger khởi động lại; chưa xem được trong Grafana (Jaeger 2.22 bỏ API v1). Bằng chứng: svc-files test_tracing.py, svc-workflow test_tracing_copy.py, scripts test_truy_vet.py; bước CI jaeger validate. Giám sát: Prometheus + Grafana chỉ có chỉ số APISIX (F3.13)
⑥Kiểm toán bất biếnSổ kiểm toán chuỗi băm (db/migrations/default/1792281600100_core_schema/), neo ra bucket khóa ghi, scripts/audit_chain.pyHoàn thiệnF2.12
⑥Trục streaming sự kiệnLuồng core.app_events (migration 1792281603200) + Hasura Event Triggers do moc app install sinh (scripts/ung_dung.py)Một phầnLõi chép sự kiện văn bản trong danh sách trắng (đến, vào sổ, phát hành; không văn bản hạn chế, 5 cột) sang core.app_events; mỗi ứng dụng khai events nhận qua webhook — Hasura gọi cổng hook nội bộ 9081 của APISIX (không publish), APISIX chuyển tới container ứng dụng ở mạng apps —, header bí mật, Hasura gửi lại khi lỗi (ít nhất một lần). Không phải broker streaming (không Kafka hay tương đương): không đọc lại từ một vị trí, không nhóm người nghe, không bảo đảm thứ tự, chỉ 3 loại sự kiện; sự kiện khác vẫn chỉ nằm trong bảng (office.document_events, core.lineage_events, core.notifications), Centrifugo chỉ đẩy tới trình duyệt. Bằng chứng: db test_app_registry.py (PostgreSQL, Hasura thật khi MOC_IT=1); bước CI make demo-cai-ung-dung (chay-thu, phần luong-chinh)
⑥GitOpsscripts/reconcile.py, platform/systemd/Hoàn thiệnF3.3; Compose + reconciler, không Argo CD
⑥Sao lưu, khôi phục thảm họarestic, scripts/backup.pyHoàn thiệnF3.4; kho sao lưu trên cùng máy chủ, chép ra ngoài bằng tay
⑦ Truyền thông, giao vậnĐịnh tuyến thông báo đa kênhsvc_workflow/channels.py, hộp thư đi core.notification_deliveries; Mailpit (profile mail) để thửMột phầnChuông trên web (Centrifugo) cộng email (SMTP), webhook chat (Mattermost, Slack, ntfy), SMS (SMS Gateway for Android) và Zalo (bot, liên kết bằng mã), tắt mặc định; trigger tạo dòng chờ gửi trong cùng giao dịch ghi thông báo, lỗi tạm thử lại giãn dần. SMS, Zalo do từng người tự bật (#365). Email chưa cho từng người tắt; webhook một URL cho cả bản cài; SMS, Zalo mới thử với cổng, bot giả trong test, chưa với tài khoản thật
⑦WebSocket thời gian thựcCentrifugo, platform/centrifugo/config.yaml, route /realtimeHoàn thiệnF2.5
⑦Hàng đợi bất đồng bộ chống chối bỏHộp thư đi office.dispatches (svc_workflow/dispatch.py); việc giao ra ngoài của Flowable (topic moc-goi-y, moc-sla)Một phầnLiên thông (F3.12) có thử lại lùi dần, so SHA-256, ghi sổ kiểm toán cả hai đơn vị trong cùng giao dịch. Chưa có hàng đợi dùng chung cho ứng dụng khác, chưa có biên nhận ký số

Tổng: 29 năng lực — Hoàn thiện 17, Dùng được 4, Minh họa 1, Một phần 7, Chưa có 0.

  • Một phần: kịch bản serverless (⑤: Node-RED có robot, flow mẫu; chưa có webhook, bước BPMN gọi flow), dịch thuật trong lõi (②), đóng băng số liệu chuỗi thời gian (③), lớp ngữ nghĩa báo cáo (③: bảng sinh khai báo báo cáo bằng dữ liệu, Văn phòng vẫn view viết tay), trục sự kiện (⑥: Event Triggers giao tới webhook của ứng dụng, chưa phải broker streaming), định tuyến thông báo đa kênh (⑦: email chưa cho từng người tắt; SMS, Zalo chưa thử với tài khoản thật), hàng đợi chống chối bỏ (⑦).
  • Bốn dịch vụ bắt buộc (SSO, cổng API, dữ liệu có cấu trúc và phi cấu trúc, quy trình) đều ở mức Hoàn thiện.

4. Kiểm lại​

Chạy ở gốc repo. Đếm tĩnh như mục 1:

for d in services/svc-files/tests services/svc-workflow/tests scripts/tests db/tests; do
printf '%s %s\n' "$d" "$(grep -rhoE '^\s*(async )?def test_' --include='*.py' "$d" | wc -l)"
done
grep -hE '^\s*(test|it)\(' apps/office/tests/*.test.ts | wc -l # giao diện (node:test)
grep -hE '^\s*(test|it)\(' apps/ca-nhan/tests/*.test.js apps/bang-tin/tests/*.test.js | wc -l # hai ứng dụng
grep -rhoE '^\s*(async )?def test_' apps/bang-tin/tests | wc -l # Bảng tin, phần Python
grep -hE '^\s*(test|it)\(' apps/nghi-phep/tests/*.test.js | wc -l # Đơn nghỉ phép (#301)
grep -rhoE '^\s*(async )?def test_' apps/nghi-phep/tests | wc -l # Đơn nghỉ phép, phần Python
grep -hE '^\s*(test|it)\(' apps/lich-giang/tests/*.test.js | wc -l # Đổi, bù lịch giảng (#482)
grep -rhoE '^\s*(async )?def test_' apps/lich-giang/tests | wc -l # Đổi, bù lịch giảng, phần Python
grep -rhoE '^\s*(async )?def test_' apps/tai-lieu-api/tests | wc -l # trang tài liệu API (#357)
grep -hE '^test\(' tests/e2e/*.spec.ts | wc -l # kịch bản Playwright
grep -cE '^\s*ok\(' scripts/smoke.py # kiểm của make smoke
grep -cE '^\s*(async )?def test_' services/svc-files/tests/test_*.py # từng tệp (đổi thư mục cho phần khác)

Chạy thật (cần gì cho từng lệnh: README, mục Kiểm thử):

make test # hai dịch vụ, scripts, apps/office và năm ứng dụng còn lại ở apps/ — như job unit
db/tests/stack.sh up # PostgreSQL + Hasura cho db/tests — như job db (lệnh đủ ở db/README.md)
make up && make seed && make smoke && make e2e # như job chay-thu (phần luong-chinh, e2e)
make demo-nong-lanh # từng màn ở mục 2; make help liệt kê tất cả

Các ô "Chưa có", "Chưa làm" ở mục 2, mục 3:

git grep -n event_triggers -- db/metadata scripts/ung_dung.py # chỉ ung_dung.py: trigger sinh lúc cài ứng dụng
git grep -niE 'kafka|nats|rabbitmq|kestra|openfaas|marquez' -- docker-compose.yml # rỗng
grep -n job_name platform/monitoring/prometheus.yml # apisix, svc-files (trạng thái thành phần), prometheus
grep -nE '^ [a-z]+:$|^ name:|^ - name:' .github/workflows/ci.yml # job và bước CI

Tính năng đổi mức thì sửa bảng ở mục 2 và mục 3 trong cùng pull request với thay đổi đó.