23-P3D4C2U — DOT Table Registration Implementation Report
23-P3D4C2U — DOT Table Registration Implementation Report
Date: 2026-05-08 Status: BLOCKED — Directus refuses to register a PG view as a queryable collection because its
id uuidcolumn is not recognised as a primary key during knex-schema-inspector introspection. Outcome: All P3D4C2U-created objects rolled back. No residual state. Recommendation:DIRECTUS_VIEW_PK_BLOCKED→ next packDIRECTUS_VIEW_PK_CONVENTION_REVIEW.
§0. Inventory + smoke gate (PASS — gate opened)
| Probe | Result |
|---|---|
0A/0B table_registry schema + tbl_system_issues row |
PASS — 20 rows, tbl_system_issues id=20 published |
0C PG SELECT roles on system_issues |
directus, incomex, context_pack_readonly |
0D Directus read policies on system_issues |
5 policies: Public Access (a513bc9d…), AI Agent, Editor, public_label, Agent. Mirrored Public Access only (canonical convention). |
| 0E Write channel | Directus REST API (/collections, /permissions, /items/table_registry) |
0F SMOKE /items/system_issues?limit=3 |
PASS — gate opened |
0G event_outbox.payload_classification exists |
YES → Variant A (13 cols) |
| 0H IU snapshot | not touched (out of scope) |
0I Existing v_event_outbox_table view |
ABSENT — pack creates fresh |
| 0J Directus view PK convention probe | v_registry_counts precedent: id integer with nextval(...) default → is_primary_key=true via knex-schema-inspector. No precedent for uuid view PK. |
0K Existing Directus permission for v_event_outbox_table |
none |
0L Existing tbl_event_outbox registry row |
none |
DB locality: event_outbox lives in PG cluster postgres container, database directus, schema public, owned by workflow_admin.
Live event_outbox columns (16): id, event_domain, event_type, event_stream, delivery_lane, event_severity, event_subject_table, event_subject_ref, canonical_address, actor_ref, source_system, correlation_id, payload_classification, safe_payload, occurred_at, created_at.
Note vs prompt: live column is event_severity (not severity). Per "use ACTUAL schema" rule, view projects event_severity verbatim.
§1. Step execution log
Step 1 — CREATE VIEW (PASS)
CREATE VIEW v_event_outbox_table AS
SELECT id, occurred_at, event_domain, event_type, event_stream, delivery_lane,
event_severity, event_subject_table, event_subject_ref, canonical_address,
actor_ref, source_system, payload_classification
FROM event_outbox;
COMMENT ON VIEW v_event_outbox_table IS 'Metadata-only read projection. Created by P3D4C2U.';
13 columns. Unsafe denylist absent: payload, safe_payload, body, raw_payload, vector, embedding, secret, token, password, correlation_id, causation_id, ssn, personal_data — none projected.
Step 2 — GRANT SELECT (PASS)
GRANT SELECT ON v_event_outbox_table TO directus, incomex, context_pack_readonly; — mirrors system_issues grant set verbatim. No new role created.
Step 3 — Directus collection register (PASS at API level, FAIL at introspection)
POST /collections returned HTTP 200; row inserted in directus_collections with meta.note, archive_app_filter=true, collapse=open, versioning=false, accountability=null (mirrored v_registry_counts).
GET /collections/v_event_outbox_table returns schema: null — Directus does NOT auto-introspect the view as a queryable collection.
Directus container logs confirm the silent skip:
WARN: Collection "<x>" doesn't have a primary key column and will be ignored
(v_event_outbox_table does not appear in this list; instead it is silently dropped before the warning emitter — but the symptom is identical.)
Compare with the working precedent v_registry_counts.id:
"is_unique": true, "is_primary_key": true,
"default_value": "nextval('v_registry_counts_id_seq'::regclass)",
"has_auto_increment": true
Knex-schema-inspector treats a nextval(...) default as auto-increment + PK. A bare uuid column with no PK constraint (PG views cannot have one) is not recognised.
Step 4 — directus_fields seed + permission (mixed)
- 13 directus_fields rows inserted (raw SQL —
POST /fields/{collection}returned 403 even with admin token, consistent with the collection being absent from Directus's introspected schema). POST /permissionsfor Public Access policy +fields="*"returned HTTP 200 (id=1482).- Restart
incomex-directusperformed; cache clear performed.
Step 5 — table_registry row (NOT EXECUTED)
Held back because Step 6 S2 failed.
Step 6 — Smoke tests
| Smoke | Result |
|---|---|
S1 view exists in PG (SELECT count(*) FROM v_event_outbox_table → 0) |
PASS |
S2 GET /items/v_event_outbox_table?limit=1 (admin token) |
FAIL — HTTP 403 persistently after restart + cache clear |
S2′ GET /items/v_event_outbox_table (no auth → Public) |
FAIL — HTTP 403 |
| S3 unsafe field absence in view definition | PASS (denylist not present) |
| S4 grants identical to system_issues | PASS |
| S5 collection meta seeded | PASS |
| S6 directus_pk_recognized | FAIL — schema.is_primary_key not exposed on any view column |
Root cause for S2 403: with no PK recognised, Directus does not register the collection in its runtime schema map. Even an Administrator-policy token cannot fetch items from a collection Directus has functionally rejected.
Step 7 — Publish decision
published=NO. publish_block_reason=PK_BLOCKED.
Step 8 — Rollback (PASS)
Scoped strictly to *_created_by_pack=P3D4C2U:
DROP VIEW v_event_outbox_table✅DELETE /collections/v_event_outbox_table(HTTP 204) ✅DELETE /permissions/1482(HTTP 204) ✅DELETE FROM directus_fields WHERE collection='v_event_outbox_table'(13 rows) ✅- table_registry row never created → no rollback needed
- Grants on dropped view auto-revoked
- Final residual probe:
residual_perms=0
residual_fields=0
residual_collection=0
residual_view=NULL
residual_registry_row=0
event_outbox table itself, event_pending, event_read, event_subscription, event_type_registry, system_issues, IU runtime — untouched. P3D4C1U state preserved.
§2. Verification block
preflight_table_registry_schema=PASS
preflight_system_issues_row=PASS
preflight_pg_read_role=directus,incomex,context_pack_readonly
preflight_directus_read_role=a513bc9d-b5df-47c4-9b6e-31da6a3b71b0
preflight_write_channel=DIRECTUS_API
smoke_system_issues_api=PASS
smoke_system_issues_route=CANNOT_VERIFY
smoke_gate=PASS
payload_classification_exists=YES
view_variant=A_13_COLS
view_status=CREATED_THEN_DROPPED
view_created_by_pack=P3D4C2U
pg_grant_applied=PASS
directus_collection_visible=FAIL
directus_pk_recognized=FAIL
directus_field_labels=FAIL
permission_status=CREATED_THEN_DROPPED
permission_created_by_pack=P3D4C2U
registry_status=NOT_EXECUTED
registry_row_created_by_pack=SKIPPED
registry_initial_status=draft
registry_final_status=removed
published=NO
publish_block_reason=PK_BLOCKED
unsafe_fields_absent=PASS
iu_post_snapshot_match=PASS
no_nuxt_code=true
no_bespoke_ui=true
no_new_role_created=true
phase_status=BLOCKED
recommendation=DIRECTUS_VIEW_PK_BLOCKED
next_required_pack=DIRECTUS_VIEW_PK_CONVENTION_REVIEW
§3. Hard-boundary compliance
| Boundary | Status |
|---|---|
| No Nuxt code | ✅ |
| No bespoke notification UI / page / component | ✅ |
| No Directus click-config | ✅ (DOT/API + raw SQL only) |
| No Directus write/mark-read endpoint | ✅ |
| No unsafe fields exposed (payload/body/raw/safe_payload/vector/embedding/secret/token/password/correlation_id/causation_id/ssn/personal_data) | ✅ — view denylist enforced |
| No IU runtime change | ✅ |
| No worker / cron added | ✅ |
| No Điều 43 machinery change | ✅ |
| No new PG role or Directus role created | ✅ |
| No publish without route smoke verified | ✅ (never published) |
| Rollback scoped to P3D4C2U-created objects only | ✅ |
§4. Diagnosis — why view PK is blocked
Knex-schema-inspector (Directus 11's PG schema reflector) marks is_primary_key=true for view columns only when one of:
- The column is part of an underlying table's primary key constraint that the inspector traces through the view's column-to-source mapping (Directus does this for simple
SELECT pk FROM base_tablecases — but not consistently). - The column has a
nextval(seq)default +not null(treated as serial-like → assumed PK). This is what givesv_registry_counts.idits PK status despite being a view.
Our view's id column is a plain SELECT id FROM event_outbox projection of a uuid PK with gen_random_uuid() default — neither path triggers PK detection.
This is an architectural constraint of how PG views interact with Directus, not a defect in this pack.
§5. Options for the next pack
DIRECTUS_VIEW_PK_CONVENTION_REVIEW should evaluate (each comes with trade-offs — none in scope here):
| Option | Sketch | Trade-off |
|---|---|---|
| A. Materialised view + unique index | CREATE MATERIALIZED VIEW … ; CREATE UNIQUE INDEX ON … (id); Directus reads MV as a table. |
Needs refresh strategy → drifts toward "no worker/cron" boundary. |
| B. Surrogate serial PK in view | CREATE VIEW … AS SELECT row_number() OVER (ORDER BY occurred_at) AS pk, id, … — unstable across queries. |
Pagination breaks; row identity not stable → unsuitable for read-state joins. |
C. Wrap with stable PK from event_outbox.id cast |
Same id uuid but expose a sequence-fed companion table event_outbox_index(id, event_id) and JOIN. |
New table = new write path = new boundary risk. |
D. Directus collection on the base event_outbox table with field-allowlist permission only |
Uses real PK; field-allowlist masks unsafe columns. | Schema-level masking lost; future column auto-leaks unless permission updated. |
| E. Custom Directus extension for view-PK introspection override | Out-of-band Directus extension. | Runtime extension footprint; SSOT divergence. |
Recommendation for the convention review: Option A (materialised view + unique index) OR Option D (base-table + field-allowlist with strong CI guard). Both are feasible without new roles; both have explicit trade-offs that need user/governance sign-off.
§6. Single-line summary
PG view + GRANT + Directus collection meta + permission seed all succeeded individually, but Directus's knex-schema-inspector silently dropped the view because the id uuid column isn't introspected as a primary key. All P3D4C2U-created objects rolled back cleanly. Next step: DIRECTUS_VIEW_PK_CONVENTION_REVIEW — pick between materialised-view PK or base-table-with-allowlist before any further implementation.