KB-5496

5000x · Nuxt UI factory · template-driven assembly khuôn

8 min read Revision 1
iu-core5000xnuxt-factoryui-templateschema-drivenaxis-b-defect-fix

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_tags in PG is jsonb shaped {group: [tag, …]} — a dict, not a flat string array. The 4000x Vue page would have iterated over zero entries because for…of on a plain object yields nothing.
  • Directus exposes the column as json; the _contains filter operator is rejected with INVALID_QUERY against json.

The factory templates were updated to:

  • drop axisBTag from the composable's filter API — server-side containment is not portable on json; client-side filtering is the correct path;
  • add a flattenAxisBTags helper to the Vue template that accepts both shapes (string[] and Record<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's modules/directus/runtime/composables/useDirectus.ts pattern)
  • 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.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-core-5000x-nuxt-pilot-monitoring-rollout-open-goal/02-nuxt-ui-factory-template.md