Ran the humanizer rules over 79 prose files in the active specs (brief, design, requirements, research and narrative docs). Removed em dashes, significance inflation, forced rule-of-three, bold-label list items and filler. Skipped tasks.md checklists and the retired specs (harnessx-*, openrouter-sast-harness, unseen-repo-evaluation). Every number, path, command, hash and identifier was preserved: checked each file against HEAD for drift in numbers, inline code and code fences, zero drift across all 79 files. Co-Authored-By: Claude Opus 4.8 <[email protected]>
31 KiB
Design Document
Overview
Purpose. Give OpenUltraSAST a second detection regime beside data flow: an operation that carries an obligation (a protected read or write, a privileged action, a security-relevant setting) is reported when an entry point reaches it and nothing on the way discharges the obligation (a path guard, an identity constraint bound from the authenticated context, an ownership check, a non-permissive value, validated input). The application's own policy says which obligations exist: the consistency of sibling handlers, and optionally a declared policy file.
Users. AppSec engineers get absence findings that name the missing discharger, the evidence for the obligation, and the known fix. Maintainers get a leave-one-out number per obligation kind and an improve lever that admits obligation shapes under the existing gate. Project owners may declare their policy in a small versioned file.
Impact. New semantic/obligations/ package (facts, shapes, siblings, declared policy, dominance protocol, checker). One more MAP proposer. Obligation rows in the existing mechanism store (second shape family). New harvest mode and one additive label field in the corpus. Reports, manifest and ranking gain obligation fields. No change to dispositions, the evidence ladder, taint, facts for flows, the call graph, or any gate.
Goals
- Absence pairs in
vibe-pyandagent-vfcmove from 0 detected to a measured leave-one-out baseline per obligation kind. - Every obligation finding states obligation, missing discharger, evidence (siblings or policy clause) and known fix.
- Works today in function-local mode; becomes path-aware when reachability lands; becomes nesting-aware when guard dominance lands, without caller changes.
- Policy is never authored by a model.
Non-Goals
- Flow taint, sink facts, walker, dispositions, ladder rungs.
- Call graph, summaries, path records, dominance IR (owned upstream).
- Sandbox proof of absence bugs.
- LLM-authored policy or shapes; any merge gate; vendoring unlicensed code.
Boundary Commitments
This Spec Owns
- Obligation facts data and loader (
obligations.toml,OperationFact,DischargerFact, closed kinds). - Obligation shape derivation, family tag, dedupe key, export into the mechanism store.
- Sibling sets, discharge detection per handler, consistency anomalies.
- Declared policy schema, loader, version hash, pending clauses.
- The
Dominanceprotocol and its order-based default implementation. - The obligation checker (path-aware and function-local), its findings, tags and labels.
- Obligation fields in markdown/SARIF, the manifest
obligationsblock, ranking weight for obligation findings. - Corpus:
handler_contextharvest mode,obligationlabel field, vocabulary additions, leave-one-out per obligation kind. - The
obligationslever (shape admit/retract, pending policy clauses) reusing the mechanisms lever machinery.
Out of Boundary
semantic/taint.py,semantic/facts.py(flow facts),semantic/cst.py,semantic/ir.py,overlay.pydispositions andALLOWED_EVIDENCE.semantic/callgraph.py,summaries.py,paths.py(reachability-flow-model); nesting-aware dominance (guard-dominance-regime).- Pair scorer rules beyond one additive detection rule for obligation-labeled rows; catalog schema beyond the
obligationfield. - Sink-shape variant search and its lever family (
corpus-seeded-mechanisms), reused unchanged. - Rule and CWE policy levers, ledger format.
Allowed Dependencies
FileIR,parse_file,named_ranges_from_ir,variants.source_kind_of_name(read),SemanticFactssources (read, for request-input provenance).EntryPointRecordfrommapping.analyze_entry_points(read).PathRecordfromsemantic/paths.pywhen present (read, optional).MechanismStore,append_from_pair-style writer,MechanismEdit,apply_mechanism_edits,StoreSnapshot,evaluate_mechanism_profiles,profile_regressions, journal.PairCase,load_pair_catalog, selectors,loo.score_pair_with_storepattern, harvest library.- Reports, manifest writer,
StaticFinding, rank.
Revalidation Triggers
- Any obligation finding above
suspicionwithout sandbox agreement. - A shape or policy clause authored by a model or written by the tool.
- Obligation kinds or discharger kinds outside the closed sets appearing in facts, shapes or policy.
Dominanceprotocol signature change (both upstream engine specs implement it).PathRecordshape change upstream (the checker's path consumer must be re-checked).- Consistency findings that demote or suppress anything.
Architecture
Existing Architecture Analysis
MAP already runs three proposers over one parse: inventory findings adjudicated by the overlay, the tool hunter, and variant search. Each produces StaticFindings at or below static_corroboration, merges with overlay records where a call site coincides, and is counted in the manifest. The entry-point mapper runs before MAP and yields routes with an access level. The mechanism store is append-only with deterministic ids and a folding loader; the improve loop has a bounded lever contract with byte-for-byte revert. This design adds a fourth proposer and a second shape family, following those patterns exactly; the only new concept is the Dominance protocol, introduced because the spec that owns real dominance is not written yet.
Architecture Pattern & Boundary Map
flowchart TB
subgraph facts[Closed data]
OF[obligations.toml: operation + discharger facts]
DP[declared policy file: resources, routes, identity source, roles]
end
subgraph corpus[Corpus]
PAIRS[trusted absence pairs, handler_context excerpts]
end
PAIRS --> DERIVE[derive_obligation: shape family obligation]
DERIVE --> STORE[mechanisms.jsonl rows with family=obligation]
OF --> OPS[find_operations over FileIR]
EP[EntryPointRecord: routes, access level] --> SIB[sibling sets]
OPS --> SIB
OF --> DIS[discharges per handler]
SIB --> CONS[consistency anomalies]
DP --> CHECK[obligation checker]
CONS --> CHECK
PATHS[PathRecord optional] --> DOM[Dominance protocol: OrderDominance default]
DOM --> CHECK
STORE --> CHECK
CHECK --> FIND[StaticFinding suspicion + obligation labels]
FIND --> REP[reports, manifest, rank]
PAIRS --> LOO[leave-one-out per obligation kind]
STORE -.-> LOO
LOO --> LEVER[obligations lever: admit/retract shapes, pending policy clauses]
LEVER --> STORE
Architecture Integration
- Selected pattern: closed facts plus learned shapes plus recovered policy, evaluated by a proposer that never claims proof.
- Domain boundaries: facts/shapes (semantic data), siblings/policy (application intent), checker (evaluation), reports/rank/lever (consumers). No component writes another's data except the lever writing the store through the existing contract.
- Existing patterns preserved: append-only store with deterministic ids; MAP proposer at
suspicion; lever with validator, snapshot, per-profile clause; leave-one-out as the objective. - New component rationale:
Dominanceprotocol decouples the checker from the two upstream engine specs so obligations are useful in function-local mode today. - Dependency direction: obligation facts → shapes → siblings/policy → checker → cli/reports/rank → loo → lever.
obligations/never imports the overlay, the scorer, orimprove.
Technology Stack
| Layer | Choice | Role | Notes |
|---|---|---|---|
| Facts | TOML data next to the flow facts | operation and discharger kinds | closed loader, separate file so facts.py stays untouched |
| Shapes | stdlib dataclasses over FileIR |
derive, key, dict round-trip | second family in the mechanism store |
| Policy | TOML file in the scanned project's calibration dir | declared resources and routes | closed schema, version hash in manifest |
| Evaluation | MAP proposer | findings at suspicion |
function-local without paths |
| Corpus | harvest mode + label field | teach and score absence pairs | additive |
| Improve | existing lever machinery | admit/retract, pending clauses | same gate |
File Structure Plan
src/openultrasast/
├── semantic/obligations/
│ ├── __init__.py # public surface: load_obligation_facts, derive_obligation, check_obligations, ObligationFinding
│ ├── facts.py # OperationFact, DischargerFact, OPERATION_KINDS, DISCHARGER_KINDS, PROVENANCE_KINDS, load_obligation_facts(dir)
│ ├── shapes.py # ObligationShape (family="obligation"), derive_obligation(vuln_ir, fixed_ir, label, facts, texts), classify_discharger
│ ├── operations.py # find_operations(ir, facts) -> Operation sites; find_discharges(ir, facts, ranges) -> Discharge witnesses
│ ├── siblings.py # SiblingSet grouping from EntryPointRecord + operations; consistency_anomalies(sets, min_siblings)
│ ├── policy.py # DeclaredPolicy schema + load_declared_policy(path) (closed; rejects unknown fields); version hash
│ ├── dominance.py # Dominance protocol; OrderDominance default (decorators, router middleware, statement order, hop order)
│ └── check.py # check_obligations(...) -> ObligationResult(findings, discharges, sibling_sets, degradations); findings_to_static
├── ruleset/obligations/{python,javascript,typescript}.toml # closed operation/discharger facts, beside (not inside) the flow facts dir that load_facts globs
├── cli.py # MAP: obligation check after variant search; manifest `obligations`; `mechanisms export` also writes obligation shapes
├── config.py # [obligations] enabled=true, min_siblings=3, policy_path=".openultrasast/obligations.toml"
├── reports.py # obligation lines in markdown, `obligation_*` SARIF properties
├── rank.py # obligation ranking weight: sensitivity x label, below any sandbox-proven finding
├── semantic/seed.py # pair_lessons yields obligation shapes for obligation-labeled rows (same exporter)
├── semantic/loo.py # score_pair_with_store handles family=obligation via check_obligations; per obligation kind grouping
├── improve/validator.py # validate_mechanism dispatches on shape family; PolicyClauseEdit (pending)
├── improve/evolve.py # propose_mechanism_edits covers obligation rows; pending clause journaling
├── pairs.py # ExpectedFinding.obligation (additive); obligation-labeled rows count an obligation finding inside the function as detection
benchmarks/pairs/
├── harvest.py # mode "handler_context": function + decorators + registration lines
├── mechanisms.toml # vocabulary additions: unconstrained_protected_read, unconstrained_protected_write, unguarded_privileged_action
tests/
├── test_obligation_facts.py, test_obligation_shapes.py, test_obligation_siblings.py, test_obligation_policy.py,
├── test_obligation_check.py (function-local + path-aware with a stub PathRecord), test_obligation_reports.py,
├── test_obligation_corpus.py (harvest mode, label, LOO per kind), test_obligation_lever.py
Modified Files
src/openultrasast/cli.py:_check_obligationsafter_search_variantsin MAP (standard/deep); manifestobligationsblock;mechanisms exportwrites obligation rows too.src/openultrasast/pairs.py:ExpectedFinding.obligation: str | None;_detections-side additive rule: for a row withobligation, a finding with tagobligation:<kind>inside the labeled function counts.src/openultrasast/semantic/mechanisms.py: no schema change;corpus_mechanisms()unchanged; a helperobligation_mechanisms(records)selectsshape["family"] == "obligation"(added inobligations/shapes.py, not in the store module).src/openultrasast/reports.py,rank.py,config.py,improve/*,semantic/seed.py,semantic/loo.py,benchmarks/pairs/harvest.py,benchmarks/pairs/mechanisms.tomlas listed above.
System Flows
Evaluation in MAP
flowchart LR
A[parsed files + EntryPointRecords] --> B[find_operations per handler]
B --> C[find_discharges per handler]
A --> D[sibling sets by resource x router]
C --> D
D --> E[consistency anomalies >= min_siblings]
P[PathRecords if present] --> F[Dominance.dominates witness, operation]
C --> F
G[declared policy if present] --> H[obligation checker]
E --> H
F --> H
H --> I[ObligationFinding: kind, discharger, provenance, evidence label, siblings/clause, known fix]
I --> J[StaticFinding suspicion; tags obligation:*, discharger:*, obligation_evidence:*]
Gating: the checker emits a finding only when (1) the operation is reached (from an entry point along a PathRecord, or, without paths, when the operation sits in a handler that is itself an entry point) and (2) no discharger of the required kind dominates, and (3) the obligation is justified by a sibling set (consistency_violation), a policy clause (declared_policy_violation) or, in degraded mode without either, by the operation fact alone (function_local). A declared public route suppresses path-guard obligations for that route only.
Teaching and measuring
flowchart LR
T[trusted absence pair] --> V[vuln + fixed FileIR, handler_context texts]
V --> W[operation in labeled function]
V --> X[discharger added on fixed side + provenance of bound value]
W --> Y[ObligationShape: operation kind, discharger kind, provenance, resource token class]
X --> Y
Y --> Z[store row family=obligation, deterministic id]
Z --> L[leave-one-out: check_obligations on held-out excerpts with the other pairs' shapes]
L --> M[per obligation kind: recall, silence, Youden; found_by]
Lever
Admit/retract of obligation shape rows follows the mechanisms lever exactly (validator dispatches on family; same snapshot, tombstone, per-profile clause via evaluate_mechanism_profiles extended to run the checker for obligation rows). A policy clause proposal (PolicyClauseEdit) is journaled as pending with its sibling evidence and is never read by a scan until a human copies it into the policy file; accepting is a human edit of the file, not a loop action.
Requirements Traceability
| Requirement | Summary | Components | Flows |
|---|---|---|---|
| 1.1–1.5 | closed operation/discharger facts, loader rejects unknown, no detection outside facts | ObligationFacts | — |
| 2.1–2.5 | shapes from trusted pairs, tiers, text-free, dedupe with provenance, skip reasons | ObligationShapes, Export | teaching |
| 3.1–3.6 | sibling sets, min count, route access evidence, never demote, fixed-twin leak rate | Siblings, Checker | evaluation |
| 4.1–4.5 | declared policy: load, reject unknown, declared violation, public routes, never written by the tool | DeclaredPolicy, Checker, Lever | evaluation, lever |
| 5.1–5.5 | path-aware, discharge recorded, function_local degraded mode, identity provenance, standard/deep only | Dominance, Checker, cli | evaluation |
| 6.1–6.5 | one label, reports, hunter intent, ranking weight, manifest block | Checker, reports, rank, IntentAdjudication | — |
| 7.1–7.5 | handler_context harvest, obligation label, vocabulary, LOO per kind, roadmap | harvest, pairs, mechanisms.toml, loo | teaching |
| 8.1–8.5 | obligations lever, closed validator, gate unchanged, pending clauses, journal | Lever | lever |
| 9.1–9.3 | offline tests, gates unchanged, extra-free green | tests | — |
Components and Interfaces
| Component | Domain | Intent | Req | Dependencies | Contract |
|---|---|---|---|---|---|
| ObligationFacts | semantic data | closed kinds and their call/attribute matches | 1 | TOML | State |
| ObligationShapes | semantic | derive, key, round-trip, export | 2, 7 | FileIR, facts, variants helpers, store | Service |
| Operations | semantic | operation sites and discharge witnesses in a file | 3, 5 | FileIR, facts | Service |
| Siblings | intent | group handlers, find anomalies | 3 | EntryPointRecord, Operations | Service |
| DeclaredPolicy | intent | load and validate policy, version | 4 | TOML | State |
| Dominance | evaluation | does a witness dominate an operation | 5 | PathRecord optional | Protocol |
| Checker | evaluation | findings with labels and evidence | 3–6 | all above | Service |
| IntentAdjudication | hunter | record intent on a finding | 6.3 | hunter client (optional) | Service |
| Corpus | corpus | harvest mode, label, LOO per kind | 7 | harvest lib, pairs, loo | Batch |
| Lever | improve | admit/retract shapes, pending clauses | 8 | validator, evolve, store | Batch |
ObligationFacts
OPERATION_KINDS = ("protected_read", "protected_write", "privileged_action", "security_setting")
DISCHARGER_KINDS = ("path_guard", "identity_constraint", "ownership_check", "non_permissive_value", "validated_input")
PROVENANCE_KINDS = ("authenticated_context", "request_input", "constant", "unknown")
@dataclass(frozen=True)
class OperationFact:
id: str
kind: str # OPERATION_KINDS
language: str
calls: tuple[str, ...] # trailing-name matches, same rule as sink facts
resource_arg: int | None # argument or receiver position that names the resource (model, table, path)
requires: tuple[str, ...] # DISCHARGER_KINDS that discharge this operation
sensitivity: str # low | medium | high (default weight for ranking)
@dataclass(frozen=True)
class DischargerFact:
id: str
kind: str # DISCHARGER_KINDS
language: str
calls: tuple[str, ...] = () # guard calls / decorators / setters
decorators: tuple[str, ...] = ()
identity_sources: tuple[str, ...] = () # text patterns bound from the authenticated context (request.user, g.user, token payload)
constraint_params: tuple[str, ...] = () # keyword or field names that constrain by identity (user, owner, user_id, tenant)
def load_obligation_facts(directory: Path | None = None) -> ObligationFacts: ...
Invariants: any kind outside the closed sets raises ObligationFactsError naming the entry; for_language(language) scoping like SemanticFacts.
ObligationShapes
@dataclass(frozen=True)
class ObligationShape:
language: str
operation_kind: str
discharger_kind: str
provenance: str # PROVENANCE_KINDS: what the fix binds the discharger to
resource_class: str # "owned" | "global" | "setting" (from the operation fact's resource_arg and the added constraint)
mechanism: str # closed vocabulary id from the label
family: str = "obligation"
def key(self) -> str: ... # text-free; family prefix so it never collides with sink shapes
def derive_obligation(vuln: FileIR, fixed: FileIR, *, function: str, obligation: str, mechanism: str, facts: ObligationFacts, flow_facts: SemanticFacts, vuln_text: str, fixed_text: str) -> ObligationShape | None: ...
def classify_discharger(vuln: FileIR, fixed: FileIR, *, function: str, facts: ObligationFacts, texts: tuple[str, str]) -> tuple[str, str] | None: ... # (discharger kind, provenance) added on the fixed side
Preconditions: both sides parse; the labeled function exists on both. Postconditions: None when no operation of a required kind is in the labeled function or no discharger was added; the shape carries no text. Stored through the existing writer with shape = to_dict(); id from key().
Operations and Siblings
@dataclass(frozen=True)
class Operation:
path: str; line: int; function: str; kind: str; fact_id: str; resource: str | None # resource token, identifier only
@dataclass(frozen=True)
class Discharge:
path: str; line: int | None; function: str; kind: str; fact_id: str; provenance: str; scope: str # scope: decorator | router | statement | hop
def find_operations(ir: FileIR, facts: ObligationFacts, ranges) -> tuple[Operation, ...]: ...
def find_discharges(ir: FileIR, facts: ObligationFacts, flow_facts: SemanticFacts, ranges, entry: EntryPointRecord | None) -> tuple[Discharge, ...]: ...
@dataclass(frozen=True)
class SiblingSet:
key: tuple[str, str] # (router or module, resource token)
handlers: tuple[str, ...] # "path::function"
discharging: dict[str, tuple[str, ...]] # discharger kind -> handlers that discharge it
def sibling_sets(entries: Sequence[EntryPointRecord], operations, discharges) -> tuple[SiblingSet, ...]: ...
def consistency_anomalies(sets, *, min_siblings: int) -> tuple[Anomaly, ...]: ... # handler, kind, siblings that discharge
Invariants: a set with fewer than min_siblings members yields no anomaly and is counted as under-populated; anomalies never touch existing findings.
DeclaredPolicy
# .openultrasast/obligations.toml (project-owned; the tool never writes it)
version = 1
identity_source = "request.user" # closed: one of the discharger facts' identity_sources or "token"
[[resource]]
name = "book" # resource token as seen in operations
sensitivity = "high" # low | medium | high
identity_field = "user_id" # constraint param that ties the resource to its owner
[[route]]
path = "/health"
access = "public" # public | authenticated | role
roles = []
load_declared_policy(path) -> DeclaredPolicy | None; unknown field or value → PolicyError naming it; the scan continues without a policy and records {"stage": "obligations", "reason": "policy_invalid", "detail"}. DeclaredPolicy.version_hash goes to the manifest.
Dominance
class Dominance(Protocol):
def dominates(self, witness: Discharge, operation: Operation, path: PathRecord | None) -> bool: ...
class OrderDominance:
"""Default: decorator and router-middleware witnesses dominate the handler; a statement witness dominates when it
precedes the operation in the same function and the function has an early exit between them or the witness is a
guard call; along a PathRecord, a witness on an earlier hop dominates."""
guard-dominance-regime provides a nesting-aware implementation behind the same protocol; the checker takes the implementation as a parameter.
Checker
@dataclass(frozen=True)
class ObligationFinding:
operation: Operation
missing: str # discharger kind
provenance: str | None # when an identity constraint exists but binds request input
label: str # consistency_violation | declared_policy_violation | function_local
evidence: tuple[str, ...] # sibling handler ids or the policy clause
known_fix: str | None # mechanism record id whose shape matches (operation kind, discharger kind)
intent: str | None = None # hunter adjudication: public | protected | unknown
@dataclass(frozen=True)
class ObligationResult:
findings: tuple[ObligationFinding, ...]
discharges: tuple[Discharge, ...]
sibling_sets: tuple[SiblingSet, ...]
degradations: tuple[dict[str, object], ...]
def check_obligations(*, irs, entries, facts, flow_facts, policy, paths, dominance, store_shapes, min_siblings) -> ObligationResult: ...
def findings_to_static(result) -> list[StaticFinding]: ... # id "obligation:<kind>:<path>:<line>:<missing>", evidence suspicion, tags
Rules: reached = a PathRecord ends at the operation, or (no paths) the operation's function is an entry point; label precedence declared > consistency > function_local; a declared public route suppresses path_guard obligations for that route; an operation with a matching store shape gains known_fix; a discharge that binds request_input where authenticated_context is required is reported with provenance = "request_input".
EntryPoints (registration-based)
mapping.analyze_entry_points names a handler from a decorator today. Real projects register handlers by call (add_url_rule(path, view_func=fn), api.add_resource(Cls, path), router.get(path, requireAuth, handler)) or by convention (Resource verb methods, Next.js route.ts exports). Because both the sibling grouping (Req 3.1) and the function-local reach test (Req 5.3) key on a named entry-point handler, an unnamed registration makes every obligation unreachable. The mapper gains those four forms; access classification, evidence, conditions and the record shape are unchanged, and middleware identifiers on a registration line are access evidence exactly as decorators are.
IntentAdjudication
When a hunter client is available (the same resolution as pairs --hunter), the checker asks one closed question per finding with a route: "Is this route meant to be public?" with the handler text and siblings as context; the answer is recorded as intent and in the rationale. It never changes the label or the evidence level; when the client is absent a degradation intent_adjudication_unavailable is recorded once.
Reports, manifest, rank
Markdown per finding: - Obligation: <kind> on <resource>, - Missing discharger: <kind> (<provenance>), - Evidence: <label> — <siblings | clause>, - Known fix: <guard shape> learned from <pairs>, - Intent: <adjudication>; a ## Obligations section lists sibling sets and the policy version. SARIF properties obligation_kind, obligation_missing, obligation_label, obligation_evidence, obligation_known_fix, obligation_intent. Manifest obligations = {sibling_sets, under_populated, operations, findings_by_label, policy_version | null, degradations, sets} (sets lists {module, resource, handlers} so the report can name them). Rank: ranking_priority = base(sensitivity) x weight(label) with declared > consistency > function_local, capped strictly below the lowest sandbox-proven finding in the run. Proof rungs live on CandidateVerdict, never on StaticFinding, so the cap takes the proven finding ids from the run's TRIGGERABLE verdicts: rank_obligations(findings, proven_ids=...) runs once at MAP and again after REGRESS when the sandbox proved anything.
Corpus and leave-one-out
Harvest mode handler_context: the labeled function plus its decorators and the registration statements that name it (router .get(...)/.post(...) calls, add_url_rule, middleware attachment) as one excerpt per side. Catalog row obligation = "<OPERATION_KIND>" (additive); vocabulary gains unconstrained_protected_read, unconstrained_protected_write, unguarded_privileged_action (existing missing_auth_guard, identity_from_request_body, permissive_default stay). loo.score_pair_with_store runs check_obligations on both sides for obligation rows; grouping adds per_obligation_kind; found_by names the shape record that supplied known_fix.
Lever
validate_mechanism dispatches on shape["family"]: obligation rows check operation_kind, discharger_kind, provenance, resource_class against the closed sets. PolicyClauseEdit(kind="resource"|"route", fields, evidence, rationale) is validated against the policy schema, journaled under pending_policy_clauses, and never applied by the loop. propose_mechanism_edits treats obligation rows like sink rows (admit when a trial store recovers a missed holdout absence pair without leaking; retract leakers); evaluate_mechanism_profiles runs the checker for obligation rows.
Data Models
- Store row: existing
Mechanismwithshape = {family: "obligation", language, operation_kind, discharger_kind, provenance, resource_class, mechanism};guardfield carriesdischarger_kindfor report continuity. - Facts:
obligations.tomlwith[[operation]]and[[discharger]]tables per language;version. - Policy: as above;
version_hash = sha1(normalized file). - Manifest:
obligationsblock;loo.jsongainsper_obligation_kind. - Journal:
pending_policy_clauses: [{clause, evidence, round, rationale}].
Error Handling
| Condition | Response |
|---|---|
| Facts entry outside closed sets | ObligationFactsError naming the entry; loader fails loud (data bug) |
| Policy file invalid | policy_invalid degradation; scan continues without policy |
| No path records | function-local mode; function_local labels; degradation per language once |
| Language without grammar | skipped with obligations_language_unsupported |
| Sibling set under minimum | counted under-populated, no finding |
| Hunter unavailable | intent_adjudication_unavailable, findings unchanged |
| Store row with malformed obligation shape | skipped with obligations_store_row_invalid |
Testing Strategy
- Unit: facts loader rejects an unknown kind (1.4);
derive_obligationon the vampi pair yieldsprotected_read/identity_constraint/authenticated_contextand a text-free key (2.1, 2.3); a pair whose fix adds no discharger is skipped with a reason (2.5);sibling_setsgroups three handlers on/booksandconsistency_anomaliesflags the unconstrained one with the two siblings as evidence, and stays silent undermin_siblings(3.2, 3.3); a public route among authenticated siblings is a missingpath_guardwith the access classification as evidence (3.4); policy loader rejects an unknown field and a declared public route suppresses the guard obligation but not the identity one (4.2, 4.4);OrderDominancewith a stubPathRecordaccepts a guard on an earlier hop and rejects one after the operation (5.1, 5.2); no paths →function_localplus degradation (5.3); identity bound fromrequest.args→provenance = request_input(5.4). - Integration: standard scan on a fixture with three handlers emits one
obligation:finding atsuspicionwith tags and report lines, manifest block present, absent in quick (5.5, 6.1, 6.2, 6.5); ranking places it below a sandbox-proven finding (6.4). - Corpus:
handler_contextexcerpt keeps decorators and registration (7.1); an obligation-labeled catalog row counts an obligation finding inside the function (7.2); LOO on a three-pair toy slice reports per obligation kind andfound_by(7.4). - Lever: admission of an obligation shape that recovers a holdout absence pair is accepted; one that fires on a fixed twin is rejected with byte-identical store; a
PolicyClauseEditlands inpending_policy_clausesand no scan reads it (8.3, 8.4). - Gates: detection, map and local pair gates unchanged; extra-free suite green with semantic tests skipping (9.2, 9.3).
Security Considerations
Shapes and policy carry identifiers only; the policy file is project-owned and never written by the tool; the hunter receives redacted handler text and can only annotate. Obligation findings never execute anything and never reach the sandbox candidate set.
Performance
Operations and discharges are found in the same parse the overlay uses; sibling grouping is linear in handlers; the checker is O(operations x witnesses) per handler and O(paths) with path records; bounded by the MAP file budget.
Migration Strategy
- Facts, shapes, exporter and vocabulary; measure how many absence pairs teach.
- Operations, discharges, siblings, policy, order dominance, checker in function-local mode; MAP wiring behind
[obligations].enabled; reports and manifest. - Corpus harvest mode and label; leave-one-out per kind; roadmap baseline.
- Lever and pending clauses.
- When
reachability-flow-modellands: passPathRecords to the checker (no interface change). Whenguard-dominance-regimelands: swap theDominanceimplementation.