KB-3297

PIDX Readiness Logic v0.2 — strict parser, precedence, usability, warnings, anti-false-green

18 min read Revision 1
workflow-manageprocedure-indexpidxreadiness-logicstrict-parserref-grammarstatus-precedenceusabilitywarning-flagsanti-false-greenlegov0.22026-06-23

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.md Status: DESIGN · v0.2 · NON-AUTHORIZING · 0 PG objects · 0 DDL/DML. Specifies the computed semantics of v_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 set READY. A declaration, manifest, note, RAG result, or seed status can route and suggest but can NEVER set READY.


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) t1t4 (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 of ref_status. A malformed ref never reaches a probe.
  • Grammar-lockedpidx_procedure_ingredient.ingredient_ref and v_pidx_inventory_current.object_ref import the SAME grammar; neither side hand-rolls a different parse.
  • Honest — returns exactly one of EXISTS · MISSING · UNKNOWN_SOURCE · INVALID_REF · READ_BLOCKED per ingredient (existence domain) plus an orthogonal usable ∈ {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) is INVALID_REF — the readiness view never silently defaults to public. Adding public. 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_part arity leak (Codex P0-3 issue #3). Because nseg must equal the exact arity, field:public.dot_tools.code.extra (nseg=4) and field:public.dot_tools (nseg=2) are both INVALID_REF before any probe — neither can truncate to a real column.

No prefix leak (Codex issue #2). (kind='approval', ref='dot:patch_ops_code') has prefix='dot' ≠ 'approval'INVALID_REF. In v0.1 this false-EXISTS'd because patch_ops_code is a real apr_action_types row.


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 proven usable = false.

MISSING vs UNKNOWN_SOURCE is 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 to MISSING.


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) precedes MISSING/EXISTS: when the role cannot read the schema, a MISSING from information_schema would be a lie, so the parser short-circuits. Derived from has_schema_privilege(current_user, <oid>, 'USAGE'), guarded so a non-existent schema is MISSING (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)

  • READYwarning_flags = {} (empty). If any ingredient produced a flag, warned_count > 0 → the rollup yields READY_WITH_WARNINGS, not READY.
  • READY_WITH_WARNINGSwarning_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 the req_missing_count / req_unknown_count / req_invalid_count / req_blocked_count / req_notusable_count columns (hard gates), plus any warnings that also happen to be present. warning_flags may be empty under NOT_READY — the count columns explain it.

6. Kind-specific rules (explicit)

  • Approval (P1-3, safe rule). approval:<action_code> is EXISTS if present in apr_action_types. If handler_ref is null/unimplemented, usable=false → a required such approval is unsatisfiedNOT_READY (+ APPROVAL_HANDLER_UNIMPLEMENTED). An optional one → READY_WITH_WARNINGS. READY_WITH_WARNINGS is non-authorizing: it never triggers execution; it is a discovery signal only.
  • Function (overload). function:schema.name is name-only existence. Overload (count>1) → EXISTS + OVERLOADED_FUNCTION, still satisfied for discovery; usable=unknown. Signature-precise function:schema.name(argtypes) is a v0.3 follow-up; until then READY_WITH_WARNINGS from an overload is non-authorizing.
  • Label (P0-4). label:<facet_id>.<code> resolves facet + code; bare label:<code> resolves by code. Bare code matching > 1 row → EXISTS + AMBIGUOUS_LABEL (never bare READY). Verified: current data has 0 multi-facet codes, so AMBIGUOUS_LABEL is 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_READYno silent READY from logical metadata alone.
  • Procedure. Probe pidx_procedure first, then workflows.process_code. Never merge the two code spaces (PROC_* self vs legacy WF-*). draft/retiredusable=false.

7. Anti-false-green rules (the whole point)

  1. 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 set READY.
  2. Strict parse before probe. Prefix mismatch, wrong arity, empty/extra/missing segment → INVALID_REF before any existence query. No split_part truncation, no fuzzy join.
  3. Zero-required ≠ ready. ingredient_count=0 or required_count=0UNMAPPED, never READY (P0-5).
  4. Existence ≠ usability. A found-but-inactive/retired/disabled/unimplemented required ingredient → NOT_READY (P1-1, P1-3).
  5. Logical ≠ physical. A required collection present only as Directus metadata → MISSINGNOT_READY (P0-3).
  6. UNKNOWN_SOURCE never fabricates existence. io/checker/template/report required → NOT_READY.
  7. Warnings cannot be silently swallowed. READY ⇒ empty flags; READY_WITH_WARNINGS ⇒ non-empty causally-complete flags (P1-2).
  8. Declared-ready that isn't computed-ready must shout. readiness_drift fires on a ready-tier hint contradicting NOT_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), dot SOURCE_NOT_USABLE (no retired DOTs — but event SOURCE_NOT_USABLE is live: 22 inactive events).
  • First v0.3 follow-ups: function signature precision, a real io/checker SSOT, a confirmed template source, and (only if scale demands) pg_trgm/vector as a two-step suggest→confirm radar that still never answers exists/missing/ready.