KB-4F91

RP UI Axis — 02 UI/API Axis Surface Patch Packet

7 min read Revision 1
topic-axisui-apinuxtpatch-packet2026-06-04

02 — UI/API Axis Surface Patch Packet (Workstream A)

Access status

UI/API source lives at web/server/api/registries-pivot/{summary,rows,node}.get.ts inside the incomex-nuxt container. The MCP read_file allowlist is /opt/incomex/docs, /opt/incomex/dot/specs, /var/log/nginx only — Nuxt source is NOT readable from this session. Therefore this is a PATCH-READY operator packet, not a live patch. Redeploy is operator-gated (Nuxt rebuild). No Nuxt math anywhere — every count/flag is already computed in PG.

Backend is fully live and read-only

The four data sources the UI needs already exist in PG and were verified live (doc 01):

  • v_registries_pivot_axis_surface (DAG-aware nodes)
  • v_axis_topic_pivots (axis pivots)
  • v_axis_topic_decision_queue (NEW this macro — decision queue)
  • fn_topic_node_substrate(text) (substrate resolver)

Routes to add (mirror existing registries-pivot pattern)

Create sibling group web/server/api/axes/:

Method/Route File PG source
GET /api/axes axes/index.get.ts SELECT axis_code, axis_name, domain, status, owner_governance_ref FROM axis_registry
GET /api/axes/:axis_code axes/[axis_code]/index.get.ts axis_registry row + v_axis_topic_pivots
GET /api/axes/:axis_code/nodes axes/[axis_code]/nodes.get.ts v_registries_pivot_axis_surface WHERE axis_code=$1
GET /api/axes/:axis_code/node/:node_code axes/[axis_code]/node/[node_code]/index.get.ts one surface row
GET /api/axes/:axis_code/node/:node_code/substrate .../substrate.get.ts SELECT fn_topic_node_substrate($node_code)
GET /api/axes/:axis_code/pivots axes/[axis_code]/pivots.get.ts v_axis_topic_pivots
GET /api/axes/:axis_code/decision-queue axes/[axis_code]/decision-queue.get.ts v_axis_topic_decision_queue WHERE axis_code=$1

Reference handler (thin SELECT passthrough — copy the registries-pivot style)

// web/server/api/axes/[axis_code]/nodes.get.ts
import { defineEventHandler, getRouterParam, createError } from 'h3'
import { pgQuery } from '~/server/utils/pg' // same helper the registries-pivot routes use

export default defineEventHandler(async (event) => {
  const axisCode = getRouterParam(event, 'axis_code')
  if (!axisCode) throw createError({ statusCode: 400, statusMessage: 'axis_code required' })
  // NO math here. The view already computed every count, flag, and status.
  const rows = await pgQuery(
    `SELECT node_code, node_label, lifecycle_status, governance_status,
            parent_codes, has_multiple_parents, count_value, child_count,
            assignment_count, warning_flags, grouping_status, pin_state
       FROM v_registries_pivot_axis_surface
      WHERE axis_code = $1
      ORDER BY count_value DESC, node_code`,
    [axisCode]
  )
  return { axis_code: axisCode, count: rows.length, nodes: rows }
})
// web/server/api/axes/[axis_code]/decision-queue.get.ts
export default defineEventHandler(async (event) => {
  const axisCode = getRouterParam(event, 'axis_code')
  const rows = await pgQuery(
    `SELECT node_code, node_label, evidence_strength, review_bucket,
            proposed_classification, proposed_lifecycle_action,
            duplicate_synonym_suspicion, iu_count, document_count,
            evidence_tag_count, source_tag_keys, gov_council_decision_needed,
            approval_ref, governance_note
       FROM v_axis_topic_decision_queue
      WHERE axis_code = $1`,
    [axisCode]
  )
  return { axis_code: axisCode, count: rows.length, queue: rows }
})
// web/server/api/axes/[axis_code]/node/[node_code]/substrate.get.ts
export default defineEventHandler(async (event) => {
  const nodeCode = getRouterParam(event, 'node_code')
  const [row] = await pgQuery(`SELECT fn_topic_node_substrate($1) AS substrate`, [nodeCode])
  return row?.substrate ?? null
})

Live response samples (from prod 2026-06-04)

GET /api/axes

[{"axis_code":"AX-TOPIC","axis_name":"Chu de noi dung (Topic Axis)","domain":"content_topic","status":"CANDIDATE","owner_governance_ref":"GOV-COUNCIL (pending ratification)"}]

GET /api/axes/AX-TOPIC/nodes → 7 rows; first:

{"node_code":"TOPIC-CAND:knowledge_graph","node_label":"knowledge_graph","lifecycle_status":"candidate","governance_status":"UNGOVERNED_CANDIDATE","parent_codes":[],"has_multiple_parents":false,"count_value":10,"child_count":0,"assignment_count":10,"warning_flags":["CANDIDATE_NODE","ORPHAN_TOPIC_NODE"],"grouping_status":"OK","pin_state":"UNPINNED"}

GET /api/axes/AX-TOPIC/decision-queue → 7 rows; first:

{"node_code":"TOPIC-CAND:knowledge_graph","evidence_strength":"STRONG","review_bucket":"LIKELY_ROOT","proposed_classification":"ROOT_CANDIDATE","proposed_lifecycle_action":"PROPOSE_PROMOTE_TO_ROOT","duplicate_synonym_suspicion":null,"iu_count":10,"gov_council_decision_needed":true,"approval_ref":null}

GET /api/axes/AX-TOPIC/node/TOPIC-CAND:knowledge_graph/substrate → resolver jsonb with 10 information_units, 10 evidence_tags, null taxonomy_node/birth_record/governance_owner, empty parents/documents/workflows.

Frontend component set (additive)

  • Axis selector dropdown over /api/axes (today: "Topic Axis [CANDIDATE]" badge).
  • Decision queue table over /api/axes/AX-TOPIC/decision-queue: columns evidence_strength badge, review_bucket chip, proposed action, IU count, sibling-suspicion warning, "Send to GOV-COUNCIL" action (files an approval_requests row — see doc 04, owner-gated; UI only drafts).
  • Node tree over /api/axes/AX-TOPIC/nodes: root layer = empty parent_codes; child layer expanded by parent_code; multi-parent → render each path (never collapse).
  • Lifecycle + governance badges from lifecycle_status / governance_status.
  • Substrate panel rendering resolver jsonb.
  • Grouping warning when grouping_status=GROUPING_REQUIRED.
  • Pin bound to existing registry_pin mechanism (axis pins = future).

Redeploy checklist (operator)

  1. Add the 7 axes/*.get.ts handlers (copy registries-pivot handlers; swap view names/columns above).
  2. Add the axis selector + decision-queue table + node tree + substrate panel components.
  3. Link from /knowledge/registries-pivot → an "Axes" tab (legacy tree untouched).
  4. docker build + redeploy incomex-nuxt (operator-gated).
  5. Smoke test: /api/axes returns AX-TOPIC; /api/axes/AX-TOPIC/nodes returns 7; /api/axes/AX-TOPIC/decision-queue returns 7; substrate for knowledge_graph returns 10 IUs.

Forbidden-action compliance

No Nuxt-side count math; legacy tree path untouched; DAG never forced into one tree; no redeploy executed; no hardcoded topic levels (depth comes from parent_codes arrays).

Back to Knowledge Hub knowledge/dev/reports/architecture/rp-ui-consume-axis-surface-decision-queue-automation-handoff-2026-06-04/02-ui-api-axis-surface-patch-or-live-report.md