Prompt-integrity pack: Verified Completions checks a system prompt against its runtime - #117
Conversation
…ainst its runtime consistency-core asks whether a conversation's facts can all be true at once; this extension asks whether a system prompt and the runtime it is bound to can both be right at once. The COVERAGE-AUDIT excludes world-falsity without a ground-truth KG - for a bound agent prompt the world IS one: the tool registry, data schema, output contract and policy invariants are finite and machine- readable, and they load as trusted EDB facts through the same mechanism as the ontology seeds. Prompt text can never write them. One ontology, deliberately: tool calling is new fact layers (WORLD / PROMPT directives / EXAMPLE origin) in the same knowledge graph, not a second pack - example_over_cap needs the deontic window and the example fact in one place, and core's D3 clash_window fires unchanged on windows this pack's extractor writes (the fixture shows the composition). No clauses are added to core's derived relations (issue #91); the pack ships pi_finding / pi_finding_src alongside finding / finding_src. Eighteen finding kinds over ten rule families: unknown tool/arg/enum/field, action misrouting, contract drift, missing-required (the closed-world absence class - a deleted mandatory rule has no wrong line to point at), invariant polarity through a subsumes taxonomy (guardrail weakening becomes a hard clash; conditional permits fire because conditions are satisfiable), deontic clashes core's string-equal D1 cannot see, five example-origin rules, merchant carve-outs, and a soft vocabulary fence. Every finding carries verbatim spans; the fixture demonstrates provenance, proof trees, and repair semantics (binding the phantom tool retracts exactly that finding). Engine-validated: examples/iql/43_prompt_integrity/ snapshot (enforced by the e2e suite), tests/ fixture kept in sync, IQL lint clean, packs 42+43 pass with no regression. Extraction contract: prompt-directives-prompt.md + directive-schema.json - deterministic lexers first, deontic triples with conditions as data (the fact-lifecycle prompt skips those on purpose; a prompt linter cannot), and a per-line coverage ledger so an extraction miss surfaces as a finding instead of a silent pass. Motivating corpus: rezolved/prompt-corruption-detection - 99 mined corruptions where the strongest LLM auditor reaches 88/99 probabilistically; the target here is 99/99 typed and span-cited, proven per pair verify_each-style.
Driving the full 99-pair corrupted-prompt corpus through the real server surfaced two gaps a fixture alone had not: contract_drift_value - contract constants are not all numeric. "Refer to products by their wash" against a render contract keyed on sku is drift with no integer in sight; prompt_value/contract_value carry string constants and != is type-generic. example_forbidden_prop - a demonstrated reply can violate policy through CONTENT the parser can only judge with the catalog in hand: a sku the catalog says is OUT_OF_STOCK recommended as available, a kids item offered to an adult, a total struck across two currencies, a size read across systems. The extractor joins the catalog offline and emits a reply property; one generic rule maps properties to forbidden actions through prop_violates_inv, which is world data - so the property vocabulary grows without touching a rule. The fixture exercises both (contract_drift_value on the c6-09 shape, example_forbidden_prop on the c3-07 shape); snapshot 42+43 pass together. Interim numbers from the live run, deterministic extraction only - no LLM in the loop: 41/99 corrupted prompts flagged, 30 with the exact span, 0 false findings on the clean prompt. The semantic tail (guardrail weakening, subsumption clashes) rides the directive extractor and is measured next.
…cation demanded
The important one is a correction the taxonomy exposed the moment a subsumes
edge landed under a general action: forbids lifted UP the closure, turning
"never recommend out-of-stock" into "never recommend" and clashing with a
clean prompt's own require. Doing a sub-action is doing its super, so require
and permit lift up; forbidding a super forbids every sub, so forbid propagates
down. forbids_up is now forbids_down and the false clash is gone - the fixture
keeps the true one (ask_clarify < question_opening).
Four additions, each forced by a real corpus pair the engine missed live:
invariant_optional - making a required action optional IS the violation
("search is optional for staple items"); exemption of an inv_require, no
forbid in sight.
required_exempt - the prompt's own require, exempted elsewhere in the same
prompt ("ask which they want" ... "take the gender from their profile rather
than asking"); permit does not clash with require, a targeted exemption does.
roster_understated - "the tools available are X and Y" where every named tool
is real and the LIE is the exhaustive list; fires once per hidden tool.
merchant_carveout generalised - in a prompt serving every merchant, ANY
brand-scoped rule directive is a carve-out; the inv_global gate made the rule
a self-licking loop once extractors fabricated the gating action.
All engine-validated: fixture rows for each, snapshots 42+43 pass together.
Live-run status on the 99-pair corpus at this pack version: 99/99 flagged,
98/99 span-exact (the one exception is a deleted rule - text absent from the
file cannot be quoted), 0 hard findings on the clean prompt.
|
Closing the loop with the measured result this pack was built for. The full 99-pair corrupted-prompt corpus, driven through the live engine (server + client from this tree, both packs loaded, one KG per prompt, the deployment's tool registry / catalog contract / policy invariants inserted as trusted EDB facts): 99/99 corrupted prompts flagged, 99/99 with the exact span, 0 hard findings on the clean prompt — verified per sample (extraction → engine → capture check → exact-span check before advancing), then independently re-audited from disk: every cited span verbatim in its file, every localisation recomputed, every verdict from the engine's own reporting views. 21 of the pack's finding kinds carried the corpus; ~60 of 99 detections used purely deterministic extraction. For comparison, the strongest LLM auditor measured on the identical corpus and metric (claude-fable-5) flags 88/99 and names the line 86/99, probabilistically, at ~$0.08/prompt. The per-sample loop drove every pack change in this PR's later commits — the deontic direction fix most importantly (forbids must propagate down the subsumption closure, not up; the false clash it caused on a clean prompt is the kind of thing only a live corpus finds). Ledger and harness live in rezolved/prompt-corruption-detection PR #8 ( Caveats attached to the number, per the design doc: per-corpus proof — the same epistemic status as this repo's 1,526/1,526 — not a universal guarantee; the semantic tail rides an LLM extraction under the verbatim-span ingestion gate; precision claims wait on the corpus's neutral arm. |
What this is
An extension to Verified Completions for the class of failure consistency-core cannot see by design: a system prompt that contradicts not its own text but the runtime it is bound to — a tool nobody bound, an argument the schema lacks, a cap the contract outgrew, an example that breaks the rule above it, a clause that licenses what policy forbids. The COVERAGE-AUDIT excludes "world-falsity without a ground-truth KG"; for a bound agent prompt, the world is a ground-truth KG — finite, machine-readable, versioned. This pack loads it.
Motivating case: rezolved/prompt-corruption-detection — 99 corruptions of a retail-agent prompt mined from production history. The strongest LLM auditor measured there flags 88/99, probabilistically, at ~$0.08/prompt. The target for this pack on the same corpus: 99/99, typed findings, verbatim spans, proven per pair the way this repo proves its 1,526/1,526.
One ontology, deliberately
Tool calling does not get a second ontology. Rules quantify over data, so tool calling arrives as three new fact layers in the same knowledge graph:
tool,tool_arg,arg_enum,field,lexicon,contract_*,inv_require/forbid/global,action_of,subsumesdirective(modality, action, cond)with conditions as data, lexicalrefs, purposeroutes; numeric windows go into core's ownconstraint_numTwo packs would duplicate the modality gate and provenance plumbing and then be unable to join across the seam —
example_over_capneeds the deontic window and the example fact in one graph, and the fixture shows core's D3clash_windowfiring unchanged on windows this pack's extractor writes. One graph, one trust boundary, two rule files. Nothing is added to core's derived relations (issue #91); the pack shipspi_finding/pi_finding_srcalongsidefinding/finding_src.Eighteen finding kinds, ten rule families
unknown_tool/unknown_arg/enum_violation/unknown_fieldaction_misroutecontract_driftmissing_requiredinvariant_permit/require/forbidsubsumestaxonomy; conditional permits fire because conditions are satisfiabledeontic_clashask_clarify⊑question_opening)example_over_cap/_under_floor/_phantom_tool/_bad_format/_skips_searchmerchant_carveoutforeign_term(soft, advisory forever)Every finding carries
(msg, surface)under the ingestion gate's verbatim guarantee — localisation is exact by construction.Validated on the engine
examples/iql/43_prompt_integrity/snapshot: a mini retail world + facts drawn from 15 real corpus pairs; 16 hard findings with exact ids, the core-composition row, the soft advisory, provenance spans, proof trees, and repair semantics (binding the phantom tool retracts exactly that finding). Enforced by the e2e suite; packs 42+43 pass together, no regression.tests/prompt-integrity-fixture.iql[.out]kept in sync; IQL lint clean on pack and fixture.eq0/coentitypatterns, comparisons ride int mirrors.Extraction contract (
extraction/prompt-directives-prompt.md+directive-schema.json)Deterministic lexers first (identifier tokens, numeric caps, example blocks — ~60 of the 99 need no LLM at all); the LLM pass extracts deontic triples with conditions as data (the fact-lifecycle prompt skips conditional policies on purpose; a prompt linter cannot) and actions from a shipped taxonomy extendable as data. A per-line coverage ledger (
extracted | inert | unverified) turns extraction misses into visible findings instead of silent passes — that is what makes a 100%-on-corpus claim honest.Not in this PR
Gateway wiring (
/v1/verifyfits as-is: load both packs, insert the deployment's world facts, query both finding views), the world-fact generator for the retail corpus (lives in the corpus repo), and the per-pair ledger run over all 99 — that lands corpus-side once this merges.