docs(spec) access-control-prover: abstract-interpretation amendment -- evidence algebra, stable symbols, label-blind acquisition, admission discipline

This commit is contained in:
Marius Ciepluch
2026-10-29 13:31:40 +01:00
parent 152f405bcf
commit 0d2b4cf15d
6 changed files with 1007 additions and 991 deletions
+360 -416
View File
@@ -1,474 +1,418 @@
# Design Document
## Overview
**Purpose**: a deterministic prover for the access-control family: given a change checked out at head, find
every protected operation reachable from an entry point without a check dominating the path, and say what is
unchecked. **Users**: the maintainer, scoring it as an arm on the training pool; the pre-push hook and the proof
arm, consuming its findings. **Impact**: adds Java and framework entry recognition to the mapper, receiver- and signature-precise call
resolution in the obligations package `[2026-10-29]`, a path builder and enforcement witnesses in the
obligations package, a prover over the hunter's per-change context, and a runner that scores it with the
valid-benchmark metrics.
The access-control prover remains a deterministic rule arm over the existing obligation checker. The amendment
changes the semantic contract: resolution, resource matching, authority matching and dominance return abstract
relations rather than booleans, and every examined path receives one of three judgments: `unguarded`,
`discharged`, or `indeterminate`. A degraded instrument may reduce precision to `indeterminate`; it cannot
manufacture either final verdict.
Target knowledge remains outside the engine. Entry shapes live in registered recognition strategies, while
operations, dischargers, resource rules and authority implications live in promoted fact data. A label-blind
initialization job produces that knowledge, and evaluation freezes its digest before admission labels are opened.
### Goals
- A finding names an entry point, the hops, the operation and the missing check; nothing else is a finding.
- Scored on the pool: recall on 23 fixes and 93 locations, false alarms per 100 ordinary pushes, seconds per change.
- Resolution precision `[2026-10-29]`: a guard discharges only the operation whose resource it binds, and a
call resolves to the class and signature it targets; an operation declared on a base class attaches only to
the classes that extend it.
- Coverage stated per repository: what was recognised, reached, checked, and left unchecked by reason.
- Produce an unguarded finding only from a complete, sufficiently resolved evidence path.
- Make call, path, resource, authority and control-flow uncertainty explicit and monotone.
- Carry target knowledge as verified data or recogniser strategies, never engine branches.
- Attribute development gains across analyzer and fact changes before measuring an untouched admission population.
- Fail the runtime and instrument gates rather than summarize partial work as a result.
### Non-Goals
- Taint flow, the Joern engine, nesting-aware dominance, families other than access control.
- Model calls inside the prover; intent adjudication stays a separate arm.
- The unseen pilot measurement and the pre-push wiring (later increments of other specs).
- Taint analysis, Joern integration, model calls inside the prover, or vulnerability families beyond access control.
- Client-side presentation gating, proof execution, pre-push wiring, or admission-policy thresholds.
- Replacing the existing order-based checker mode for consumers that do not opt into prover semantics.
## Boundary Commitments
### This Spec Owns
- Entry recognisers for Stapler, Jetty handlers, Symfony attributes, Livewire and Laravel controllers, Remix
exports, Tornado handlers, and programmatic servlet/handler registration sites `[2026-10-29]`
(`mapping.py` blocks, each with evidence).
- `semantic/obligations/resolution.py`: the class table and receiver- and signature-qualified call
resolution `[2026-10-29]`.
- The optional `receiver` field of `semantic/ir.py`'s `CallSite` and its capture in `semantic/cst.py`
(additive, default `None`; every existing consumer unchanged) `[2026-10-29]`.
- `semantic/obligations/paths.py`: `PathRecord` and `compose_paths` over the caller index.
- `semantic/obligations/enforcement.py`: router, decorator, voter `[2026-10-29]` witnesses from route files
and attributes, each carrying the resource it binds `[2026-10-29]`.
- `hunt/prover.py`: `prove_change` and `Coverage`.
- `benchmarks/valid/prover_arm.py`: the runner and its summary.
- Operation and discharger rows added to `ruleset/obligations/{java,php,python,typescript}.toml`.
- The abstract evidence domain and three-way pair/path judgment.
- Stable program identities, conservative call-target sets and bounded interprocedural summaries.
- Resource- and authority-sensitive operation/witness matching.
- Prover-mode control-flow dominance without textual-order fallback.
- Acquired entry, operation, discharger and authority knowledge needed by this family.
- Deterministic traces, failure-visible coverage, development arms and the admission-run contract.
### Out of Boundary
- The facts loader's existing fields, `OrderDominance`, and the checker's existing decision rules (owned by
`authorization-obligations`). Three extensions are allowed and owned here: the handler-named operation fields
(`handlers`, `framework`) in the facts; the `assume_entries_protected` keyword and the `unguarded_path` label
(see the checker block below); and, `[2026-10-29]`, the optional `resource` field on `Discharge` with the
`resource_bound` keyword and the all-kind caller-hop re-judgement in `_on_caller_hop` (see the checker block
below). Each is additive or default-off; every existing checker test unchanged.
- The feature table, the DSPy and hunter arms, the ablation (`hunter-learning-loop`).
- The proof arm's demonstration logic (`search-with-proof`); this spec only shapes the finding.
- The legacy checker behavior used by existing callers; default APIs remain backward compatible.
- Target labels and benchmark curation, owned by `valid-detection-benchmark`.
- Runtime demonstrations, owned by `search-with-proof`.
- Hook admission and rollout, owned by `pre-push-safety-net`.
- Continuous fact evolution after the one-time initialization pass.
### Allowed Dependencies
- `openultrasast.source` (callers_of, code_index, product_files, generated_reason), `openultrasast.mapping`,
`openultrasast.semantic.cst` and `ir`, `openultrasast.semantic.obligations` (check, facts, dominance),
`openultrasast.hunt.producers.Context`, `benchmarks.valid.metrics`, `benchmarks.unseen.score`.
- `openultrasast.source`, `openultrasast.mapping`, the existing semantic IR and CFG, and the existing obligation
facts/checker contracts.
- The hunter `Context` caches, valid-benchmark metrics, replay record envelopes and reserved-population guard.
- No new third-party dependency.
### Revalidation Triggers
- A change to `EntryPointRecord` fields or `Discharge` scopes.
- A change to `CallSite` fields or the caller-index keying `[2026-10-29]`.
- A change to the replay record shape `benchmarks.valid.metrics` scores.
- A change to the hunt defect shape (`defect_from`).
- A change to `EntryPointRecord`, `Operation`, `Discharge`, `Trace`, hunt-defect or replay-record contracts.
- A change to call-site receiver/signature capture, target-knowledge schemas or authority implication semantics.
- A change to path aggregation, judgment ordering, admission population isolation or the 120-second budget.
## Architecture
### Existing Architecture Analysis
The checker already judges reached operations by path and witnesses by scope; the hunter's `Context` already
caches everything the prover reads. The missing pieces are the inputs: entries for Java and the pool's PHP,
TypeScript and Python frameworks; paths; witnesses from outside the function; operation rows for the pool's
protected calls. The prover is wiring plus those inputs.
The delivered baseline already recognises entries, composes bounded paths, discovers operations and witnesses,
builds structural CFGs, emits traces and scores replay records. Its decision interfaces are boolean: unresolved
receivers fall back to a bare name, unavailable CFGs fall back to textual order, and a witness either discharges or
does not. The amendment preserves the existing checker as the single verdict owner while inserting typed abstract
values before that boolean boundary and adding an opt-in three-way judgment path.
### Architecture Pattern & Boundary Map
```mermaid
~~~mermaid
flowchart LR
C[hunt.producers.Context\nproduct files, texts, IR, call index, entry points] --> P[hunt.prover.prove_change]
M[mapping.analyze_entry_points\n+ Stapler, Jetty, Symfony, Livewire, Remix, Tornado\n+ programmatic registration] --> C
C --> RES[obligations.resolution\nclass table, receiver-qualified calls]
RES --> P
P --> PA[obligations.paths.compose_paths]
P --> EN[obligations.enforcement.witnesses\nrouter, decorator, voter, resource-bound]
PA --> CK[obligations.check.check_obligations]
EN --> CK
RES --> CK
F[ruleset/obligations/*.toml\noperations, dischargers] --> CK
CK --> P
P --> R[benchmarks/valid/prover_arm.py\nrecords, metrics, coverage, anchor refusal]
P --> H[pre-push admission / proof arm\nhunt defects]
```
- Selected pattern: inputs assembled in the prover, decision in the existing checker.
- Existing patterns preserved: closed facts in TOML with checked headers; `_entry` recogniser blocks; per-change
`Context`; replay record shape; counts and digests in records.
- Steering compliance: no model call, instrument gate (bytes read), plain English, standard metrics.
TK[Target knowledge] --> MR[Mapping registry]
TK --> FI[Fact interpreter]
MR --> CTX[Per change context]
FI --> OP[Operations and witnesses]
CTX --> SI[Symbol index]
SI --> CR[Call resolution]
CR --> FS[Function summaries]
OP --> FS
FS --> PM[Concrete path materializer]
CTX --> CFG[Control flow relations]
PM --> J[Three way judgment]
CFG --> J
OP --> J
J --> TR[Canonical traces]
J --> PR[Prover findings and coverage]
TR --> RUN[Development and admission runners]
PR --> RUN
~~~
Dependency direction is:
`abstract domain -> fact and recogniser adapters -> symbol resolution -> summaries and CFG -> checker judgment
-> prover integration -> runners`. Engine modules never import target-named strategy modules. Target-aware
adapters may import engine contracts.
- **Strategy + Registry**: entry recognisers are registered per recognition concern.
- **Interpreter**: promoted fact rows describe operations, dischargers, resource rules and authority implications.
- **Adapter**: the prover converts repository IR, target knowledge and context caches into engine contracts.
- **Abstract interpretation**: finite domains and monotone joins propagate evidence and incompleteness.
### Technology Stack
| Layer | Choice | Role | Notes |
|---|---|---|---|
| Analysis | Python 3.11, tree-sitter IR, regex caller index | entries, paths, operations | existing |
| Facts | TOML under `ruleset/obligations/` | operations, dischargers | existing loader |
| Scoring | `benchmarks.valid.metrics`, `benchmarks.unseen.score` | recall, false alarms per 100 | existing |
| Layer | Choice | Role |
|---|---|---|
| Analysis | Python 3.11, existing semantic IR and structural CFG | symbols, calls, summaries, dominance |
| Knowledge | Existing TOML fact files and mapping strategies | target-specific vocabulary |
| Scoring | Existing valid-benchmark metrics and replay records | development and admission results |
## File Structure Plan
```
~~~text
src/openultrasast/
├── mapping/ # recogniser registry [2026-10-29]: one strategy module per
│ │ # recognition concern, each registering itself in one table
│ ├── registry.py # the registry the language dispatch consults
│ ├── registration_sites.py # call-site handler registration strategy (data-driven vocabulary)
│ └── stapler.py, jetty.py, symfony.py, livewire.py, remix.py, tornado.py
│ # strategy modules migrating from mapping.py's legacy blocks
├── mapping.py # dispatch consults the registry during migration; blocks retire
│ # family by family (breadth map tracks the order)
├── mapping/
│ ├── registry.py # strategy registration and dispatch
│ ├── registration_sites.py # generic call-site registration adapter
│ └── <target>.py # one target-aware strategy per migrated concern
├── semantic/
│ ├── cst.py # + capture call receiver text in _call [2026-10-29]
│ ├── ir.py # + CallSite.receiver: str | None [2026-10-29]
│ ├── ir.py # CallSite receiver and signature evidence
│ ├── cst.py # receiver, arity and signature capture
│ └── obligations/
│ ├── resolution.py # ClassRecord, class_table, qualify — receiver/signature resolution [2026-10-29]
│ ├── paths.py # PathRecord, compose_paths(callers, entries, targets, depth, fan_out)
│ └── enforcement.py # witnesses(root, texts, entries, facts) -> list[Discharge]
├── hunt/prover.py # prove_change(root, change, ctx) -> ProverResult(findings, coverage)
└── ruleset/obligations/{java,php,python,typescript}.toml # + operation and discharger rows, checked headers
src/openultrasast/tools/fact_init.py # the one-time initialization job: extract + propose -> verify -> promote [2026-10-29]
benchmarks/valid/prover_arm.py # run / summarize; records per change; coverage table; anchor refusal
tests/test_mapping_registry.py, tests/test_mapping_registration_sites.py, tests/test_obligation_resolution.py,
tests/test_obligation_paths.py, tests/test_obligation_enforcement.py, tests/test_hunt_prover.py,
tests/test_prover_arm.py, tests/test_fact_init.py
```
│ ├── abstract_domain.py # relations, completeness, resources, authorities, judgments
│ ├── resolution.py # SymbolId, symbol index and CallTargetSet
│ ├── summaries.py # monotone FunctionSummary computation over call components
│ ├── paths.py # concrete evidence paths and cutoff records
│ ├── operations.py # operation/witness abstract identities
│ ├── dominance.py # legacy order mode, unchanged by default
│ ├── cfg_dominance.py # prover-mode control-flow relation, no order fallback
│ ├── check.py # opt-in abstract matching and PairJudgment
│ ├── enforcement.py # target-aware enforcement adapter
│ └── trace.py # canonical trace with abstract relations and completeness
├── hunt/
│ ├── producers.py # cached symbol index and summaries
│ └── prover.py # integration, path aggregation, findings and coverage
└── tools/
└── fact_init.py # label-blind extract/propose/verify/promote workflow
ruleset/obligations/*.toml # promoted operation, witness and authority knowledge
benchmarks/valid/
├── prover_arm.py # records, stage funnel, gates and four-arm comparison
└── prover_admission.py # frozen-manifest admission orchestration
tests/
├── test_obligation_abstract_domain.py
├── test_obligation_resolution.py
├── test_obligation_summaries.py
├── test_obligation_judgment.py
├── test_obligation_trace.py
├── test_mapping_registry.py
├── test_fact_init.py
├── test_hunt_prover.py
└── test_prover_arm.py
~~~
### Modified Files
- `src/openultrasast/mapping.py`: dispatch consults the `mapping/` registry (Strategy pattern); legacy blocks
migrate into strategy modules family by family; `EntryPointRecord` unchanged.
- `src/openultrasast/hunt/producers.py`: `Context` exposes `entry_points` and `ir(path)` already; add nothing
unless a cache is missing (then a `cached_property`, same pattern).
- `src/openultrasast/ruleset/obligations/*.toml`: rows only, admitted through the fact gate.
- Existing facts, IR, checker, dominance, trace, prover and arm modules receive additive or opt-in contracts.
- Existing callers retain legacy defaults; only the prover selects three-way judgment and strict CFG semantics.
- Existing completed benchmark records remain immutable development baselines.
## System Flows
```mermaid
sequenceDiagram
participant R as prover_arm
participant X as Context
participant P as prove_change
participant PA as compose_paths
participant EN as enforcement
participant CK as check_obligations
R->>X: checkout head, Context(root, change)
R->>P: prove_change(root, change, ctx)
P->>X: entry_points, call_index, texts, ir(path) per touched file
P->>PA: targets = touched functions + callees holding operations
PA-->>P: PathRecords (hops entry..operation function), unreached list
P->>EN: route files, attributes, entries
EN-->>P: router/decorator Discharges
P->>CK: irs of touched + hop files, entries, facts, paths, OrderDominance(texts), extra_witnesses
CK-->>P: ObligationResult (findings, discharges, degradations)
P-->>R: ProverResult(findings as hunt defects with entry, hops, operation, missing; coverage)
R->>R: record per change; metrics.summarize; coverage table
```
Gating: a file that fails to parse, a language without facts, or an unrecognised framework yields coverage
counts and no finding. A declared-public route suppresses `path_guard` only.
### Analysis
~~~mermaid
flowchart TD
A[Read cached source] --> B[Recognise entries and facts]
B --> C[Build symbol index]
C --> D[Resolve call target sets]
D --> E[Compute function summaries]
E --> F[Materialise evidence paths]
F --> G[Match witnesses to obligations]
G --> H[Evaluate control flow]
H --> I{Path state}
I -->|complete and missing| U[Unguarded]
I -->|complete and governed| DCH[Discharged]
I -->|possible unknown or cut| IND[Indeterminate]
U --> FIND[Finding and trace]
DCH --> TRACE[Trace only]
IND --> TRACE
~~~
### Evaluation
~~~mermaid
flowchart LR
ACQ[Label blind acquisition] --> FREEZE[Freeze candidate bundle]
BASE[Materialize baseline bundle] --> DEV[Four arm development comparison]
FREEZE --> DEV
DEV --> AUDIT[Review ordinary labels]
AUDIT --> HOLD[Untouched paired admission run]
HOLD --> GATE{Instrument budget and quality gates}
GATE -->|pass| GO[G1 eligible]
GATE -->|fail| NOGO[No go]
~~~
## Requirements Traceability
| Requirement | Components |
|---|---|
| 1.1-1.8 | mapping recognisers |
| 1.9 | mapping `_jetty_registration_entries` + `resolution.class_table` |
| 2.1-2.8 | enforcement.witnesses, facts rows, checker (existing) |
| 2.9 | facts rows (handler-named webhook and data-exposure patterns), `handlers_of` |
| 2.10 | enforcement resource tokens and voter witnesses; checker `resource_bound` |
| 3.1-3.6 | paths.compose_paths |
| 3.7 | hunt.prover target enumeration (down-call budget) |
| 3.8 | trace `guards_before_call` + checker `_on_caller_hop` all-kind re-judgement |
| 3.9 | resolution.qualify, class_table, `CallSite.receiver` |
| 3.10 | checker `_on_caller_hop` + prover witness assembly |
| 4.1-4.6 | hunt.prover.prove_change |
| 5.1-5.5 | prover_arm |
| 5.6 | prover_arm anchor refusal |
| 6.1-6.2 | prover_arm timing, Context caches |
| 7.1-7.2 | Coverage in prove_change, prover_arm summary |
| 7.3 | prove_change error reasons |
| 8.1-8.2 | finding shape in prove_change |
| 9.1-9.6 | trace serialisation in prove_change (obligations.trace) |
| 10.1-10.7 | cfg.FunctionCfg, CfgDominance |
## Pattern Application and Fact Acquisition `[2026-10-29]`
The implementation follows the steering pattern discipline (`code-standards.md`): the engine stays
target-agnostic; per-target vocabulary is data. Concretely for this spec:
- **Strategy + Registry** for entry recognisers: the recognisers this spec adds (registration sites, and the
migration of existing framework blocks) live as strategy modules in a `mapping/` registry -- one module per
recognition concern, each naming its target family in the filename, registering itself in one table. The
dispatch in `mapping.py` consults the registry; it does not name frameworks.
- **Interpreter** for obligation facts: operation rows, discharger rows, handler-name families and
registration-site vocabulary are TOML data interpreted by the engine; adding a target's vocabulary is a data
change reviewable in one place.
- **Adapter** at the engine seams: `hunt/prover.py`'s callers adapter binds the IR to the caller index
(receiver-qualified through `resolution`); `obligations/enforcement.py` adapts framework constructs into
`Discharge` witnesses and is target-aware territory -- engine modules never import it.
### Fact acquisition (the one-time initialization pass)
Per-target vocabulary is produced by a one-time initialization job, not by hand-specced enumerations. The job
has two modes and one gate:
1. **Extraction mode**: walks the repository's existing recogniser code and framework knowledge (the legacy
`mapping.py` blocks, the current TOML rows) and extracts the target vocabulary already embedded there into
the acquirable structures (row families, strategy registrations).
2. **Proposal mode**: an LLM pass (or the maintainer) proposes the vocabulary a new target needs -- entry
shapes, registration sites, handler-name families, policy-decision constructs -- against the target's
repository.
3. **The gate**: every extracted or proposed row passes the same header discipline the facts files already
enforce (source, version, region) and engine verification admits each row (the row must match what the
code says); only then is it promoted to a closed row. Nothing skips the gate.
The initialization job is a pipeline tool in this spec's scope (it produces the data the engine consumes);
the standing fact-proposal lane (LLM proposes continuously, engine verifies, repetition promotes) remains the
roadmap's growth path and is out of boundary here. First runs: extraction over the current embedded
vocabulary, proposal for the training pool's target families, with the pool repositories as evidence.
| 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 | mapping registry, registration adapter, fact initialization, coverage |
| 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10 | fact interpreter, abstract domain, operations, enforcement, witness matcher |
| 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10 | symbol resolution, summaries, path materializer |
| 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7 | pair judgment, checker integration, prover findings |
| 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7 | prover arm, four-arm development comparison |
| 6.1, 6.2, 6.3 | context caches, deadline state, runner budget gate |
| 7.1, 7.2, 7.3, 7.4, 7.5, 7.6 | completeness, prover coverage, runner funnel |
| 8.1, 8.2, 8.3 | finding contract, trace references, proof-arm filter |
| 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7 | canonical trace and record provenance |
| 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7 | strict CFG dominance and legacy order compatibility |
| 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8 | abstract domain, path and operation aggregation, transition report |
| 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9 | acquisition record, freeze manifest, admission runner |
## Components and Interfaces
### mapping recognisers
| Field | Detail |
|---|---|
| Intent | name web-exposed methods of the pool's frameworks as `route` entries with evidence |
| Requirements | 1.1-1.8 |
### Abstract evidence domain
~~~python
class EvidenceRelation(Enum):
MUST = "must"
MAY = "may"
NO = "no"
UNKNOWN = "unknown"
Each block follows the existing pattern: scan the file text (and the IR's function spans when a method body is
needed), call `_entry(target, start, end, handler, name, kind, access, trust_boundary, evidence, conditions)`.
Stapler: class text matches `extends|implements` one of the exposed types, or a method parameter is typed
`StaplerRequest`, `StaplerRequest2`, `StaplerResponse` or annotated `@QueryParameter`/`@AncestorInPath`; method is
`public` and its name matches `^do[A-Z]`. Access `public` only for `UnprotectedRootAction`. Jetty: classes
extending `AbstractHandler`, `HttpServlet` or annotated `@Path`/`@GET`/`@POST`. Symfony: methods with
`#[Route(` or `@Route(`; classes extending `AbstractController`. Livewire: classes extending `Component`, public
methods not starting with `_`, plus `Route::` registrations naming a controller method. Remix: `export const|function
action|loader` under `app/routes`. Tornado: classes extending `RequestHandler` or `WebSocketHandler`, request
methods and `open`.
class PairJudgment(Enum):
UNGUARDED = "unguarded"
DISCHARGED = "discharged"
INDETERMINATE = "indeterminate"
Registration sites `[2026-10-29]` (1.9): the call-site registration concept the Python route
registrations and the WordPress registration table already use, stated once, generically: when the
repository's own code registers a handler at a call site (a constructor or registration method whose argument
names the handler class), the registered class's request methods become `route` entries at the method's
declaration line in the registered class's file, with the registration site (`path:line`) and the registered
class name as evidence and `registered_by` set. The registration-site vocabulary -- which constructors and
methods count as registrations, and which methods of the registered class are request methods -- is acquirable
data under the facts discipline (R2 AC7), carried by a registration strategy module in the recogniser
registry, never hardcoded in the engine. Class-literal resolution goes through `resolution.class_table`. A
registration whose target class the repository does not define yields coverage (`registration_unresolved`) and
no entry. Jetty's programmatic servlet registration is the first data case (seatunnel evidence), not the
definition.
### obligations.paths
```python
@dataclass(frozen=True)
class PathRecord:
hops: tuple[tuple[str, int | None], ...] # ("path::function", line of the call site in the caller), entry first
entry: str # "path::function" of the entry hop
complete: bool # True when hops[0] is an entry point
class Completeness:
is_complete: bool
reasons: tuple[str, ...] = ()
def compose_paths(
*, callers: Callable[[str, str], Sequence[tuple[str, int]]], # (path, function) -> caller sites
entries: set[str], targets: Sequence[tuple[str, str]], max_depth: int = 3, fan_out: int = 25,
) -> tuple[list[PathRecord], list[str]]: # complete paths, unreached "path::function" targets
```
- Preconditions: targets name functions with IR; `callers` resolves a function to its call sites through the
cached index and the enclosing function of each site (`source.enclosing_at` or the IR spans).
- Postconditions: every path's last hop is a target; no path implies nothing; unreached targets are listed.
### obligations.resolution `[2026-10-29]` (3.7, 3.9; feeds 1.9, 2.10, 3.10)
```python
@dataclass(frozen=True)
class ClassRecord:
path: str # file declaring the class
class ResourceRef:
kind: Literal["exact", "aliases", "request_global", "unknown"]
values: tuple[str, ...] = ()
@dataclass(frozen=True)
class AuthorityRef:
values: tuple[str, ...]
is_unknown: bool = False
~~~
The partial order places definite evidence below possible or unknown evidence. Join never converts `MAY` or
`UNKNOWN` into `MUST` or `NO`. The precision order is:
~~~text
MUST <= MAY <= UNKNOWN
NO <= MAY <= UNKNOWN
~~~
`MUST` and `NO` are incomparable. The alternative-path join is fixed by this table:
| join | MUST | NO | MAY | UNKNOWN |
|---|---|---|---|---|
| MUST | MUST | MAY | MAY | UNKNOWN |
| NO | MAY | NO | MAY | UNKNOWN |
| MAY | MAY | MAY | MAY | UNKNOWN |
| UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
Three separate operations prevent one table from being used for the wrong quantifier:
1. `join_alternatives` uses the table above when alternative call targets or control paths contribute evidence.
2. `conjoin_dimensions` combines kind, scope, resource, authority and dominance for one witness: any `NO`
produces `NO`; otherwise any `UNKNOWN` produces `UNKNOWN`; otherwise any `MAY` produces `MAY`; only
all-`MUST` produces `MUST`.
3. `aggregate_witnesses` asks whether any witness definitely governs one obligation: any `MUST` produces
`MUST`; otherwise `UNKNOWN` takes precedence over `MAY`, and `MAY` takes precedence over `NO`.
An empty witness set is `NO` only when discovery and facts are complete; otherwise it is `UNKNOWN`.
For one complete path, all required obligations at `MUST` yield `discharged`; any `MAY` or `UNKNOWN` yields
`indeterminate`; otherwise any `NO` yields `unguarded` and every `NO` obligation is reported missing.
Thus a mixed `[MUST, NO]` obligation vector is unguarded for the `NO` obligation. Resource
alias sets join by finite union; an unbounded or unresolvable value joins to `unknown`. Authority implication is
read from promoted facts and contributes its fact identifier to the match evidence.
### Symbol resolution
~~~python
@dataclass(frozen=True, order=True)
class SymbolId:
module: str
enclosing_type: str | None
name: str
superclass: str | None
abstract: bool
methods: tuple[str, ...]
def class_table(texts: Mapping[str, str], language: str) -> Mapping[str, ClassRecord]
# bare class name -> record; one table per change, cached on the Context
def qualify(*, receiver: str | None, enclosing_class: str | None, name: str,
arity: int | None, table: Mapping[str, ClassRecord]) -> str
# the qualified key for a callee: "Class.name" when the receiver text names a class
# in the table (or `this`/`self`/bare call in a class's own file), "Class.name/arity"
# when the class declares same-name methods with distinct parameter lists and the
# call site's arity is known, and the bare "name" when the receiver is unresolvable
# (dynamic languages, framework objects the repository does not define).
```
- Preconditions: `receiver` is the receiver expression text captured on `CallSite` (new field; `None` for
bare calls in dynamic languages); `class_table` covers the repository's own classes only.
- Postconditions: same-name methods on different classes yield different keys; overloads with known distinct
arity yield different keys; unresolvable receivers fall back to the bare name and are counted
`receiver_unresolved` in coverage, never silently merged.
- Base-class attachment rule: an operation or handler-named entry declared on a class attaches to a path only
when the entry's (or call site's) class is that class or an in-repo subclass of it; `abstract` classes and
framework bases never project onto unrelated routes.
- Consumers: the prover's `callers` adapter returns, for a target key `Class.name`, only caller files whose
IR binds the callee to a receiver of that class (or subclass); the target enumeration joins callees by
qualified key instead of trailing name. `compose_paths` and `check_obligations` consume `path::function`
strings unchanged -- qualification happens before them, so `PathRecord` keeps its shape.
- Budget: the class table is one pass over texts (already read); resolution adds no tree walks. The target
enumeration extends down through calls within its own budget (`down_depth`, default 2, sharing the fan-out
bound), so an operation one call deeper than the handler is reached without unbounded descent.
### checker extension (opt-in)
`check_obligations(..., assume_entries_protected: bool = False)`. With path records, no declared clause and no
sibling norm, the checker today returns no finding (gating 3 of `authorization-obligations`): nothing justified
the obligation. The prover's premise justifies it: a recognised entry is protected unless declared public. With
the keyword on, `_finding` labels such a case `unguarded_path` when the path's entry hop is an entry record whose
access is not `public` and the route is not declared public; evidence carries the entry's kind and evidence
strings and the sentence "no declared policy clause and no sibling norm; the prover assumes a recognised entry is
protected". `findings_to_static` explains the label like the others. `LABELS` gains the value.
Resource-bound discharge `[2026-10-29]` (2.10): `check_obligations(..., resource_bound: bool = False)`. With the
keyword on, a witness whose `resource` field is set suppresses only operations whose resource token equals it
(a token computed the way `Operation.resource` already is: receiver first segment, table name, or the row's
`resource_arg`); a witness with `resource` unset keeps today's behaviour. Default off; existing tests unchanged.
Caller-hop re-judgement `[2026-10-29]` (3.8, 3.10): `_on_caller_hop` re-judges a witness on an earlier hop for
every discharger kind, not only `path_guard` and `ownership_check`: the witness is re-judged against the
operation moved to the hop's call line, by the selected dominance. A guard recorded on a hop before the call
(`guards_before_call`) participates in the same re-judgement, so a finding is never emitted when the pair's own
trace shows a dominating guard of a required kind. Router and decorator scopes keep their whole-handler
semantics.
### facts extension: handler-named operations (owned here, schema change)
`[[operation]]` rows gain two optional fields: `handlers` (a list of regular expressions over the function name)
and `framework` (a recogniser id the mapper's evidence names: `stapler`, `tornado`, `remix`, `jetty`,
`symfony`, `laravel`). A row with `handlers` and no `calls` is a handler-named operation: `find_operations`
yields it at the function's declaration line for every function whose name matches and that the caller lists as
a recognised entry of that framework (new keyword `handlers_of: Mapping[str, str] | None`, function name to
framework id; default None keeps today's behaviour). The smoke run of delivery A showed why: 15 of the 17
Jenkins fixes are form-validation and list-filling web methods whose body calls no listed operation; the
method is the operation. Jenkins documents the rule (form validation methods must check a permission).
Handler-named rows for the webhook, build-trigger and data-exposure shapes `[2026-10-29]` (2.9): the
`handlers` patterns extend to verb families whose handler name itself carries the obligation -- endpoints
that act with repository or job privileges when called (webhook and build-trigger shapes) and endpoints that
expose protected data (data-exposure shapes) -- as operation rows on framework-recognised entries, at the
declaration line. The families are rows in the facts files, not code: a new verb family is a row addition
acquired through the one-time initialization pass for the target (or proposed by the fact-proposal lane), and
a row without `handlers` never matches a declaration name. The gate-1 misses (Stapler webhook and
chart-data endpoints) are the first evidence set, not the definition.
### trace: the ordered serialisation (owned here)
```python
@dataclass(frozen=True)
class Guard: kind: str; scope: str; line: int | None; provenance: str; fact_id: str
@dataclass(frozen=True)
class Hop: function: str; call_line: int | None; guards_before_call: tuple[Guard, ...]; forwarded: tuple[str, ...]
@dataclass(frozen=True)
class Trace:
entry_kind: str; boundary: str; access: str; parameters: tuple[str, ...]
hops: tuple[Hop, ...] # entry first; the last hop holds the operation
operation_kind: str; fact_id: str; operation_line: int
arguments: tuple[tuple[str, str], ...] # (argument text, provenance) in call order
guards_after: tuple[Guard, ...]
requires: tuple[str, ...]; dominating: tuple[str, ...]; missing: str | None
def text(self) -> str: ... # one component per line, in order
def record(self) -> dict: ... # canonical JSON
```
Argument provenance: within each hop's function, follow the IR's binds backwards from the operation's argument
identifiers to the function's parameters or to a request-source fact (`request_input`), to an identity source
(`authenticated_context`), or to a literal (`constant`); a parameter that the previous hop's call site fills
from its own request-derived values keeps `request_input`; anything the walk cannot follow is `unknown`. No
inter-procedural dataflow beyond the call sites on the path. The trace never carries language, framework,
repository, size or count fields.
### cfg and CfgDominance (owned here)
`src/openultrasast/semantic/cfg.py`: `FunctionCfg.from_cst(node, language)` builds basic blocks and edges from
the tree-sitter syntax tree of one function (blocks, if/else, switch arms, loops with back edges, try/catch/
finally, return, throw, break, continue, exit) and computes dominators (`dominates(a_line, b_line)`) and the
control context of a line (`context(line) -> ControlContext(conditions, in_loop, in_try, catches, on_exception_path)`).
`src/openultrasast/semantic/obligations/cfg_dominance.py::CfgDominance` implements the `Dominance` protocol:
a statement witness dominates when its block dominates the operation's block and the operation is unreachable
from the guard's failing arm: either that arm exits (`throws`, `returns`, `exits`), or the operation lies only
inside the passing branch (`if (can(x)) { op(); }`, effect `branches`); a bare boolean whose value no branch or
exit consumes before the operation is `value_only` and never discharges; router and decorator scopes
dominate as before; along a path record, a witness on an earlier hop dominates as before. Without a graph for
a function, `CfgDominance` falls back to `OrderDominance` and records `cfg_unavailable`.
The trace's `Guard` gains `relation` (`all_paths`, `some_paths`, `none`, `after`) and `effect`
(`throws`, `returns`, `exits`, `branches`, `value_only`; `constrains` for an identity constraint or a
non-permissive value on the operation's own line, which has no failing arm; router and decorator witnesses carry
`exits`); the trace gains `operation_context: ControlContext`. Coarse spots accepted for this increment: a
ternary or switch expression is one node, several statements on one line share it, a guard's result held in a
variable is followed by name without reassignment tracking.
### obligations.enforcement
```python
def witnesses(*, root: Path, texts: Mapping[str, str], entries: Sequence[EntryPointRecord], language: str) -> list[Discharge]
```
Reads Laravel route files (`routes/*.php`) for `->middleware([...])` and controller constructors for
`$this->middleware`/`authorizeResource`; Symfony attributes and `config/packages/security.yaml`
`access_control`; Tornado `@authenticated`; emits `Discharge(kind="path_guard", scope="router"|"decorator", path,
function, line=None, evidence=...)` for each covered handler. Names recognised live in the facts files' headers.
Resource binding and policy witnesses `[2026-10-29]` (2.10): every witness this module mints carries the
resource it binds (`Discharge` gains an optional `resource: str | None`, default `None`): the route path for
router middleware, the attribute or subject expression for a policy-layer guard, the middleware name for
`auth`-class guards (no resource -- `None`). A policy-layer witness covers an action's operations when a
policy decision (voter, capability check, or equivalent) reachable from the action -- in the action body or a
function it calls -- names the action's attribute or subject; the witness is scoped to that action's
operations with the attribute or subject as its resource. Which constructs count as policy decisions is
acquirable data in the facts files (the discharger rows already carry the vocabulary); the witness shapes are
engine behaviour. Discharge matching is the checker's `resource_bound` keyword (above); the resource token
computation is shared with `Operation.resource` so the two sides compare like with like. Symfony's voter
attributes are the first data case (kimai evidence), not the definition.
### hunt.prover
```python
@dataclass(frozen=True)
class Coverage: files_by_language, frameworks_recognised, entry_points, reached, unreached, operations_checked,
operations_discharged, unchecked_by_reason # all ints or dicts of ints
signature: tuple[str, ...] | None
@dataclass(frozen=True)
class ProverResult: findings: tuple[dict, ...]; coverage: Coverage; degradations: tuple[dict, ...]
class CallTargetSet:
targets: tuple[SymbolId, ...]
completeness: Completeness
~~~
def prove_change(root: Path, change: Mapping[str, Any], ctx: Context | None = None, *, max_depth: int = 3) -> ProverResult
```
- Touched functions come from `_candidates`-style enumeration (touched head lines to function spans), build
output left out via `source.generated_reason`.
- Targets = touched functions plus callees that hold a protected operation, joined by qualified key
(`resolution.qualify`) and followed down through calls within the `down_depth` budget (default 2) sharing the
fan-out bound, so an operation one call deeper than the handler is reached `[2026-10-29]` (3.7).
- The `callers` adapter resolves a target to its call sites through the caller index and keeps only callers
whose receiver binds to the target's class (or subclass); unresolvable receivers fall back to the bare name
and are counted `receiver_unresolved` in coverage `[2026-10-29]` (3.9).
- Findings: for each `ObligationFinding`, a hunt defect dict with `path`, `line`, `family="access_control"`,
`function`, `evidence` (entry record summary, hops, operation id and calls, missing kind), `id` as the checker
emits.
- Degradations and every unchecked reason land in `Coverage`.
The symbol index covers repository-defined declarations and inheritance. Exact receiver and full signature produce
one complete target. Partial type or signature information produces a finite candidate set with an incompleteness
reason. No call falls back to a global bare-name key. Base declarations attach through explicit descendant edges.
### prover_arm
`python -m benchmarks.valid.prover_arm run POOL --repos REPOS --out DIR [--slice 1] [--only all] [--max-depth 3]`
writes `<sha256(change id)>.json` per change (replay record envelope copied from `hunter_arm`: id, case_id,
repo, kind, family, path, function_lines, files, head, instrument {changed_bytes, hook_bytes_read, head_verified},
findings, coverage, seconds, status), resumes by skipping done records, then `summarize` writes `summary.json`
(metrics from `benchmarks.valid.metrics.summarize`, per-repository and per-framework coverage, changes over 120 s,
instrument failures) and refuses when any record read zero bytes of a file git holds.
### Function summaries and paths
~~~python
@dataclass(frozen=True)
class FunctionSummary:
symbol: SymbolId
operations: tuple[Operation, ...]
witnesses: tuple[Discharge, ...]
calls: tuple[CallTargetSet, ...]
completeness: Completeness
~~~
Anchor refusal `[2026-10-29]` (5.6): `_prove` validates every returned finding's `path` against the files it
actually read (`instrument.file_bytes`); a finding on an absent path marks the record
`status="instrument_failure"`, reason `stale_anchor`, findings cleared -- the same treatment as a zero-byte
read. `summarize` refuses to summarise a run containing such records, listing them by record id.
Summaries are computed by a monotone worklist over strongly connected call components. Their finite contents grow
by union until stable. A budget cutoff stops growth, records the cutoff and leaves affected summaries incomplete.
Concrete paths are then materialised from recognised entries through exact targets for evidence. Candidate edges
may contribute diagnostic traces but cannot complete a final path.
### Witness matching and judgment
~~~python
@dataclass(frozen=True)
class WitnessMatch:
relation: EvidenceRelation
resource_relation: EvidenceRelation
authority_relation: EvidenceRelation
evidence: tuple[str, ...]
@dataclass(frozen=True)
class PathJudgment:
judgment: PairJudgment
missing: tuple[str, ...]
completeness: Completeness
matches: tuple[WitnessMatch, ...]
~~~
A witness governs an obligation only when kind, scope, resource, authority and dominance are all `MUST`.
Any `MAY` or `UNKNOWN` required relation makes the path indeterminate. With complete evidence and no governing
witness for a required kind, the path is unguarded. Multiple paths aggregate as follows:
- any complete unguarded path -> `unguarded`;
- complete enumeration and every path discharged -> `discharged`;
- otherwise -> `indeterminate`.
The checker gains an opt-in abstract mode. Legacy callers keep the current boolean result and order-based default.
### Strict CFG dominance
`CfgDominance.relation` returns an evidence relation and a guard effect. Prover mode never calls
`OrderDominance` when a graph, line mapping or construct is unavailable; it returns `UNKNOWN` with
`cfg_unavailable` or `line_without_node`. Route and declaration scopes return `MUST` only after exact handler
attachment. The legacy dominance class remains unchanged.
### Target knowledge and acquisition
The mapping registry and fact interpreter are target-aware adapters. The initialization job has:
- extraction mode for existing strategies and fact rows;
- proposal mode for source- or documentation-derived candidate knowledge;
- verification that checks source, version, region and concrete matches;
- promotion that writes only verified rows or enabled strategies;
- an acquisition record containing input revision, evidence digest, accepted/rejected proposals and output digest.
Admission mode prevents the acquisition process from receiving benchmark labels or fix locations. A changed output
digest after labels are opened spends the population.
### Prover integration
`prove_change` orchestrates cached reads, recognition, resolution, summaries, paths, strict dominance and
judgment. It emits findings only for `unguarded` pairs and preserves all three judgments as traces. Coverage
contains stage counts, reason counts, pair judgments and exploration cutoffs. Exceptions are recorded at the stage
that raised them.
### Development and admission runners
Before comparison, the development runner materialises and validates two immutable bundles:
~~~python
@dataclass(frozen=True)
class AnalyzerBundle:
analyzer_revision: str
fact_set_digest: str
acquisition_record_digest: str
configuration_digest: str
~~~
The baseline bundle points to the analyzer revision and fact set that produced the immutable 2026-10-28 records;
the candidate bundle points to the amended equivalents. The runner refuses a four-arm comparison if either
revision or fact artifact cannot be materialised and digest-verified. It then registers baseline
analyzer/baseline facts, baseline analyzer/candidate facts, candidate analyzer/baseline facts and candidate
analyzer/candidate facts. This separates knowledge coverage from engine behavior.
The admission runner accepts only a frozen manifest naming analyzer, facts, acquisition, configuration and
population digests. The population manifest is produced and reviewed by `valid-detection-benchmark`; this spec
only consumes and validates it. The runner verifies its review and isolation fields, scores its paired
vulnerable/fixed cases and reviewed ordinary labels, refuses zero-byte or stale-anchor records, fails any change
over 120 seconds, and never converts indeterminate pairs to clean results.
## Data Models
### Canonical trace
Each trace carries:
- entry identity and target-knowledge evidence;
- exact hops and call-target completeness;
- operation kind, resource and authority;
- witnesses with kind, scope, provenance, resource, authority, dominance and match relations;
- control context and guard effects;
- completeness reasons and final path judgment;
- analyzer, fact-set and input digests.
Repository identity, repository size and function-size features remain outside decision input.
### Run manifest
The manifest contains population role (`development` or `admission`), immutable input digest, analyzer revision,
fact-set digest, acquisition-record digest, configuration digest, label-review state and spent-state marker.
The analyzer/fact bundle is a separate immutable input whose artifacts must be materialisable by revision and
digest; a manifest cannot redefine or synthesize the baseline.
## Error Handling
- Exceptions inside `prove_change` per file are recorded under their own reason key
`error:<exception class name>` `[2026-10-29]` (7.3), never folded into `framework_unrecognised` or any other
quiet reason; the change is still proved for the other files. A provider never appears (no model call). A
checkout failure is an instrument failure on the record (`status="instrument_failure"`, reason type only).
- Parse, recognition, resolution, summary, CFG and judgment exceptions become stage-specific incompleteness reasons.
- Deadline, fan-out, depth and recursion limits preserve accumulated evidence and return indeterminate affected
pairs.
- Zero-byte reads and stale anchors are instrument failures that invalidate the run.
- Missing or mismatched manifest digests invalidate admission rather than falling back to development mode.
## Testing Strategy
- Unit: each recogniser on a fixture file written in the test (not copied from clones) with positive and negative
cases; `compose_paths` on a three-file fixture (depth 1, 2, 3, unreached, fan-out cut, same-name in two files);
`witnesses` for a Laravel route file, a Symfony attribute, a Tornado decorator. `[2026-10-29]`:
`class_table`/`qualify` on a fixture with two same-name methods on different classes (distinct keys), two
same-name methods with different arity (distinct keys), an unresolvable receiver (bare name +
`receiver_unresolved` counted), an abstract base method reached through a subclass (attaches) and through an
unrelated class (does not); a programmatic `addServlet` registration fixture yielding entries at the
registered class's methods with registration evidence, and an unresolved registration yielding coverage only;
a voter fixture (`isGranted` with attribute, `voteOnAttribute` implementation) yielding a `voter` witness with
the attribute as resource; a resource-mismatch fixture (guard binds another object's resource -> operation
stays undischarged).
- Integration: `prove_change` on a git fixture with a Stapler-style class: an unguarded `doTestConnection` that
calls `lookupCredentials` yields one finding with hops and missing `path_guard`; the same with
`Jenkins.get().checkPermission(...)` above the call yields none; a Laravel controller behind `auth` middleware
yields none; an unrecognised framework yields coverage only. `[2026-10-29]`: the guard sits one call deeper
than the handler (a helper checks permission before the operation) and discharges; a guard whose trace shows
it dominating before the call produces no finding; an operation on a base handler class attaches to the
subclass route only; two classes with the same method name keep separate verdicts.
- Runner: a two-change fixture pool produces records the metrics summarise; a zero-byte read refuses;
a fixture finding anchored to an absent path refuses with `stale_anchor` `[2026-10-29]`; a fixture raising a
recognition exception counts it under `error:<class>` and not under `framework_unrecognised`
`[2026-10-29]`.
- Unit: the complete join table, dimension conjunction, witness aggregation and monotonicity laws; symbol
collisions and overload candidates; resource aliases and authority
implication; SCC termination; CFG unknown behavior; three-way aggregation.
- Metamorphic: renaming a type preserves judgment; adding an unresolved overload can only preserve a judgment or
move it to indeterminate; removing semantic evidence cannot switch directly between discharged and unguarded.
- Integration: exact unguarded and discharged paths; a guard in a called helper; registration-site entries; base
attachment; resource mismatch; authority insufficiency; ambiguous calls; CFG unavailable; budget cutoff.
- Runner: baseline/candidate bundle materialisation, four-arm attribution, paired vulnerable/fixed scoring,
reviewed-manifest validation, freeze-digest guard, zero-byte and stale-anchor refusal, indeterminate rate, and
hard 120-second gate.
## Migration Strategy
1. Add abstract types and adapters without changing legacy checker defaults.
2. Add strict resolution, summaries and judgment behind the prover's opt-in path.
3. Migrate traces and coverage, then enable the amended development arms.
4. Freeze facts and analyzer before running a separate admission population.
Rollback is the previous prover arm and fact-set digest; baseline records remain immutable and comparable.
+236 -261
View File
@@ -1,304 +1,279 @@
# Requirements Document
## Project Description (Input)
An access-control prover: a deterministic check that an operation needing authorization is reachable from an
entry point without a check dominating the path, modelled per framework, built from the existing obligation facts,
caller index and entry-point mapper, scored as a rule on the access-control training pool (recall on 23 fix
changes, false alarms per 100 ordinary pushes) and feeding the proof arm.
An access-control prover: a deterministic check that a protected operation is reachable from an attacker-facing
entry point without a required authorization check governing that path. The prover consumes acquired entry,
operation and enforcement facts, resolves calls and resources conservatively, emits an ordered trace for every
examined pair, and is measured separately on development and admission populations.
## Introduction
The maintainer's finding of 2026-10-14: the hunter's features (diff shape, function size, callers, entry-point
counts, name roles, textual obligation order) describe the shape of code around a change and cannot decide that
code is unsafe. Unsafety for access control is a property: an operation that needs authorization is reachable by a
caller that no check has constrained. The access-control training pool (23 fix changes, 93 verified locations,
460 ordinary pushes, 23 application repositories) holds exactly that shape: a permission check added before a
protected operation in a web-exposed method. Seventeen of the 23 fixes are Jenkins plugin web methods.
The security object for access control is not the shape of a changed function. It is an ordered path from an
attacker-facing entry to a protected operation, together with the authorization evidence that governs every
relevant control-flow and call-flow step. The 2026-10-28 development measurement established a useful baseline:
15 of 23 fix changes detected, 15.0 flagged ordinary pushes per 100, and no instrument failures. The subsequent
ablation showed that unresolved calls, over-broad resource matching, missing registration facts and silent
fallbacks account for both missed detections and false alarms.
This spec delivers the prover for that one family, as a deterministic rule with no model call, measured on the
pool with the standard metrics, and emitting findings that carry the unguarded path so the proof arm can act on
them. It is the increment that follows the annotation ablation in `hunter-learning-loop`.
This amendment makes uncertainty part of the result rather than a side counter. A pair is `unguarded`,
`discharged`, or `indeterminate`; degraded resolution, missing control-flow semantics, incomplete facts and
budget cutoffs cannot create a final security verdict. It also separates label-blind fact acquisition and
development regression from admission measurement on untouched repositories.
**Amendment 2026-10-29 (after the 2026-10-28 measurements).** Gates 1-3 measured the built prover on the pool:
recall 65.2% (15/23), 15.0 false alarms per 100 ordinary pushes, a zero-shot judge that costs no recall but
drops almost no false alarms. The taxonomy of the 8 misses and of 12 sampled kept false alarms points at one
cross-cutting gap: the prover resolves too shallowly and too loosely. Operations and guards one call deeper
than a handler are invisible; same-name methods on different classes are conflated; operations declared on
abstract bases are projected onto routes that never implement them; coarse guards (a path guard, a generic
ownership check) discharge operations whose resource they do not bind. This amendment adds the resolution
requirements (Requirement 3 acceptance criteria 7-10, Requirement 2 acceptance criteria 9-10), the
programmatic-registration entry facts (Requirement 1 acceptance criterion 9), and the failure-visibility
criteria (Requirement 5 acceptance criterion 6, Requirement 7 acceptance criterion 3). Acceptance criteria
added or changed by this amendment carry the tag `[2026-10-29]`.
Per the specification-level steering rule, this amendment states **acquisition capabilities**, not per-target
inventories: the target vocabulary implied below (registration sites, handler-name families, policy-witness
shapes) is data the fact pipeline acquires per target -- a one-time initialization job per target can produce
it, and can extract the target knowledge already embedded in the repository's recogniser code into the same
structures -- with the engine's verification discipline admitting each row. The named repositories appear only
as benchmark evidence the acquired data must satisfy.
Concrete frameworks and repositories are benchmark evidence, not acceptance-criteria inventories. Target
vocabulary is acquired as reviewed data or recogniser strategies and is verified before promotion. The abstract
interpretation engine remains target-agnostic.
## Boundary Context
- **In scope**: entry-point recognition for the pool's frameworks; reachability paths from entry points to
touched functions through the existing caller index; framework enforcement points that discharge an obligation
outside the function (route middleware, controller attributes, declared public routes); resolution of calls on
those paths precise enough to place operations and guards in the function that holds them `[2026-10-29]`;
the verdict per change; a runner that scores the verdict as an arm on the pool; a coverage statement per
repository.
- **Out of scope**: taint flow, the Joern engine, any model call inside the prover, new families beyond access
control, a project-owned declared policy file (exists in `authorization-obligations` and is consumed as is),
the unseen pilot measurement (G1 exit, run by the maintainer after this arm exists), the pre-push hook wiring
(`pre-push-safety-net`), client-side JavaScript permission gating (hiding controls in a browser frontend --
the byobu miss of gate 3 is not a server-side security object) `[2026-10-29]`, tuning the zero-shot judge
prompt (gate 2 measured the judge inert on this pool; the lever is the trace, not the prompt)
`[2026-10-29]`, re-auditing the pool's negative pushes (owned by `valid-detection-benchmark`)
`[2026-10-29]`.
- **Adjacent expectations**: `authorization-obligations` owns the facts schema, the checker and the dominance
protocol; this spec extends facts and supplies paths and witnesses through the checker's existing parameters.
`hunter-learning-loop` owns the feature table and the hunter arms; the prover is scored beside them with the
same metric functions. `search-with-proof` consumes the finding's path and operation to build a demonstration.
`valid-detection-benchmark` owns the pool: 3 of 12 sampled "ordinary" pushes carry real missing-check bugs
(pre-2019 negatives), so false-alarm counts read against that pool overstate the prover's error until the
negatives are re-audited `[2026-10-29]`.
- **In scope**: acquired entry, operation and enforcement knowledge; exact and conservative call resolution;
resource- and authority-sensitive witness matching; bounded interprocedural summaries and concrete evidence
paths; control-flow-sensitive dominance; three-way pair judgments; failure-visible coverage; development and
admission runners.
- **Out of scope**: taint flow; the Joern engine; model calls inside the prover; vulnerability families other than
access control; client-side presentation gating; pre-push hook wiring; proof execution; hand-tuning vocabulary
from evaluation labels.
- **Adjacent expectations**: `authorization-obligations` supplies the existing checker and fact contracts;
`valid-detection-benchmark` supplies reviewed populations; `search-with-proof` consumes unguarded traces;
`pre-push-safety-net` may consume the arm only after admission passes.
## Requirements
### Requirement 1: Entry points of the pool's frameworks
**Objective:** As a maintainer, I want the entry-point mapper to name the web-exposed methods of the frameworks
the pool uses, so that reachability starts where an attacker starts.
### Requirement 1: Acquired attacker-facing entry points
**Objective:** As a maintainer, I want entry recognition to consume verified target knowledge, so that new targets
can be covered without adding target vocabulary to the analysis engine.
#### Acceptance Criteria
1. When a Java file declares a public method whose name starts with `do` in a class that extends or implements
a Jenkins or Stapler exposed type (Descriptor, Action, RootAction, Plugin, UnprotectedRootAction,
AbstractDescribableImpl, or a parameter typed StaplerRequest, StaplerResponse or annotated QueryParameter),
the mapper shall record an entry point of kind `route` with trust boundary `http_request` and the method's
line range, with access `public` when the class implements UnprotectedRootAction and `unknown` otherwise.
2. When a PHP file declares a Symfony controller action (a method carrying a `Route` attribute or annotation, or
a class extending AbstractController with public methods named by a routes file), the mapper shall record it
as a `route` entry with the attribute's path as evidence.
3. When a PHP Livewire component or Laravel controller method is registered by a route file, a `Route::` call, a
`wire:` binding or a public method on a class extending Livewire Component, the mapper shall record it as a
`route` entry, and shall carry a declared access of `public` only when the route file names no middleware.
4. When a TypeScript or JavaScript file under a Remix or React Router routes directory exports `action` or
`loader`, the mapper shall record each export as a `route` entry with trust boundary `http_request`.
5. When a Python class extends a Tornado RequestHandler or WebSocketHandler, the mapper shall record its `get`,
`post`, `put`, `delete`, `patch` and `open` methods as `route` entries.
6. When a Java class extends a Servlet, a Jetty handler or a JAX-RS resource, the mapper shall record the
request methods as `route` entries (existing Servlet and JAX-RS rows are kept; Jetty handlers are added).
7. The mapper shall record every recogniser's evidence strings so a finding can name why a method is an entry.
8. If a file's framework is not recognised, then the mapper shall record nothing for it and the coverage
statement (Requirement 7) shall count the file as unchecked; silence is never a pass.
9. `[2026-10-29]` When the repository's own code registers a handler at a call site rather than declaring it
through annotations or configuration, the mapper shall record the registered class's request methods as
`route` entries with the registration site and the registered class as evidence, so that a server assembled
in code still yields entry points. The registration-site vocabulary for a target is acquirable data under
Requirement 2 acceptance criterion 7's discipline, not engine code; Jetty's programmatic servlet
registrations are the first case (the seatunnel miss of gate 3).
1. When verified target knowledge describes a declarative entry shape, the mapper shall record every matching
repository-defined handler as an entry with its source range, trust boundary and recognition evidence.
2. When verified target knowledge describes a call-site registration, the mapper shall resolve the registered
repository-defined handler and record its request methods as entries with the registration site and resolved
handler identity as evidence.
3. If a registration target cannot be resolved to repository-defined code, the mapper shall emit no entry for it
and shall record `registration_unresolved` coverage.
4. When acquired knowledge proves that an entry is intentionally public, the mapper shall record access `public`;
otherwise, the mapper shall record access `unknown` rather than infer public access.
5. The mapper shall preserve the promoted fact or strategy identifier and the concrete source evidence on every
entry record.
6. If no promoted entry knowledge covers a file, the prover shall record `framework_unrecognised` and shall not
interpret the absence of entries as a clean result.
7. When target entry knowledge is added or updated, the system shall apply it through data or a registered
recognition strategy without changing target-agnostic analysis modules.
### Requirement 2: Enforcement points outside the function
**Objective:** As a maintainer, I want checks that frameworks apply outside the handler to count as discharges,
so that a guarded route is not reported because its handler body shows no check.
### Requirement 2: Acquired protected operations and enforcement evidence
**Objective:** As a maintainer, I want operations and authorization evidence represented as verified knowledge,
so that the prover compares the correct action, resource and authority.
#### Acceptance Criteria
1. When a Laravel route registration attaches `auth`, `can:`, `role` or `permission` middleware, or a controller
calls `authorizeResource` or `middleware` in its constructor, the prover shall supply a discharge witness of
scope `router` for every handler the registration covers.
2. When a Symfony action carries `IsGranted`, `Security` or a `denyAccessUnlessGranted` call, or security.yaml's
`access_control` covers its path with a role, the prover shall supply a `decorator` or `router` witness.
3. When a Jenkins web method, or a method it calls on its own path, calls `checkPermission`,
`checkAnyPermission` or guards on `hasPermission` with an exit, the existing statement witnesses shall apply;
`RequirePOST`, `POST` and `Interceptor` annotations shall not count as authorization.
4. When a Remix action or loader calls a session requirement (`requireUser`, `requireUserId`,
`requireAuthentication`, `authenticateApiRequest`, `authorize`) before the operation, the prover shall treat
that call as a path guard, and when the operation reads a record by an identifier from the request without an
ownership constraint bound to the authenticated principal the prover shall report the missing
`ownership_check` (the pool's CWE-639 case).
5. When a Tornado handler is decorated with `authenticated` or `tornado.web.authenticated`, or the class
overrides `get_current_user` and checks it before the operation, the prover shall treat that as a path guard.
6. Where a declared policy marks a route public, the prover shall suppress `path_guard` findings for it and
keep `ownership_check` findings, as the checker already does.
8. Where a framework's security model makes the web method itself the protected operation (Jenkins form
validation and list-filling methods `doCheck*`, `doFill*Items`, `doTest*`, `doValidate*`, `doVerify*`, which
Jenkins requires to check a permission; Tornado handler methods; a Remix action), the facts shall be able to
name the obligation by handler name pattern and framework, and the prover shall apply such an operation only
to functions the mapper recognised as entries of that framework, at the method's declaration line.
7. The prover shall write every enforcement fact as closed data in the obligation facts files, with the header
naming the clones and references checked, and shall never add a name that does not occur in the checked code
or the framework's documentation. A proposed fact (from a maintainer or an LLM proposal pass) is promoted to
a closed row only after the same header discipline is satisfied `[2026-10-29]`; per-target vocabulary enters
as data through this gate, never as engine code.
9. `[2026-10-29]` The facts shall be able to name, by handler-name pattern and by the privileged calls the
handler makes, webhook and build-trigger obligations and protected-data exposure obligations, applied at the
method's declaration line to entries the mapper recognised. The name families for a target are acquirable
data under acceptance criterion 7's discipline -- a one-time initialization pass per target can produce them
(and can extract the vocabulary already embedded in the repository's recogniser code into those structures)
-- and the pool's Stapler webhook and data-exposure handlers are the first cases (the git-plugin, p4,
analysis-core and global-build-stats misses of gate 3 all reached the entry and checked no operation).
10. `[2026-10-29]` A guard shall discharge an operation only when it binds that operation's resource: when the
guard constrains a different path, a different object, or a permission level the operation does not require,
the prover shall treat the operation as undischarged (the crx and kimai misses of gate 3: a generic
`path_guard` or `ownership_check` dominated a credentials form-fill and invoice actions whose resource it
never touches). Where the target's policy layer decides access by an attribute or subject of the request --
a voter, policy, or capability decision reachable from the action or a function it calls -- the prover shall
supply a policy witness scoped to that action's operations with the attribute or subject as its resource;
Symfony's voter decisions are the first case.
1. When promoted operation knowledge matches a call or recognised handler, the prover shall record the protected
operation, its required authorization kinds, its resource identity and any required authority.
2. When promoted enforcement knowledge matches a statement, decorator, route, policy decision or equivalent
construct, the prover shall record a witness with its scope, resource identity, authority and provenance.
3. When a handler name itself denotes a protected action, the prover shall apply that operation only to an entry
recognised for the same promoted target knowledge.
4. When a route or declared policy marks an entry public, the prover shall waive only a request-level path guard
and shall preserve resource, ownership and authority obligations.
5. When a witness binds a different resource or an insufficient authority, the prover shall not use that witness
to discharge the operation.
6. When a witness is request-global, the prover shall use it without resource equality only when the operation
fact explicitly declares that witness kind sufficient at request scope.
7. If resource identity or authority compatibility cannot be decided, the prover shall treat the witness match as
unknown rather than matching it broadly or rejecting it as definitely unrelated.
8. When one authority implies another, the prover shall apply that implication only from promoted target
knowledge and shall record the implication used in the trace.
9. When target knowledge is extracted or proposed, the acquisition workflow shall verify its source, version,
region and match evidence before promoting it to closed data or an enabled strategy.
10. If proposed target knowledge contradicts the inspected source or lacks verification evidence, the acquisition
workflow shall reject it with a reason and shall not modify promoted knowledge.
### Requirement 3: Reachability paths through the caller index
**Objective:** As a maintainer, I want a path from an entry point to each touched function, so that an
obligation inside a helper is judged by the checks on the way to it.
### Requirement 3: Conservative interprocedural reachability
**Objective:** As a maintainer, I want calls resolved to stable program identities and paths carried through
helpers, so that unrelated same-name code neither creates nor suppresses findings.
#### Acceptance Criteria
1. When a change is checked out at head, the prover shall build, for every touched function and every function
that holds a protected operation it calls, the caller chains that end at an entry point, through the
per-change caller index, up to a configurable depth with default 3 and a configurable fan-out with default 25
callers per step.
2. The prover shall emit each chain as a path record whose hops are `path::function` strings from the entry to
the operation's function, in the shape `check_obligations` and `OrderDominance` already consume.
3. If a touched function is itself an entry point, then the prover shall emit the one-hop path.
4. If no chain reaches an entry point within the depth, then the prover shall record the function as
`unreached` in the coverage statement and shall produce no finding for it.
5. The prover shall deduplicate chains, shall stop at the first entry point on a chain, and shall bound the work
so a change of the pool's largest repositories finishes within the budget of Requirement 6.
6. When two functions share a name in different files, the prover shall keep the chains separate by file.
7. `[2026-10-29]` When a function on a path calls another function of the repository that holds a protected
operation, the prover shall extend the path through that call (within the depth and fan-out budget) so the
operation is checked even though no touched function holds it directly; an operation buried one call deeper
than the handler shall not be invisible (the gate 3 operation-fact misses).
8. `[2026-10-29]` When a guard sits in a called function on the path, before the call or before the operation,
the trace shall carry it with its own function, line and kind, and dominance shall honour it as if it sat in
the hop's own function; a guard one call deeper than the handler shall still discharge (the dependency-track
and shopper false alarms of gate 3).
9. `[2026-10-29]` The prover shall resolve calls by signature and receiver: same-name methods on different
classes (the vault and p4 false alarms) and same-name overloads with different parameter lists shall stay
distinct, and an operation or entry declared on an abstract or base class shall attach only to paths whose
concrete class the repository defines (the mage-ai false alarm: `BaseDetailHandler.get` projected onto an
unrelated internal route).
10. `[2026-10-29]` A guard already recorded on a hop before a call shall be carried into the pair's trace and
dominance verdict; the prover shall not emit a finding whose own trace shows a dominating guard of the
required kind (the trigger.dev false alarm of gate 3).
1. When a touched function or reachable helper contains a protected operation, the prover shall construct caller
chains from recognised entries to that operation within the configured work budget.
2. When a call target can be identified by module, enclosing type, method and signature, the prover shall keep
that identity distinct from every other same-name target.
3. When more than one call target remains feasible, the prover shall preserve the finite candidate set and shall
mark the call incomplete rather than merge the candidates under a bare name.
4. If no bounded candidate set can be established, the prover shall mark the call unresolved and shall not use it
to create a final discharged or unguarded judgment.
5. When an operation or entry is declared on a base type, the prover shall attach it only to repository-defined
concrete descendants whose resolved identities support that attachment.
6. When a helper calls another helper that contains an operation or governing authorization evidence, the prover
shall continue the analysis through that call while budget remains.
7. When recursion is present, the prover shall terminate and shall preserve the accumulated operations,
authorization effects and incompleteness reasons for the recursive component.
8. If depth, fan-out, time or another work bound truncates exploration, the prover shall record the exact cutoff
reason and shall treat affected pairs as incomplete.
9. When a complete chain is materialised, the prover shall emit its hops from entry to operation with the call
site of every transition.
10. When multiple complete chains reach the same operation, the prover shall evaluate each chain independently so
that a guarded chain cannot hide an unguarded chain.
### Requirement 4: The verdict
**Objective:** As a developer about to push, I want a finding only when the tool can show an unguarded path to a
protected operation, so that every alarm names what to add and where.
### Requirement 4: Access-control verdicts
**Objective:** As a developer about to push, I want a finding only for a demonstrated unguarded path, so that every
alarm names a concrete security obligation and repair location.
#### Acceptance Criteria
1. When a protected operation is reached by a path record and no witness of a required discharger kind
dominates it along that path or inside its function, the prover shall emit one finding per operation and
missing kind with family `access_control`, the operation's path and line, the entry point, the hops, the
missing kind and the evidence strings.
1a. The prover's premise shall be explicit: a recognised entry point is protected unless the framework or a
declared policy marks it public. The checker shall apply that premise only when the caller opts in, shall
label such findings `unguarded_path` (distinct from `declared_policy_violation` and
`consistency_violation`), and shall say in the evidence that no policy clause and no sibling norm backed it.
The checker's default behaviour with path records (no clause, no sibling norm, no finding) is unchanged.
2. When every required discharger kind is dominated, the prover shall emit no finding and shall count the
operation as `discharged` in the coverage statement.
3. The prover shall never emit a finding for a function whose file failed to parse, whose language has no
obligation facts, or whose framework is unrecognised; each such case shall be counted under its reason.
4. The prover shall make no model call.
5. The prover shall report findings in the hunt defect shape (`path`, `line`, `family`, `evidence`,
`function`), so the existing scoring, the proof arm and the pre-push admission consume them unchanged.
6. When a touched function holds no protected operation and reaches none, the prover shall emit nothing for it.
1. When a complete path reaches a protected operation, at least one required authorization kind is proven absent,
and every other required kind is proven either governing or absent, the prover shall judge the pair
`unguarded` and shall report each absent kind.
2. When every required authorization kind is proven to govern the operation on a complete path, the prover shall
judge the pair `discharged`.
3. If the evidence is insufficient for either final judgment, the prover shall judge the pair `indeterminate`.
4. When a pair is `unguarded`, the prover shall emit one finding per operation and missing authorization kind with
family `access_control`, exact source anchor, entry, hops and evidence.
5. When a pair is `discharged` or `indeterminate`, the prover shall emit no vulnerability finding for that pair.
6. The prover shall apply the recognised-entry-is-protected premise only in its explicit opt-in mode and shall
identify that premise in the evidence.
7. The prover shall make no model call and shall return findings in the existing hunt-defect contract.
### Requirement 5: Scoring as a rule arm on the pool
**Objective:** As a maintainer, I want the prover scored on the training pool with the standard metrics, so that
its number stands beside the hunter arms and decides whether it becomes a G1 arm.
### Requirement 5: Development measurement
**Objective:** As a maintainer, I want repeatable development measurements, so that changes to facts and analysis
can be attributed without being mistaken for admission evidence.
#### Acceptance Criteria
1. The runner shall run the prover over every change of a pool slice on the training clones, one checkout per
change, and shall write one record per change in the replay record shape that `benchmarks.valid.metrics`
scores (id, kind, repo, status, findings, bytes read, seconds).
2. The runner shall report recall on fix changes and on verified locations with Wilson intervals, false alarms
per 100 ordinary pushes, precision, instrument failures, and seconds per change, and shall refuse to
summarise when any record read zero bytes of a tree git holds.
3. The runner shall report a per-framework and per-repository coverage table: entry points found, touched
functions reached, operations checked, functions unchecked by reason.
4. The runner shall checkpoint records per change and resume by skipping finished ones.
5. The record of a run shall carry counts, digests and framework names, never repository names of reserved
populations, as the reserved-repository guard requires.
6. `[2026-10-29]` The runner shall refuse to summarise a run whose findings anchor to a path that is not
present in the change's analysed tree, the same way it refuses zero-byte reads: a finding on an absent file
is an instrument defect, not a result (the crx stale anchor of gate 3).
1. When a development population is run, the runner shall write one resumable record per change with analyzer,
fact-set, population and input digests.
2. When development records are summarised, the runner shall report recall on fix changes and verified locations,
flagged ordinary pushes per 100, precision, vulnerable/fixed pair correctness, instrument failures and runtime
distributions with uncertainty intervals where defined.
3. The runner shall report a stage funnel from entry recognition through call resolution, operation discovery,
control-flow evaluation, witness matching and pair judgment.
4. The runner shall report per-target-family and per-repository coverage without disclosing reserved repository
identities in shared records.
5. When comparing an amended analyzer and amended facts, the runner shall support the four registered arms formed
by old/new analyzer crossed with old/new fact set.
6. If any record reads zero bytes for a file present in the analysed tree or anchors a finding outside files read,
the runner shall mark an instrument failure and shall refuse the summary.
7. The runner shall label results from populations used to choose facts or analyzer behavior as development or
regression evidence and shall not present them as admission evidence.
### Requirement 6: Budget
**Objective:** As a developer, I want the prover to finish inside the pre-push window, so that it can run on
every push.
### Requirement 6: Pre-push budget
**Objective:** As a developer, I want the prover to finish inside the pre-push window, so that eligible detection
can run on every push.
#### Acceptance Criteria
1. The prover shall finish a change of the pool within 120 seconds on the development host, measured by the
runner, and the runner shall list every change over that bound.
2. The prover shall reuse the per-change context caches (product files, texts, call index, file IR, entry
points) and shall not walk or read the product tree more than once per change.
1. When a measured change exceeds 120 seconds, the runner shall fail the budget gate and shall identify the
change, stage and elapsed time.
2. The prover shall reuse per-change file, text, syntax, call, entry, symbol and summary caches and shall not read
the product tree more than once per change.
3. If analysis stops because of a deadline, the prover shall return incomplete coverage and indeterminate affected
pairs rather than a partial clean verdict.
### Requirement 7: Coverage statement
**Objective:** As a developer, I want the tool to say what it did not check, so that no finding count reads as a
clean bill.
### Requirement 7: Failure-visible coverage
**Objective:** As a developer, I want the tool to state what it could not decide, so that silence never reads as a
clean bill of health.
#### Acceptance Criteria
1. The prover shall return, with its findings, the counts of files by language, files whose framework was
recognised, entry points found, touched functions reached and unreached, operations checked and discharged,
and functions unchecked by reason (`parse_failed`, `language_unsupported`, `framework_unrecognised`,
`unreached`, `no_facts`).
2. When the coverage statement shows zero recognised frameworks for a repository, the runner shall mark the
repository `uncovered` in its summary, and recall for that repository shall be reported separately from the
covered recall.
3. `[2026-10-29]` When a recognition or resolution pass over a file raises an exception, the coverage statement
shall count that file under a reason that names the exception class, and shall never fold it into
`framework_unrecognised` or any other quiet reason (the byobu ValueErrors of gate 3 surfaced only by
inspection of the record).
1. The prover shall report files by language, recognised entry families, entries, reached and unreached targets,
operations, discharged pairs, unguarded pairs and indeterminate pairs.
2. The prover shall count unchecked or incomplete work by specific reason, including parse failure, unsupported
language, unrecognised target, absent facts, unresolved registration, ambiguous or unresolved call, exploration
cutoff, unavailable control flow, unknown resource, unknown authority and analysis exception.
3. If no target family is recognised for a repository, the runner shall mark the repository `uncovered` and shall
report its recall separately from covered recall.
4. When a recognition, resolution, summary or judgment pass raises an exception, the prover shall record the
exception class at that stage and shall not fold it into a quiet coverage reason.
5. When the same pair has multiple incompleteness reasons, the prover shall preserve every reason in the trace and
aggregate each reason in coverage.
6. The runner shall report the indeterminate rate separately and shall not include indeterminate pairs in either
clean or discharged counts.
### Requirement 8: Proof-arm feed
**Objective:** As the proof arm, I want each finding to carry what a demonstration needs, so that a finding
can be proved or dismissed without re-deriving it.
**Objective:** As the proof arm, I want each unguarded finding to carry a complete analysis witness, so that a
demonstration can be attempted without re-deriving the decision.
#### Acceptance Criteria
1. Each finding shall carry the entry point record (kind, path, line, evidence), the hop list, the operation
(id, kind, calls matched, line) and the missing discharger kind.
2. The finding id shall be `obligation:<operation kind>:<path>:<line>:<missing>` as the checker already emits.
1. When a finding is emitted, it shall carry the entry record, exact hop list, operation, missing authorization
kind, resource and authority relations, pair judgment, trace identifier and completeness evidence.
2. The finding identifier shall remain stable for the same operation, source anchor and missing authorization
kind across deterministic replays.
3. If the pair is indeterminate, the prover shall expose its trace only as diagnostic coverage and shall not send
it to the proof arm as a vulnerability finding.
### Requirement 9: The ordered trace is the prover's output for every examined pair
**Objective:** As the maintainer, I want every (entry, operation) pair the prover examined serialised as an ordered
trace with its verdict, so that a decision model sees the security-relevant structure and nothing else.
### Requirement 9: Ordered traces for every examined pair
**Objective:** As a maintainer, I want every examined pair represented by the same deterministic trace, so that
decisions and degradations can be inspected and compared.
#### Acceptance Criteria
1. For every path record and every protected operation at its end, the prover shall emit one trace: the entry
(kind, trust boundary, declared access, parameter names), each hop in order (function, the call line, the
guards that dominate the call in that function with their kind, scope, line and provenance, the entry
parameters and request-derived values forwarded through the call), the operation (kind, fact id, line, the
provenance of each argument: request_input, authenticated_context, constant, unknown), the guards after the
operation, and the verdict (required kinds, dominating kinds, missing kind or discharged).
2. The prover shall emit traces for discharged pairs and for unguarded pairs alike, and shall list unreached
targets with their reason, so that negatives carry the same structure as positives.
3. The argument provenance shall be traced within each hop's function through the IR's binds from the entry's
parameters and the request-source facts, forwarded through the call sites on the path; a value the trace
cannot follow shall be `unknown`, never `constant`.
4. The trace shall carry no language, framework name, repository name, file size, line count, or any count over
the function or the repository: only the ordered components above.
5. The prover-arm record shall carry the traces, and a trace shall serialise to one canonical text form (one
component per line, in order) and one canonical JSON form, both deterministic.
6. The hunt defect of an unguarded pair shall reference its trace, so the proof arm and the pre-push admission
can show the path.
1. When the prover examines an entry-operation path, it shall emit one trace containing the entry, ordered hops,
operation, authorization witnesses, resource and authority relations, control-flow relations, completeness
state and pair judgment.
2. The prover shall emit traces for unguarded, discharged and indeterminate pairs alike.
3. When an argument or resource is forwarded through a call, the trace shall preserve its origin through the
resolved hop; if origin cannot be followed, the trace shall record `unknown`.
4. The trace shall carry target knowledge identifiers and source evidence needed to reproduce the judgment but
shall omit repository identity, file-size and repository-size features from decision input.
5. The trace shall serialise to one deterministic canonical text form and one deterministic canonical JSON form.
6. When a work bound or fallback would have been required, the trace shall record the cutoff or unavailable
semantic fact rather than silently substituting a weaker analysis.
7. The runner shall persist traces and their analyzer, fact-set and input digests with the corresponding record.
### Requirement 10: Control flow decides dominance, and the trace says so
**Objective:** As the maintainer, I want a guard to count only when control flow makes the operation unreachable
without it, so that a check in a branch the attacker can avoid, or one whose result is ignored, is not a discharge.
### Requirement 10: Control-flow-sensitive dominance
**Objective:** As a maintainer, I want authorization evidence to count only when control flow guarantees it governs
the operation, so that avoidable or ignored checks do not discharge obligations.
#### Acceptance Criteria
1. The prover shall build a structural control-flow graph per function on a path from the syntax tree it already
parses: statement blocks, if and else branches, switch arms, loops, try, catch and finally, return, throw,
break and continue, and the function's exit; with no grammar for a construct the function shall be marked
`cfg_unavailable` and judged by order with that degradation recorded.
2. A guard shall dominate an operation when every path from the function's entry to the operation passes the
guard's statement (dominators over the graph); the trace shall record each guard's relation to the operation
as `all_paths`, `some_paths`, `none` or `after`.
3. The trace shall record each guard's failure effect: `throws`, `returns`, `exits` (break, continue, exit call),
`branches` (the operation lies only inside the guard's passing branch), or `value_only` when the guard's
result is not consumed by a branch or an exit before the operation; a `value_only` guard shall not discharge.
4. The trace shall record the operation's control context: the branch conditions it sits under in order (the
guard's own condition when it is one), whether it is inside a loop, inside a try whose catch swallows the
guard's exception type, or on an exception path.
5. Router and decorator witnesses shall keep their scope semantics (they dominate the handler as a whole).
6. The parameter list of a Java entry shall carry parameter names only, never annotation names; the arguments
of a handler-named operation shall be the handler's own parameters with their provenance.
7. The existing order-based dominance shall remain the default of the checker; the prover shall select the
control-flow dominance, and the checker's tests shall stay unchanged.
1. When authorization evidence appears before an operation, the prover shall determine whether every feasible
path to the operation passes that evidence.
2. When a guard's failing branch terminates or the operation exists only in the passing branch, the prover shall
record the guard effect and may treat it as governing the operation when resource and authority also match.
3. When a predicate result is ignored or appears only on some paths, the prover shall not treat it as a governing
authorization check.
4. When an operation is inside a loop, exception region or conditional branch, the trace shall record that control
context and any exception handling that can bypass the guard.
5. When route- or declaration-scoped authorization evidence covers the whole handler, the prover shall preserve
that declared scope without inventing a statement location.
6. If control-flow semantics or source-to-control-flow correspondence is unavailable, the prover shall mark the
affected relation unknown and the pair indeterminate rather than decide it by textual order.
7. The existing order-based checker mode shall remain available to callers that do not opt into prover semantics.
### Requirement 11: Three-way abstract judgment
**Objective:** As a maintainer, I want uncertainty to propagate monotonically through the analysis, so that losing
precision cannot manufacture a vulnerability or a discharge.
#### Acceptance Criteria
1. The prover shall represent call resolution, path completeness, resource compatibility, authority compatibility
and dominance with values that distinguish definite, possible, absent and unknown evidence.
2. When all required evidence on a complete path is definite and at least one required obligation has no governing
witness, the prover shall produce `unguarded` and shall report every such obligation, including when other
required obligations are definitely governed.
3. When every required obligation has a definite governing witness on a complete path, the prover shall produce
`discharged` for that path.
4. If any required relation is possible or unknown, or path enumeration is incomplete, the prover shall produce
`indeterminate` unless another complete path already proves `unguarded`.
5. When multiple paths reach one operation, the prover shall aggregate them as `unguarded` if any complete path
is unguarded, `discharged` only if enumeration is complete and every path is discharged, and `indeterminate`
otherwise.
6. When analysis precision is reduced by adding an unresolved candidate or removing semantic information, a final
judgment shall remain unchanged or move to `indeterminate`; it shall not switch directly between `unguarded`
and `discharged`.
7. The prover shall keep abstract-domain operations deterministic and independent of target names.
8. The runner shall expose judgment counts and transitions between compared arms so that precision loss cannot be
hidden inside aggregate recall or false-alarm metrics.
### Requirement 12: Label-isolated admission workflow
**Objective:** As a maintainer, I want admission measured on untouched, reviewed evidence after the analyzer and
facts are frozen, so that a better development score is not mistaken for generalisation.
#### Acceptance Criteria
1. When an admission run is prepared, the workflow shall freeze the analyzer revision, promoted fact-set digest,
acquisition record, configuration and population manifest before opening admission labels.
2. While acquiring target knowledge for an admission repository, the workflow shall use repository source and
approved documentation without vulnerability, fix-location or ordinary-change labels.
3. If acquisition observes evaluation labels or is changed in response to an admission result, the workflow shall
mark that population spent and shall not use it for admission.
4. When ordinary changes are prepared for a false-alarm denominator, the benchmark workflow shall review and
remove or relabel changes that contain real missing-authorization defects before scoring.
5. When a vulnerable case has a fixed twin, the admission run shall require the expected unguarded trace on the
vulnerable side and quiet or discharged behavior at the corresponding fixed operation.
6. When an amended prover is considered for the G1 arm, the workflow shall measure it on untouched repositories
that were not used to choose facts, recognition strategies, abstract-domain behavior or thresholds.
7. The admission report shall include recall, verified-location recall, flagged ordinary pushes per 100,
precision, paired vulnerable/fixed correctness, indeterminate rate, coverage, runtime and instrument failures
with sample counts and uncertainty intervals.
8. If the budget gate, instrument gate or pre-registered quality criteria fail, the workflow shall return NO-GO
and shall not enable the arm in pre-push admission.
9. The workflow shall retain the current 23-repository population as development regression evidence and shall
require a separate untouched population for admission claims.
@@ -205,3 +205,72 @@ records the code survey behind the design amendment (same date tag).
lane (deferred injection spec) is the designated place to grow type facts, not this spec.
- `down_depth` widens target enumeration; the fan-out bound and the 120 s budget cap it. Per-repository
seconds-per-change in the runner summary will show any cost.
---
## Abstract judgment and admission amendment -- 2026-10-29
### Context
The design review after the capability-level lift found that the proposed amendment still allowed two degraded
instruments to participate in binary decisions: unresolved receivers fell back to a bare-name call key, and
unavailable CFGs fell back to source order. It also found that the fact-initialization task used known development
misses and then remeasured the same population, which is valid regression work but cannot qualify an admission arm.
### Synthesis
1. **Generalization**: call certainty, path completeness, resource compatibility, authority compatibility and
dominance are instances of one evidence-relation problem. They share a finite domain
(`MUST | MAY | NO | UNKNOWN`) and one incompleteness record instead of separate booleans and counters.
2. **Build vs adopt**: the repository already owns the semantic IR, structural CFG, obligation checker and path
records. A small abstract-domain layer and monotone function summaries extend those contracts; adopting another
graph or abstract-interpretation engine would duplicate the current architecture and violate steering.
3. **Simplification**: the prover keeps one checker and one concrete trace. Function summaries accelerate and
stabilize interprocedural discovery, but final evidence remains a materialised path. No second verdict engine is
introduced.
### Decisions
#### Decision: three-way judgment
- **Selected approach**: every path and aggregate pair is `unguarded`, `discharged`, or `indeterminate`.
- **Invariant**: less precise evidence may preserve a verdict or move it to `indeterminate`; it may not switch
directly between the two final verdicts.
- **Algebra**: `MUST` and `NO` are incomparable, both below `MAY`, which is below `UNKNOWN`. Alternative
evidence uses the least-upper-bound table; dimensions of one witness use conjunction; witnesses for one
obligation use existential aggregation. These operations are distinct so `MUST + NO` on alternative paths
becomes `MAY`, while one definite governing witness remains sufficient for the obligation it covers.
- **Consequence**: prover mode does not use textual order as a semantic fallback. Legacy checker callers retain it.
#### Decision: stable symbols and finite candidate sets
- **Selected approach**: identify repository declarations by module, enclosing type, method and available signature.
Partial resolution returns a finite candidate set plus incompleteness; unbounded resolution returns unresolved.
- **Rejected approach**: global bare-name fallback, because it conflates unrelated methods and can both invent paths
and borrow unrelated guards.
#### Decision: resource and authority are separate abstract values
- **Selected approach**: match witness kind, scope, resource and authority independently, joining each relation into
the pair judgment. Request-global guards are valid only when the operation fact explicitly allows that scope.
- **Rejected approach**: `None` as a resource wildcard. It cannot distinguish intentionally global authentication
from missing resource evidence.
#### Decision: summaries discover; concrete paths prove
- **Selected approach**: monotone summaries over strongly connected call components collect operations, witnesses,
call target sets and incompleteness. Concrete paths are materialised from exact edges for trace evidence.
- **Trade-off**: summary and path code add one engine boundary, but they remove recursion and depth handling from
ad-hoc traversal and make every cutoff explicit.
#### Decision: development attribution before admission
- **Selected approach**: compare old/new analyzer crossed with old/new facts on the current development population,
then freeze analyzer, facts, acquisition record, configuration and a separate untouched population for admission.
- **Consequence**: the 2026-10-28 numbers remain the immutable baseline. A better rerun on the same repositories is
regression evidence, not a G1 admission claim.
### Risks and mitigations
- **Indeterminate inflation**: strict uncertainty may initially lower apparent coverage. Report the stage funnel and
reason distribution; improve the dominant reason rather than weakening the lattice.
- **Summary cost**: a repository-wide symbol graph may threaten the pre-push budget. Reuse Context caches, use finite
summary sets, and fail the budget gate when any measured change exceeds 120 seconds.
- **Authority vocabulary growth**: permission implication can become target-specific code. Keep implications in
promoted facts and include the fact identifier in every match.
- **Admission scarcity**: the current pool is already development data. Source and review a separate population
through `valid-detection-benchmark`; do not relabel the current pool as untouched.
+5 -5
View File
@@ -1,15 +1,15 @@
{
"feature_name": "access-control-prover",
"created_at": "2026-10-14T13:48:18Z",
"updated_at": "2026-10-29T15:40:00Z",
"updated_at": "2026-10-29T12:16:52Z",
"language": "en",
"phase": "requirements-generated",
"phase": "tasks-generated",
"approvals": {
"requirements": {"generated": true, "approved": false},
"design": {"generated": true, "approved": false},
"requirements": {"generated": true, "approved": true},
"design": {"generated": true, "approved": true},
"tasks": {"generated": true, "approved": false}
},
"ready_for_implementation": false,
"depends_on": ["authorization-obligations", "valid-detection-benchmark", "hunter-learning-loop"],
"approval_note": "2026-10-14 maintainer directive in session: spec the prover and queue implementation with Codex (fast-track; the three gates were passed on that instruction, review the documents before the second delivery). 2026-10-29: requirements amended after the 2026-10-28 gates 1-5 measurements (trace resolution, programmatic entry facts, failure visibility); requirements await re-approval, then design and tasks must be revisited against the amendment before implementation resumes. 2026-10-29 (later): maintainer instruction in session ('can you address the issues?') treated as requirements approval and as the directive to regenerate the design; design.md amended in merge mode against the amendment (resolution component, resource-bound and voter witnesses, programmatic Jetty registration, stale-anchor refusal, error reasons), tasks.md still to be regenerated after design approval. 2026-10-29 (latest): maintainer directives lifted the specification to the capability level -- per-target vocabulary is acquired data (one-time initialization job with extraction and proposal modes behind the facts gate), never enumerated in acceptance criteria; engine/target split, GoF pattern discipline, complexity caps, and idiomatic naming recorded in .kiro/steering/code-standards.md; breadth map added at .kiro/steering/breadth-map.md. requirements.md, design.md and tasks.md were re-lifted accordingly; all three phases re-opened for approval in order."
"approval_note": "2026-10-14 maintainer directive in session: spec the prover and queue implementation with Codex (fast-track; the three gates were passed on that instruction, review the documents before the second delivery). 2026-10-29: requirements amended after the 2026-10-28 gates 1-5 measurements (trace resolution, programmatic entry facts, failure visibility). 2026-10-29 (later): maintainer instruction in session ('can you address the issues?') treated as requirements approval and as the directive to regenerate the design. 2026-10-29 (capability lift): per-target vocabulary became acquired data; engine/target split, pattern discipline and complexity caps were recorded in steering; all phases reopened. 2026-10-29 (abstract-interpretation amendment): the maintainer accepted the design-review recommendations by asking to make the amendments. Requirements and design were regenerated with a three-way judgment, explicit evidence algebra, stable symbols, resource/authority domains, label-blind acquisition and separate admission measurement; that directive is recorded as approval to regenerate tasks. The merged task plan passed an independent task-graph sanity review after two design repairs. Tasks await human approval before implementation."
}
+326 -299
View File
@@ -1,321 +1,348 @@
# Implementation Plan
- [x] 1. Paths from the caller index
- [x] 1.1 Add `PathRecord` and `compose_paths` in `src/openultrasast/semantic/obligations/paths.py` (P)
- Upward walk through a `callers(path, function)` callable, depth and fan-out bounds, stop at the first entry,
chains kept per file, deduplicated; unreached targets returned
- Done when `tests/test_obligation_paths.py` covers depth 1 to 3, unreached, fan-out cut and same-name files,
and `check_obligations` accepts the records as `paths` (one test runs the checker with them)
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
- _Boundary: obligations.paths_
## Completed baseline
- [x] 1.2 Checker opt-in `assume_entries_protected` and label `unguarded_path` in `semantic/obligations/check.py`
- Default off; `_finding` receives the entry records' access by handler; evidence as the design states;
`findings_to_static` explains the label; `LABELS` updated
- Done when the existing checker tests pass unchanged and a new test shows: with paths, no policy, no siblings,
the default gives no finding and the opt-in gives one `unguarded_path` finding for a non-public entry and none
for an entry whose record access is `public`
- _Requirements: 4.1a_
- _Boundary: obligations.check (opt-in only)_
The completed work below produced the immutable 2026-10-28 development baseline. Amendment tasks 7-13 replace
its final resolution, dominance, fact-acquisition and measurement semantics without erasing the evidence.
- [x] 2. Entry recognisers
- [x] 2.1 Stapler and Jetty recognisers in `mapping.py` (P)
- Public `do[A-Z]...` methods on exposed types or with Stapler-typed or `@QueryParameter` parameters; Jetty
`AbstractHandler`, `HttpServlet`, JAX-RS resources; evidence strings; `public` only for `UnprotectedRootAction`
- Done when `tests/test_mapping_frameworks.py` names the entries of a fixture Descriptor, rejects a private
helper named `doX`, and the existing mapping tests still pass
- _Requirements: 1.1, 1.6, 1.7, 1.8_
- _Boundary: mapping_
- [x] 2.2 Symfony, Livewire and Laravel controller, Remix export and Tornado recognisers in `mapping.py` (P)
- Done when fixture files of each shape yield `route` entries with evidence and the negative fixtures yield none
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.7, 1.8_
- _Boundary: mapping_
- [x] 1. Baseline paths and checker premise
- [x] 1.1 Add bounded caller-index paths with concrete hop records
- Delivered depth/fan-out traversal, entry stopping, file-qualified hops and unreached targets.
- Baseline fixtures cover depth, fan-out, deduplication and multiple same-name files.
- The final call semantics are superseded by tasks 8-9.
- _Requirements: 3.1, 3.9, 3.10_
- _Boundary: Concrete Paths_
- [x] 1.2 Add the opt-in recognised-entry-is-protected premise
- Existing checker defaults remain unchanged; prover evidence identifies the premise.
- _Requirements: 4.6_
- _Boundary: Obligation Checker_
- [ ] 3. Enforcement witnesses and facts
- [x] 3.1 `semantic/obligations/enforcement.py` producing router and decorator `Discharge` witnesses (P)
- Laravel route middleware and controller `middleware`/`authorizeResource`; Symfony `IsGranted`, `Security`,
security.yaml `access_control`; Tornado `authenticated`; names listed in the facts headers
- Done when `tests/test_obligation_enforcement.py` shows a guarded Laravel route, a Symfony attribute and a
Tornado decorator each produce one witness that `OrderDominance` accepts for the handler's operation
- _Requirements: 2.1, 2.2, 2.5, 2.6_
- _Boundary: obligations.enforcement_
- [ ] 3.2 Operation and discharger rows for the pool's shapes in `ruleset/obligations/{java,php,python,typescript}.toml` (P)
- Java: credential lookups and outbound connection tests in `doTestConnection`-style handlers, item reads and
writes, `checkPermission`/`checkAnyPermission` as path guards, `hasPermission` as ownership check;
Remix: session requirements as path guards, record reads by request id as protected reads needing
`ownership_check`; Tornado: handler `open`/`get` as entries needing `path_guard`; Jetty service start as
privileged action. Every name checked in the clones or documentation, said in the header
- Done when `tests/test_obligation_facts.py` loads the files and one operation and one discharger per
language resolve
- _Requirements: 2.3, 2.4, 2.7_
- _Boundary: ruleset/obligations_
- [x] 2. Baseline entry recognition
- [x] 2.1 Add the first Java entry recognisers and evidence
- Delivered development cases for declared web methods, servlets, resources and handlers.
- _Requirements: 1.1, 1.5_
- _Boundary: Mapping_
- [x] 2.2 Add the initial PHP, JavaScript, TypeScript and Python entry recognisers
- Delivered positive and negative fixture coverage with entry evidence.
- _Requirements: 1.1, 1.5_
- _Boundary: Mapping_
- [x] 3.3 Handler-named operations: `handlers` and `framework` fields in the facts loader, `find_operations(..., handlers_of=)`, Stapler form-validation rows in `java.toml`, Tornado handler rows in `python.toml`, Remix action rows in `typescript.toml`
- Done when `tests/test_obligation_facts.py` loads a handler-named row, `tests/test_obligation_operations.py`
(or the existing operations test file) yields the operation at the declaration line only for listed
handlers, and the prover smoke on the 23 fixes reaches and checks the Jenkins form-validation methods
- _Requirements: 2.8_
- _Boundary: obligations.facts, obligations.operations_
- [x] 3. Baseline enforcement and operation facts
- [x] 3.1 Add route-, declaration- and policy-scoped enforcement witnesses
- Delivered initial external-handler witnesses and integration fixtures.
- _Requirements: 2.2, 2.4_
- _Boundary: Enforcement Adapter_
- [x] 3.2 Close the unfinished per-target fact inventory as superseded
- The partially completed inventory is not treated as complete implementation.
- Tasks 11.3-11.7 replace it with label-blind acquisition and promoted target knowledge.
- _Requirements: 2.1, 2.2, 2.9, 2.10_
- _Boundary: Target Knowledge_
- [x] 3.3 Add handler-named operation facts
- Delivered opt-in handler operations at declaration lines for recognised entries.
- _Requirements: 2.3_
- _Boundary: Fact Interpreter_
- [x] 4. The prover
- [x] 4.1 `hunt/prover.py`: `prove_change` over the hunter `Context`, `Coverage`, findings as hunt defects
- Touched functions from head lines, build output left out, targets include direct callees holding operations,
IRs of hop files passed to the checker, extra witnesses from enforcement, coverage counts by reason, no model
- Done when `tests/test_hunt_prover.py` shows: an unguarded Stapler handler calling `lookupCredentials` gives
one finding with entry, hops and missing `path_guard`; a `checkPermission` above the call gives none; a
guarded Laravel route gives none; an unrecognised framework gives coverage only; the finding dict has
`path`, `line`, `family`, `function`, `evidence`, `id`
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 6.2, 7.1, 8.1, 8.2_
- _Depends: 1.1, 2.1, 3.1, 3.2_
- [x] 4. Baseline prover, traces and structural control flow
- [x] 4.1 Integrate entries, paths, operations and witnesses in the deterministic prover
- Delivered hunt-defect findings and failure-visible baseline coverage with no model calls.
- Final three-way judgment is superseded by task 10.6.
- _Requirements: 4.7_
- _Boundary: Prover Integration_
- [x] 4.2 Emit deterministic ordered traces and local argument provenance
- Delivered canonical text/JSON traces for discharged and unguarded baseline pairs.
- _Requirements: 9.1, 9.2, 9.3, 9.5_
- _Boundary: Canonical Trace_
- [x] 4.3 Attach traces to prover records and findings
- Delivered trace references through the replay envelope.
- _Requirements: 8.1, 9.7_
- _Boundary: Prover Integration_
- [x] 4.4 Build structural control-flow graphs
- Delivered branches, loops, exits, exception regions, dominators and control context.
- _Requirements: 10.1, 10.2, 10.3, 10.4_
- _Boundary: Structural CFG_
- [x] 4.5 Add baseline CFG dominance and guard effects
- Delivered value-only rejection and control-context trace fields.
- Strict unknown semantics are superseded by task 10.1.
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
- _Boundary: CFG Dominance_
- [x] 4.2 `semantic/obligations/trace.py`: `Guard`, `Hop`, `Trace`, `text()` and `record()`; argument provenance through the IR's binds within each hop, forwarded along the path
- Done when `tests/test_obligation_trace.py` serialises a three-hop fixture deterministically (text and JSON),
traces an argument from an entry parameter to `request_input`, from a literal to `constant`, from an identity
source to `authenticated_context`, and marks an unfollowable value `unknown`
- _Requirements: 9.1, 9.3, 9.4, 9.5_
- _Boundary: obligations.trace_
- [x] 4.3 The prover emits a trace per examined pair (discharged and unguarded), the record and the defect carry them
- Done when `tests/test_hunt_prover.py` shows a discharged pair and an unguarded pair each produce one trace with
the verdict, the unguarded finding references its trace, and `prover_arm` records carry `traces`
- _Requirements: 9.1, 9.2, 9.5, 9.6_
- _Depends: 4.2_
- [x] 5. Baseline replay arm
- Delivered checkpointed records, standard metrics, coverage tables, zero-byte refusal and immutable records.
- _Requirements: 5.1, 5.2, 5.4, 5.6_
- _Boundary: Development Runner_
- [x] 4.4 `semantic/cfg.py`: structural control-flow graph per function from the syntax tree, dominators, control context (P)
- Java, PHP, Python, JavaScript and TypeScript constructs of requirement 10.1; `cfg_unavailable` otherwise
- Done when `tests/test_semantic_cfg.py` shows, per language, that a guard before an unconditional return
dominates, a guard inside an if-branch does not dominate a statement after the branch, a guard inside a
loop body does not dominate a statement after the loop, a try/catch is represented, and context reports
conditions and loops
- _Requirements: 10.1, 10.2, 10.4_
- _Boundary: semantic.cfg_
- [x] 4.5 `obligations/cfg_dominance.py::CfgDominance`, failure effects, and the trace's control components; the prover selects it; Java parameter names and handler-operation arguments cleaned
- Done when `tests/test_obligation_cfg_dominance.py` shows a value-only predicate does not discharge while the
same predicate guarding a throw does; a branch-only guard yields `some_paths` and no discharge; the prover
smoke on the 23 fixes keeps or raises its recall; `tests/test_mapping_frameworks.py` shows Java parameter
names without annotations
- _Requirements: 10.2, 10.3, 10.4, 10.5, 10.6, 10.7_
- _Depends: 4.4_
- [x] 6. Baseline development measurement
- Measured 23 repositories and 483 changes: 65.2% fix-push recall, 15.0 flagged ordinary pushes per 100,
0 instrument failures; recorded at benchmarks/measurements/2026-10-28-access-control-prover/.
- The population is development evidence only and remains immutable.
- _Requirements: 5.2, 5.7, 12.9_
- _Boundary: Development Measurement_
- [ ] 5. The arm
- [x] 5.1 `benchmarks/valid/prover_arm.py`: run over a pool slice with checkpoints, summarise with the valid metrics
- Record envelope copied from `hunter_arm`; instrument fields; resume; `summary.json` with recall (pushes and
locations, Wilson), false alarms per 100, precision, instrument failures, seconds per change and the list
over 120 s, per-repository and per-framework coverage, `uncovered` repositories; refuse on zero-byte reads
- Done when `tests/test_prover_arm.py` runs a two-change fixture pool end to end and the summary carries
every field above; a record with zero bytes read makes `summarize` refuse
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 6.1, 7.2_
- _Depends: 4.1_
## Amendment implementation
- [ ] 6. Measurement (maintainer, not Codex)
- [x] 6.1 Run the arm on `benchmarks/valid/train/pool-train.toml` over the training clones; write
`benchmarks/measurements/<date>-access-control-prover/` with NOTES, summary, coverage table; compare with the
DSPy and hunter arms of the same pool
- _Requirements: 5.2, 5.3, 7.2_
- Done 2026-10-28: `benchmarks/measurements/2026-10-28-access-control-prover/` -- 23-repo
pool (483 changes, 93 locations), recall 65.2% (Wilson 44.9-81.2), 15.0 FA/100, 0
instrument failures; on the 5 repos shared with the 2026-10-14 hunt arms, prover 4/5 vs
DSPy 2/5 and hunter 2/5 (union 4/5), including trigger.dev which neither arm found.
- [ ] 7. Abstract evidence foundation
- [ ] 7.1 Implement the evidence-relation algebra and completeness value
- Encode the specified precision order, complete join table, dimension conjunction and witness aggregation.
- Preserve every incompleteness reason deterministically and deduplicate reasons without losing stage identity.
- Done when exhaustive table tests and algebraic law tests pass for every relation pair.
- _Requirements: 11.1, 11.6, 11.7_
- _Boundary: Abstract Evidence Domain_
- [ ] 7.2 Add resource abstract values and operation-fact fields
- Represent exact resources, finite alias sets, request-global scope and unknown resources.
- Extend operation facts to state when a request-global guard is sufficient.
- Done when resource parsing and joins distinguish exact, alias, global and unknown fixtures.
- _Requirements: 2.1, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Resource Domain_
- _Depends: 7.1_
- [ ] 7.3 Add authority abstract values and promoted implication facts
- Represent exact, finite and unknown authorities separately from resources.
- Load authority implications from promoted facts and retain the supporting fact identifier.
- Done when sufficient, insufficient and unknown authority fixtures produce the specified relations.
- _Requirements: 2.1, 2.2, 2.5, 2.7, 2.8, 11.1_
- _Boundary: Authority Domain_
- _Depends: 7.1_
- [ ] 7.4 Extend judgment, trace and coverage contracts with abstract state
- Add three-way judgments, completeness and evidence relations without changing legacy defaults.
- Add canonical serialization for resource and authority relations and all incompleteness reasons.
- Done when old trace fixtures remain stable in legacy mode and new fixtures round-trip all abstract fields.
- _Requirements: 7.1, 7.2, 7.5, 7.6, 9.1, 9.2, 9.6_
- _Boundary: Judgment Contracts, Canonical Trace_
- _Depends: 7.1, 7.2, 7.3_
- [ ] 7. Resolution foundation `[2026-10-29]`
- [ ] 7.1 `CallSite` receiver capture: `receiver: str | None` field on `semantic/ir.py`'s `CallSite`, captured
in `semantic/cst.py` call extraction (receiver expression text for `$obj->method` and `x.y.z` shapes; `None`
for bare calls); every existing consumer unchanged
- Done when IR fixtures show receiver text carried through call extraction for PHP/TS shapes, `CallSite` still
constructs without it for existing callers, and all existing IR/cst tests pass unchanged
- _Requirements: 3.9_
- _Boundary: semantic.ir, semantic.cst_
- [ ] 7.2 (P) `obligations.resolution`: `ClassRecord`, `class_table(texts, language)` cached per change on the
hunter `Context`, `qualify(receiver, enclosing_class, name, arity, table)` returning `Class.name`,
`Class.name/arity`, or the bare name; base-class attachment rule (attaches to extending classes only)
- Done when `tests/test_obligation_resolution.py` covers: two same-name methods on different classes give
distinct keys; same name different arity gives distinct keys; an unresolvable receiver gives the bare name
with `receiver_unresolved` counted; an abstract-base method attaches to a subclass call and not to an
unrelated class
- _Requirements: 3.9_
- _Boundary: obligations.resolution, hunt.producers (Context cache field only)_
- [ ] 8. Guard binding and policy witnesses `[2026-10-29]`
- [ ] 8.1 (P) Resource-bound witnesses: optional `resource` field on `Discharge` (additive, default `None`);
enforcement mints tokens (route path for router middleware, attribute/subject for a policy-layer guard,
`None` for auth-class guards); policy-layer witness scoped to the action with the attribute or subject as
resource (a policy decision -- voter, capability check, or equivalent -- reachable from the action or a
function it calls). Which constructs count as policy decisions comes from the facts rows (task 10), never
engine code
- Done when `tests/test_obligation_enforcement.py` shows a resource-mismatch fixture (guard binds another
object's resource, the operation stays undischarged under `resource_bound`) and a policy-witness fixture
yielding a witness scoped to the action with the attribute as resource
- _Requirements: 2.10_
- _Boundary: obligations.enforcement, obligations.operations (dataclass field only)_
- [ ] 8.2 Checker opt-in `resource_bound` and all-kind caller-hop re-judgement in `check.py`'s `_on_caller_hop`:
a witness with `resource` set suppresses only operations whose resource token matches; every discharger kind
is re-judged on earlier hops at the hop's call line; `guards_before_call` participates in the re-judgement
- Done when the existing checker tests pass unchanged and new tests show: keyword off keeps today's behaviour
(including the `assume_entries_protected`-gated hop re-judgement); keyword on, a mismatched-resource witness
does not suppress; a guard one call deeper than the handler discharges; a guard dominating before a hop
call yields no finding
- _Requirements: 2.10, 3.8, 3.10_
- _Boundary: obligations.check (opt-in only)_
- [ ] 8. Stable symbols and conservative call resolution
- [ ] 8.1 Capture call receiver and arity evidence in the semantic IR
- Preserve receiver expression and argument count for supported syntax while keeping existing constructors valid.
- Done when language fixtures retain receiver and arity evidence without regressing existing IR consumers.
- _Requirements: 3.2, 3.3_
- _Boundary: Semantic IR_
- [ ] 8.2 Build stable declaration identities and the per-change symbol index
- Identify declarations by module, enclosing type, name and available signature.
- Cache the symbol and inheritance indexes on the per-change context.
- Done when duplicate type names, same-name methods and overloads receive stable distinct identities.
- _Requirements: 3.2, 3.5, 6.2_
- _Boundary: Symbol Index_
- _Depends: 8.1_
- [ ] 8.3 Resolve exact, finite-candidate and unresolved call targets
- Return complete exact targets, incomplete finite candidate sets, or unresolved results with reasons.
- Remove global bare-name fallback from prover-mode resolution.
- Done when ambiguity never merges methods and unresolved calls cannot produce complete paths.
- _Requirements: 3.3, 3.4, 11.1, 11.6_
- _Boundary: Call Resolution_
- _Depends: 8.2_
- [ ] 8.4 Resolve inheritance and base-declaration attachment
- Attach base operations and entries only through repository-defined descendant relations.
- Preserve ambiguous inheritance as incomplete rather than projecting to unrelated routes.
- Done when subclass and unrelated-class fixtures produce distinct attachments and judgments.
- _Requirements: 3.5_
- _Boundary: Call Resolution_
- _Depends: 8.2, 8.3_
- [ ] 9. Recogniser registry `[2026-10-29]`
- [ ] 9.1 `mapping/` package with a recogniser registry (Strategy pattern): registry table consulted by the
language dispatch in `mapping.py`; the registration-sites strategy lands first -- call-site handler
registration resolved through `resolution.class_table`, entries at the registered class's request-method
declarations with `registered_by` evidence, registration-site vocabulary carried as fact data (task 10), not
engine code; the recogniser returns unresolved registrations alongside the entries so the prover can count
them
- Done when `tests/test_mapping_registration_sites.py` shows a registration-site fixture (vocabulary from
data rows) yielding entries at the registered class's file with registration evidence, an unresolved
registration returned as such (no entry), `mapping.py`'s dispatch consults the registry, and the existing
mapping tests pass unchanged
- _Requirements: 1.9_
- _Boundary: mapping_
- _Depends: 7.2_
- [ ] 9.2 Migrate the first legacy recogniser family (Stapler) from `mapping.py`'s inline blocks into a
`mapping/` strategy module, proving the migration path; remaining families migrate per the breadth map, not
this task
- Done when Stapler entries come from `mapping/stapler.py` through the registry with the existing Stapler
tests unchanged, and `mapping.py` contains no Stapler-specific regex
- _Requirements: 1.1_
- _Boundary: mapping_
- [ ] 9. Interprocedural summaries and concrete paths
- [ ] 9.1 Extract local function summaries
- Collect each function's operations, witnesses, resolved calls and local completeness without traversing callers.
- Done when summaries are deterministic and contain no target names outside fact identifiers.
- _Requirements: 3.1, 3.6, 11.7_
- _Boundary: Function Summaries_
- _Depends: 7.4, 8.3_
- [ ] 9.2 Compute monotone summaries over recursive call components
- Iterate finite summary sets over strongly connected components until stable.
- Done when direct recursion and mutual recursion terminate with stable accumulated evidence.
- _Requirements: 3.7, 11.6_
- _Boundary: Function Summaries_
- _Depends: 9.1_
- [ ] 9.3 Add summary caches and explicit exploration cutoffs
- Reuse summaries per change and carry depth, fan-out, deadline and other cutoff reasons into completeness.
- Done when each cutoff fixture returns accumulated evidence plus the exact incomplete reason.
- _Requirements: 3.8, 6.2, 6.3, 7.2_
- _Boundary: Function Summaries, Context Cache_
- _Depends: 9.2_
- [ ] 9.4 Materialise exact evidence paths from summaries
- Emit concrete entry-to-operation hops only through exact target edges.
- Preserve every complete chain independently and retain candidate-edge diagnostics separately.
- Done when guarded and unguarded paths to one operation remain separate and candidate edges never complete a path.
- _Requirements: 3.9, 3.10, 11.4, 11.5_
- _Boundary: Concrete Paths_
- _Depends: 9.3_
- [ ] 10. Fact initialization job `[2026-10-29]`
- [ ] 10.1 (P) `tools/fact_init.py`: the one-time initialization pass per target. Extraction mode walks the
legacy `mapping.py` blocks and current TOML rows and extracts embedded target vocabulary into the acquirable
structures. Proposal mode takes an LLM or maintainer proposal for a target's vocabulary (entry shapes,
registration sites, handler-name families, policy-decision constructs). The gate: every row passes the facts
header discipline (source, version, region) and engine verification admits it (the row must match what the
target's code says); only then promoted to a closed row
- Done when `tests/test_fact_init.py` shows: extraction mode recovers the vocabulary of one legacy family
into rows; a proposal matching a fixture repository is promoted; a proposal the fixture contradicts is
rejected with the reason; nothing lands in the TOML files without passing the gate
- _Requirements: 2.7_
- _Boundary: tools.fact_init, ruleset/obligations (rows it produces)_
- [ ] 10.2 First initialization run for the training pool's target families: produce the handler-name rows for
webhook/build-trigger and data-exposure shapes, the registration-site vocabulary, and the policy-decision
constructs, as data rows admitted through the gate (names verified against the pool clones; header updated)
- Done when the pool's gate-1 miss shapes (Stapler webhook/data-exposure handlers, programmatic registration,
policy voters) are all carried as rows with headers, no engine module names a target, and
`tests/test_obligation_facts.py` loads each family
- _Requirements: 2.7, 2.9, 2.10, 1.9_
- _Boundary: ruleset/obligations_
- _Depends: 10.1, 9.1, 8.1_
- [ ] 10. Strict authorization judgment
- [ ] 10.1 Return abstract dominance relations without prover fallback
- Return MUST, MAY, NO or UNKNOWN with guard effect and control context.
- Keep legacy order dominance unchanged, but return UNKNOWN in prover mode for unavailable graphs or line mappings.
- Done when no prover fixture is discharged or flagged solely through textual-order fallback.
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7_
- _Boundary: CFG Dominance_
- _Depends: 7.1_
- [ ] 10.2 Match witness kind, scope and resource
- Apply request-global sufficiency only when the operation fact declares it.
- Return NO for definite mismatch and UNKNOWN for unresolved resource compatibility.
- Done when same-resource, alias, global, mismatch and unknown fixtures match the design algebra.
- _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Witness Matcher_
- _Depends: 7.2, 10.1_
- [ ] 10.3 Match authority and promoted implications
- Apply only promoted authority implications and preserve their evidence.
- Return NO for insufficient authority and UNKNOWN when compatibility cannot be decided.
- Done when stronger, weaker, unrelated and unknown authorities produce the expected relations.
- _Requirements: 2.2, 2.5, 2.7, 2.8, 11.1_
- _Boundary: Witness Matcher_
- _Depends: 7.3, 10.1_
- [ ] 10.4 Compute one judgment per complete or diagnostic path
- Conjoin witness dimensions, aggregate witnesses per obligation and apply the mixed-obligation rule.
- Emit discharged only for all-MUST, indeterminate for any MAY/UNKNOWN, and unguarded for at least one NO.
- Done when MUST/NO, MAY/NO, UNKNOWN and empty-witness fixtures match the specified truth table.
- _Requirements: 4.1, 4.2, 4.3, 11.2, 11.3, 11.4_
- _Boundary: Pair Judgment_
- _Depends: 10.2, 10.3_
- [ ] 10.5 Aggregate multiple paths in the opt-in checker mode
- Return unguarded if any complete path is unguarded, discharged only for complete all-discharged enumeration,
and indeterminate otherwise.
- Keep the legacy checker API and defaults unchanged.
- Done when mixed guarded, unguarded and incomplete path sets produce the specified aggregate.
- _Requirements: 3.10, 4.1, 4.2, 4.3, 4.4, 4.5, 11.5_
- _Boundary: Obligation Checker_
- _Depends: 9.4, 10.4_
- [ ] 10.6 Integrate three-way judgments into the prover
- Emit hunt findings only for unguarded pairs and send no indeterminate pair to the proof arm.
- Carry all judgments, completeness reasons, resources, authorities and stable identifiers in traces and coverage.
- Record stage-specific exceptions without folding them into quiet coverage.
- Done when end-to-end fixtures distinguish unguarded, discharged and indeterminate records and findings.
- _Requirements: 4.4, 4.5, 4.6, 4.7, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 8.1, 8.2, 8.3, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7_
- _Boundary: Prover Integration, Canonical Trace_
- _Depends: 7.4, 9.4, 10.5_
- [ ] 11. Prover resolution integration `[2026-10-29]`
- [ ] 11.1 Target enumeration joins callees by qualified key with a `down_depth` budget (default 2, sharing the
fan-out bound) so operations one call deeper are reached; the `callers` adapter keeps only callers whose
receiver binds to the target's class or subclass; unresolvable receivers fall back to the bare name and are
counted `receiver_unresolved`; unresolved registrations from mapping are counted `registration_unresolved` in
`Coverage`
- Done when `tests/test_hunt_prover.py` shows: an operation one call deeper than the handler is reached and
checked; a same-name method on another class yields a separate verdict (no cross-contamination); coverage
carries `receiver_unresolved` when a receiver does not resolve and `registration_unresolved` when a
registration target is outside the repository
- _Requirements: 3.7, 3.9_
- _Boundary: hunt.prover_
- _Depends: 7.1, 7.2, 9.1_
- [ ] 11. Target knowledge registry and acquisition
- [ ] 11.1 (P) Add the entry-recognition strategy registry
- Route mapping dispatch through one registry without embedding target names in engine modules.
- Done when a fixture strategy registers, runs and records its strategy identifier and evidence.
- _Requirements: 1.1, 1.5, 1.7_
- _Boundary: Mapping Registry_
- _Depends: 7.1_
- [ ] 11.2 Add the generic call-site registration strategy
- Resolve repository-defined registered handlers through stable symbols and return unresolved registration coverage.
- Done when exact and unresolved registration fixtures produce entries or coverage respectively.
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6_
- _Boundary: Registration Adapter_
- _Depends: 8.4, 11.1_
- [ ] 11.3 (P) Extract existing target knowledge into candidate records
- Read legacy recognition strategies and current facts without changing promoted outputs.
- Done when extraction produces deterministic candidate records with source/version/region evidence.
- _Requirements: 2.9_
- _Boundary: Fact Initialization_
- _Depends: 7.1_
- [ ] 11.4 Verify and promote proposed target knowledge
- Accept source- or approved-documentation proposals, reject contradictions, and write only verified outputs.
- Done when matching proposals promote and contradicted or evidence-free proposals leave promoted knowledge unchanged.
- _Requirements: 2.9, 2.10_
- _Boundary: Fact Initialization_
- _Depends: 11.3_
- [ ] 11.5 Record acquisition provenance and enforce label-isolated mode
- Persist input revision, evidence digest, accepted/rejected proposals and output digest.
- Reject admission acquisition inputs containing vulnerability, fix-location or ordinary-change labels.
- Done when changed output after labels are opened marks the population spent.
- _Requirements: 12.1, 12.2, 12.3_
- _Boundary: Fact Initialization, Admission Guard_
- _Depends: 11.4_
- [ ] 11.6 Migrate one legacy recogniser through the registry
- Move one development-evidence family out of inline dispatch without changing its entries.
- Done when old fixtures stay stable and inline target-specific matching is removed for that family.
- _Requirements: 1.1, 1.7_
- _Boundary: Mapping Registry_
- _Depends: 11.1_
- [ ] 11.7 Run the first label-blind development acquisition
- Produce registration, handler-operation, enforcement, resource and authority knowledge through the gate.
- Treat the output as development data and record its acquisition digest; do not encode names in engine modules.
- Done when promoted outputs load, rejected proposals have reasons, and the old unfinished task 3.2 has no residue.
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 5.7, 12.9_
- _Boundary: Target Knowledge_
- _Depends: 10.3, 11.2, 11.5, 11.6_
- [ ] 12. Arm and coverage `[2026-10-29]`
- [ ] 12.1 (P) Recognition or resolution exceptions inside `prove_change` are recorded under
`error:<exception class name>` reason keys in coverage, never folded into `framework_unrecognised` or any
other quiet reason
- Done when a fixture raising inside a recognition or resolution pass counts under `error:<class>` and
`framework_unrecognised` stays empty for that fixture
- _Requirements: 7.3_
- _Boundary: hunt.prover_
- [ ] 12.2 (P) Stale-anchor refusal in `prover_arm._prove`: every returned finding's `path` is validated
against the files actually read; an absent path marks the record `instrument_failure` with reason
`stale_anchor` and findings cleared; `summarize` refuses such runs listing the record ids
- Done when `tests/test_prover_arm.py` shows a fixture finding on an absent path makes `_prove` mark the
record and makes `summarize` refuse, mirroring the zero-byte refusal
- _Requirements: 5.6_
- _Boundary: prover_arm_
- [ ] 12. Development and admission runners
- [ ] 12.1 Materialise and verify baseline and candidate analyzer bundles
- Resolve analyzer revisions, fact sets, acquisition records and configurations to immutable digest-verified inputs.
- Refuse comparison when any bundle artifact is unavailable or mismatched.
- Done when the immutable baseline and a fixture candidate bundle can be reconstructed exactly.
- _Requirements: 5.1, 5.5, 12.1_
- _Boundary: Run Manifests_
- [ ] 12.2 Add record provenance, stage funnel and judgment transitions
- Persist analyzer, fact, acquisition, configuration, population and input digests per record.
- Summarise every analysis stage, completeness reason and transition between compared judgments.
- Done when a fixture run accounts for every record from entry recognition through final judgment.
- _Requirements: 5.1, 5.3, 5.4, 7.1, 7.6, 9.7, 11.8_
- _Boundary: Development Runner_
- _Depends: 10.6, 12.1_
- [ ] 12.3 Enforce instrument and hard runtime gates
- Preserve zero-byte and stale-anchor refusal and add stage-aware deadline records.
- Fail the run if any measured change exceeds 120 seconds; never summarize the overrun as a passing result.
- Done when zero-byte, stale-anchor, deadline and 121-second fixtures all refuse the summary.
- _Requirements: 5.6, 6.1, 6.3_
- _Boundary: Runner Gates_
- _Depends: 12.2_
- [ ] 12.4 Register the four-arm development comparison
- Run baseline/candidate analyzer crossed with baseline/candidate facts over identical inputs.
- Report metric deltas, stage-funnel deltas and judgment transitions without admission language.
- Done when fixture arms isolate a fact-only lift from an analyzer-only lift.
- _Requirements: 5.2, 5.5, 5.7, 11.8_
- _Boundary: Development Runner_
- _Depends: 12.1, 12.2, 12.3_
- [ ] 12.5 Consume and validate an externally reviewed admission manifest
- Accept only a manifest produced by valid-detection-benchmark with review, isolation and immutable digest fields.
- Reject label exposure, changed acquisition output or a spent population.
- Done when valid, malformed, exposed and spent manifests produce deterministic accept/refuse results.
- _Requirements: 12.1, 12.2, 12.3, 12.6, 12.8_
- _Boundary: Admission Runner_
- _Depends: 11.5, 12.3_
- [ ] 12.6 Add paired scoring and the admission report
- Score vulnerable/fixed twins, reviewed ordinary changes, indeterminate rate, coverage, runtime and instruments.
- Preserve sample counts and uncertainty intervals and return NO-GO on any pre-registered gate failure.
- Done when fixture pairs detect vulnerable-only, fixed-side regression and mislabeled-negative cases.
- _Requirements: 5.2, 12.4, 12.5, 12.7, 12.8, 12.9_
- _Boundary: Admission Runner_
- _Depends: 12.5_
- [ ] 13. Measurement after the amendment (maintainer, not Codex) `[2026-10-29]`
- [ ] 13.1 Re-run the arm on `benchmarks/valid/train/pool-train.toml` after tasks 7-12 land; write
`benchmarks/measurements/<date>-access-control-prover-v2/` with NOTES, summary, coverage table; compare
against the 2026-10-28 gate 1 run (recall 65.2%, FA 15.0/100) on the same pool
- _Requirements: 5.2, 5.3, 7.2_
- [ ] 13. Cross-cutting validation and measurements
- [ ] 13.1 Validate abstract-domain and metamorphic laws
- Cover the full join table, conjunction, witness aggregation, monotonicity and mixed-obligation rule.
- Prove that type renaming preserves a judgment and adding ambiguity can only preserve it or make it indeterminate.
- Done when property and fixture tests pass deterministically across repeated runs.
- _Requirements: 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_
- _Boundary: Abstract Evidence Validation_
- _Depends: 10.5_
- [ ] 13.2 (P) Validate cross-component analysis scenarios
- Exercise helper guards, registration entries, base attachment, resource mismatch, authority insufficiency,
ambiguous calls, unavailable CFGs, recursion and budget cutoff.
- Done when every scenario yields the expected trace, coverage reasons and three-way judgment.
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.5, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 4.1, 4.2, 4.3, 10.6_
- _Boundary: Prover Integration Validation_
- _Depends: 10.6, 11.7_
- [ ] 13.3 Run the four-arm development regression
- Run all four registered arms on the immutable current development population after proving nonzero input reads.
- Publish analyzer/fact attribution, stage funnel, three-way judgments, budget result and comparison to the
2026-10-28 baseline; label the result development-only.
- Done when all arm records and summary digests are complete and no admission claim appears.
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 6.1, 7.3, 11.8, 12.9_
- _Boundary: Development Measurement_
- _Depends: 12.4, 13.1, 13.2_
- [ ] 13.4 Run admission from an externally prepared untouched manifest
- Consume a reviewed manifest from valid-detection-benchmark after analyzer and facts are frozen.
- Verify label isolation, paired vulnerable/fixed behavior, reviewed ordinary labels and every admission gate.
- Done when the report returns an evidence-backed GO or NO-GO without modifying facts, strategies or thresholds.
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9_
- _Boundary: Admission Measurement_
- _Depends: 12.6, 13.3_
### Task constraints `[2026-10-29]`
## Task constraints
From `.kiro/steering/code-standards.md`, binding on every task above:
- Complexity: McCabe at most 10 per touched function, at most 50 logical lines per function and 400 per new module.
- Engine/target split: abstract-domain, resolution, summary, judgment and trace modules contain no target names.
- Target knowledge: names enter only through promoted facts or registered target-aware strategies.
- Compatibility: legacy checker defaults and completed baseline records remain unchanged.
- Instrument rule: validation must prove nonzero input reads before interpreting any clean or zero result.
- Complexity caps: McCabe <= 10 per function; <= 50 LLOC per function; <= 400 LLOC per new module. A task is
not done while a function it touched exceeds a cap; split the function.
- Pattern discipline: tasks 9.1-9.2 implement Strategy + Registry; task 10.1 implements the acquisition
pipeline behind the Interpreter discipline (rows are the language); task 11.1 is the Adapter at the
engine seam. New recognisers land in `mapping/` strategy modules, never as `mapping.py` blocks.
- Idiomatic naming: names state what a value semantically is in the pipeline vocabulary; collections name
their elements (`entry_point_records`, `judgments_by_key`); booleans are predicates; no abbreviations like
`eps`, `recs`, `d2`.
## Baseline implementation record
## Implementation Notes
- Delivery A (2026-10-14) implements paths, the opt-in checker premise, Stapler/Jetty/Servlet/JAX-RS
entries, the Java facts, the deterministic prover and the checkpointed arm. No commit was made.
- Task 3.2 stays open for the PHP, Python and TypeScript facts. Task 4.1's delivery A core is delivered;
its full-spec checkbox stays open for delivery B's enforcement witnesses and guarded Laravel acceptance
case. This delivery intentionally passes `extra_witnesses=()` as required by the maintainer's brief.
- Default checker behavior is preserved. Only opt-in checking includes a caller's statement witness,
using the existing dominance contract at the outgoing call site. The prover checks each complete path
independently so a guarded chain cannot hide an unguarded chain to the same operation.
- Java additions were checked against actual Git HEAD source objects in the sparse training clones;
checked call sites are named in the Java TOML header. No network or model call was made.
- Mapping accepts cached source texts and an opt-out from external manifest scanning. Default callers
retain their previous behavior; the delivery A prover uses source entries and its Context caches.
- Delivery A review was performed in the main context because the brief forbids model calls. The exact
requested static gate passed (format, Ruff and mypy); the exact pytest gate passed with 112 passed,
zero failed, exit 0. UV_NO_SYNC=1 was set; UV_CACHE_DIR was placed under /tmp because the default
cache is outside the writable sandbox. The unchanged unjustified-path checker test remains intact.
- Delivery B (2026-10-14) implements task 2.2's Symfony, Livewire/Laravel, Remix and Tornado
entries, task 3.1's scoped enforcement witnesses, task 3.3's handler facts and operation discovery,
and task 4.1's integration. Changes remain uncommitted as the brief requires.
- Handler facts are explicitly opt-in through file-qualified handler maps in the checker. The prover
uses handler obligations where a Stapler or Tornado entry reaches no listed call operation; existing
call-site findings and Delivery A tests remain unchanged. Remix retains its handler operation even
when listed calls exist, and independently requires a path guard and an ownership check. Public
access waives only the path guard for these handler facts. The checker default is unchanged.
- The prover supplies a local dominance adapter for handler facts: it checks guards within the handler
body while reporting the operation at its declaration. A final ordinary return does not turn a bare
permission predicate into a guard. OrderDominance and the existing producers and arms are unchanged.
- Remix arrow exports are named locally in the prover's IR copy, including TSX route modules. Symfony
recognises positional and named route paths and class prefixes. Tornado recognises local subclasses
and the pool terminal's terminado base when it explicitly invokes Tornado's WebSocketHandler method.
- PHP CST Point.row access crashed on the larger Kimai controller with the installed native parser.
New recognisers derive line numbers from byte offsets instead; a fixture past line 320 and the
23-change source smoke complete without a crash. Existing CST code was not modified.
- Task 3.2 remains partially complete: TypeScript adds clone-verified requireUserId and
authenticateApiRequest, retaining requireUser and authorize; PHP rows are retained and their header
records enforcement sources. Python retains the existing authenticated decorator and gains the
Tornado handler operation. No get_current_user fact was added: Git searches of Mage HEAD, the pool
revision and websocket fix 66440e4d3 found neither that API nor an authenticated decorator use, and
local framework documentation was unavailable. The brief forbids network access and unsourced names.
- Review used the main-context fallback because the brief prohibits model calls. Verdict: APPROVED
for tasks 2.2, 3.1, 3.3 and 4.1; task 3.2 remains open for its source-verification gap. No new
placeholder markers, credentials, dependencies, producer edits or arm edits were introduced.
- Fresh verification: UV_NO_SYNC=1 and UV_CACHE_DIR=/tmp/ousast-uv. The exact static gate passed:
ruff format --check ., ruff check ., mypy src/openultrasast (178 source files). The exact requested
pytest gate plus test_obligation_operations.py passed with 145 passed, zero failed, exit 0.
Initial missing-feature tests produced 24 failed and 14 passed; integration tests initially produced
eight failed and 20 passed. No test_search_* test was run.
- A separate file-scoped smoke read immutable Git source objects for all 23 access-control changes,
validated their commit objects and source digests, and recorded nonzero input bytes for every change.
All 23 executions completed, zero errors; 16 changes checked operations and seven remained unchecked.
It materialised only changed source files in temporary directories, so it is not a full-repository
recall measurement. The maintainer-owned task 6.1 and release readiness remain unclaimed.
- Delivery C (2026-10-14) implements tasks 4.2 and 4.3: ordered traces for every examined
path/operation pair, argument origins through local bindings and forwarded parameters, traces in
prover results and arm records, and trace references on unguarded findings. Existing checker
decisions and coverage accounting are unchanged. Declared route access is retained in the trace.
- Delivery C review used the main context because the brief prohibits model calls. Verdict: APPROVED
for tasks 4.2 and 4.3. Regression cases cover unresolved collections, overwritten bindings,
source-name boundaries, missing IR arguments, method parameter forwarding and return-order guards.
- Fresh delivery C verification with UV_NO_SYNC=1 and UV_CACHE_DIR=/tmp/ousast-uv: the exact
requested static gate passed (format, Ruff, mypy over 179 source files); the exact pytest gate
passed with 102 passed, zero failed, exit 0. The initial trace test failed collection because
trace.py did not yet exist; additional regression tests failed before their fixes. No
test_search_* test, network request or model call was made. Changes remain uncommitted.
- Deliveries A-C implemented the baseline paths, recognisers, witnesses, handler facts, prover, traces and CFG with
static checks and focused tests passing at delivery time.
- The full development arm then processed all 483 changes with zero instrument failures. Its maximum runtime exceeded
the amended hard budget, so the baseline is evidence, not an eligible arm.
- The 2026-10-28 ablation identified call resolution, resource/authority precision and failure visibility as the
amendment levers. Those observations inform tasks 7-13 but are not admission labels.
+11 -10
View File
@@ -5,23 +5,24 @@ One screen: every feature's lane, its state today, and its next move. Aligns `.k
| lane | state today | next move | owner |
|---|---|---|---|
| Engine: obligation checker + dominance | Stable; target-agnostic; checker owns verdicts | All-kind caller-hop re-judgement (task 8.2, opt-in keyword) | access-control-prover |
| Engine: resolution | Designed (class table + receiver/signature-qualified calls); code not started | Task 7.2 `obligations/resolution.py` | access-control-prover |
| Engine: paths / CFG / trace | Stable; `PathRecord` shape kept by design | No change; resolution plugs in before them | access-control-prover |
| Prover integration | `hunt/prover.py` proves changes; caller join is name-keyed | Receiver-qualified target join + down-depth budget (task 11.1) | access-control-prover |
| Runner | `prover_arm.py` metrics, coverage, zero-byte refusal; gate 1 = 65.2% recall, 15.0 FA/100 | Stale-anchor refusal (task 12.2); re-measure after 7-12 (task 13) | access-control-prover |
| Engine: obligation checker + dominance | Baseline checker stable; amended three-way judgment and strict CFG relation designed | Abstract domain + strict opt-in judgment (tasks 7, 10); legacy defaults unchanged | access-control-prover |
| Engine: resolution | Stable-symbol and finite-candidate design approved; code not started | IR/symbol/call resolution tasks 8.1-8.4 | access-control-prover |
| Engine: summaries / paths / trace | Baseline concrete paths and traces stable; uncertainty is not yet first-class | Monotone summaries + exact path materialization (task 9), abstract trace fields (7.4) | access-control-prover |
| Prover integration | `hunt/prover.py` proves changes with name-keyed joins and binary outcomes | Three-way finding/trace/coverage integration (task 10.6) | access-control-prover |
| Runner | Baseline arm: 65.2% recall, 15.0 flagged ordinary pushes/100; max runtime exceeded the amended 120s hard budget, so it is development evidence, not an eligible arm | Bundle/funnel/gate/four-arm work (task 12), then development and admission runs (task 13) | access-control-prover |
| Judge (zero-shot over traces) | `judge_pass.py`; gate 2 measured: safe but inert (keeps 15/15 TPs, drops 5/69 FAs) | No tuning; revisit only if trace facts change the FA mix | roadmap (measured 2026-10-28) |
| Recogniser registry | Designed (Strategy+Registry, `mapping/` package); code not started | Task 9.1 registry + registration-sites strategy; 9.2 migrates Stapler as the proof | access-control-prover |
| Recogniser registry | Strategy+Registry approved; code not started | Registry + registration adapter + one-family migration (tasks 11.1, 11.2, 11.6) | access-control-prover |
| mapping.py monolith | All framework families inline (1,200+ lines); WordPress/Python registration blocks included | Migrate family by family after Stapler: jetty, symfony, livewire, remix, tornado, then PHP/Python registration tables | breadth (this map) |
| Obligation facts (ruleset TOMLs) | Closed rows, hand-authored; header discipline enforced by loader | Extraction into acquirable structures via fact-init (task 10.1) | access-control-prover |
| Fact initialization job | Designed (`tools/fact_init.py`: extract + propose -> gate -> promote); code not started | Task 10.1 build; 10.2 first run for pool families as data rows | access-control-prover |
| Obligation facts (ruleset TOMLs) | Closed rows, hand-authored; resource/authority algebra designed but not implemented | Resource/authority fields (tasks 7.2-7.3), label-blind development acquisition (11.7) | access-control-prover |
| Fact initialization job | Label-blind extraction/proposal/verification/promotion and acquisition record approved; code not started | Tasks 11.3-11.5 | access-control-prover |
| Fact-proposal lane (standing) | Roadmap-level: LLM proposes, engine verifies, repetition promotes | Stays deferred to injection-spec track (roadmap gate 5) | roadmap |
| Injection-family object | Brief only (`engine-change-scoped-analysis`); taint flow + banks MVP exist as evidence | Spec after post-fix pilot holds recall | roadmap |
| Complexity gate | Caps in `code-standards.md`, review-time only | Wire ruff C901 / xenon into CI; threshold per steering | breadth (this map) |
| Idiomatic naming | Rule active for new/amended code; legacy debt un inventoried | Record rename inventory during family migrations; no drive-by renames | breadth (this map) |
| Training pool | pool-train 23 repos / 483 changes frozen 2026-10-14 | Pre-2019 negatives re-audit before any future run (3/12 sampled carry real missing-check bugs) | valid-detection-benchmark |
| Development pool | pool-train 23 repos / 483 changes frozen 2026-10-14; explicitly not admission evidence | Four-arm regression only after tasks 7-12; pre-2019 negatives still require review | access-control-prover |
| Admission population | Not yet supplied; current pool is spent for development | `valid-detection-benchmark` prepares a reviewed untouched manifest; access-control-prover only consumes it (task 13.4) | valid-detection-benchmark |
| Pre-push safety net | Implementation 67/79; wiring unchanged by this work | Unaffected until prover recall/FA hold at pilot | pre-push-safety-net |
Migration order for mapping.py families is deliberate: Stapler first (task 9.2 proves the registry path on
Migration order for mapping.py families is deliberate: Stapler first (task 11.6 proves the registry path on
the family the pool exercises most), then one family per migration, each deleting its legacy block from
`mapping.py` in the same change.