PIDX Readiness Logic v0.2 — strict parser, precedence, usability, warnings, anti-false-green
PIDX Readiness Logic v0.2 — strict parser, precedence, usability, warnings, anti-false-green
Path:
knowledge/dev/laws-new/workflow-manage/design/pidx-readiness-logic-v0.2.mdStatus: DESIGN · v0.2 · NON-AUTHORIZING · 0 PG objects · 0 DDL/DML. Specifies the computed semantics ofv_pidx_procedure_readiness. Date: 2026-06-23 Companions:pidx-build-design-v0.2.md(architecture/grammar),pidx-ddl-candidate-v0.2.sql.md(candidate SQL),pidx-test-plan-v0.2.md,pidx-seed-slice-v0.2.md. Truth rule (non-negotiable): Only PG/SQL-derived existence and usability can setREADY. A declaration, manifest, note, RAG result, or seed status can route and suggest but can NEVER setREADY.
0. LEGO separation (reaffirmed; v0.2 keeps the pieces separable)
The readiness computation is assembled from seven reusable pieces, each a CTE in the candidate SQL, each independently testable. None is collapsed into a workflow engine:
| Piece | CTE | Responsibility | Reusable for |
|---|---|---|---|
| procedure | pidx_procedure (table) |
the declared procedure row | inventory, treeview, links later |
| ingredient | pidx_procedure_ingredient (table) |
one declared need | reverse lookup "who uses X" |
| ref (grammar) | t1–t4 (parser) |
tokenize + strict-validate <kind>:<ident> |
inventory object_ref uses the SAME grammar |
| source probe | t5 |
narrow EXISTS per kind; READ_BLOCKED; logical-collection |
any "does X exist" question |
| readiness status | t6 |
computed_status + usable |
per-ingredient panes |
| warning flag | t7.warns + proc_warn |
flag production + aggregation | inventory warnings reuse the same flags |
| missing route | t7.missing_route_exists |
"if missing, go where, does it exist" | next-route suggestion |
PIDX remains the eye: it sees which procedure exists, which ingredients it declares, which PG can verify, which are missing/unknown/invalid/blocked, and which route to check next. It does not execute the procedure.
1. Resolver contract (one strict parser — P0-2)
A single side-effect-free resolver maps (ingredient_kind, ingredient_ref, ref_status) → (computed_status, usable, warns) using ONLY the probes in §3. Properties:
- Deterministic & pure — read-only
SELECT/EXISTS; no writes, no side effects. - Bounded / narrow — resolves one ref at a time; the hot path resolves only the handful of refs of ONE queried procedure. Never scans the whole catalog to answer one question.
- Strict before probing — parse validity (
structurally_valid) is computed before any existence probe, independently ofref_status. A malformed ref never reaches a probe. - Grammar-locked —
pidx_procedure_ingredient.ingredient_refandv_pidx_inventory_current.object_refimport the SAME grammar; neither side hand-rolls a different parse. - Honest — returns exactly one of
EXISTS · MISSING · UNKNOWN_SOURCE · INVALID_REF · READ_BLOCKEDper ingredient (existence domain) plus an orthogonalusable ∈ {true, false, unknown}.
1.1 The parse (the v0.1 false-green killer)
prefix = text before the FIRST ':' (NULL if no ':')
ident = text after the FIRST ':' (NULL if no ':')
seg = string_to_array(ident, '.') (literal split, not regex)
nseg = array_length(seg, 1) (0 if empty)
structurally_valid ⇔
has ':' (prefix and ident exist)
AND length(ident) > 0
AND no empty segment ('' not in seg) (catches 'a..b', '.a', 'a.')
AND prefix = ingredient_kind (P0-2: prefix MUST equal the declared kind)
AND nseg = arity(kind) (P0-2: EXACT per-kind segment count)
Exact per-kind arity (segments split on .):
| kind | arity | rationale |
|---|---|---|
dot, approval, procedure |
nseg = 1 |
codes never contain . (verified: 0 dotted DOT/approval codes) |
event |
nseg ≥ 2 |
domain.type; event_type may itself contain . (verified: 27/52 types are dotted, e.g. backfill.sweep_completed). Split on the FIRST dot only. |
label |
nseg ∈ {1,2} |
bare code or facet_id.code |
collection, view, function |
nseg = 2 |
schema.table / schema.name; schema REQUIRED (no silent default) |
field, trigger |
nseg = 3 |
schema.table.column / schema.table.trigger |
io, checker, template, report |
nseg ≥ 1 |
shape-only; always resolve UNKNOWN_SOURCE |
Schema-default policy (single, explicit — Codex P0-2). CATALOG canonical form is fully schema-qualified. A CATALOG ref missing its schema (e.g.
collection:dot_tools,nseg=1) isINVALID_REF— the readiness view never silently defaults topublic. Addingpublic.is a write-side concern (the optional normalizer, §9), not a read-side guess. This is the strict, fail-closed, no-silent-fallback choice.
No
split_partarity leak (Codex P0-3 issue #3). Becausensegmust equal the exact arity,field:public.dot_tools.code.extra(nseg=4) andfield:public.dot_tools(nseg=2) are bothINVALID_REFbefore any probe — neither can truncate to a real column.
No prefix leak (Codex issue #2).
(kind='approval', ref='dot:patch_ops_code')hasprefix='dot' ≠ 'approval'→INVALID_REF. In v0.1 this false-EXISTS'd becausepatch_ops_codeis a realapr_action_typesrow.
2. Per-kind probe + usability (write-side and probe-side agree)
CATALOG class (schema-qualified, catalog-proven, no lifecycle column → usable = unknown)
| kind | canonical | probe (existence) | usable |
|---|---|---|---|
collection |
collection:<schema>.<table> |
physical base table in information_schema.tables (BASE TABLE); logical = directus_collections (public) computed separately |
unknown (raw catalog object) |
view |
view:<schema>.<view> |
information_schema.views ∪ matview (pg_class relkind='m') |
unknown |
field |
field:<schema>.<table>.<column> |
information_schema.columns |
unknown |
trigger |
trigger:<schema>.<table>.<name> |
information_schema.triggers |
unknown |
function |
function:<schema>.<name> |
pg_proc ⋈ pg_namespace (name-only) |
unknown |
CODE class (case-SENSITIVE, registry-proven, lifecycle → usable tri-state)
| kind | canonical | probe | usable = true when | usable = false when |
|---|---|---|---|---|
dot |
dot:<CODE> |
dot_tools.code |
status ∈ {active,published} |
status ∈ {retired,deprecated,disabled,inactive,archived} |
approval |
approval:<action_code> |
apr_action_types.action_code |
handler implemented and status='active' |
handler null/unimplemented, or status ∈ {retired,disabled,inactive} |
event |
event:<domain>.<type> |
event_type_registry(event_domain, event_type); type after first dot |
active = true |
active = false |
procedure |
procedure:<code> |
pidx_procedure.procedure_code then workflows.process_code |
status='active' |
status ∈ {retired, draft} |
label |
label:<facet_id>.<code> or label:<code> |
facet-qualified → taxonomy(facet_id::text, code); bare → taxonomy(code) |
status ∈ {active,published} and replaced_by IS NULL |
status ∈ {retired,deprecated,replaced} or replaced_by IS NOT NULL |
io,checker,template,report |
<kind>:<code> |
none | — | — (always UNKNOWN_SOURCE) |
usable = unknown(NULL) means "the source has no lifecycle signal we trust." We do not downgrade readiness on unknown (no overclaim, Codex P1-1). We downgrade only on a provenusable = false.
MISSINGvsUNKNOWN_SOURCEis load-bearing.MISSING= "source known, object not there → go create it."UNKNOWN_SOURCE= "no clean PG source → triage the source; do NOT fabricate a green." The four UNKNOWN_SOURCE kinds never resolve toMISSING.
3. Per-ingredient precedence
3.1 computed_status (existence domain — first match wins)
1. ref_status = 'UNNORMALIZED' OR NOT structurally_valid -> INVALID_REF
2. kind ∈ {io, checker, template, report} -> UNKNOWN_SOURCE
3. CATALOG kind AND schema exists AND querying role lacks USAGE -> READ_BLOCKED
4. ref resolves AND object found -> EXISTS
5. ref resolves AND object NOT found -> MISSING
READ_BLOCKED(rule 3) precedesMISSING/EXISTS: when the role cannot read the schema, aMISSINGfrominformation_schemawould be a lie, so the parser short-circuits. Derived fromhas_schema_privilege(current_user, <oid>, 'USAGE'), guarded so a non-existent schema isMISSING(rule 5), not an error (P2-3).
3.2 usable (orthogonal, only meaningful when computed_status='EXISTS')
usable ∈ {true, false, unknown} per §2. Drives the satisfaction test and lifecycle warnings (P1-1).
3.3 satisfaction
req_satisfied ⇔ computed_status = 'EXISTS' AND usable IS NOT FALSE
A required ingredient is satisfied only if it exists and is not proven-unusable. usable = unknown (catalog objects) still satisfies — we cannot prove it unusable.
4. Per-procedure rollup (computed once — P2-1)
The gating set = ingredients with required_level='required'. optional/nice_to_have/UNKNOWN/NEEDS_TRIAGE are non-gating.
ingredient_count = 0 -> UNMAPPED
required_count = 0 -> UNMAPPED (P0-5: zero-required NEVER READY)
any REQUIRED ingredient NOT req_satisfied -> NOT_READY
(covers required MISSING / UNKNOWN_SOURCE / INVALID_REF /
READ_BLOCKED, and required EXISTS-but-usable=false:
inactive/retired/disabled, unimplemented approval handler)
else, any warning flag present on the procedure -> READY_WITH_WARNINGS
else (all required satisfied, zero warnings) -> READY
computed_readiness is computed once in CTE roll; readiness_drift is derived from it in the outer SELECT (no duplicated CASE).
4.1 READINESS_DRIFT (orthogonal non-null boolean)
readiness_drift ⇔
COALESCE(declared_maturity ∈ {checklist_ready, dot_sequence_ready, one_button_ready}, false)
AND computed_readiness ∈ {NOT_READY, UNMAPPED}
A procedure whose hint claims a ready tier but whose required ingredients are not satisfied (or which is unmapped) computes NOT_READY/UNMAPPED and raises readiness_drift. READY_WITH_WARNINGS does not raise drift — its warning_flags already surface the gap. COALESCE guarantees a non-null boolean (P2-4).
Full status set emitted: per-ingredient EXISTS, MISSING, UNKNOWN_SOURCE, INVALID_REF, READ_BLOCKED (+ STALE reserved); per-procedure UNMAPPED, NOT_READY, READY_WITH_WARNINGS, READY; orthogonal readiness_drift.
5. Warning flags catalog (all produced — P1-2)
| flag | grain | raised when | drives |
|---|---|---|---|
OVERLOADED_FUNCTION |
ingredient | function: name resolves to > 1 proc in the schema (signature unchecked in v0.2) |
warning only (still satisfied) |
AMBIGUOUS_LABEL |
ingredient | bare label:<code> (no facet) matches > 1 taxonomy row |
warning only |
LOGICAL_PHYSICAL_MISMATCH |
ingredient | collection: in public where physical XOR logical (Directus) |
warning; if physical missing → also MISSING |
APPROVAL_HANDLER_UNIMPLEMENTED |
ingredient | approval:<code> exists but handler_ref null/unimplemented |
usable=false → required ⇒ NOT_READY |
SOURCE_NOT_USABLE |
ingredient | non-approval EXISTS with usable=false (inactive event, retired dot/label/procedure) |
usable=false → required ⇒ NOT_READY |
OPTIONAL_MISSING |
ingredient | non-gating ingredient MISSING |
warning (non-blocking) |
OPTIONAL_UNKNOWN_SOURCE |
ingredient | non-gating ingredient UNKNOWN_SOURCE |
warning |
OPTIONAL_INVALID_REF |
ingredient | non-gating ingredient INVALID_REF |
warning |
OPTIONAL_READ_BLOCKED |
ingredient | non-gating ingredient READ_BLOCKED |
warning |
REQUIRED_LEVEL_UNTRIAGED |
ingredient | required_level ∈ {UNKNOWN, NEEDS_TRIAGE} |
warning |
STALE_SOURCE |
ingredient | a registry-cache probe reports refresh_required (reserved; pure v0.2 views are always fresh → inert) |
warning |
Top-level warning_flags = the DISTINCT, sorted union of its ingredients' flags (CTE proc_warn, aggregated once, P0-1).
5.1 Causal completeness + the green invariant (P1-2)
READY⇒warning_flags = {}(empty). If any ingredient produced a flag,warned_count > 0→ the rollup yieldsREADY_WITH_WARNINGS, notREADY.READY_WITH_WARNINGS⇒warning_flags ≠ {}(non-empty, causally complete). Every cause of a non-green-but-not-blocked outcome (optional problem, warning, untriaged level) emits an explicit flag.- For
NOT_READY, the causal evidence is thereq_missing_count / req_unknown_count / req_invalid_count / req_blocked_count / req_notusable_countcolumns (hard gates), plus any warnings that also happen to be present.warning_flagsmay be empty underNOT_READY— the count columns explain it.
6. Kind-specific rules (explicit)
- Approval (P1-3, safe rule).
approval:<action_code>isEXISTSif present inapr_action_types. Ifhandler_refis null/unimplemented,usable=false→ a required such approval is unsatisfied →NOT_READY(+APPROVAL_HANDLER_UNIMPLEMENTED). An optional one →READY_WITH_WARNINGS.READY_WITH_WARNINGSis non-authorizing: it never triggers execution; it is a discovery signal only. - Function (overload).
function:schema.nameis name-only existence. Overload (count>1) →EXISTS+OVERLOADED_FUNCTION, still satisfied for discovery;usable=unknown. Signature-precisefunction:schema.name(argtypes)is a v0.3 follow-up; until thenREADY_WITH_WARNINGSfrom an overload is non-authorizing. - Label (P0-4).
label:<facet_id>.<code>resolves facet + code; barelabel:<code>resolves by code. Bare code matching > 1 row →EXISTS+AMBIGUOUS_LABEL(never bareREADY). Verified: current data has 0 multi-facet codes, soAMBIGUOUS_LABELis armed but inert. - Collection (P0-3). Existence = physical base table. Logical (Directus) presence computed separately, public-only. Physical XOR logical →
LOGICAL_PHYSICAL_MISMATCH. Logical-only (Directus folder, no table) →MISSING(+ mismatch) → required ⇒NOT_READY— no silent READY from logical metadata alone. - Procedure. Probe
pidx_procedurefirst, thenworkflows.process_code. Never merge the two code spaces (PROC_*self vs legacyWF-*).draft/retired→usable=false.
7. Anti-false-green rules (the whole point)
- Only PG/SQL existence + usability sets
READY.manifest_jsonb,declared_maturity,note,source_ref, seed status, and any future RAG/vector output may route/suggest but can never setREADY. - Strict parse before probe. Prefix mismatch, wrong arity, empty/extra/missing segment →
INVALID_REFbefore any existence query. Nosplit_parttruncation, no fuzzy join. - Zero-required ≠ ready.
ingredient_count=0orrequired_count=0→UNMAPPED, neverREADY(P0-5). - Existence ≠ usability. A found-but-inactive/retired/disabled/unimplemented required ingredient →
NOT_READY(P1-1, P1-3). - Logical ≠ physical. A required collection present only as Directus metadata →
MISSING→NOT_READY(P0-3). - UNKNOWN_SOURCE never fabricates existence.
io/checker/template/reportrequired →NOT_READY. - Warnings cannot be silently swallowed.
READY⇒ empty flags;READY_WITH_WARNINGS⇒ non-empty causally-complete flags (P1-2). - Declared-ready that isn't computed-ready must shout.
readiness_driftfires on a ready-tier hint contradictingNOT_READY/UNMAPPED.
8. Automation boundary (v0.2 = seeing/checking/routing, NOT executing)
v0.2 automates: ref parsing, PG source probing, EXISTS/MISSING/UNKNOWN_SOURCE/INVALID_REF/READ_BLOCKED detection, usability detection, warning computation, readiness rollup, and missing_route_exists resolution. v0.2 does NOT automate, imply, or design: procedure execution, auto-fix, auto-register DOT, auto-create collection/schema, auto-approval, or one-button runtime execution. READY/READY_WITH_WARNINGS are read-only signals; they authorize nothing. Any future mutation/build/apply goes through an Owner-authorized governed path (DOT / registered migration), per pidx-build-design-v0.2.md §11.
9. Write-side normalizer (optional, documented — not a gate)
A future, optional write-side helper MAY, at insert/update time, lowercase CATALOG identifiers, prepend public. to a schema-less CATALOG ref, and set ref_status. It is a normalizer that sets the flag, never a regex CHECK that rejects the row (Codex issue #8). The readiness view does not depend on it — it re-parses every ref strictly regardless of ref_status.
10. v0.2 scope of this logic
- SAFE to compute now:
dot, collection, view, field, trigger, function, approval, event, procedure, label(label now full, P0-4). - Always
UNKNOWN_SOURCE:io, checker, template, report. - Armed-but-inert on current data (verified 2026-06-23):
AMBIGUOUS_LABEL(0 multi-facet codes),STALE_SOURCE(pure views always fresh), dotSOURCE_NOT_USABLE(no retired DOTs — but eventSOURCE_NOT_USABLEis live: 22 inactive events). - First v0.3 follow-ups:
functionsignature precision, a realio/checkerSSOT, a confirmedtemplatesource, and (only if scale demands)pg_trgm/vector as a two-step suggest→confirm radar that still never answers exists/missing/ready.