KB-9334

P10D — Official Laws Assembly Inventory (2026-04-30)

14 min read Revision 1
p10dlawsinventorytacassembly-first

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, user nmhuyen)
  • Runtime host: vmi3080463.contaboserver.net
  • Repo root on VPS: /opt/incomex
  • Postgres container: postgres → db directus/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 — exports buildDocsTree, filterDocsByTitle
  • web/components/DocsTreeView.vue — recursive view component
  • web/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 path on /. All but last segment → folder nodes (deduped via folderMap). Last segment → doc node (with .md stripped 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_docs collection — NOT tac_*.
  • 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_order
  • tac_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].vue uses an inline manual v-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/laws page: render selected unit's body via markdownToHtml(...) from ~/utils/markdown into a <article class="prose ..."> (the same micromark+gfm pipeline that pages/knowledge/[...slug].vue uses). 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") using parent_id chain (or canonical_address.split('-')), then buildDocsTree(...).
  • ~15 lines: selectedNode = ref(null) + onSelect(node) + computed bodyHtml via markdownToHtml(selectedNode.value.document.body).
  • ~30 lines: template — new <div class="mb-10">, <DocsTreeView> left, <article class="prose"> right with v-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, carry document payload). For TAC: leaves = unit versions; folders = ancestor LUs — OR — folders = publications + intermediate LUs.
  • Đ38-style nested mapping: the parent_id chain maps 1:1 to folder hierarchy. Mapping options:
    1. From parent_id: walk ancestors, use each ancestor's unit_version.title (or canonical_address segment) as a folder name. Most readable.
    2. From canonical_address: split on -, e.g. D38-DIEU35-S4-P1-1D38/DIEU35/S4/P1/1. Trivial but ugly labels. Recommend (1) for human-readable folders, fall back to (2) for any LU with missing title.
  • Hierarchy preserved. No flatten required. Sort by sort_order (or render_order) to keep statutory order — pass items to buildDocsTree already sorted, and inside each folder buildDocsTree's alphabetical sort will be overridden if we set path to 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

Single file edit: web/pages/knowledge/laws/index.vue. Add a 4th section "TAC Publications (Đ38)" above/below the existing 3 sections.

Pseudo-flow:

  1. Fetch publications: readItems('tac_publication', { fields: ['id','doc_code','version','name','lifecycle_status'], sort: ['doc_code'] }).
  2. For each publication (or all in one query), fetch members with deep-read of logical_unit_id.* + unit_version_id.* sorted by render_order.
  3. Map each member → AgentView-shaped record:
    • id: unit_version_id.id
    • source_id: logical_unit_id.canonical_address
    • title: unit_version_id.title
    • path: ${pub.doc_code}/${ancestorTitles.join('/')}/${unit_version.title}.md (build ancestorTitles by walking parent_id in a Map of LUs)
    • Carry body into document.body for the reader.
  4. tree = buildDocsTree(records); pass to <DocsTreeView :nodes="tree" :selected-path @select="onSelect" />.
  5. onSelect(node) sets selected.value = node.document. Right pane renders markdownToHtml(selected.value.body) via v-html inside <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.