docs(spec,steering): lift access-control-prover to capability level; engine/target split, pattern discipline, complexity caps, breadth map

This commit is contained in:
Marius Ciepluch
2026-10-29 12:27:44 +01:00
parent f09dc66592
commit 152f405bcf
6 changed files with 355 additions and 59 deletions
+90 -39
View File
@@ -101,29 +101,36 @@ flowchart LR
## File Structure Plan
```
src/openultrasast/
├── mapping.py # + recognisers: _stapler_entries, _jetty_entries, _symfony_entries,
│ # _livewire_entries, _remix_exports (extend), _tornado_entries,
│ # _jetty_registration_entries [2026-10-29]
├── 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)
├── semantic/
│ ├── cst.py # + capture call receiver text in _call [2026-10-29]
│ ├── ir.py # + CallSite.receiver: str | None [2026-10-29]
│ ├── cst.py # + capture call receiver text in _call [2026-10-29]
│ ├── ir.py # + CallSite.receiver: str | None [2026-10-29]
│ └── 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
benchmarks/valid/prover_arm.py # run / summarize; records per change; coverage table; anchor refusal
tests/test_mapping_frameworks.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
│ ├── 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
```
### Modified Files
- `src/openultrasast/mapping.py`: new recogniser blocks, registered where `analyze_entry_points` dispatches by
language and path; `EntryPointRecord` unchanged.
- `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.
- `src/openultrasast/ruleset/obligations/*.toml`: rows only, admitted through the fact gate.
## System Flows
```mermaid
@@ -172,6 +179,43 @@ counts and no finding. A declared-public route suppresses `path_guard` only.
| 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.
## Components and Interfaces
### mapping recognisers
@@ -191,15 +235,18 @@ methods not starting with `_`, plus `Route::` registrations naming a controller
action|loader` under `app/routes`. Tornado: classes extending `RequestHandler` or `WebSocketHandler`, request
methods and `open`.
Programmatic registration `[2026-10-29]` (1.9): a separate recogniser follows the call-site registration
pattern the Python route registrations and the WordPress registration table already use. It scans Java text
for registration sites -- `new ServletContextHandler(`, `new ServletHolder(`, `addServlet`,
`addServletWithMapping`, `addHandler`, `addServletMapping` -- resolves the registered class literal through
`resolution.class_table`, and emits the registered class's request methods (`doGet`, `doPost`, `service`,
`handle`) as `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. A
registration whose target class the repository does not define yields coverage (`registration_unresolved`)
and no entry. Framework tag: `jetty`.
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
@@ -287,11 +334,13 @@ Jenkins fixes are form-validation and list-filling web methods whose body calls
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 the verb families seen in the gate-1 misses -- `^doNotifyCommit$`,
`^doChange(Submit)?$`, `^doEvent$`, `^doBuild(Submit)?$` (webhook and build-trigger endpoints that act with
repository or job privileges) and `^doJson$`, `^doPython$` (protected-data exposure) -- as operation rows on
`stapler`-recognised entries, at the declaration line. The families are rows in the facts files, not code: a
new verb family is a row addition, and a row without `handlers` never matches a declaration name.
`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
@@ -345,15 +394,17 @@ Reads Laravel route files (`routes/*.php`) for `->middleware([...])` and control
`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 voters `[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 `IsGranted`/`denyAccessUnlessGranted`/`isGranted`, the
middleware name for `auth`-class guards (no resource -- `None`). A voter decision is a `voter` witness: when a
Symfony action (or a function it calls) calls `isGranted`/`denyAccessUnlessGranted` with an attribute, or a
voter class in the repository implements `voteOnAttribute` for that attribute, the witness is scoped to that
action's operations with the attribute as its resource. 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.
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
@@ -30,6 +30,13 @@ programmatic-registration entry facts (Requirement 1 acceptance criterion 9), an
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.
## 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
@@ -80,11 +87,12 @@ the pool uses, so that reachability starts where an attacker starts.
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 a Java program in the repository registers a servlet or handler programmatically (Jetty
`ServletContextHandler`, `ServletHolder`, an add-servlet or add-handler registration call in the repository's
own code), 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, not annotations,
still yields entry points (the seatunnel miss of gate 3).
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).
### Requirement 2: Enforcement points outside the function
**Objective:** As a maintainer, I want checks that frameworks apply outside the handler to count as discharges,
@@ -115,21 +123,24 @@ so that a guarded route is not reported because its handler body shows no check.
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.
9. `[2026-10-29]` The handler-named operations shall cover the pool's webhook and data-exposure handlers: the
facts shall name, by handler-name pattern and by the privileged calls the handler makes, the build-trigger
and webhook-submit operations (the pool's `doNotifyCommit`, `doChange`, `doChangeSubmit`, `doEvent`,
`doBuild`, `doBuildSubmit` shapes) and the protected-data exposure operations (the pool's `doJson`,
`doPython` shapes), applied at the method's declaration line to entries the mapper recognised (the
git-plugin, p4, analysis-core and global-build-stats misses of gate 3 all reached the entry and checked no
operation).
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 a Symfony action or a function it calls is guarded by a voter decision on the action's
attribute (`isGranted`, `denyAccessUnlessGranted`, a voter's `voteOnAttribute`), the prover shall supply a
`voter` witness scoped to that action's operations.
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.
### 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
+4 -4
View File
@@ -1,15 +1,15 @@
{
"feature_name": "access-control-prover",
"created_at": "2026-10-14T13:48:18Z",
"updated_at": "2026-10-29T13:05:00Z",
"updated_at": "2026-10-29T15:40:00Z",
"language": "en",
"phase": "design-generated",
"phase": "requirements-generated",
"approvals": {
"requirements": {"generated": true, "approved": true},
"requirements": {"generated": true, "approved": false},
"design": {"generated": true, "approved": false},
"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."
"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."
}
+134
View File
@@ -116,6 +116,140 @@
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. 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)_
- _Depends: 8.1_
- [ ] 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_
- [ ] 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_
- [ ] 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_
- [ ] 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_
- [ ] 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_
### Task constraints `[2026-10-29]`
From `.kiro/steering/code-standards.md`, binding on every task above:
- 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`.
## Implementation Notes
+27
View File
@@ -0,0 +1,27 @@
# Breadth Map (2026-10-29)
One screen: every feature's lane, its state today, and its next move. Aligns `.kiro/steering/roadmap.md`
(goals, gates) with the tree (what exists). Update a row when its state changes, not on every commit.
| 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 |
| 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 |
| 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 |
| 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 |
| 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
the family the pool exercises most), then one family per migration, each deleting its legacy block from
`mapping.py` in the same change.
+73
View File
@@ -0,0 +1,73 @@
# Code Standards (2026-10-29)
Applies to new code and amended code from this date. Existing modules are grandfathered
until their migration is scheduled in the breadth map; no mass renames.
## Specification level (the horizontal-capability rule)
Specifications describe the way and the concept, not per-target inventories.
- A spec defines **capabilities of the acquisition mechanism**: how recognition
knowledge for any target is represented, proposed, verified, and promoted. It never
enumerates a target's vocabulary (`ServletHolder`, `doNotifyCommit`,
`voteOnAttribute`, ...) as work items or acceptance criteria.
- Per-target knowledge is **data** (fact rows, strategy registrations) produced through
the acquisition pipeline -- an LLM can produce it per target; the engine verifies it.
- Concrete repositories and frameworks appear in specs only as **benchmark evidence**:
named cases the acquired data must satisfy, never lists to implement.
- Litmus: if a requirement's acceptance criteria would not survive swapping the target
repository family, the requirement is written at the wrong level. Lift it.
## Engine / target split (the anti-monolith rule)
The abstract-interpretation engine is agnostic. Target knowledge (framework names,
recognition vocabulary, WordPress/Stapler/Symfony/Jetty/Laravel specifics) never lives
inside engine modules.
- **Engine modules** (`semantic/obligations/check.py`, `dominance.py`, `cfg_dominance.py`,
`paths.py`, `resolution.py`, `operations.py`, `trace.py`): no framework or product names.
If a framework identifier appears in an engine module, the split has been violated.
- **Target knowledge lives in exactly two visible places:**
1. **Data**: `ruleset/obligations/*.toml` and future fact files -- operation rows,
discharger rows, handler-name families. Adding a framework's vocabulary is a data
change, reviewable in one place.
2. **Strategy modules**: one module per recognition concern under `mapping/` (registry
pattern, see below), each file naming its target in its filename and registering
itself in one table.
- **The system adjusts by growing data and strategies, never by editing the engine.** The
fact-proposal lane (LLM proposes, engine verifies, repetition promotes to rows) is the
automated path for the same rule.
## Pattern discipline (GoF)
- **Strategy + Registry** for entry recognisers: `mapping/` gains a recogniser registry;
each framework family is one strategy module. New recognisers land there; the legacy
blocks inside `mapping.py` migrate family by family (tracked in the breadth map).
- **Interpreter (data-driven)** for obligation facts: TOML rows are the language; the
checker interprets them. New target vocabulary = new rows.
- **Adapter** at engine seams: `hunt/prover.py`'s callers adapter binds the IR to the
caller index; witnesses flow into the checker through one interface
(`check_obligations`).
- **Factory** for witnesses stays inside `obligations/enforcement.py` for now, but
enforcement is a target-aware seam (it reads Symfony/Laravel/Tornado constructs): treat
it as adapter territory; engine modules must not import it.
## Complexity caps
- Cyclomatic (McCabe) complexity: **<= 10 per function**.
- Logical lines of code: **<= 50 per function, <= 400 per new module**.
- A task is not done while a function it touched exceeds the caps; split the function.
- Automated gate (ruff C901 / xenon in CI) is not wired yet; caps are enforced at review
time. Wiring the gate is tracked in the breadth map.
## Idiomatic naming
- Names state what a value semantically is, in the pipeline's vocabulary: `entry_point`,
`trace`, `obligation`, `witness`, `discharge`, `finding`, `judgment`.
- Collections name their elements: `entry_point_records`, `judgments_by_key`,
`findings_by_push`. Mappings use `_by_<key>`; sequences are plural. No `eps`, `recs`,
`d2`, `tmp2`.
- Booleans are predicates: `is_public`, `has_dominating_guard`, `dominates_entry`. Avoid
negated names (`not_unflagged`).
- Row/record destructuring uses the record's field names, not positional `a, b`.
- Renaming debt in existing modules is recorded in the breadth map, not fixed drive-by.