P10D — Official Laws Assembly Inventory (2026-04-30)
P10D — Official Laws Assembly Inventory
Date: 2026-04-30
Scope: Read-only inventory + wiring plan. NO implementation.
Goal: Make /knowledge/laws show TAC publications as a multi-level folder tree (KB-style), source = PG via Directus.
GATE 0 — Machine Identity ✅
- Control host:
Nguyens-MacBook-Air.local(Mac, usernmhuyen) - Runtime host:
vmi3080463.contaboserver.net - Repo root on VPS:
/opt/incomex - Postgres container:
postgres→ dbdirectus/directus - Directus container:
incomex-directus(port 8055/tcp, internal only) - Web app on VPS:
/opt/incomex/docker/nuxt-repo/web(NOT/opt/incomex/web)
All commands below executed via ssh contabo '...'.
PHASE 1 — INVENTORY (raw evidence)
1A. Tree API endpoint
No docs/tree HTTP endpoint exists. Tree is built client-side from a flat document list.
Files referencing tree-building:
web/composables/useAgentViews.ts— exportsbuildDocsTree,filterDocsByTitleweb/components/DocsTreeView.vue— recursive view componentweb/pages/knowledge/index.vue— KB hub (uses Directus SDK + buildDocsTree)web/pages/knowledge/[...slug].vue— KB detail (same sidebar)web/server/api/docs/context.get.ts— unrelated context API
Conclusion: folder nodes are path-derived (not records, not collection-derived). Each input doc has a path string; splitting on / produces the folder hierarchy.
1B. DocsTreeView + buildDocsTree
DocsTreeView.vue props/emits:
interface Props {
nodes: DocsTreeNode[];
level?: number; // default 0
selectedPath?: string;
}
emits: { select: [node: DocsTreeNode] }
- Fully recursive (renders itself for
node.children). - Tracks expanded folders internally; auto-expands ancestors of
selectedPath. - Folder click → toggle. Doc click → emit
select. - Has hard-coded
FOLDER_ROUTES = { 'current-tasks': '/knowledge/current-tasks' }for level-0 only — does NOT interfere for our use case.
DocsTreeNode shape (inferred from usage):
{ id: string; name: string; path: string; isFolder: boolean;
children: DocsTreeNode[]; document?: AgentView }
buildDocsTree(documents) algorithm:
- Input: flat
AgentView[]with{ id, source_id, title, path, tags }. - Strips
^(docs|knowledge)/prefix. - Splits
pathon/. All but last segment → folder nodes (deduped viafolderMap). Last segment → doc node (with.mdstripped from name). - Sorts: folders first; folder priority via
FOLDER_ORDER = {current-tasks:1, current-state:2, dev:3, other:4}(level-0 KB-specific, neutral for other roots); else alphabetical.
Reusable as-is for any source if we synthesize a /-delimited path per item.
1C. Current /knowledge/laws/index.vue
- File:
web/pages/knowledge/laws/index.vue - Fetch: Directus SDK
readItems('governance_docs', { filter: status=published, sort: [category, sort], limit: -1 }). - Renders 3 flat sections: Constitutions (cards), Laws (UTable), SSOT Tables (UTable).
- Uses
governance_docscollection — NOTtac_*. - No tree, no markdown rendering, no reader. Links only.
1D. Directus tac_* deep-read (verified via PG; Directus REST not reachable from outside container — token redacted)
All tac_* tables ARE registered as Directus collections (verified directus_collections):
tac_birth_gate_config tac_publication
tac_change_set tac_publication_member
tac_change_set_member tac_publication_type_vocab
tac_cs_lifecycle_vocab tac_pub_lifecycle_vocab
tac_logical_unit tac_review_state_vocab
tac_lu_lifecycle_vocab tac_section_type_vocab
tac_unit_version
tac_uv_lifecycle_vocab
→ Directus SDK readItems('tac_publication' | 'tac_publication_member' | …) will work (subject to role permissions).
Counts: tac_publication=3, tac_publication_member=86, tac_logical_unit=86, tac_unit_version=86.
Publications (lifecycle_status=proposed):
| doc_code | version | name |
|---|---|---|
| DIEU-28 | v2.0 | ĐIỀU 28: LUẬT KỸ THUẬT HIỂN THỊ — v2.0 BAN HÀNH |
| DIEU-32 | v1.1 | Điều 32: Luật Phê duyệt — v1.1 BAN HÀNH |
| DIEU-35 | v5.2 | DIEU 35: LUAT QUAN TRI DOT — v5.2 FINAL |
Schema highlights:
tac_publication: id (uuid), doc_code, version, name, lifecycle_status, …tac_publication_member: publication_id, logical_unit_id, unit_version_id, render_ordertac_logical_unit: id, canonical_address (UNIQUE, e.g.D38-DIEU35-S4-P1-1), doc_code, parent_id (self-FK), sort_order, section_type, section_code, …tac_unit_version: logical_unit_id, version_number, title, body (markdown content), description, content_hash, …
Sample tree (DIEU-35, render_order):
D38-DIEU35-ROOT article null (sort 0)
D38-DIEU35-S0 heading → ROOT (1)
D38-DIEU35-S1 principle → ROOT (2)
D38-DIEU35-S2 paragraph → ROOT (3)
D38-DIEU35-S3 definition → ROOT (4)
D38-DIEU35-S4 technical_spec → ROOT (5)
D38-DIEU35-S4-P1 technical_spec → S4 (6)
D38-DIEU35-S4-P1-1 technical_spec → S4-P1 (7)
…
→ Hierarchy is intrinsic via parent_id chain AND mirrored in canonical_address (segments separated by -).
1E. Markdown rendering
web/utils/markdown.ts:import { micromark } from 'micromark';import { gfm, gfmHtml } from 'micromark-extension-gfm';export function markdownToHtml(markdown: string) { return micromark(markdown, { extensions:[gfm()], htmlExtensions:[gfmHtml()] });}- Imported by
pages/knowledge/[...slug].vue. Used in KB reader sidebar. pages/knowledge/docs/[...slug].vueuses an inline manualv-for line.startsWith(...)renderer (legacy). NOT what we want.- No nuxt-content / MDCRenderer in repo.
- Existence of
web/components/typography/directory (TypographyTitle, TypographyProse expected — confirmed indirectly via imports in knowledge pages).
1F. Multi-level folders
pages/knowledge/index.vue + pages/knowledge/[...slug].vue already render arbitrarily-nested folders via <DocsTreeView :nodes="docsTree" :selected-path="..." @select="..." />. applyFolderLabels() recursively prettifies folder names. Multi-level support is built in — no additional component needed.
PHASE 2 — Q1–Q6 Answers
Q1: DocsTreeView dùng được cho tac_* data không?
CÓ. Component is data-shape-agnostic. It accepts any DocsTreeNode[]. We just build the tree from tac_* rows via buildDocsTree after synthesizing a path field. Evidence: props only require {nodes,selectedPath}; FOLDER_ROUTES map only fires at level===0 for current-tasks, harmless for our root.
Q2: Directus deep-read trả đủ tree data không?
CÓ. All tac_* collections are registered (directus_collections). One call:
readItems('tac_publication_member', {
filter: { publication_id: { _eq: <pubId> } },
fields: ['render_order',
'logical_unit_id.id','logical_unit_id.canonical_address',
'logical_unit_id.parent_id','logical_unit_id.sort_order',
'logical_unit_id.section_type',
'unit_version_id.title','unit_version_id.body'],
sort: ['render_order'], limit: -1
})
returns everything needed (hierarchy via parent_id OR via canonical_address split). Optionally a sibling call to readItems('tac_publication') for the publication list.
Q3: /knowledge/laws thêm section dùng component nào đã có?
<DocsTreeView> + helper buildDocsTree (composable). No new component. Page already uses Directus SDK pattern; we add one more useAsyncData block + a new <div class="mb-10"> section.
Q4: Click publication → reader dùng component nào đã có? Two equally-valid existing patterns; pick one:
- (Preferred) Inline reader pane on the same
/knowledge/lawspage: render selected unit'sbodyviamarkdownToHtml(...)from~/utils/markdowninto a<article class="prose ...">(the same micromark+gfm pipeline thatpages/knowledge/[...slug].vueuses). Zero new pages. - Or navigate to a new sub-route — but that would be new code; rejected per Assembly First.
Q5: Cần bao nhiêu dòng config?
Estimated ~90–130 lines added to a single file web/pages/knowledge/laws/index.vue:
- ~25 lines: second
useAsyncData('tac-pubs-tree', …)fetching publications + members. - ~30 lines: build per-publication
AgentView-shaped list (path = "${doc_code}/${ancestorTitles…}/${title}.md") usingparent_idchain (orcanonical_address.split('-')), thenbuildDocsTree(...). - ~15 lines:
selectedNode = ref(null)+onSelect(node)+ computedbodyHtmlviamarkdownToHtml(selectedNode.value.document.body). - ~30 lines: template — new
<div class="mb-10">,<DocsTreeView>left,<article class="prose">right withv-html.
No new files. No new server routes. No DDL/DML.
Q6: KB folder tree nhiều tầng reuse được cho /knowledge/laws?
CÓ. Multi-level is native to DocsTreeView (renders itself recursively). Differences vs KB doc tree:
- Folder nodes vs document nodes: identical mechanism. Folders are path-derived synthetic nodes (
isFolder: true,id: 'folder:<path>'); documents are leaf nodes (isFolder: false, carrydocumentpayload). For TAC: leaves = unit versions; folders = ancestor LUs — OR — folders = publications + intermediate LUs. - Đ38-style nested mapping: the
parent_idchain maps 1:1 to folder hierarchy. Mapping options:- From
parent_id: walk ancestors, use each ancestor'sunit_version.title(orcanonical_addresssegment) as a folder name. Most readable. - From
canonical_address: split on-, e.g.D38-DIEU35-S4-P1-1→D38/DIEU35/S4/P1/1. Trivial but ugly labels. Recommend (1) for human-readable folders, fall back to (2) for any LU with missing title.
- From
- Hierarchy preserved. No flatten required. Sort by
sort_order(orrender_order) to keep statutory order — pass items tobuildDocsTreealready sorted, and inside each folder buildDocsTree's alphabetical sort will be overridden if we setpathto include a zero-padded order prefix (e.g.DIEU-35/04 §4. SCHEMA/01 4.1 dot_tools/...). Simple and effective.
PHASE 3 — Capability Matrix
| Capability | Existing evidence | Reusable as-is? | Needs config/wiring? | Would be new code? |
|---|---|---|---|---|
| Tree view component | components/DocsTreeView.vue |
✅ Yes | No | No |
| Build tree utility | composables/useAgentViews.ts → buildDocsTree |
✅ Yes | No | No |
| Folder multi-level | DocsTreeView is self-recursive; pages/knowledge/index.vue proves it |
✅ Yes | No | No |
| Directus SDK fetch | useNuxtApp().$directus + readItems (used in current laws page + KB hub) |
✅ Yes | One new useAsyncData block |
No |
| Markdown rendering | utils/markdown.ts (micromark + gfm) |
✅ Yes | Import & call | No |
| Document reader | Pattern in pages/knowledge/[...slug].vue (v-html + prose class) |
✅ Yes (pattern) | Inline pane in laws page | No |
| Publication listing | tac_publication collection registered |
✅ Yes | readItems('tac_publication') |
No |
| Auth/permissions | Existing Directus session via $directus; role must include READ on tac_publication, tac_publication_member, tac_logical_unit, tac_unit_version |
⚠️ Verify | Possibly grant role read perms in Directus admin (config, not code) | No |
Recommended Wiring Plan (NOT executed — read-only phase)
Single file edit: web/pages/knowledge/laws/index.vue. Add a 4th section "TAC Publications (Đ38)" above/below the existing 3 sections.
Pseudo-flow:
- Fetch publications:
readItems('tac_publication', { fields: ['id','doc_code','version','name','lifecycle_status'], sort: ['doc_code'] }). - For each publication (or all in one query), fetch members with deep-read of
logical_unit_id.*+unit_version_id.*sorted byrender_order. - Map each member →
AgentView-shaped record:id:unit_version_id.idsource_id:logical_unit_id.canonical_addresstitle:unit_version_id.titlepath:${pub.doc_code}/${ancestorTitles.join('/')}/${unit_version.title}.md(build ancestorTitles by walkingparent_idin a Map of LUs)- Carry
bodyintodocument.bodyfor the reader.
tree = buildDocsTree(records); pass to<DocsTreeView :nodes="tree" :selected-path @select="onSelect" />.onSelect(node)setsselected.value = node.document. Right pane rendersmarkdownToHtml(selected.value.body)viav-htmlinside<article class="prose ...">.
Permissions sanity check (do BEFORE coding):
- Confirm the public/web Directus role has READ on the 4
tac_*tables. If missing, that's the only Directus-side change required (admin UI, not code).
STOP
This is a read-only inventory. No files created, no code modified, no DDL/DML, no Directus mutations executed.
Next step (separate session): apply the wiring plan in §"Recommended Wiring Plan" with explicit user go-ahead.