5000x · Nuxt UI factory · template-driven assembly khuôn
02 — Nuxt UI factory · template-driven assembly khuôn
1. Why a factory, not another one-off
4000x shipped ui-package/nuxt-iu-three-axis/ — a Vue page + composable
hand-tailored to iu_three_axis_envelope. It was deploy-ready but
not repeatable: each new three-axis surface would have required
another hand-authored page + composable, with the same field names baked
into different files.
The 5000x constitution rules that out:
Postgres canonical objects -> Directus collections/permissions/API -> UI assembly template/factory -> generated Nuxt-compatible artifact -> gated deployment.
The right deliverable is therefore a schema-driven factory whose output is the Nuxt artifact, not the artifact itself.
2. What was authored
ui-package/nuxt-three-axis-factory/
├── README.md
├── templates/
│ ├── three-axis.page.template.vue
│ ├── three-axis.composable.template.ts
│ ├── compose.snippet.template.yml
│ └── runtimeConfig.snippet.template.ts
├── descriptors/
│ ├── iu_three_axis_envelope.json # real
│ └── sample_doc_tree_envelope.json # repeatability proof
└── generated/ # output (regenerated, checked in)
├── iu_three_axis_envelope/
│ ├── pages/admin/iu-three-axis.vue
│ ├── composables/useIuThreeAxis.ts
│ ├── compose.snippet.yml
│ └── runtimeConfig.snippet.ts
└── sample_doc_tree_envelope/
├── pages/admin/sample-doc-tree-three-axis.vue
├── composables/useSampleDocTree.ts
├── compose.snippet.yml
└── runtimeConfig.snippet.ts
cutter_agent/iu_core/ui_factory.py # the renderer
tests/test_iu_core_5000x_nuxt_factory.py # 15 contract tests
3. Factory contract (tests, not prose)
tests/test_iu_core_5000x_nuxt_factory.py asserts every boundary
property as a separate TestCase:
| TestCase | Property |
|---|---|
TestFactoryTemplatesSchemaAgnostic |
the template body (with comments stripped) carries no IU-specific field name — every field name in generated artifacts originates from the descriptor at render time |
TestFactoryDeterministic |
regenerating the same descriptor produces byte-identical output (SHA256 over sorted relpath + content pairs) |
TestFactoryRepeatable |
two distinct descriptors produce two non-overlapping artifact trees with distinct digests; sample descriptor's generated artifact contains source_doc / topic_tags / parent_node_id and none of the IU literals |
TestGeneratedArtifactsDirectusOnly |
generated .ts files import @directus/sdk only; no PG driver; no Bearer / api-key literal |
TestGeneratedArtifactsUseEnvDrivenCollection |
the composable reads the collection name from useRuntimeConfig().public[<config_key>]; the Vue page contains no literal iu_three_axis_envelope |
TestNoLiveNuxtRepoMutation |
ui_factory.py writes only under ui-package/; the module body has no /opt/incomex/docker/nuxt-repo literal |
TestFactoryDescriptorValidation |
malformed descriptors are rejected by load_descriptor (no silent default that could leak a literal) |
Result: 15 / 15 PASS on first author + every subsequent regenerate.
4. The two descriptors
4.1 iu_three_axis_envelope.json — real IU Core surface
collection : iu_three_axis_envelope
env_var : IU_CORE_DIRECTUS_COLLECTION
config_key : iuCoreDirectusCollection
page_slug : iu-three-axis
composable_basename : useIuThreeAxis
id_field : unit_id
label_field : canonical_address
axis_a : { group: axis_a_doc_code, section: axis_a_section_code, sort: axis_a_sort_order }
axis_b : { tag: axis_b_tags }
axis_c : { parent: axis_c_parent_id, depth: axis_c_depth }
Generates: generated/iu_three_axis_envelope/... (digest
413143da9efa51bf…).
4.2 sample_doc_tree_envelope.json — repeatability proof
collection : sample_doc_tree_envelope
env_var : SAMPLE_DOC_TREE_DIRECTUS_COLLECTION
config_key : sampleDocTreeDirectusCollection
page_slug : sample-doc-tree-three-axis
composable_basename : useSampleDocTree
id_field : node_id
label_field : node_label
axis_a : { group: source_doc, section: source_section, sort: source_seq }
axis_b : { tag: topic_tags }
axis_c : { parent: parent_node_id, depth: node_depth }
Generates: generated/sample_doc_tree_envelope/... (digest
d8bf292d30f069ec…). No Directus collection exists for it on
production — it exists solely to prove the template renders
non-overlapping artifacts under a different schema.
5. 5000x-discovered defect in the 4000x ui-package
The first run of the 4000x-style composable (_contains against
axis_b_tags) and Vue page (iterate for (const tag of row.axis_b_tags)
as if it were a flat array) failed when validated against the live
Directus REST + the live PG payload shape:
axis_b_tagsin PG isjsonbshaped{group: [tag, …]}— a dict, not a flat string array. The 4000x Vue page would have iterated over zero entries becausefor…ofon a plain object yields nothing.- Directus exposes the column as
json; the_containsfilter operator is rejected withINVALID_QUERYagainstjson.
The factory templates were updated to:
- drop
axisBTagfrom the composable's filter API — server-side containment is not portable onjson; client-side filtering is the correct path; - add a
flattenAxisBTagshelper to the Vue template that accepts both shapes (string[]andRecord<string, string[]>), so the template is robust against either descriptor convention.
The 4000x ui-package itself is frozen as-is as the "first one-off" historical record; it is superseded by the factory output and is not on the future deploy path.
6. Boundary preserved (no live Nuxt repo mutation)
The factory writes only under
ui-package/nuxt-three-axis-factory/generated/<descriptor-name>/.
TestNoLiveNuxtRepoMutation enforces this both by the asserted output
root and by grepping the factory source for the live Nuxt repo path.
Read-only discovery of the live Nuxt repo via SSH happened against
/opt/incomex/docker/nuxt-repo/web (Nuxt 3, modules/directus,
composables/useDirectusTable.ts, pages/admin/{knowledge-tree, super-session, users}.vue). The factory templates honour the same
conventions:
import { readItems, aggregate } from '@directus/sdk'useDirectus<Output>(readItems(...))(the repo'smodules/directus/runtime/composables/useDirectus.tspattern)useRuntimeConfig().public[<config_key>]definePageMeta({ middleware: 'auth' })useAsyncData+ a refresh button — no manual cache
7. Deploy runbook (frontend / DevOps owned — NOT executed in this macro)
Recorded verbatim in ui-package/nuxt-three-axis-factory/README.md
§"Deploy runbook". Summary:
SRC=ui-package/nuxt-three-axis-factory/generated/iu_three_axis_envelope
DST=/opt/incomex/docker/nuxt-repo/web
# review diff against current Nuxt repo state, then:
cp $SRC/pages/admin/*.vue $DST/pages/admin/
cp $SRC/composables/*.ts $DST/composables/
# merge compose.snippet.yml + runtimeConfig.snippet.ts
cd /opt/incomex/docker && docker compose up -d --build nuxt
dot_iu_nuxt_config_verify
curl -fS http://incomex-nuxt:3000/admin/iu-three-axis
5000x stops at "generator authored + artifacts regenerated"; the
docker compose up -d --build nuxt step is owned by the frontend /
DevOps team and is the residual recorded in doc 06.