diff --git a/README.md b/README.md index 8cb2c7b..37fc799 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,11 @@ data/ └── insights.json # Recurring causal patterns packs/ -└── worldcup-2026/ # Optional vertical pack for live causal timelines +├── worldcup-2026/ # Live sports/event intelligence +└── vn-ecommerce-compliance-2026/ # Regulation → control → evidence timeline ├── README.md + ├── manifest.json # Optional pack metadata + ├── views.json # Optional Canvas/audience projections ├── events.json ├── links.json └── insights.json @@ -94,8 +97,9 @@ Current packs: | Pack | Purpose | |------|---------| | [`worldcup-2026`](packs/worldcup-2026/) | Proof of concept for live sports/event intelligence using World Cup 2026 results, causal implications, and watchpoints | +| [`vn-ecommerce-compliance-2026`](packs/vn-ecommerce-compliance-2026/) | Evidence-first timeline connecting Vietnamese legal changes to role-owned controls and proof of execution | -See [docs/PACKS.md](docs/PACKS.md) for pack structure and quality guidelines. +See [docs/PACKS.md](docs/PACKS.md) for pack structure and quality guidelines, and [docs/PACK-VIEWS.md](docs/PACK-VIEWS.md) for optional Canvas and teaching projections. ## Relationship types diff --git a/docs/PACK-VIEWS.md b/docs/PACK-VIEWS.md new file mode 100644 index 0000000..67162b2 --- /dev/null +++ b/docs/PACK-VIEWS.md @@ -0,0 +1,153 @@ +# Pack Manifests and Canvas Views + +Causari packs contain a canonical causal graph and may optionally contain presentation metadata. + +The separation is deliberate: + +```text +canonical graph data ++ audience/view projection += timeline, canvas, lesson, recap, or decision workflow +``` + +## Canonical files + +Every pack continues to use: + +```text +events.json +links.json +insights.json +``` + +These files must remain portable across the MCP server, Canvas, static imports, runtime fetches, and future consumers. + +## Optional files + +A pack may add: + +```text +manifest.json +views.json +``` + +### `manifest.json` + +The manifest describes the pack rather than changing graph semantics. + +Recommended fields: + +```json +{ + "id": "pack-id", + "version": "0.1.0", + "title": "Pack title", + "description": "Purpose and scope", + "language": "en", + "defaultLocale": "vi", + "jurisdiction": "VN", + "temporalMode": "effective-date-timeline", + "graphMode": "instrument-to-control-to-evidence", + "updateCadence": "event-driven", + "asOf": "2026-07-30", + "files": { + "events": "events.json", + "links": "links.json", + "insights": "insights.json", + "views": "views.json" + }, + "canvas": { + "defaultView": "executive-impact", + "recommendedLayout": "timeline-lanes", + "laneField": "lane", + "nodeTypeField": "nodeType" + } +} +``` + +Consumers must treat unknown manifest fields as optional. + +### `views.json` + +A view is a projection over the graph. It may define: + +- audience +- guiding question +- included or focused event ids +- lane definitions +- highlighted causal paths +- story steps +- teaching notes + +A view must not duplicate canonical events or links. + +Example: + +```json +[ + { + "id": "teaching-causal-chain", + "title": "Teach the causal chain", + "audiences": ["teacher", "student"], + "projection": { + "trigger": ["instrument", "effective-date"], + "response": ["action"], + "adaptation": ["control"], + "observableOutcome": ["evidence"] + } + } +] +``` + +## Why views are generic + +A compliance pack may use: + +```text +law → implementation → control → evidence +``` + +A World War I lesson pack may use: + +```text +structural cause → trigger → escalation → outcome → source evidence +``` + +A World Cup pack may use: + +```text +match result → table implication → affected team → next watchpoint +``` + +The domains differ, but all are narrative projections over event-and-link data. + +## Design rule: data is canonical, views are disposable + +Do not encode a lecturer's slide order, an executive dashboard filter, or one UI layout into the causal graph itself. + +Instead: + +1. Curate events and links once. +2. Add provenance and honest confidence. +3. Create multiple views for different jobs. +4. Allow consumers to ignore view metadata entirely. + +This avoids three common failures: + +- duplicating nodes for each audience +- coupling data packs to one frontend +- weakening causal semantics to satisfy a presentation layout + +## Suggested Canvas behavior + +A Canvas consumer may: + +1. Load `manifest.json` when present. +2. Load the canonical graph. +3. Offer the view list from `views.json`. +4. Filter or emphasize nodes according to the selected view. +5. Render lanes from event metadata such as `lane`. +6. Enter story mode using `highlightPaths` or `storySteps`. +7. Always preserve access to sources and confidence. + +Views should degrade gracefully. A consumer that only understands `events.json`, `links.json`, and `insights.json` must still load the pack successfully. diff --git a/docs/PACKS.md b/docs/PACKS.md index b00f878..0cedac2 100644 --- a/docs/PACKS.md +++ b/docs/PACKS.md @@ -18,11 +18,15 @@ Examples: ```text packs/{pack-id}/ ├── README.md # pack purpose, audience, curation notes +├── manifest.json # optional pack metadata and consumer hints +├── views.json # optional Canvas, audience, and teaching projections ├── events.json # CKGEvent-compatible events ├── links.json # CausalLink-compatible relationships └── insights.json # recurring patterns found inside this pack ``` +`events.json`, `links.json`, and `insights.json` are canonical. Optional manifest and view files must not be required by older consumers. See [PACK-VIEWS.md](PACK-VIEWS.md) for the projection model. + ## Compatibility Pack files should remain compatible with the existing Causari schemas: @@ -44,6 +48,8 @@ For live or short-horizon timelines, packs may use the following optional event These optional fields let a pack model live event intelligence without changing the core schema. +Domain packs may also add optional fields such as `nodeType`, `lane`, `audiences`, `normativeStatus`, or `lifecycleStatus`. Consumers must ignore unknown optional fields and preserve the canonical event/link semantics. + ## Graph modeling rules (required for clean loading) A pack is a causal graph. To load cleanly into any Causari consumer (the visual explorer, the MCP store, an agent), it must follow the same structural rules as the core dataset: @@ -51,6 +57,7 @@ A pack is a causal graph. To load cleanly into any Causari consumer (the visual - **Links connect events to events.** A `CausalLink`'s `fromEvent` and `toEvent` must both be `id`s of events **in the same pack**. Never point a link at an insight id or at a node that isn't defined — that produces dangling edges in the visual. - **Insights attach to links, not the other way around.** An `Insight.instances` array lists the `CausalLink` ids that demonstrate the pattern. Insights are not graph nodes and are never link endpoints. - **Upcoming fixtures are events with `status: "scheduled"`.** Model a "watchpoint" (a match that hasn't happened yet) as a real scheduled event, then link the completed result to it. This keeps `nextWatchpoints` (free-text hints) separate from the actual graph edges. +- **Operational controls must not masquerade as completed work.** For action/control/evidence nodes, use a domain-specific field such as `lifecycleStatus`; reserve the core `status` field for event state. - **Ids are kebab-case and globally unique** (e.g. `wc2026-brazil-draws-morocco`). Avoid `--` inside an event id so the link id `{from}--{rel}-->{to}` stays unambiguous. Run `node scripts/validate-pack.mjs ` before every commit — it enforces all of the above (referential integrity, id format, enums, 0–1 ranges) and is wired into CI. @@ -117,4 +124,6 @@ A visual explorer can then render: Event → causal link → affected entity → next watchpoint ``` +When `manifest.json` and `views.json` exist, a Canvas consumer may add lane layouts, audience filters, highlighted paths, or teaching story mode while still loading the same canonical graph. + See [LIVE-UPDATES.md](LIVE-UPDATES.md) for the daily match-day update workflow. diff --git a/packs/vn-ecommerce-compliance-2026/README.md b/packs/vn-ecommerce-compliance-2026/README.md new file mode 100644 index 0000000..530987c --- /dev/null +++ b/packs/vn-ecommerce-compliance-2026/README.md @@ -0,0 +1,179 @@ +# Vietnam E-commerce Compliance 2026 + +> An evidence-first causal pack connecting legal change to operational control and proof of execution. + +This pack is an initial research preview for Vietnamese e-commerce, social commerce, livestream selling, household-business tax operations, electronic invoices, and cybersecurity. + +It does **not** try to summarize every law effective from July 1, 2026. It models a narrower, testable wedge: + +```text +legal instrument +→ effective rule +→ role-owned action +→ operating control +→ evidence +``` + +## Why this pack exists + +SMEs usually do not fail because they cannot download legal documents. They fail because they cannot quickly determine: + +- whether a rule applies to their business model +- which team owns the response +- which product or operating process must change +- what evidence proves the control actually ran +- what changed after the last review + +The pack turns those questions into a causal graph that can be used by humans, AI agents, and timeline/canvas interfaces. + +## Scope + +The initial version focuses on: + +- E-Commerce Law No. 122/2025/QH15 +- Decree No. 248/2026/NĐ-CP +- Tax Administration Law No. 108/2025/QH15 +- Decree No. 252/2026/NĐ-CP +- Decree No. 254/2026/NĐ-CP on electronic invoices and documents +- Cybersecurity Law No. 116/2025/QH15 +- derived operating controls for seller governance, livestream identity and product claims, transaction-to-invoice reconciliation, access and incident auditability, and evidence management + +Out of scope for this first version: + +- a complete inventory of all laws, decrees, and circulars effective in 2026 +- organization-specific legal conclusions +- penalty calculations +- automated legal advice +- replacing review by qualified legal, tax, or accounting professionals + +## Files + +```text +packs/vn-ecommerce-compliance-2026/ +├── README.md +├── manifest.json +├── views.json +├── events.json +├── links.json +└── insights.json +``` + +The three canonical graph files remain compatible with the existing Causari pack loader: + +- `events.json` +- `links.json` +- `insights.json` + +`manifest.json` and `views.json` are optional projection metadata for Canvas, teaching, role-based filtering, and story mode. + +## Node model + +Every node is still a Causari event, but optional fields describe how a consumer may project it: + +| Field | Meaning | +|---|---| +| `nodeType` | `instrument`, `effective-date`, `action`, `control`, or `evidence` | +| `lane` | `law`, `implementation`, `operations`, or `evidence` | +| `normativeStatus` | `binding`, `derived-control`, or `recommended-practice` | +| `lifecycleStatus` | Current operating state for non-legal nodes, such as `active` | +| `audiences` | Roles that should see the node | +| `sources` | Provenance for factual or binding claims | + +The distinction between `normativeStatus` values is essential: + +- **binding**: directly represents an enacted legal instrument or effective provision +- **derived-control**: an operational interpretation grounded in an official summary +- **recommended-practice**: a Causari implementation recommendation, not a direct quotation of law + +## Canvas timeline + +The recommended Canvas layout uses four lanes: + +```text +Law + ↓ +Implementation + ↓ +Operations + ↓ +Evidence +``` + +`views.json` provides reusable projections: + +- executive impact +- tax and accounting operations +- e-commerce and livestream +- IT and security +- teaching causal chain + +A consumer should load the canonical graph once, then apply a view without copying or rewriting the underlying data. + +## Relation to teaching packs such as WWI + +The same view abstraction can power a historical lesson: + +```text +historical trigger +→ institutional response +→ military/political adaptation +→ observable outcome +→ primary-source evidence +``` + +For compliance, the equivalent is: + +```text +legal trigger +→ implementation rule +→ organizational adaptation +→ operating evidence +``` + +This means `views.json` is not a legal-specific UI configuration. It is a generic narrative projection layer over a causal graph. A WWI pack might define views such as: + +- long-term causes +- July Crisis escalation +- alliance propagation +- fronts and turning points +- consequences and counterfactuals + +The data remains canonical; lecturers, executives, analysts, and AI agents see different paths through it. + +## Intended users + +### SME leaders + +Understand which changes deserve budget, ownership, and sequencing. + +### Tax, legal, and operations teams + +Move from document reading to an executable backlog and evidence register. + +### Software and platform teams + +Translate regulation into identity, workflow, logging, reconciliation, and retention requirements. + +### Lecturers and facilitators + +Use story mode to explain how a trigger propagates through a system. + +### AI agents + +Answer questions such as: + +- Which binding event led to this control? +- Is this node law, derived guidance, or a recommendation? +- Which roles are affected? +- What evidence should exist? +- Which source supports the claim? + +## Quality and safety + +- Binding events must use official government or ministry sources. +- Derived controls must never be presented as verbatim legal requirements. +- Every update must preserve an `asOf` date. +- Applicability must be evaluated for each organization. +- High-risk interpretations should be reviewed by qualified professionals. + +> This pack is a research and operational-design aid, not legal or tax advice. diff --git a/packs/vn-ecommerce-compliance-2026/events.json b/packs/vn-ecommerce-compliance-2026/events.json new file mode 100644 index 0000000..c9401b7 --- /dev/null +++ b/packs/vn-ecommerce-compliance-2026/events.json @@ -0,0 +1 @@ +[{"id":"vnce-law-tax-management-108-passed","title":"Tax Administration Law No. 108/2025/QH15 passed","title_vi":"Thông qua Luật Quản lý thuế số 108/2025/QH15","description":"Vietnam passed Tax Administration Law No. 108/2025/QH15; most provisions took effect on July 1, 2026, with selected household-business rules effective from January 1, 2026.","description_vi":"Việt Nam thông qua Luật Quản lý thuế số 108/2025/QH15; phần lớn quy định có hiệu lực từ 1/7/2026, một số quy định cho hộ kinh doanh có hiệu lực từ 1/1/2026.","yearNum":2025,"yearLabel":"2025","precision":"day","domains":["economy","systems"],"impactScore":0.88,"tags":["vietnam","compliance","tax","law-108-2025-qh15","ecommerce"],"date":"2025-12-10","dateLabel":"December 10, 2025","status":"completed","nodeType":"instrument","lane":"law","normativeStatus":"binding","audiences":["ceo","cfo","tax-accounting","legal","ecommerce","it"],"whyItMatters":"The law shifts tax administration further toward standardized digital data, electronic procedures, and traceable taxpayer obligations.","whyItMatters_vi":"Luật đẩy quản lý thuế sâu hơn theo hướng dữ liệu số chuẩn hóa, thủ tục điện tử và nghĩa vụ có thể truy vết.","sources":[{"type":"official","citation":"Government policy portal — Tax Administration Law No. 108/2025/QH15","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-luat-quan-ly-thue-co-hieu-luc-tu-1-7-2026-119260626174633402.htm"}]},{"id":"vnce-law-cybersecurity-116-passed","title":"Cybersecurity Law No. 116/2025/QH15 passed","title_vi":"Thông qua Luật An ninh mạng số 116/2025/QH15","description":"Vietnam passed Cybersecurity Law No. 116/2025/QH15, consolidating relevant cybersecurity and cyberinformation-security rules, effective July 1, 2026.","description_vi":"Việt Nam thông qua Luật An ninh mạng số 116/2025/QH15, hợp nhất các quy định liên quan và có hiệu lực từ 1/7/2026.","yearNum":2025,"yearLabel":"2025","precision":"day","domains":["technology","systems"],"impactScore":0.84,"tags":["vietnam","compliance","cybersecurity","law-116-2025-qh15","data-governance"],"date":"2025-12-10","dateLabel":"December 10, 2025","status":"completed","nodeType":"instrument","lane":"law","normativeStatus":"binding","audiences":["ceo","legal","it","security","data"],"whyItMatters":"Cybersecurity obligations increasingly become operating-system requirements: access control, incident handling, accountability, and evidence.","whyItMatters_vi":"Nghĩa vụ an ninh mạng ngày càng trở thành yêu cầu của hệ điều hành doanh nghiệp: kiểm soát truy cập, xử lý sự cố, trách nhiệm và bằng chứng.","sources":[{"type":"official","citation":"Government policy portal — new contents of Cybersecurity Law No. 116/2025/QH15","url":"https://xaydungchinhsach.chinhphu.vn/nhung-noi-dung-moi-trong-luat-an-ninh-mang-so-116-2025-qh15-119260629145615168.htm"}]},{"id":"vnce-law-ecommerce-122-passed","title":"E-Commerce Law No. 122/2025/QH15 passed","title_vi":"Thông qua Luật Thương mại điện tử số 122/2025/QH15","description":"Vietnam passed E-Commerce Law No. 122/2025/QH15, covering platforms, social commerce, livestream selling, affiliate marketing, cross-border activity, and supporting services.","description_vi":"Việt Nam thông qua Luật Thương mại điện tử số 122/2025/QH15, điều chỉnh nền tảng, social commerce, livestream, affiliate, xuyên biên giới và dịch vụ hỗ trợ.","yearNum":2025,"yearLabel":"2025","precision":"day","domains":["economy","technology","systems"],"impactScore":0.92,"tags":["vietnam","compliance","ecommerce","law-122-2025-qh15","platform","livestream"],"date":"2025-12-10","dateLabel":"December 10, 2025","status":"completed","nodeType":"instrument","lane":"law","normativeStatus":"binding","audiences":["ceo","legal","ecommerce","marketing","platform-operations","it"],"whyItMatters":"Platforms and commercial participants move from loosely connected actors toward an accountable operating network with explicit information, identity, complaint, and cooperation duties.","whyItMatters_vi":"Nền tảng và các chủ thể kinh doanh chuyển từ những tác nhân kết nối lỏng sang một mạng vận hành có trách nhiệm rõ về thông tin, định danh, khiếu nại và phối hợp xử lý.","sources":[{"type":"official","citation":"Government legal-document portal — E-Commerce Law No. 122/2025/QH15","url":"https://vanban.chinhphu.vn/?classid=1&docid=216503&pageid=27160&typegroupid=3"}]},{"id":"vnce-household-tax-einvoice-rules-effective","title":"Household-business tax and e-invoice provisions take effect","title_vi":"Quy định thuế và hóa đơn điện tử cho hộ kinh doanh có hiệu lực","description":"Selected tax-declaration, tax-calculation, and electronic-invoice provisions for household and individual businesses took effect on January 1, 2026.","description_vi":"Một số quy định về kê khai, tính thuế và hóa đơn điện tử cho hộ, cá nhân kinh doanh có hiệu lực từ 1/1/2026.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["economy","systems"],"impactScore":0.83,"tags":["vietnam","household-business","tax-declaration","electronic-invoice","transition"],"date":"2026-01-01","dateLabel":"January 1, 2026","status":"completed","nodeType":"effective-date","lane":"implementation","normativeStatus":"binding","audiences":["owner","tax-accounting","ecommerce","software-provider"],"whyItMatters":"A major seller segment entered data-based declaration and invoice workflows before the broader July compliance wave.","whyItMatters_vi":"Một nhóm người bán rất lớn bước vào quy trình kê khai và hóa đơn dựa trên dữ liệu trước làn sóng tuân thủ chung từ tháng 7.","sources":[{"type":"official","citation":"Government policy portal — Tax Administration Law effective-date provisions","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-luat-quan-ly-thue-co-hieu-luc-tu-1-7-2026-119260626174633402.htm"}]},{"id":"vnce-decree-ecommerce-248-effective","title":"Decree No. 248/2026/NĐ-CP takes effect","title_vi":"Nghị định số 248/2026/NĐ-CP có hiệu lực","description":"Decree No. 248/2026/NĐ-CP detailed parts of the E-Commerce Law and took effect on July 1, 2026.","description_vi":"Nghị định số 248/2026/NĐ-CP quy định chi tiết một số điều của Luật Thương mại điện tử và có hiệu lực từ 1/7/2026.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["economy","technology","systems"],"impactScore":0.9,"tags":["vietnam","ecommerce","decree-248-2026","platform-governance","livestream"],"date":"2026-07-01","dateLabel":"July 1, 2026","status":"completed","nodeType":"instrument","lane":"implementation","normativeStatus":"binding","audiences":["ceo","legal","ecommerce","marketing","platform-operations","it"],"whyItMatters":"The high-level law becomes concrete platform and seller workflows that can be implemented, tested, and audited.","whyItMatters_vi":"Luật cấp cao được chuyển thành các quy trình nền tảng và người bán có thể triển khai, kiểm thử và kiểm toán.","sources":[{"type":"official","citation":"Ministry of Industry and Trade — briefing on E-Commerce Law and Decree No. 248/2026/NĐ-CP","url":"https://moit.gov.vn/tin-tuc/bo-cong-thuong-pho-bien-luat-thuong-mai-dien-tu-va-nghi-dinh-so-248-2026-nd-cp.html"}]},{"id":"vnce-decree-tax-252-effective","title":"Decree No. 252/2026/NĐ-CP takes effect","title_vi":"Nghị định số 252/2026/NĐ-CP có hiệu lực","description":"Decree No. 252/2026/NĐ-CP detailed and organized implementation of the Tax Administration Law from July 1, 2026.","description_vi":"Nghị định số 252/2026/NĐ-CP quy định chi tiết và tổ chức thi hành Luật Quản lý thuế từ 1/7/2026.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["economy","systems"],"impactScore":0.86,"tags":["vietnam","tax","decree-252-2026","implementation","digital-tax"],"date":"2026-07-01","dateLabel":"July 1, 2026","status":"completed","nodeType":"instrument","lane":"implementation","normativeStatus":"binding","audiences":["ceo","cfo","tax-accounting","legal","it"],"whyItMatters":"Tax compliance becomes an implementation system rather than only a legal reading exercise.","whyItMatters_vi":"Tuân thủ thuế trở thành một hệ thống thực thi thay vì chỉ là bài toán đọc văn bản.","sources":[{"type":"official","citation":"Government policy portal — Decree No. 252/2026/NĐ-CP","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-252-2026-nd-cp-huong-dan-thi-hanh-luat-quan-ly-thue-119260715155021635.htm"}]},{"id":"vnce-decree-einvoice-254-effective","title":"Decree No. 254/2026/NĐ-CP takes effect","title_vi":"Nghị định số 254/2026/NĐ-CP có hiệu lực","description":"Decree No. 254/2026/NĐ-CP set rules for electronic invoices and electronic documents from July 1, 2026.","description_vi":"Nghị định số 254/2026/NĐ-CP quy định về hóa đơn điện tử và chứng từ điện tử từ 1/7/2026.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["economy","technology","systems"],"impactScore":0.89,"tags":["vietnam","electronic-invoice","decree-254-2026","data-standard","accounting"],"date":"2026-07-01","dateLabel":"July 1, 2026","status":"completed","nodeType":"instrument","lane":"implementation","normativeStatus":"binding","audiences":["cfo","tax-accounting","ecommerce","it","erp"],"whyItMatters":"The invoice becomes both a legal document and a standardized data object used for tax administration.","whyItMatters_vi":"Hóa đơn vừa là chứng từ pháp lý vừa là đối tượng dữ liệu chuẩn phục vụ quản lý thuế.","sources":[{"type":"official","citation":"Government policy portal — Decree No. 254/2026/NĐ-CP","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-so-254-2026-nd-cp-ve-hoa-don-dien-tu-chung-tu-dien-tu-119260713164251972.htm"}]},{"id":"vnce-role-based-compliance-backlog","title":"Create a role-based compliance backlog","title_vi":"Lập backlog tuân thủ theo vai trò","description":"Translate applicable legal changes into owned work for executive, tax, legal, e-commerce, IT-security, HR, and import-export roles.","description_vi":"Chuyển thay đổi pháp luật có áp dụng thành công việc có người phụ trách cho điều hành, thuế, pháp chế, e-commerce, IT-an ninh, HR và XNK.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["systems","economy"],"impactScore":0.78,"tags":["compliance-operations","backlog","ownership","implementation","recommended-control"],"date":"2026-07-01","dateLabel":"From July 1, 2026","nodeType":"action","lane":"operations","normativeStatus":"recommended-practice","lifecycleStatus":"active","audiences":["ceo","cfo","legal","project-manager","all-functions"],"whyItMatters":"A large law inventory is not executable until it is filtered by applicability and converted into owned work.","whyItMatters_vi":"Một danh mục luật lớn không thể thực thi nếu chưa được lọc theo phạm vi áp dụng và chuyển thành công việc có người chịu trách nhiệm.","sources":[]},{"id":"vnce-platform-seller-governance-control","title":"Implement platform seller-governance controls","title_vi":"Triển khai kiểm soát quản trị người bán trên nền tảng","description":"Operationalize seller-information management, published rules, complaint intake, information retention, regulator cooperation, and violation handling.","description_vi":"Vận hành hóa quản lý thông tin người bán, quy chế, khiếu nại, lưu giữ, phối hợp cơ quan quản lý và xử lý vi phạm.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["technology","systems","economy"],"impactScore":0.85,"tags":["platform-control","seller-governance","complaints","retention","violation-handling"],"date":"2026-07-01","dateLabel":"From July 1, 2026","nodeType":"control","lane":"operations","normativeStatus":"derived-control","lifecycleStatus":"active","audiences":["legal","ecommerce","platform-operations","trust-safety","it"],"whyItMatters":"Compliance moves into product flows, moderation queues, data retention, and support operations.","whyItMatters_vi":"Tuân thủ đi vào luồng sản phẩm, hàng đợi kiểm duyệt, lưu giữ dữ liệu và vận hành hỗ trợ.","sources":[{"type":"official","citation":"Ministry of Industry and Trade — platform duties under the E-Commerce Law and Decree 248","url":"https://moit.gov.vn/tin-tuc/bo-cong-thuong-pho-bien-luat-thuong-mai-dien-tu-va-nghi-dinh-so-248-2026-nd-cp.html"}]},{"id":"vnce-livestream-identity-content-control","title":"Verify livestream sellers and control product claims","title_vi":"Xác thực người livestream và kiểm soát nội dung giới thiệu sản phẩm","description":"Verify commercial livestream hosts and make product claims traceable, truthful, and reviewable.","description_vi":"Xác thực người livestream thương mại và bảo đảm nội dung giới thiệu sản phẩm có thể truy vết, trung thực và được rà soát.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["technology","economy","culture"],"impactScore":0.84,"tags":["livestream","identity-verification","product-claims","consumer-protection","kol-koc"],"date":"2026-07-01","dateLabel":"From July 1, 2026","nodeType":"control","lane":"operations","normativeStatus":"derived-control","lifecycleStatus":"active","audiences":["legal","ecommerce","marketing","platform-operations","it"],"whyItMatters":"Marketing content becomes a governed transaction surface rather than an informal media activity.","whyItMatters_vi":"Nội dung marketing trở thành bề mặt giao dịch được quản trị thay vì chỉ là hoạt động truyền thông phi chính thức.","sources":[{"type":"official","citation":"Ministry of Industry and Trade — livestream and affiliate requirements under the new regime","url":"https://moit.gov.vn/tin-tuc/bo-cong-thuong-pho-bien-luat-thuong-mai-dien-tu-va-nghi-dinh-so-248-2026-nd-cp.html"}]},{"id":"vnce-transaction-invoice-reconciliation-control","title":"Reconcile orders, payments, invoices, returns, and tax records","title_vi":"Đối soát đơn hàng, thanh toán, hóa đơn, hoàn trả và dữ liệu thuế","description":"Map orders across sales channels to payments, electronic invoices, returns, cancellations, and tax records; route mismatches into an owned exception workflow.","description_vi":"Ánh xạ đơn hàng đa kênh với thanh toán, hóa đơn, hoàn trả, hủy và dữ liệu thuế; đưa sai lệch vào quy trình ngoại lệ có người xử lý.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["economy","technology","systems"],"impactScore":0.88,"tags":["reconciliation","order","payment","invoice","returns","tax-data","recommended-control"],"date":"2026-07-01","dateLabel":"From July 1, 2026","nodeType":"control","lane":"operations","normativeStatus":"recommended-practice","lifecycleStatus":"active","audiences":["cfo","tax-accounting","ecommerce","it","data"],"whyItMatters":"Many compliance failures are not caused by missing documents but by inconsistent data across systems.","whyItMatters_vi":"Nhiều lỗi tuân thủ không đến từ thiếu chứng từ mà từ dữ liệu không nhất quán giữa các hệ thống.","sources":[{"type":"official","citation":"Government policy portal — electronic invoices as standardized tax-administration data","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-so-254-2026-nd-cp-ve-hoa-don-dien-tu-chung-tu-dien-tu-119260713164251972.htm"}]},{"id":"vnce-cybersecurity-audit-control","title":"Establish access, incident, and audit controls","title_vi":"Thiết lập kiểm soát truy cập, sự cố và nhật ký kiểm toán","description":"Define risk-based access, logging, credential protection, incident documentation, and evidence retention.","description_vi":"Xác định kiểm soát truy cập, nhật ký, bảo vệ thông tin xác thực, hồ sơ sự cố và lưu bằng chứng theo rủi ro.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["technology","systems"],"impactScore":0.82,"tags":["cybersecurity","access-control","audit-log","incident-response","evidence","recommended-control"],"date":"2026-07-01","dateLabel":"From July 1, 2026","nodeType":"control","lane":"operations","normativeStatus":"recommended-practice","lifecycleStatus":"active","audiences":["ceo","legal","it","security","data"],"whyItMatters":"A policy cannot be proven or investigated without logs, ownership, and reconstructable incident records.","whyItMatters_vi":"Không thể chứng minh hoặc điều tra việc thực thi chính sách nếu thiếu nhật ký, trách nhiệm và hồ sơ sự cố có thể tái hiện.","sources":[{"type":"official","citation":"Government policy portal — Cybersecurity Law No. 116/2025/QH15 overview","url":"https://xaydungchinhsach.chinhphu.vn/nhung-noi-dung-moi-trong-luat-an-ninh-mang-so-116-2025-qh15-119260629145615168.htm"}]},{"id":"vnce-compliance-evidence-register","title":"Maintain a compliance evidence register","title_vi":"Duy trì sổ bằng chứng tuân thủ","description":"Link every applicable obligation and control to its source, owner, implementation record, operating evidence, exceptions, approvals, and review date.","description_vi":"Liên kết mỗi nghĩa vụ và kiểm soát với nguồn, người phụ trách, hồ sơ triển khai, bằng chứng vận hành, ngoại lệ, phê duyệt và ngày rà soát.","yearNum":2026,"yearLabel":"2026","precision":"day","domains":["systems","technology","economy"],"impactScore":0.86,"tags":["evidence-register","auditability","provenance","compliance-ops","recommended-control"],"date":"2026-07-01","dateLabel":"From July 1, 2026","nodeType":"evidence","lane":"evidence","normativeStatus":"recommended-practice","lifecycleStatus":"active","audiences":["ceo","cfo","legal","compliance","auditor","all-functions"],"whyItMatters":"Compliance becomes defensible only when an organization can show what rule applied, what it did, and what evidence proves continued operation.","whyItMatters_vi":"Tuân thủ chỉ có thể bảo vệ doanh nghiệp khi tổ chức chứng minh được quy định nào áp dụng, đã làm gì và bằng chứng nào cho thấy kiểm soát vẫn vận hành.","sources":[]}] diff --git a/packs/vn-ecommerce-compliance-2026/insights.json b/packs/vn-ecommerce-compliance-2026/insights.json new file mode 100644 index 0000000..ae77a63 --- /dev/null +++ b/packs/vn-ecommerce-compliance-2026/insights.json @@ -0,0 +1 @@ +[{"id":"pattern--regulation-control-evidence-chain","pattern":"Regulation → Control → Evidence","pattern_vi":"Quy định → Kiểm soát → Bằng chứng","description":"A legal instrument has little operational value until it is translated into a control with an owner and then into recurring evidence. This pattern works beyond compliance: a history-teaching pack can similarly project trigger → response → outcome → source evidence.","description_vi":"Văn bản pháp luật có ít giá trị vận hành nếu chưa được chuyển thành kiểm soát có người phụ trách và bằng chứng lặp lại. Mẫu này cũng dùng được ngoài compliance: pack giảng dạy lịch sử có thể chiếu theo chuỗi tác nhân → phản ứng → kết quả → bằng chứng nguồn.","instances":["vnce-decree-ecommerce-248-effective--enabled-->vnce-platform-seller-governance-control","vnce-platform-seller-governance-control--enabled-->vnce-compliance-evidence-register","vnce-decree-einvoice-254-effective--enabled-->vnce-transaction-invoice-reconciliation-control","vnce-transaction-invoice-reconciliation-control--enabled-->vnce-compliance-evidence-register","vnce-law-cybersecurity-116-passed--enabled-->vnce-cybersecurity-audit-control","vnce-cybersecurity-audit-control--enabled-->vnce-compliance-evidence-register"],"predictiveValue":0.91,"domains":["systems","economy","technology"]},{"id":"pattern--platforms-become-governance-infrastructure","pattern":"Platforms Become Governance Infrastructure","pattern_vi":"Nền tảng trở thành hạ tầng quản trị","description":"As regulation assigns identity, information, complaint, retention, and violation-handling duties to platforms, product architecture becomes part of the enforcement surface rather than a neutral transaction layer.","description_vi":"Khi quy định giao cho nền tảng trách nhiệm về định danh, thông tin, khiếu nại, lưu giữ và xử lý vi phạm, kiến trúc sản phẩm trở thành một phần của bề mặt thực thi thay vì chỉ là lớp giao dịch trung lập.","instances":["vnce-law-ecommerce-122-passed--enabled-->vnce-decree-ecommerce-248-effective","vnce-decree-ecommerce-248-effective--enabled-->vnce-platform-seller-governance-control","vnce-decree-ecommerce-248-effective--enabled-->vnce-livestream-identity-content-control"],"predictiveValue":0.87,"domains":["technology","systems","economy"]},{"id":"pattern--compliance-debt-is-data-debt","pattern":"Compliance Debt Is Often Data Debt","pattern_vi":"Nợ tuân thủ thường là nợ dữ liệu","description":"When orders, payments, invoices, returns, identities, and logs cannot be reconciled, the organization cannot reliably prove compliance. Standardized data and exception workflows reduce both operational and legal uncertainty.","description_vi":"Khi đơn hàng, thanh toán, hóa đơn, hoàn trả, danh tính và nhật ký không thể đối soát, tổ chức không thể chứng minh tuân thủ một cách đáng tin cậy. Dữ liệu chuẩn hóa và quy trình ngoại lệ làm giảm cả bất định vận hành lẫn pháp lý.","instances":["vnce-household-tax-einvoice-rules-effective--accelerated-->vnce-transaction-invoice-reconciliation-control","vnce-decree-einvoice-254-effective--enabled-->vnce-transaction-invoice-reconciliation-control","vnce-transaction-invoice-reconciliation-control--enabled-->vnce-compliance-evidence-register"],"predictiveValue":0.89,"domains":["systems","economy","technology"]}] diff --git a/packs/vn-ecommerce-compliance-2026/links.json b/packs/vn-ecommerce-compliance-2026/links.json new file mode 100644 index 0000000..905e22b --- /dev/null +++ b/packs/vn-ecommerce-compliance-2026/links.json @@ -0,0 +1 @@ +[{"id":"vnce-law-tax-management-108-passed--enabled-->vnce-household-tax-einvoice-rules-effective","fromEvent":"vnce-law-tax-management-108-passed","toEvent":"vnce-household-tax-einvoice-rules-effective","relationship":"enabled","confidence":0.99,"evidence":"The law explicitly set January 1, 2026 as the effective date for household and individual business declaration, tax-calculation, and electronic-invoice provisions.","evidence_vi":"Luật quy định rõ ngày 1/1/2026 là thời điểm có hiệu lực đối với kê khai, tính thuế và hóa đơn điện tử của hộ, cá nhân kinh doanh.","sources":[{"type":"official","citation":"Tax Administration Law effective-date provisions","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-luat-quan-ly-thue-co-hieu-luc-tu-1-7-2026-119260626174633402.htm"}]},{"id":"vnce-law-tax-management-108-passed--enabled-->vnce-decree-tax-252-effective","fromEvent":"vnce-law-tax-management-108-passed","toEvent":"vnce-decree-tax-252-effective","relationship":"enabled","confidence":0.99,"evidence":"Decree 252 was issued to detail and organize implementation of the Tax Administration Law.","evidence_vi":"Nghị định 252 được ban hành để quy định chi tiết và tổ chức thi hành Luật Quản lý thuế.","sources":[{"type":"official","citation":"Decree No. 252/2026/NĐ-CP","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-252-2026-nd-cp-huong-dan-thi-hanh-luat-quan-ly-thue-119260715155021635.htm"}]},{"id":"vnce-law-tax-management-108-passed--enabled-->vnce-decree-einvoice-254-effective","fromEvent":"vnce-law-tax-management-108-passed","toEvent":"vnce-decree-einvoice-254-effective","relationship":"enabled","confidence":0.99,"evidence":"Decree 254 details the Tax Administration Law's electronic-invoice and electronic-document implementation.","evidence_vi":"Nghị định 254 quy định chi tiết việc triển khai hóa đơn điện tử và chứng từ điện tử theo Luật Quản lý thuế.","sources":[{"type":"official","citation":"Decree No. 254/2026/NĐ-CP","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-so-254-2026-nd-cp-ve-hoa-don-dien-tu-chung-tu-dien-tu-119260713164251972.htm"}]},{"id":"vnce-law-ecommerce-122-passed--enabled-->vnce-decree-ecommerce-248-effective","fromEvent":"vnce-law-ecommerce-122-passed","toEvent":"vnce-decree-ecommerce-248-effective","relationship":"enabled","confidence":0.99,"evidence":"Decree 248 was issued to detail parts of the E-Commerce Law and took effect on the same date.","evidence_vi":"Nghị định 248 được ban hành để quy định chi tiết một số điều của Luật Thương mại điện tử và có hiệu lực cùng ngày.","sources":[{"type":"official","citation":"MOIT briefing on the E-Commerce Law and Decree 248","url":"https://moit.gov.vn/tin-tuc/bo-cong-thuong-pho-bien-luat-thuong-mai-dien-tu-va-nghi-dinh-so-248-2026-nd-cp.html"}]},{"id":"vnce-decree-ecommerce-248-effective--enabled-->vnce-platform-seller-governance-control","fromEvent":"vnce-decree-ecommerce-248-effective","toEvent":"vnce-platform-seller-governance-control","relationship":"enabled","confidence":0.93,"evidence":"The Ministry of Industry and Trade described platform duties covering seller information, operating rules, complaints, retention, cooperation, and violation handling.","evidence_vi":"Bộ Công Thương nêu các trách nhiệm nền tảng về thông tin người bán, quy chế, khiếu nại, lưu giữ, phối hợp và xử lý vi phạm.","sources":[{"type":"official","citation":"MOIT briefing on platform responsibilities","url":"https://moit.gov.vn/tin-tuc/bo-cong-thuong-pho-bien-luat-thuong-mai-dien-tu-va-nghi-dinh-so-248-2026-nd-cp.html"}]},{"id":"vnce-decree-ecommerce-248-effective--enabled-->vnce-livestream-identity-content-control","fromEvent":"vnce-decree-ecommerce-248-effective","toEvent":"vnce-livestream-identity-content-control","relationship":"enabled","confidence":0.95,"evidence":"The official briefing requires identity verification before commercial livestreaming and truthful product information without misleading claims.","evidence_vi":"Hướng dẫn chính thức yêu cầu xác thực danh tính trước livestream bán hàng và cung cấp thông tin sản phẩm trung thực, không gây nhầm lẫn.","sources":[{"type":"official","citation":"MOIT briefing on livestream selling","url":"https://moit.gov.vn/tin-tuc/bo-cong-thuong-pho-bien-luat-thuong-mai-dien-tu-va-nghi-dinh-so-248-2026-nd-cp.html"}]},{"id":"vnce-decree-tax-252-effective--enabled-->vnce-transaction-invoice-reconciliation-control","fromEvent":"vnce-decree-tax-252-effective","toEvent":"vnce-transaction-invoice-reconciliation-control","relationship":"enabled","confidence":0.78,"evidence":"The implementation decree increases the operational importance of complete, consistent taxpayer data across registration, declaration, payment, and examination processes; reconciliation is a practical derived control.","evidence_vi":"Nghị định làm tăng vai trò vận hành của dữ liệu người nộp thuế đầy đủ và nhất quán qua đăng ký, kê khai, nộp và kiểm tra; đối soát là kiểm soát thực hành được suy ra.","sources":[{"type":"official","citation":"Decree No. 252/2026/NĐ-CP","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-252-2026-nd-cp-huong-dan-thi-hanh-luat-quan-ly-thue-119260715155021635.htm"}]},{"id":"vnce-decree-einvoice-254-effective--enabled-->vnce-transaction-invoice-reconciliation-control","fromEvent":"vnce-decree-einvoice-254-effective","toEvent":"vnce-transaction-invoice-reconciliation-control","relationship":"enabled","confidence":0.9,"evidence":"Electronic invoices must use standard data formats, contain required fields, accurately reflect economic transactions, and support tax administration; reconciliation is the operational control that tests those properties across systems.","evidence_vi":"Hóa đơn điện tử phải dùng định dạng chuẩn, đủ trường, phản ánh chính xác nghiệp vụ và phục vụ quản lý thuế; đối soát là kiểm soát vận hành để kiểm tra các thuộc tính này giữa các hệ thống.","sources":[{"type":"official","citation":"Decree No. 254/2026/NĐ-CP principles","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-nghi-dinh-so-254-2026-nd-cp-ve-hoa-don-dien-tu-chung-tu-dien-tu-119260713164251972.htm"}]},{"id":"vnce-household-tax-einvoice-rules-effective--accelerated-->vnce-transaction-invoice-reconciliation-control","fromEvent":"vnce-household-tax-einvoice-rules-effective","toEvent":"vnce-transaction-invoice-reconciliation-control","relationship":"accelerated","confidence":0.82,"evidence":"The earlier effective date exposed household and individual sellers to invoice and declaration data requirements before the general July implementation wave.","evidence_vi":"Mốc hiệu lực sớm khiến hộ và cá nhân kinh doanh phải đáp ứng yêu cầu dữ liệu kê khai, hóa đơn trước làn sóng triển khai chung tháng 7.","sources":[{"type":"official","citation":"Tax Administration Law transition provisions","url":"https://xaydungchinhsach.chinhphu.vn/toan-van-luat-quan-ly-thue-co-hieu-luc-tu-1-7-2026-119260626174633402.htm"}]},{"id":"vnce-law-cybersecurity-116-passed--enabled-->vnce-cybersecurity-audit-control","fromEvent":"vnce-law-cybersecurity-116-passed","toEvent":"vnce-cybersecurity-audit-control","relationship":"enabled","confidence":0.79,"evidence":"The consolidated cybersecurity framework increases the need for risk-based access, incident, accountability, and evidence controls; the exact scope depends on applicable implementing guidance.","evidence_vi":"Khung an ninh mạng hợp nhất làm tăng nhu cầu kiểm soát truy cập, sự cố, trách nhiệm và bằng chứng theo rủi ro; phạm vi cụ thể phụ thuộc hướng dẫn áp dụng.","sources":[{"type":"official","citation":"Cybersecurity Law No. 116/2025/QH15 overview","url":"https://xaydungchinhsach.chinhphu.vn/nhung-noi-dung-moi-trong-luat-an-ninh-mang-so-116-2025-qh15-119260629145615168.htm"}]},{"id":"vnce-role-based-compliance-backlog--enabled-->vnce-platform-seller-governance-control","fromEvent":"vnce-role-based-compliance-backlog","toEvent":"vnce-platform-seller-governance-control","relationship":"enabled","confidence":0.84,"evidence":"Assigning legal, product, operations, and IT ownership is a practical precondition for implementing cross-functional platform controls.","evidence_vi":"Giao trách nhiệm cho pháp chế, sản phẩm, vận hành và IT là điều kiện thực tế để triển khai kiểm soát nền tảng liên phòng ban."},{"id":"vnce-role-based-compliance-backlog--enabled-->vnce-livestream-identity-content-control","fromEvent":"vnce-role-based-compliance-backlog","toEvent":"vnce-livestream-identity-content-control","relationship":"enabled","confidence":0.84,"evidence":"Identity verification and claim governance require coordinated ownership across seller operations, marketing, legal, product, and engineering.","evidence_vi":"Xác thực danh tính và quản trị nội dung giới thiệu đòi hỏi phối hợp trách nhiệm giữa vận hành người bán, marketing, pháp chế, sản phẩm và kỹ thuật."},{"id":"vnce-role-based-compliance-backlog--enabled-->vnce-transaction-invoice-reconciliation-control","fromEvent":"vnce-role-based-compliance-backlog","toEvent":"vnce-transaction-invoice-reconciliation-control","relationship":"enabled","confidence":0.87,"evidence":"Reconciliation fails when ownership is split across channels, finance, tax, and engineering without one explicit exception workflow.","evidence_vi":"Đối soát thất bại khi trách nhiệm bị chia cắt giữa kênh bán, tài chính, thuế và kỹ thuật mà không có một quy trình ngoại lệ rõ ràng."},{"id":"vnce-role-based-compliance-backlog--enabled-->vnce-cybersecurity-audit-control","fromEvent":"vnce-role-based-compliance-backlog","toEvent":"vnce-cybersecurity-audit-control","relationship":"enabled","confidence":0.85,"evidence":"Access and incident controls need named system owners, approvers, responders, and reviewers.","evidence_vi":"Kiểm soát truy cập và sự cố cần chỉ rõ chủ hệ thống, người phê duyệt, người ứng phó và người rà soát."},{"id":"vnce-platform-seller-governance-control--enabled-->vnce-compliance-evidence-register","fromEvent":"vnce-platform-seller-governance-control","toEvent":"vnce-compliance-evidence-register","relationship":"enabled","confidence":0.9,"evidence":"Seller verification, complaints, retention, and violation decisions create evidence that must remain linked to the governing control.","evidence_vi":"Xác thực người bán, khiếu nại, lưu giữ và quyết định xử lý vi phạm tạo ra bằng chứng cần được liên kết với kiểm soát quản trị."},{"id":"vnce-livestream-identity-content-control--enabled-->vnce-compliance-evidence-register","fromEvent":"vnce-livestream-identity-content-control","toEvent":"vnce-compliance-evidence-register","relationship":"enabled","confidence":0.91,"evidence":"Identity checks, approved claims, product evidence, and responsibility records need to be retained to reconstruct a livestream transaction.","evidence_vi":"Kiểm tra danh tính, nội dung được phê duyệt, bằng chứng sản phẩm và hồ sơ trách nhiệm cần được lưu để tái hiện một giao dịch livestream."},{"id":"vnce-transaction-invoice-reconciliation-control--enabled-->vnce-compliance-evidence-register","fromEvent":"vnce-transaction-invoice-reconciliation-control","toEvent":"vnce-compliance-evidence-register","relationship":"enabled","confidence":0.93,"evidence":"Reconciliation reports, exception queues, corrections, and approvals are the operating evidence that invoice and tax-data controls ran.","evidence_vi":"Báo cáo đối soát, hàng đợi sai lệch, chỉnh sửa và phê duyệt là bằng chứng vận hành cho thấy kiểm soát hóa đơn và dữ liệu thuế đã chạy."},{"id":"vnce-cybersecurity-audit-control--enabled-->vnce-compliance-evidence-register","fromEvent":"vnce-cybersecurity-audit-control","toEvent":"vnce-compliance-evidence-register","relationship":"enabled","confidence":0.94,"evidence":"Access reviews, logs, incident records, and remediation approvals are core evidence for cybersecurity governance.","evidence_vi":"Rà soát quyền truy cập, nhật ký, hồ sơ sự cố và phê duyệt khắc phục là bằng chứng cốt lõi của quản trị an ninh mạng."}] diff --git a/packs/vn-ecommerce-compliance-2026/manifest.json b/packs/vn-ecommerce-compliance-2026/manifest.json new file mode 100644 index 0000000..ec8acbc --- /dev/null +++ b/packs/vn-ecommerce-compliance-2026/manifest.json @@ -0,0 +1,38 @@ +{ + "id": "vn-ecommerce-compliance-2026", + "version": "0.1.0", + "title": "Vietnam E-commerce Compliance 2026", + "title_vi": "Tuân thủ thương mại điện tử Việt Nam 2026", + "description": "An evidence-first causal pack connecting major 2026 legal changes to operational controls, owners, and proof of execution for Vietnamese e-commerce and multi-channel retail.", + "description_vi": "Pack nhân quả ưu tiên bằng chứng, kết nối các thay đổi pháp luật lớn năm 2026 với kiểm soát vận hành, người phụ trách và bằng chứng thực thi cho thương mại điện tử và bán lẻ đa kênh tại Việt Nam.", + "language": "en", + "localizations": [ + "vi", + "en" + ], + "jurisdiction": "VN", + "temporalMode": "effective-date-timeline", + "graphMode": "instrument-to-control-to-evidence", + "updateCadence": "event-driven", + "asOf": "2026-07-30", + "files": { + "events": "events.json", + "links": "links.json", + "insights": "insights.json", + "views": "views.json" + }, + "canvas": { + "defaultView": "executive-impact", + "recommendedLayout": "timeline-lanes", + "supportsStoryMode": true, + "laneField": "lane", + "nodeTypeField": "nodeType" + }, + "quality": { + "causalProfile": "policy-systemic", + "sourcePolicy": "Official government and ministry sources are required for binding legal events. Derived controls must be explicitly labelled.", + "legalReviewStatus": "research-preview", + "disclaimer": "This pack is a research and operational-design aid, not legal or tax advice. Applicability and deadlines must be confirmed for each organization." + }, + "defaultLocale": "vi" +} diff --git a/packs/vn-ecommerce-compliance-2026/views.json b/packs/vn-ecommerce-compliance-2026/views.json new file mode 100644 index 0000000..3d01b0c --- /dev/null +++ b/packs/vn-ecommerce-compliance-2026/views.json @@ -0,0 +1,195 @@ +[ + { + "id": "executive-impact", + "title": "Executive impact", + "title_vi": "Tác động điều hành", + "audiences": [ + "ceo", + "founder", + "cfo" + ], + "question": "Which legal changes alter operating risk, which controls must be funded, and what evidence proves execution?", + "question_vi": "Thay đổi pháp luật nào làm đổi rủi ro vận hành, cần đầu tư kiểm soát nào và bằng chứng nào chứng minh đã thực thi?", + "lanes": [ + { + "id": "law", + "label": "Legal instruments", + "label_vi": "Văn bản luật", + "nodeTypes": [ + "instrument" + ] + }, + { + "id": "implementation", + "label": "Effective rules", + "label_vi": "Quy định triển khai", + "nodeTypes": [ + "instrument", + "effective-date" + ] + }, + { + "id": "operations", + "label": "Operating controls", + "label_vi": "Kiểm soát vận hành", + "nodeTypes": [ + "action", + "control" + ] + }, + { + "id": "evidence", + "label": "Evidence", + "label_vi": "Bằng chứng", + "nodeTypes": [ + "evidence" + ] + } + ], + "focusEventIds": [ + "vnce-law-ecommerce-122-passed", + "vnce-law-tax-management-108-passed", + "vnce-law-cybersecurity-116-passed", + "vnce-role-based-compliance-backlog", + "vnce-compliance-evidence-register" + ], + "highlightPaths": [ + [ + "vnce-law-ecommerce-122-passed", + "vnce-decree-ecommerce-248-effective", + "vnce-platform-seller-governance-control", + "vnce-compliance-evidence-register" + ], + [ + "vnce-law-tax-management-108-passed", + "vnce-decree-einvoice-254-effective", + "vnce-transaction-invoice-reconciliation-control", + "vnce-compliance-evidence-register" + ], + [ + "vnce-law-cybersecurity-116-passed", + "vnce-cybersecurity-audit-control", + "vnce-compliance-evidence-register" + ] + ] + }, + { + "id": "tax-accounting", + "title": "Tax and accounting operations", + "title_vi": "Vận hành thuế và kế toán", + "audiences": [ + "cfo", + "tax-accounting", + "erp", + "data" + ], + "question": "Where can transaction data diverge from invoices and tax records?", + "question_vi": "Dữ liệu giao dịch có thể lệch khỏi hóa đơn và hồ sơ thuế ở đâu?", + "includeEventIds": [ + "vnce-law-tax-management-108-passed", + "vnce-household-tax-einvoice-rules-effective", + "vnce-decree-tax-252-effective", + "vnce-decree-einvoice-254-effective", + "vnce-role-based-compliance-backlog", + "vnce-transaction-invoice-reconciliation-control", + "vnce-compliance-evidence-register" + ], + "storySteps": [ + "law", + "early-transition", + "implementation", + "control", + "evidence" + ] + }, + { + "id": "ecommerce-livestream", + "title": "E-commerce and livestream", + "title_vi": "Thương mại điện tử và livestream", + "audiences": [ + "legal", + "ecommerce", + "marketing", + "platform-operations", + "trust-safety" + ], + "question": "How do platform, seller, host, and product-content responsibilities connect?", + "question_vi": "Trách nhiệm của nền tảng, người bán, người livestream và nội dung sản phẩm kết nối với nhau thế nào?", + "includeEventIds": [ + "vnce-law-ecommerce-122-passed", + "vnce-decree-ecommerce-248-effective", + "vnce-role-based-compliance-backlog", + "vnce-platform-seller-governance-control", + "vnce-livestream-identity-content-control", + "vnce-compliance-evidence-register" + ], + "storySteps": [ + "law", + "decree", + "platform-control", + "livestream-control", + "evidence" + ] + }, + { + "id": "it-security", + "title": "IT and security controls", + "title_vi": "Kiểm soát IT và an ninh", + "audiences": [ + "cio", + "cto", + "it", + "security", + "data" + ], + "question": "Which product, access, logging, and evidence capabilities are needed to make compliance operable?", + "question_vi": "Cần năng lực sản phẩm, truy cập, nhật ký và bằng chứng nào để biến tuân thủ thành vận hành thực tế?", + "includeEventIds": [ + "vnce-law-cybersecurity-116-passed", + "vnce-decree-ecommerce-248-effective", + "vnce-decree-einvoice-254-effective", + "vnce-platform-seller-governance-control", + "vnce-livestream-identity-content-control", + "vnce-transaction-invoice-reconciliation-control", + "vnce-cybersecurity-audit-control", + "vnce-compliance-evidence-register" + ], + "storySteps": [ + "requirements", + "product-controls", + "data-controls", + "security-controls", + "evidence" + ] + }, + { + "id": "teaching-causal-chain", + "title": "Teach the causal chain", + "title_vi": "Giảng dạy chuỗi nhân quả", + "audiences": [ + "teacher", + "lecturer", + "student", + "facilitator" + ], + "question": "How does a policy trigger propagate through institutions, organizations, systems, and observable evidence?", + "question_vi": "Một tác nhân chính sách lan truyền qua thể chế, tổ chức, hệ thống và bằng chứng quan sát được như thế nào?", + "projection": { + "trigger": [ + "instrument", + "effective-date" + ], + "response": [ + "action" + ], + "adaptation": [ + "control" + ], + "observableOutcome": [ + "evidence" + ] + }, + "teachingNote": "This projection intentionally mirrors a historical-causality canvas such as a WWI lesson pack: long canonical graph data can be remixed into a narrative sequence without duplicating the underlying nodes.", + "teachingNote_vi": "Phép chiếu này cố ý tương đồng với canvas nhân quả lịch sử như pack bài giảng WWI: dữ liệu graph chuẩn có thể được phối lại thành chuỗi kể chuyện mà không cần nhân bản node gốc." + } +] diff --git a/packs/worldcup-2026/manifest.json b/packs/worldcup-2026/manifest.json new file mode 100644 index 0000000..c132c96 --- /dev/null +++ b/packs/worldcup-2026/manifest.json @@ -0,0 +1,22 @@ +{ + "id": "worldcup-2026", + "version": "0.1.0", + "title": "World Cup 2026", + "description": "Live sports and historical-lineage causal timeline for the FIFA World Cup 2026.", + "language": "en", + "localizations": [ + "vi", + "en" + ], + "temporalMode": "live-event-timeline", + "graphMode": "result-to-implication-to-watchpoint", + "updateCadence": "daily", + "files": { + "events": "events.json", + "links": "links.json", + "insights": "insights.json" + }, + "quality": { + "causalProfile": "sports-live" + } +} diff --git a/packs/worldcup-2026/quality-baseline.json b/packs/worldcup-2026/quality-baseline.json new file mode 100644 index 0000000..d523013 --- /dev/null +++ b/packs/worldcup-2026/quality-baseline.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "reviewBy": "2026-08-15", + "purpose": "Temporary no-new-debt baseline for five pre-existing late-knockout events whose whyItMatters fields still contain scorer-only ingest text. New semantic-quality issues remain blocking.", + "allowedIssues": [ + { + "code": "templated-why", + "key": "wc2026-argentina-switzerland-0711", + "reason": "Existing scorer-only quarter-final summary; replace with verified causal stakes." + }, + { + "code": "templated-why", + "key": "wc2026-france-spain-0714", + "reason": "Existing scorer-only semi-final summary; replace with verified causal stakes." + }, + { + "code": "templated-why", + "key": "wc2026-england-argentina-0715", + "reason": "Existing scorer-only semi-final summary; replace with verified causal stakes." + }, + { + "code": "templated-why", + "key": "wc2026-france-england-0718", + "reason": "Existing scorer-only third-place summary; replace with verified causal stakes." + }, + { + "code": "templated-why", + "key": "wc2026-spain-argentina-0719", + "reason": "Existing final record contains unresolved result detail; verify before writing a champion narrative." + } + ] +} diff --git a/scripts/add-match-day.mjs b/scripts/add-match-day.mjs index d74ffc2..77ff0f3 100644 --- a/scripts/add-match-day.mjs +++ b/scripts/add-match-day.mjs @@ -11,7 +11,7 @@ // // Exit 0 = pack updated + valid. Exit 1 = nothing written (see printed errors). -import { readFileSync, writeFileSync } from 'node:fs'; +import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { validatePackData } from './validate-pack.mjs'; @@ -24,6 +24,7 @@ function readJson(path) { try { return JSON.parse(readFileSync(path, 'utf8')); } catch (e) { die(`cannot read ${path}: ${e.message}`); } } +function readOptional(path) { return existsSync(path) ? readJson(path) : undefined; } const inputPath = process.argv[2]; const packId = process.argv[3] || 'worldcup-2026'; @@ -33,10 +34,14 @@ const packDir = join(ROOT, 'packs', packId); const eventsPath = join(packDir, 'events.json'); const linksPath = join(packDir, 'links.json'); const insightsPath = join(packDir, 'insights.json'); +const manifestPath = join(packDir, 'manifest.json'); +const baselinePath = join(packDir, 'quality-baseline.json'); const events = readJson(eventsPath); const links = readJson(linksPath); const insights = readJson(insightsPath); +const manifest = readOptional(manifestPath); +const qualityBaseline = readOptional(baselinePath); const input = readJson(inputPath); const eventsById = new Map(events.map((e) => [e.id, e])); @@ -48,7 +53,7 @@ const topDate = input.date; const topDateLabel = input.dateLabel; function upsertEvent(src, status) { - if (!src.id) die(`an event in input has no id`); + if (!src.id) die('an event in input has no id'); const existing = eventsById.get(src.id) || {}; const ev = { ...DEFAULTS, @@ -63,58 +68,60 @@ function upsertEvent(src, status) { } // 1) Completed results — honesty gate: each MUST carry a source. -for (const r of input.results || []) { - if (!Array.isArray(r.sources) || r.sources.length === 0) { - die(`result ${r.id || ''}: a completed result requires a non-empty "sources" citation (honesty rule)`); +for (const result of input.results || []) { + if (!Array.isArray(result.sources) || result.sources.length === 0) { + die(`result ${result.id || ''}: a completed result requires a non-empty "sources" citation (honesty rule)`); } - upsertEvent(r, 'completed'); + upsertEvent(result, 'completed'); } // 2) New upcoming fixtures. -for (const s of input.scheduled || []) upsertEvent(s, 'scheduled'); +for (const scheduled of input.scheduled || []) upsertEvent(scheduled, 'scheduled'); // 3) Causal links (event -> event). Id is derived; dupes skipped. -for (const l of input.links || []) { - if (!l.fromEvent || !l.toEvent || !l.relationship) die(`a link is missing fromEvent/toEvent/relationship`); - const id = `${l.fromEvent}--${l.relationship}-->${l.toEvent}`; +for (const link of input.links || []) { + if (!link.fromEvent || !link.toEvent || !link.relationship) die('a link is missing fromEvent/toEvent/relationship'); + const id = `${link.fromEvent}--${link.relationship}-->${link.toEvent}`; if (linkIds.has(id)) continue; - links.push({ id, fromEvent: l.fromEvent, toEvent: l.toEvent, relationship: l.relationship, confidence: l.confidence, evidence: l.evidence }); + links.push({ id, fromEvent: link.fromEvent, toEvent: link.toEvent, relationship: link.relationship, confidence: link.confidence, evidence: link.evidence }); linkIds.add(id); } // 4) Attach links to insight patterns (dedup) or add whole new insights. -for (const u of input.insightInstances || []) { - const ins = insightsById.get(u.insightId); - if (!ins) die(`insightInstances: unknown insight "${u.insightId}"`); - const set = new Set(ins.instances); - for (const lid of u.addLinkIds || []) set.add(lid); - ins.instances = [...set]; +for (const update of input.insightInstances || []) { + const insight = insightsById.get(update.insightId); + if (!insight) die(`insightInstances: unknown insight "${update.insightId}"`); + const set = new Set(insight.instances); + for (const linkId of update.addLinkIds || []) set.add(linkId); + insight.instances = [...set]; } -for (const ni of input.newInsights || []) { - if (insightsById.has(ni.id)) die(`newInsights: insight "${ni.id}" already exists`); - insights.push(ni); - insightsById.set(ni.id, ni); +for (const newInsight of input.newInsights || []) { + if (insightsById.has(newInsight.id)) die(`newInsights: insight "${newInsight.id}" already exists`); + insights.push(newInsight); + insightsById.set(newInsight.id, newInsight); } const merged = { events: [...eventsById.values()], links, insights }; -// 5) Validate BEFORE writing. Nothing is written if invalid. -// (a) structural validation — ids resolve, enums in range, referential integrity. +// 5a) Structural validation — ids resolve, enums are valid, references are intact. const errors = validatePackData(merged, packId); if (errors.length > 0) { console.error(`✗ ${errors.length} validation error(s) — nothing written:`); - for (const e of errors) console.error(` - ${e}`); + for (const error of errors) console.error(` - ${error}`); process.exit(1); } -// (b) causal-quality gate — the layer must be real, not templated. Catches a -// "Scorers: …" whyItMatters, a single-relationship graph, a backwards scoreline, -// a scheduled event carrying a result, an unresolved round/team, etc. This is -// what keeps the daily editorial honest even when structure is fine. -const { errors: qErrors, warnings: qWarnings } = assessCausalQuality(merged, packId); -for (const w of qWarnings) console.warn(` ! ${w}`); -if (qErrors.length > 0) { - console.error(`✗ ${qErrors.length} causal-quality error(s) — nothing written:`); - for (const e of qErrors) console.error(` - ${e}`); + +// 5b) Profile-aware causal-quality gate. The pack manifest selects domain rules; +// an exact quality baseline can temporarily downgrade known debt while new debt fails. +const profile = manifest?.quality?.causalProfile || (packId === 'worldcup-2026' ? 'sports-live' : 'generic'); +const { errors: qualityErrors, warnings: qualityWarnings } = assessCausalQuality(merged, packId, { + profile, + baseline: qualityBaseline, +}); +for (const warning of qualityWarnings) console.warn(` ! ${warning}`); +if (qualityErrors.length > 0) { + console.error(`✗ ${qualityErrors.length} causal-quality error(s) — nothing written:`); + for (const error of qualityErrors) console.error(` - ${error}`); process.exit(1); } @@ -124,4 +131,4 @@ write(linksPath, merged.links); write(insightsPath, merged.insights); console.log(`✓ ${packId} updated: ${merged.events.length} events, ${merged.links.length} links, ${merged.insights.length} insights`); -console.log(' Review the diff, then commit. CI re-validates on push.'); +console.log(` Quality profile: ${profile}. Review the diff, then commit; CI re-validates on push.`); diff --git a/scripts/causal-quality.mjs b/scripts/causal-quality.mjs index 17aa6d2..f1bece8 100644 --- a/scripts/causal-quality.mjs +++ b/scripts/causal-quality.mjs @@ -1,23 +1,8 @@ #!/usr/bin/env node -// Causal-quality gate for the worldcup-2026 pack. -// -// scripts/validate-pack.mjs proves the pack is STRUCTURALLY sound (ids resolve, -// enums in range, referential integrity). It does NOT prove the causal layer is -// real rather than templated — a pack of 104 "Scorers: …" whyItMatters lines and -// 20 identical `accelerated 0.62` links passes it cleanly. That is exactly the -// live regression this file exists to catch. -// -// This is the SEMANTIC gate: it fails a pack whose causal substrate is mechanical, -// backwards, or dishonest. Run it after validate-pack, before any push. -// -// node scripts/causal-quality.mjs # gate every pack that opts in -// node scripts/causal-quality.mjs worldcup-2026 # gate one pack -// node scripts/causal-quality.mjs --from # gate an exported dir -// -// Also exports assessCausalQuality({events,links,insights}) for in-memory use -// (so add-match-day.mjs can gate BEFORE writing to disk). -// -// Exit 0 = causal layer is real. Exit 1 = templated/broken (details printed). +// Profile-aware semantic quality gate for Causari packs. +// Structural integrity belongs to validate-pack.mjs; this file catches +// mechanical causal text, duplicated evidence, dishonest confidence, and +// domain-specific regressions such as malformed World Cup scorelines. import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'; import { join, dirname } from 'node:path'; @@ -26,10 +11,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const PACKS_DIR = join(ROOT, 'packs'); -// Teams the UI can resolve to a flag + 3-letter code. Kept in sync with the -// CODE/ISO2/NAME_ALIASES maps in apps/saas/public/embed/wc-bracket.html: a team -// name that is not here (nor an alias) renders as a grey fallback pill on the -// public bracket — a visible quality failure. Update BOTH when the field changes. const KNOWN_TEAMS = new Set([ 'Algeria', 'Argentina', 'Australia', 'Austria', 'Belgium', 'Bosnia & Herzegovina', 'Brazil', 'Canada', 'Cape Verde', 'Colombia', 'Croatia', 'Curaçao', 'Czech Republic', @@ -48,7 +29,6 @@ const TEAM_ALIASES = { 'bosnia and herzegovina': 'Bosnia & Herzegovina', 'democratic republic of the congo': 'DR Congo', 'congo dr': 'DR Congo', 'cabo verde': 'Cape Verde', }; -// Round labels the bracket's canonRound() accepts (ROUND_ALIASES in wc-bracket.html). const KNOWN_ROUNDS = new Set([ 'round of 32', 'r32', '1/16', 'last 32', 'round of 16', 'r16', 'last 16', '1/8', @@ -57,259 +37,201 @@ const KNOWN_ROUNDS = new Set([ 'match for third place', 'third place', 'third-place play-off', '3rd place', 'bronze final', 'final', ]); -// A group label (Group A … Group L) is a legal entity too — it is not a round. const GROUP_RE = /^group [a-l]$/i; +const TITLE_RE = /^(.*?)\s+(\d{1,2})\s*[–-]\s*(\d{1,2})(?:\s*\(\s*(\d{1,2})\s*[–-]\s*(\d{1,2})\s*pens?\.?\s*\))?\s+(.*)$/; +const ANY_SCORE_RE = /\b(\d{1,2})\s*[–-]\s*(\d{1,2})\b/; +const TEMPLATED_WHY = [/^scorers:/i, /^full-time\s+\d/i, /^upcoming\s+.*match/i]; function canonTeam(n) { const s = String(n ?? '').trim(); if (KNOWN_TEAMS.has(s)) return s; return TEAM_ALIASES[s.toLowerCase()] || null; } -function isKnownEntity(e) { - const s = String(e ?? '').trim(); - if (!s) return false; - if (GROUP_RE.test(s)) return true; - if (KNOWN_ROUNDS.has(s.toLowerCase())) return true; - if (canonTeam(s)) return true; - // Venues / other free-text entities are allowed — we only hard-check that the - // TEAM entities of a knockout tie resolve (see below); this keeps the gate - // strict where the UI is strict and lenient where it renders free text. - return null; // "unknown, but maybe a venue" — handled by the caller -} - -// Parse "A n–m B" (optionally "(x–y pens)") out of a completed-match title, the -// same regex the bracket uses (tie()). Returns {a,b,sa,sb,penA,penB} or null. -const TITLE_RE = /^(.*?)\s+(\d{1,2})\s*[–-]\s*(\d{1,2})(?:\s*\(\s*(\d{1,2})\s*[–-]\s*(\d{1,2})\s*pens?\.?\s*\))?\s+(.*)$/; function parseTitle(title) { const m = String(title ?? '').match(TITLE_RE); if (!m) return null; - return { - a: m[1].trim(), sa: +m[2], sb: +m[3], - penA: m[4] != null ? +m[4] : null, penB: m[5] != null ? +m[5] : null, - b: m[6].trim(), - }; + return { a: m[1].trim(), sa: +m[2], sb: +m[3], penA: m[4] == null ? null : +m[4], penB: m[5] == null ? null : +m[5], b: m[6].trim() }; } function winnerOf(p) { if (p.sa !== p.sb) return p.sa > p.sb ? p.a : p.b; if (p.penA != null && p.penA !== p.penB) return p.penA > p.penB ? p.a : p.b; - return null; // an honest draw (group stage) — no single winner + return null; } +function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +function countBy(arr) { const out = {}; for (const x of arr) out[x] = (out[x] || 0) + 1; return out; } +function normalizeEvidence(s) { return String(s ?? '').trim().toLowerCase().replace(/\s+/g, ' '); } +function defaultProfile(packId) { return packId === 'worldcup-2026' ? 'sports-live' : 'generic'; } -// Any scoreline "n–m" or "n-m" appearing in free text (to catch a scheduled -// event that leaked a result, or evidence that contradicts the title). -const ANY_SCORE_RE = /\b(\d{1,2})\s*[–-]\s*(\d{1,2})\b/; - -const TEMPLATED_WHY = [ - /^scorers:/i, // the live-bug template - /^full-time\s+\d/i, // ingest fallback template - /^upcoming\s+.*match/i, // scheduled fallback template -]; +function baselineKeys(baseline) { + return new Set((baseline?.allowedIssues || []).map((x) => `${x.code}:${x.key}`)); +} /** - * Assess the causal quality of an in-memory pack. Returns - * { errors, warnings, stats }. `errors` non-empty ⇒ the gate fails. - * Pure: no I/O, no console — safe to call before writing to disk. + * Returns { errors, warnings, stats }. A baseline may temporarily downgrade an + * exact known issue to a warning, preserving a no-new-debt ratchet. */ -export function assessCausalQuality({ events, links, insights }, packId = 'pack') { +export function assessCausalQuality({ events, links, insights }, packId = 'pack', options = {}) { + const profile = options.profile || defaultProfile(packId); + const allowed = baselineKeys(options.baseline); const errors = []; const warnings = []; - const E = (m) => errors.push(`[${packId}] ${m}`); - const W = (m) => warnings.push(`[${packId}] ${m}`); + const E = (code, key, message) => { + const full = `[${packId}] ${message}`; + if (allowed.has(`${code}:${key}`)) warnings.push(`${full} (quality baseline)`); + else errors.push(full); + }; + const W = (message) => warnings.push(`[${packId}] ${message}`); const evList = Array.isArray(events) ? events : []; const lkList = Array.isArray(links) ? links : []; + const sports = profile === 'sports-live'; + const live = sports ? evList.filter((e) => e && e.status) : evList.filter((e) => e && e.whyItMatters); + const completed = sports ? live.filter((e) => e.status === 'completed') : []; + const scheduled = sports ? live.filter((e) => e.status === 'scheduled' || e.status === 'live') : []; - const live = evList.filter((e) => e && e.status); // day-scale fixtures - const completed = live.filter((e) => e.status === 'completed'); - const scheduled = live.filter((e) => e.status === 'scheduled' || e.status === 'live'); - - // ── 1. whyItMatters must be real, not a template ───────────────────────── + // 1. Narrative text must be causal, not an ingest template. let templatedWhy = 0; for (const e of live) { const why = String(e.whyItMatters ?? ''); if (TEMPLATED_WHY.some((re) => re.test(why))) { templatedWhy++; - E(`event ${e.id}: whyItMatters is templated ("${why.slice(0, 40)}…") — write real stakes / what it changes / what it sets up`); - } else if (e.status === 'completed' && why.trim().length < 40) { + E('templated-why', e.id, `event ${e.id}: whyItMatters is templated ("${why.slice(0, 40)}…") — write real stakes / what it changes / what it sets up`); + } else if (why.trim() && why.trim().length < 40) { W(`event ${e.id}: whyItMatters is very short (${why.trim().length} chars) — likely thin`); } } - // ── 2. completed events must carry a parseable scoreline + honest attribution ─ - // A link can touch two completed events (both endpoints played) — report each - // backwards-scoreline link at most once across the whole pass. + // 2-4. Sports-only checks. Other domains must not be forced through a scoreline model. const reportedBackwards = new Set(); - for (const e of completed) { - const p = parseTitle(e.title); - if (!p) { - E(`event ${e.id}: completed match title "${e.title}" has no parseable "A n–m B" scoreline`); - continue; - } - // 2a. backwards-scoreline bug: any link touching this event whose evidence - // names the winner with the LOSER's scoreline (the "Mexico's 0–3 win" live bug). - const winner = winnerOf(p); - if (winner) { - const touching = lkList.filter((l) => l.fromEvent === e.id || l.toEvent === e.id); - for (const l of touching) { - if (reportedBackwards.has(l.id)) continue; - const ev = String(l.evidence ?? ''); - // evidence of the form "'s a–b win" must state the winner's own - // margin (higher–lower), never the reversed pair from a losing-order title. - // The [’'] class covers a curly or straight apostrophe after the team name. - const claim = new RegExp(`${escapeRe(winner)}['’]?s\\s+(\\d{1,2})\\s*[–-]\\s*(\\d{1,2})\\s+win`, 'i'); - const mm = ev.match(claim); - if (mm && +mm[1] < +mm[2]) { - reportedBackwards.add(l.id); - E(`link ${l.id}: evidence says "${winner}'s ${mm[1]}–${mm[2]} win" but a win means the winner's score is higher — backwards scoreline (the live "Mexico's 0–3 win" bug)`); + if (sports) { + for (const e of completed) { + const parsed = parseTitle(e.title); + if (!parsed) { + E('invalid-score-title', e.id, `event ${e.id}: completed match title "${e.title}" has no parseable "A n–m B" scoreline`); + continue; + } + const winner = winnerOf(parsed); + if (winner) { + const touching = lkList.filter((l) => l.fromEvent === e.id || l.toEvent === e.id); + for (const l of touching) { + if (reportedBackwards.has(l.id)) continue; + const claim = new RegExp(`${escapeRe(winner)}['’]?s\\s+(\\d{1,2})\\s*[–-]\\s*(\\d{1,2})\\s+win`, 'i'); + const match = String(l.evidence ?? '').match(claim); + if (match && +match[1] < +match[2]) { + reportedBackwards.add(l.id); + E('backwards-score', l.id, `link ${l.id}: evidence reverses the winner's scoreline`); + } } } } - } - // ── 3. scheduled events must NOT carry a result ────────────────────────── - for (const e of scheduled) { - const sm = String(e.title ?? '').match(ANY_SCORE_RE); - if (sm && parseTitle(e.title)) { - E(`event ${e.id}: status="${e.status}" but the title "${e.title}" carries a scoreline — a scheduled fixture has no result`); + for (const e of scheduled) { + if (ANY_SCORE_RE.test(String(e.title ?? '')) && parseTitle(e.title)) { + E('scheduled-score', e.id, `event ${e.id}: status="${e.status}" but title "${e.title}" carries a result`); + } } - } - // ── 4. round + team entities must resolve to the UI's maps ─────────────── - for (const e of live) { - const ents = Array.isArray(e.entities) ? e.entities : []; - const roundEnts = ents.filter((x) => KNOWN_ROUNDS.has(String(x).trim().toLowerCase())); - const isKnockout = roundEnts.length > 0 - || /\b(round of 32|round of 16|quarter|semi|final|third place)\b/i.test(String(e.title ?? '')); - // Unrecognized round label that LOOKS like a round → fail (would drop off the bracket). - for (const x of ents) { - const s = String(x).trim(); - if (GROUP_RE.test(s) || KNOWN_ROUNDS.has(s.toLowerCase()) || canonTeam(s)) continue; - // heuristic: strings mentioning "final"/"round"/"quarter"/"semi" but not canonical - if (/\b(final|round|quarter|semi|last \d|1\/\d)\b/i.test(s) && !KNOWN_ROUNDS.has(s.toLowerCase())) { - E(`event ${e.id}: entity "${s}" looks like a round label but is not one canonRound() accepts`); + for (const e of live) { + const entities = Array.isArray(e.entities) ? e.entities : []; + const roundEntities = entities.filter((x) => KNOWN_ROUNDS.has(String(x).trim().toLowerCase())); + const isKnockout = roundEntities.length > 0 || /\b(round of 32|round of 16|quarter|semi|final|third place)\b/i.test(String(e.title ?? '')); + for (const x of entities) { + const s = String(x).trim(); + if (GROUP_RE.test(s) || KNOWN_ROUNDS.has(s.toLowerCase()) || canonTeam(s)) continue; + if (/\b(final|round|quarter|semi|last \d|1\/\d)\b/i.test(s)) { + E('unknown-round', `${e.id}:${s}`, `event ${e.id}: entity "${s}" is not a recognized round label`); + } } - } - // A knockout tie's two team entities must resolve to the CODE/ISO2 maps — - // an unresolved team renders as a grey fallback pill on the public bracket. - // We only hard-fail entities that clearly ARE team names (title-case, short, - // not a venue) so a free-text venue entity never trips the gate. - if (isKnockout) { - const teamEnts = ents.filter((x) => !GROUP_RE.test(String(x)) && !KNOWN_ROUNDS.has(String(x).trim().toLowerCase())); - const looksLikeTeam = (x) => /^[A-Z][a-zé&' -]{1,28}$/.test(String(x).trim()) - && !/stadium|arena|park|field|metlife|azteca|bay area|new jersey|city|angeles|francisco/i.test(x); - for (const x of teamEnts) { - if (canonTeam(x)) continue; - if (looksLikeTeam(x)) { - E(`event ${e.id}: knockout entity "${x}" does not resolve to a known team or alias — it would render as a grey fallback pill on the public bracket`); + if (isKnockout) { + const looksLikeTeam = (x) => /^[A-Z][a-zé&' -]{1,28}$/.test(String(x).trim()) + && !/stadium|arena|park|field|metlife|azteca|bay area|new jersey|city|angeles|francisco/i.test(x); + for (const x of entities) { + if (GROUP_RE.test(String(x)) || KNOWN_ROUNDS.has(String(x).trim().toLowerCase()) || canonTeam(x)) continue; + if (looksLikeTeam(x)) E('unknown-team', `${e.id}:${x}`, `event ${e.id}: knockout entity "${x}" does not resolve to a known team or alias`); } } } } - // ── 5. relationship variety — no single verb may dominate ──────────────── - const rels = lkList.map((l) => l.relationship); - const relCounts = countBy(rels); + // 5. Relationship diversity is profile-sensitive. Systemic policy graphs may + // legitimately use mostly "enabled"; sports narrative graphs should vary. + const relCounts = countBy(lkList.map((l) => l.relationship)); const distinctRels = Object.keys(relCounts).length; - if (lkList.length >= 8) { + if (sports && lkList.length >= 8) { const [topRel, topN] = Object.entries(relCounts).sort((a, b) => b[1] - a[1])[0] || ['', 0]; const share = topN / lkList.length; - if (share > 0.7) { - E(`links: "${topRel}" is ${Math.round(share * 100)}% of all ${lkList.length} links (>70%) — a real causal graph uses varied relationships (caused/enabled/accelerated/inspired/delayed/prevented)`); - } - if (distinctRels < 3) { - E(`links: only ${distinctRels} distinct relationship type(s) across ${lkList.length} links — need ≥3`); - } + if (share > 0.7) E('relationship-dominance', topRel, `links: "${topRel}" is ${Math.round(share * 100)}% of ${lkList.length} links (>70%)`); + if (distinctRels < 3) E('relationship-variety', 'all', `links: only ${distinctRels} relationship type(s) across ${lkList.length} links — need ≥3 for sports-live`); + } else if (!sports && lkList.length >= 8 && distinctRels < 2) { + W(`links: only ${distinctRels} relationship type across ${lkList.length} links — review whether the graph is over-normalized`); } - // ── 6. confidence variety — not every link at one value ────────────────── - const confs = lkList.map((l) => l.confidence).filter((c) => typeof c === 'number'); - if (confs.length >= 8) { - const distinctConf = new Set(confs.map((c) => c.toFixed(2))).size; - if (distinctConf <= 1) { - E(`links: every link has confidence ${confs[0]} — vary it honestly (caused 0.85-0.95, enabled 0.7-0.85, accelerated 0.5-0.7, inspired 0.4-0.7)`); - } else if (distinctConf === 2 && confs.length >= 20) { - W(`links: only ${distinctConf} distinct confidence values across ${confs.length} links — likely under-calibrated`); - } - } + // 6-7. Generic causal-substrate checks. + const confidences = lkList.map((l) => l.confidence).filter((c) => typeof c === 'number'); + const distinctConf = new Set(confidences.map((c) => c.toFixed(2))).size; + if (confidences.length >= 8 && distinctConf <= 1) E('confidence-uniform', 'all', `links: every link has confidence ${confidences[0]} — calibrate claims honestly`); - // ── 7. duplicated evidence strings (mechanical template tell) ──────────── - const evCounts = countBy(lkList.map((l) => normalizeEvidence(l.evidence))); - for (const [ev, n] of Object.entries(evCounts)) { - if (ev && n > 1) { - E(`links: the same evidence sentence is reused ${n}× ("${ev.slice(0, 50)}…") — each causal claim needs its own specific evidence`); - } + const evidenceCounts = countBy(lkList.map((l) => normalizeEvidence(l.evidence))); + for (const [evidence, count] of Object.entries(evidenceCounts)) { + if (evidence && count > 1) E('duplicate-evidence', evidence, `links: the same evidence sentence is reused ${count}× ("${evidence.slice(0, 50)}…")`); } - // ── 8. watchpoint coverage on completed matches ────────────────────────── + // 8. Watchpoints are sports-specific; translation coverage is generic. const withWatch = completed.filter((e) => Array.isArray(e.nextWatchpoints) && e.nextWatchpoints.length > 0); const watchCoverage = completed.length ? withWatch.length / completed.length : 1; - // 90% is the KR-001 bar; below 50% is a hard fail (the layer is effectively absent). - if (completed.length >= 4 && watchCoverage < 0.5) { - E(`nextWatchpoints: only ${withWatch.length}/${completed.length} completed matches (${Math.round(watchCoverage * 100)}%) carry watchpoints — the daily editorial must add 2-5 per completed match`); - } else if (completed.length >= 4 && watchCoverage < 0.9) { + if (sports && completed.length >= 4 && watchCoverage < 0.5) { + E('watchpoint-coverage', 'all', `nextWatchpoints: only ${withWatch.length}/${completed.length} completed matches (${Math.round(watchCoverage * 100)}%) carry watchpoints`); + } else if (sports && completed.length >= 4 && watchCoverage < 0.9) { W(`nextWatchpoints: ${withWatch.length}/${completed.length} completed matches (${Math.round(watchCoverage * 100)}%) carry watchpoints — target ≥90%`); } - // ── 8b. bilingual coverage — every EN causal string wants a fluent VI twin ─ - // VI is ADDITIVE: a missing _vi falls back to English on the page, so these are - // warnings (they degrade the page, they don't break it) — but the coverage % is - // printed so a regression in the daily VI output is visible in CI. const sameText = (a, b) => normalizeEvidence(a) === normalizeEvidence(b) && String(a ?? '').trim() !== ''; const withWhy = live.filter((e) => String(e.whyItMatters ?? '').trim() && !TEMPLATED_WHY.some((re) => re.test(String(e.whyItMatters)))); let whyViCount = 0; for (const e of withWhy) { const vi = String(e.whyItMatters_vi ?? '').trim(); - if (!vi) { W(`event ${e.id}: has whyItMatters but no whyItMatters_vi — a Vietnamese reader sees English fallback`); continue; } + if (!vi) { W(`event ${e.id}: has whyItMatters but no whyItMatters_vi`); continue; } whyViCount++; - if (sameText(e.whyItMatters_vi, e.whyItMatters)) { - W(`event ${e.id}: whyItMatters_vi is identical to the English — it was not actually translated`); - } + if (sameText(vi, e.whyItMatters)) W(`event ${e.id}: whyItMatters_vi is identical to English`); } - const withWatchAll = live.filter((e) => Array.isArray(e.nextWatchpoints) && e.nextWatchpoints.length > 0); + const withWatchAll = sports ? live.filter((e) => Array.isArray(e.nextWatchpoints) && e.nextWatchpoints.length > 0) : []; let watchViCount = 0; for (const e of withWatchAll) { const vi = e.nextWatchpoints_vi; if (!Array.isArray(vi) || vi.length === 0) { W(`event ${e.id}: has nextWatchpoints but no nextWatchpoints_vi`); continue; } - if (vi.length !== e.nextWatchpoints.length) { - W(`event ${e.id}: nextWatchpoints_vi has ${vi.length} item(s) but nextWatchpoints has ${e.nextWatchpoints.length} — one VI string per EN watchpoint`); - } + if (vi.length !== e.nextWatchpoints.length) W(`event ${e.id}: nextWatchpoints_vi count does not match English`); watchViCount++; - const identical = vi.every((v, i) => sameText(v, e.nextWatchpoints[i])); - if (identical) W(`event ${e.id}: nextWatchpoints_vi is identical to the English — not actually translated`); } - const whyViCoverage = withWhy.length ? whyViCount / withWhy.length : 1; - const watchViCoverage = withWatchAll.length ? watchViCount / withWatchAll.length : 1; - // ── 9. lineage + insight presence (the substrate must not be empty) ────── - const lineage = evList.filter((e) => !e.status && !e.date); // static history-spine events - if (lineage.length === 0) { - W(`lineage: 0 history-spine events — the 160-year World Cup lineage is not present (historical-resonance links have nothing to point at)`); - } - if ((insights?.length ?? 0) < 2) { - W(`insights: only ${insights?.length ?? 0} pattern(s) — the pattern lens is thin (curated pack ships 5)`); - } + const lineage = sports ? evList.filter((e) => !e.status && !e.date) : []; + if (sports && lineage.length === 0) W('lineage: 0 history-spine events'); + if ((insights?.length ?? 0) < 2) W(`insights: only ${insights?.length ?? 0} pattern(s) — the pattern lens is thin`); - const stats = { - events: evList.length, live: live.length, completed: completed.length, - scheduled: scheduled.length, links: lkList.length, insights: insights?.length ?? 0, - lineage: lineage.length, distinctRels, relCounts, - distinctConf: new Set(confs.map((c) => c.toFixed(2))).size, - templatedWhy, watchCoverage: Math.round(watchCoverage * 100), - whyViCoverage: Math.round(whyViCoverage * 100), - watchViCoverage: Math.round(watchViCoverage * 100), - whyViCount, whyTotal: withWhy.length, watchViCount, watchTotal: withWatchAll.length, + const whyViCoverage = withWhy.length ? whyViCount / withWhy.length : 1; + const watchViCoverage = withWatchAll.length ? watchViCount / withWatchAll.length : 1; + return { + errors, + warnings, + stats: { + profile, events: evList.length, live: live.length, completed: completed.length, + scheduled: scheduled.length, links: lkList.length, insights: insights?.length ?? 0, + lineage: lineage.length, distinctRels, relCounts, distinctConf, templatedWhy, + watchCoverage: Math.round(watchCoverage * 100), + whyViCoverage: Math.round(whyViCoverage * 100), watchViCoverage: Math.round(watchViCoverage * 100), + whyViCount, whyTotal: withWhy.length, watchViCount, watchTotal: withWatchAll.length, + }, }; - return { errors, warnings, stats }; } -function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -function countBy(arr) { const o = {}; for (const x of arr) o[x] = (o[x] || 0) + 1; return o; } -function normalizeEvidence(s) { return String(s ?? '').trim().toLowerCase().replace(/\s+/g, ' '); } - function readPackFromDisk(packId) { const dir = join(PACKS_DIR, packId); - const read = (n) => JSON.parse(readFileSync(join(dir, n), 'utf8')); - return { events: read('events.json'), links: read('links.json'), insights: read('insights.json') }; + const read = (name) => JSON.parse(readFileSync(join(dir, name), 'utf8')); + const readOptional = (name) => existsSync(join(dir, name)) ? read(name) : undefined; + return { + pack: { events: read('events.json'), links: read('links.json'), insights: read('insights.json') }, + manifest: readOptional('manifest.json'), + baseline: readOptional('quality-baseline.json'), + }; } function main() { @@ -320,40 +242,41 @@ function main() { let targets; if (fromDir) { - const read = (n) => JSON.parse(readFileSync(join(fromDir, n), 'utf8')); - targets = [[packId || fromDir, { events: read('events.json'), links: read('links.json'), insights: read('insights.json') }]]; + const read = (name) => JSON.parse(readFileSync(join(fromDir, name), 'utf8')); + const readOptional = (name) => existsSync(join(fromDir, name)) ? read(name) : undefined; + targets = [[packId || fromDir, { + pack: { events: read('events.json'), links: read('links.json'), insights: read('insights.json') }, + manifest: readOptional('manifest.json'), baseline: readOptional('quality-baseline.json'), + }]]; } else if (packId) { targets = [[packId, readPackFromDisk(packId)]]; } else { - // Every pack under packs/ that carries live events opts in automatically. if (!existsSync(PACKS_DIR)) { console.error('No packs/ directory.'); process.exit(1); } targets = readdirSync(PACKS_DIR) .filter((d) => statSync(join(PACKS_DIR, d)).isDirectory()) .map((id) => [id, readPackFromDisk(id)]) - .filter(([, p]) => p.events.some((e) => e.status)); + .filter(([id, data]) => data.manifest?.quality?.causalProfile || id === 'worldcup-2026'); } let failed = false; - for (const [id, pack] of targets) { - const { errors, warnings, stats } = assessCausalQuality(pack, id); - console.log(`\n=== causal-quality: ${id} ===`); + for (const [id, data] of targets) { + const profile = data.manifest?.quality?.causalProfile || defaultProfile(id); + const { errors, warnings, stats } = assessCausalQuality(data.pack, id, { profile, baseline: data.baseline }); + console.log(`\n=== causal-quality: ${id} [${profile}] ===`); console.log(` events=${stats.events} (completed ${stats.completed}, scheduled ${stats.scheduled}) links=${stats.links} insights=${stats.insights} lineage=${stats.lineage}`); console.log(` relationships=${stats.distinctRels} distinct ${JSON.stringify(stats.relCounts)} · confidence=${stats.distinctConf} distinct`); console.log(` templated whyItMatters=${stats.templatedWhy} · watchpoint coverage=${stats.watchCoverage}%`); - const viExcl = stats.templatedWhy ? ` (excludes ${stats.templatedWhy} templated)` : ''; - console.log(` VI coverage: whyItMatters_vi ${stats.whyViCount}/${stats.whyTotal} (${stats.whyViCoverage}%)${viExcl} · nextWatchpoints_vi ${stats.watchViCount}/${stats.watchTotal} (${stats.watchViCoverage}%)`); - for (const w of warnings) console.log(` ! WARN ${w}`); + console.log(` VI coverage: whyItMatters_vi ${stats.whyViCount}/${stats.whyTotal} (${stats.whyViCoverage}%) · nextWatchpoints_vi ${stats.watchViCount}/${stats.watchTotal} (${stats.watchViCoverage}%)`); + for (const warning of warnings) console.log(` ! WARN ${warning}`); if (errors.length) { failed = true; console.log(`\n FAIL: ${errors.length} causal-quality error(s):`); - for (const e of errors) console.log(` x ${e}`); + for (const error of errors) console.log(` x ${error}`); } else { - console.log(' OK — causal layer is real, not templated.'); + console.log(' OK — causal layer passes its profile-aware quality gate.'); } } process.exit(failed ? 1 : 0); } -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - main(); -} +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(); diff --git a/scripts/validate-pack.mjs b/scripts/validate-pack.mjs index 2fafd33..0034965 100644 --- a/scripts/validate-pack.mjs +++ b/scripts/validate-pack.mjs @@ -4,14 +4,16 @@ // Checks each pack under packs// for schema conformance AND referential // integrity (the part JSON Schema can't express): every link endpoint must be a // real event, every insight instance must be a real link, ids must be unique and -// well-formed. Run before every commit that touches a pack — this is what keeps -// the daily live updates from shipping a broken graph to the public visual. +// well-formed. Optional manifest.json and views.json files are also checked when +// present so Canvas projections cannot silently reference missing graph nodes. +// Run before every commit that touches a pack — this is what keeps live updates +// from shipping a broken graph to the public visual. // // node scripts/validate-pack.mjs # validate all packs // node scripts/validate-pack.mjs worldcup-2026 // -// Also exports validatePackData({events,links,insights}) for in-memory checks -// (used by add-match-day.mjs to validate BEFORE writing to disk). +// Also exports validatePackData({events,links,insights,views,manifest}) for +// in-memory checks. Optional files may be omitted by existing callers. // // Exit 0 = clean, exit 1 = errors found. @@ -39,17 +41,18 @@ function isNonEmptyStr(v) { return typeof v === 'string' && v.trim().length > 0; * Pure validation of a pack's in-memory data. Returns an array of error strings * (empty = valid). No I/O, no console — safe to call before writing to disk. */ -export function validatePackData({ events, links, insights }, packId = 'pack') { +export function validatePackData({ events, links, insights, views, manifest }, packId = 'pack') { const errors = []; const E = (msg) => errors.push(`[${packId}] ${msg}`); const eventIds = new Set(); const linkIds = new Set(); + const viewIds = new Set(); // --- Events --- if (!Array.isArray(events)) { E('events must be an array'); return errors; } for (const ev of events) { const id = ev?.id ?? ''; - if (!isNonEmptyStr(ev.id)) E(`event has no id`); + if (!isNonEmptyStr(ev.id)) E('event has no id'); else if (!KEBAB.test(ev.id)) E(`event id not kebab-case: ${ev.id}`); else if (eventIds.has(ev.id)) E(`duplicate event id: ${ev.id}`); else eventIds.add(ev.id); @@ -73,7 +76,7 @@ export function validatePackData({ events, links, insights }, packId = 'pack') { if (!Array.isArray(links)) { E('links must be an array'); return errors; } for (const ln of links) { const id = ln?.id ?? ''; - if (!isNonEmptyStr(ln.id)) E(`link has no id`); + if (!isNonEmptyStr(ln.id)) E('link has no id'); else if (linkIds.has(ln.id)) E(`duplicate link id: ${ln.id}`); else linkIds.add(ln.id); if (!RELATIONSHIPS.has(ln.relationship)) E(`link ${id}: invalid relationship "${ln.relationship}"`); @@ -92,7 +95,7 @@ export function validatePackData({ events, links, insights }, packId = 'pack') { const insightIds = new Set(); for (const ins of insights) { const id = ins?.id ?? ''; - if (!isNonEmptyStr(ins.id)) E(`insight has no id`); + if (!isNonEmptyStr(ins.id)) E('insight has no id'); else if (!PATTERN_ID.test(ins.id)) E(`insight ${id}: id should follow "pattern--{kebab-name}"`); else if (insightIds.has(ins.id)) E(`duplicate insight id: ${ins.id}`); else insightIds.add(ins.id); @@ -107,6 +110,53 @@ export function validatePackData({ events, links, insights }, packId = 'pack') { } } + // --- Optional Canvas/audience views --- + if (views !== undefined) { + if (!Array.isArray(views)) E('views must be an array when views.json is present'); + else { + for (const view of views) { + const id = view?.id ?? ''; + if (!isNonEmptyStr(view.id)) E('view has no id'); + else if (!KEBAB.test(view.id)) E(`view id not kebab-case: ${view.id}`); + else if (viewIds.has(view.id)) E(`duplicate view id: ${view.id}`); + else viewIds.add(view.id); + if (!isNonEmptyStr(view.title)) E(`view ${id}: missing title`); + + for (const field of ['focusEventIds', 'includeEventIds']) { + if (view[field] === undefined) continue; + if (!Array.isArray(view[field])) E(`view ${id}: ${field} must be an array`); + else for (const ref of view[field]) { + if (!eventIds.has(ref)) E(`view ${id}: ${field} references missing event "${ref}"`); + } + } + + if (view.highlightPaths !== undefined) { + if (!Array.isArray(view.highlightPaths)) E(`view ${id}: highlightPaths must be an array`); + else for (const [pathIndex, path] of view.highlightPaths.entries()) { + if (!Array.isArray(path)) { E(`view ${id}: highlightPaths[${pathIndex}] must be an array`); continue; } + for (const ref of path) { + if (!eventIds.has(ref)) E(`view ${id}: highlightPaths[${pathIndex}] references missing event "${ref}"`); + } + } + } + } + } + } + + // --- Optional manifest --- + if (manifest !== undefined) { + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) E('manifest must be an object when manifest.json is present'); + else { + if (manifest.id !== undefined && manifest.id !== packId) E(`manifest id "${manifest.id}" must match pack directory "${packId}"`); + const defaultView = manifest.canvas?.defaultView; + if (defaultView !== undefined) { + if (!isNonEmptyStr(defaultView)) E('manifest canvas.defaultView must be a non-empty string'); + else if (!Array.isArray(views)) E(`manifest defaultView "${defaultView}" requires views.json`); + else if (!viewIds.has(defaultView)) E(`manifest defaultView "${defaultView}" is not defined in views.json`); + } + } + } + return errors; } @@ -114,14 +164,22 @@ export function validatePackData({ events, links, insights }, packId = 'pack') { export function validatePackFromDisk(packId) { const dir = join(PACKS_DIR, packId); const read = (name) => JSON.parse(readFileSync(join(dir, name), 'utf8')); + const readOptional = (name) => existsSync(join(dir, name)) ? read(name) : undefined; let data; try { - data = { events: read('events.json'), links: read('links.json'), insights: read('insights.json') }; + data = { + events: read('events.json'), + links: read('links.json'), + insights: read('insights.json'), + views: readOptional('views.json'), + manifest: readOptional('manifest.json'), + }; } catch (e) { return { errors: [`[${packId}] cannot read pack — ${e.message}`], counts: '' }; } const errors = validatePackData(data, packId); - const counts = `${data.events.length} events, ${data.links.length} links, ${data.insights.length} insights`; + const viewCount = Array.isArray(data.views) ? `, ${data.views.length} views` : ''; + const counts = `${data.events.length} events, ${data.links.length} links, ${data.insights.length} insights${viewCount}`; return { errors, counts }; }