ChangeContext records the paths Git reports as added. An entailed, undischarged operation in an added file, with complete head evidence and no identical operation in any base answer, is classified new (file_added_in_head) instead of base_incomplete; admission accepts that reason as change evidence. Unpaired add/delete pushes and every other unknown comparison keep their vetoes. An engine finding whose novelty is established and whose only vetoes are capability vetoes is shown as an advisory engine finding, not an alert. Requirement 9.4. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
134 KiB
Implementation Plan: pre-push-safety-net
Status: the existing task plan remains approved. Groups 1–7 and tasks 8.1, 8.2 and 8.4 are verified (28/29 executable subtasks, including group 9); task 8.3 and rollout remain NO-GO. Later work used the documented manual review fallback where agent dispatch reached its limit.
The accepted release milestone spec now orders remaining work: M1a diagnose → M1b repair → M2 speed → M3 shared-language transfer → M4 independent qualification → M5 narrow advisory rollout. All milestones remain unverified. Milestones are release outcomes, not checked implementation tasks. Group 9 below is the bounded owner amendment M1a requires; it adds the task-local recorded-veto evaluation and its three-revision development run. It enables no capability and changes no gate. Preserve all completed task evidence below.
Execution contract
-
Preserve the ranker as the scope mechanism and vendor exclusion from targets AND graphs.
-
Use PMPro for core development and a JavaScript/Express workload for transfer. Keep the core frozen for the first Node measurement; separate adapter repairs from core changes.
-
Each executable subtask is a bounded work unit targeting 1–3 hours of engineering, excluding unattended measurement time. If a new abstract capability is needed, record the gap and return it to the owning engine/ranking spec; do not hide that implementation in an integration task.
-
Cross-spec engine changes below are explicit integrations of the accepted push contracts. They do not approve unrelated contributor/ranker backlog or automatically complete its tasks.
-
Development runs retain experimental candidate/admission diagnostics without enabling normal hook alerts. A capability becomes eligible for normal alerts only after task 8.3 passes its gates.
-
The first end-to-end milestone is 5.2, before persistent caching and hook installation polish. Negative performance evidence there informs caching; a broken instrument or absent supported positive control requires predecessor remediation before claiming that milestone successful.
-
Sequential order is the default. Only 3.1 may run alongside group 2 after its explicit prerequisite; shared backend/driver, cache and report edits otherwise remain serial. No optional acceptance tests.
-
1. Establish the runtime, evaluation inputs and shared contracts
-
1.1 Reproduce the evaluated engine environment
- Align the shipped image, Compose and host-install defaults with Joern 4.0.625; preserve version override and checksum verification.
- Exercise PHP with the native interpreter and the installed JavaScript frontend; open a source and the frontend parser where applicable, recording byte counts and graph census.
- Done when a local image smoke produces valid small PHP and JavaScript graph/query payloads with recorded versions, and an unreadable source fails explicitly. This builds locally; it does not deploy.
- Boundary: Cache and Engine Boundary, packaging integration
- Requirements: 7.1, 7.3, 8.2
- Verified 2026-09-12: independent review APPROVED; 888 tests passed, 9 skipped. Actual local image PHP/JavaScript graph census and source witnesses passed, unreadable PHP explicitly refused. Parent verified current provenance hashes, image identity, non-root user and CLI/frontend liveness. Evidence:
benchmarks/measurements/2026-09-12-engine-runtime-smoke.json. Runtime readiness only; no hook latency/detection claim. Existing mypy errors inmodel/scan.pyat 299/302/310 remain unchanged.
-
1.2 Prepare labeled development and reserved evaluation changes
- Pin a readable PMPro change/fix and a supported Node/Express change/fix, plus benign controls; verify license and source bytes before accepting recipes.
- Use NodeGoat only if its selected mechanism fits a current arbiter; label authored repairs as authored. Reserve an independently selected Node API repository with modern module/async context and reviewable history before tuning on transfer results.
- Keep Python regression and libpng envelope recipes, explicit supported/unsupported populations, ref identities and development/holdout roles in machine-readable evaluation inputs.
- Reserve untouched evaluation for each language/framework/family intended for admission; the Node holdout cannot qualify PHP or Python. Missing per-capability evaluation leaves it experimental and does not delay the initial PMPro/Node diagnostic experiment.
- Done when recipe validation resolves immutable inputs and labels or names an explicit missing prerequisite; no favorable scanner result is required to select a recipe.
- Boundary: Evaluation harness
- Requirements: 6.1, 6.3, 8.3, 8.4
- Verified 2026-09-12: independent review APPROVED; 902 tests passed, 9 skipped, then 34 focused tests passed after a diagnostic-only correction. Fresh offline validation opened 506,502 bytes and retained all 11 cases/9 snapshots: 7 cases input-ready, 4 with named missing prerequisites. See
benchmarks/push/inputs.jsonandinput-validation.json. Ghost checkout/reviewed labels and independent per-capability PHP/Python populations remain missing; every capability stays experimental. No detector run or admission claim.
-
1.3 Make the baseline and push scorer fail honestly
- Retain unmatched targets and unanswered queries in the declared population; require a queried witness before labeling a target transitively detected.
- Emit actual vulnerable/fixed outcomes beside rank, selected mode, engine/facts/scope and elapsed stage costs. Freeze a versioned workload/profile before measuring.
- Done when a deliberately missing target and failed query cannot improve reported recall, while a verified transitive positive is counted with its witness.
- Boundary: Evaluation harness, repository benchmark integration
- Requirements: 6.2, 6.3, 6.4, 6.5, 7.3
- Verified 2026-09-12: independent review APPROVED; 925 tests passed, 9 skipped. Parent reproduced the frozen profile and actual VAmPI static rerun (15,942 pinned bytes, all 3 targets retained, SQLI unmatched, no transitive/detection claim, exit 1). Empty-run scoring retains 11 unresolved cases and explicitly reports unmeasured/0 recorded cases. Historical baselines remain unchanged with a caveat; new scorer consumes associated execution/review evidence and does not implement replay or admission.
-
1.4 Establish typed push and engine integration contracts
- Add importable push components and validated configuration for comparison base, shared deadline/cancellation allowance, local cache limit, advisory/blocking and separate coverage policy.
- Define ownership of update/comparison, change context, selected/deferred scope, graph identity/lease and separate finding/coverage/push statuses as described in design.
- Keep generic engine contracts outside the push package; leave ordinary scan defaults compatible and capabilities experimental until evaluated.
- Done when contract/config round trips preserve all identities/statuses, invalid values are rejected, and the engine has no dependency on the push package.
- Boundary: Push runner, Cache and Engine Boundary, CLI/config integration
- Requirements: 4.1, 4.3, 5.3, 5.4, 5.5, 7.1, 7.3
- Verified 2026-09-12: independent review APPROVED; 965 tests passed, 9 skipped; touched-source Ruff/mypy passed. Parent rebuilt the local image and verified non-root, network-disabled CLI startup, all contract imports, an 83-byte configuration round trip and invalid-budget rejection. Comparison-owned completion preserves different bases for a shared head. Contracts are structural; runtime snapshot/cache/runner/admission implementations remain in later tasks.
-
2. Resolve immutable pushed content
-
2.1 Resolve every supplied ref update and comparison
- Parse Git's complete update input using object-format-aware validation; resolve existing tips, new-branch configured bases, force-pushes, merges, peeled tags, deleted refs and unsupported objects.
- Deduplicate equal targets without merging different base comparisons; retain every ref association. Do not silently substitute HEAD or fetch missing objects.
- Done when multi-ref and missing-base fixtures produce exact comparison identities and explicit dispositions, including deletion-only no-analysis results.
- Boundary: Snapshot Adapter
- Requirements: 1.1, 1.3, 1.4, 7.1
- Verified 2026-09-12: independent review APPROVED after Unicode-ref repair; 997 tests passed, 9 skipped; focused 76 passed and touched Ruff/mypy passed. Real SHA-1/SHA-256 fixtures cover exact comparisons, tags, deletion, missing/ambiguous bases, replacement/graft suppression and blocked promisor fetch. Parent read 2,547 pinned source bytes and verified real-repository comparison/ref identity with dirty state and index preserved. ASCII protocol framing retains valid Unicode ref names.
-
2.2 Materialize isolated object snapshots
- Read immutable tracked blobs into bounded scratch storage using machine-readable paths; do not checkout into the live tree or run filters, project scripts or network requests.
- Preserve path identity and report symlinks, gitlinks, unavailable LFS blobs and unsupported content; never follow source outside the snapshot.
- Done when non-HEAD pushes from a dirty checkout read only the intended object bytes and leave index, files and refs byte-identical after success, cancellation and failure.
- Boundary: Snapshot Adapter
- Requirements: 1.1, 1.2, 1.3, 2.3, 7.1, 7.3
- Verified 2026-09-12: independent review APPROVED; 1,037 tests passed, 9 skipped; focused 116 passed; touched Ruff/mypy passed. Real SHA-1/SHA-256 fixtures verify binary-safe paths, exact blobs, no filters, explicit symlink/gitlink/LFS limits, cancellation and unchanged live state. Independent hanging-child cleanup took 0.202 s. Parent reopened and checked all 1,157 materialized repository files (8,737,198 bytes), with scratch removed and live state preserved; evidence
benchmarks/measurements/2026-09-12-snapshot-materialization.jsonrecords a 5.783 s preparation-only lab run, not end-to-end hook latency.
-
2.3 Attach changed spans and deletion/rename context
- Compare recorded base/head objects using NUL-safe path handling; retain removed spans, renamed identities and declaration/config changes.
- Supply change context to the shared ranker contract rather than selecting an independent changed-files-only scope.
- Done when renamed functions retain correspondence and a removed guard remains an impact seed for an unchanged operation, with missing correspondence explicitly unresolved.
- Boundary: Snapshot Adapter
- Requirements: 1.4, 2.1, 2.4, 3.4
- Verified 2026-09-12: independent review APPROVED; fresh final suite 1,069 passed, 9 skipped; focused 148 passed. Global Ruff/format, touched mypy and regression gates passed. PHP/JavaScript fixtures retain removed spans and unchanged-operation anchors across declaration renames; uncertain moves, repeated lines, binary and unavailable inputs remain unresolved. Forty independent real-Git comparisons checked exact spans/anchors. Network-disabled image smoke preserved committed PHP/JavaScript bytes and dirty non-HEAD state, with cleanup verified; see
implementation-group-2.md. Lexical anchors do not establish semantic impact or novelty.
-
3. Integrate bounded execution and ranker scope with the existing engine
-
3.1 (P) Enforce one remaining-time budget in the backend
- Thread the absolute deadline through readability probes, frontend builds, overlays, census, query batches, fallbacks and retries; no phase gets a fresh full budget.
- Bound process-group termination and lease/scratch cleanup by the cancellation allowance, replacing any longer fixed cleanup waits on the push path.
- Done when controlled hanging subprocesses at build/overlay/query stages terminate without orphan children inside the declared allowance, while ordinary scans preserve their prior behavior.
- Boundary: Cache and Engine Boundary, generic driver integration
- Depends: 1.4
- Requirements: 4.1, 4.2, 7.3
- Verified 2026-09-13: independent review APPROVED; fresh suite 1,089 passed, nine skipped; global Ruff/format and mypy (108 files) passed. Real hanging probe/frontend/overlay/census/query processes obey the shared deadline and cancellation allowance; retries do not reset it. Non-root packaged PHP/JavaScript graph/query smoke passed with matching source hashes under one 900-second lab budget. See
implementation-group-3.mdandbenchmarks/measurements/2026-09-13-engine-budget-smoke.json. Runtime readiness only; no hook latency claim. Former three mypy errors resolved by a behavior-preserving variable rename.
-
3.2 Return the ranker's exact selected and deferred work
- Integrate the existing ranker through explicit mode selection and shared question identity; record selected/deferred IDs, evidence and reasons directly from the final order.
- Preserve unanswered evidence as unknown rather than tier zero; report per-family coverage so taint ranking cannot masquerade as authorization/configuration coverage.
- Add paired PHP/JavaScript normalized-evidence contract tests asserting identical tier/score and selection decisions for equivalent input and budgets, without repository/language priority exceptions.
- Done when changing order produces matching execution and manifest IDs, with no reconstruction from an earlier list, and normalized cross-language equivalence tests pass.
- Boundary: Ranker Scope Contract, generic driver integration
- Requirements: 2.1, 2.2, 6.4, 7.3, 8.1, 8.2
- Verified 2026-09-13: independent review APPROVED; 1,105 passed, nine skipped; global Ruff/format/mypy passed. Twenty-four independent order/budget/missing-family combinations retained all question identities. Packaged PHP/JavaScript smoke selected the same controlled risky operation at tier 4/score 2.5, with exact execution/outcome IDs and reconciled family coverage. Missing evidence, partial graphs and interrupted arbitration remain explicit; CLI consumes authoritative scope. Evidence:
benchmarks/measurements/2026-09-13-ranker-scope-smoke.json. No new ranking weights or hook admission claim.
-
3.3 Connect change context to supported first-party relationships
- Use existing entry/callee, field, hook and guard relationships to supply affected context to ranked questions, including unchanged sinks after source/guard changes.
- Preserve current abstract semantics; absent dynamic or external relationships remain unresolved rather than guessed safe. A required new feature or fact is reported to its owning spec.
- Done when controlled cross-file and removed-guard cases reach the relevant question/witness, while an unresolved boundary cannot produce a complete negative.
- Boundary: Ranker Scope Contract, Snapshot Adapter integration
- Depends: 2.3, 3.2
- Requirements: 2.1, 2.2, 2.4, 8.1, 8.2
- Verified 2026-09-13: independent review APPROVED after an unknown-call boundary repair; fresh suite 1,128 passed, nine skipped; global Ruff/format/mypy passed. Real PHP/JavaScript Git guard-removal snapshots reached unchanged helper-file witnesses with local dirty state preserved. Production negative regression reproduced then fixed unknown(req.body) being pruned as tier zero: modeled arguments no longer summarize unknown consumers. Existing ranking and detector semantics unchanged; unsupported projections remain explicit. Evidence:
benchmarks/measurements/2026-09-13-change-context-smoke.json. Lexical attribution is not semantic novelty or alert admission.
-
3.4 Apply vendor exclusion and explicit language partitions
- Prerequisite resolved 2026-09-13: contributor-scan 2.18 independently approved and verified at
51fabef; both JavaScript test filters repaired by explicit versioned input policy. Task 3.4 subsequently passed its own independent review. - Reuse declared layout exclusions before target enumeration and graph build; retain unshipped first-party tests and separate browser/server or language capability coverage.
- Build supported frontend partitions explicitly; preserve unknown cross-language and vendor semantics without restoring excluded sources.
- Done when PHP and Node graphs/targets contain no declared vendor code, first-party callers remain visible, and unsupported partitions appear as coverage limits rather than clean results.
- Boundary: Ranker Scope Contract, Cache and Engine Boundary integration
- Requirements: 2.2, 2.3, 7.3, 8.2, 8.4
- Verified 2026-09-13: independent review APPROVED; 1,151 passed, nine skipped; Ruff/format/mypy pass. Real PHP/JavaScript graphs retain tests/browser source, omit vendor nodes and share one ranker limit. Named census survives PHP aggregation; missing source names and unreadable discovery inputs remain explicit. Python exercises all three batch census schemas. Evidence:
benchmarks/measurements/2026-09-13-partition-scope-smoke.json. No hook capability or latency claim.
- Prerequisite resolved 2026-09-13: contributor-scan 2.18 independently approved and verified at
-
4. Implement change attribution and actionable output
-
4.1 Compare targeted base/head evidence for defect novelty
- Establish stable mechanism/operation identity with rename/span mapping and collect comparable base evidence under the same semantics and remaining deadline.
- Distinguish new, worsened, unchanged and unknown; an absent finding from a failed/truncated base is unknown. Support removed guards and new source connections at unchanged sinks.
- Done when benign movement and unchanged backlog do not become new defects, while a supported removed-guard case has a concrete change-to-witness relationship.
- Verified 2026-09-13: independent review APPROVED; 1,167 tests passed, nine skipped; real JavaScript guard/movement and PHP connection controls passed. Final policy rechecked against captured actual answers after malformed-schema repair. See
implementation-group-4.md. - Boundary: Delta and Actionability Policy, engine comparison integration
- Depends: 2.3, 3.3, 3.4
- Requirements: 2.4, 3.1, 3.4, 4.1
-
4.2 Enforce capability and actionability admission
- Require evaluated capability provenance, applicable semantics, a sufficient rung, exact witness/location, supported change attribution and a concrete repair direction.
- Keep experimental, suspicious, wrongly sanitized and unknown-novelty candidates in diagnostic records; do not let model confidence, a sanitizer name or printed-before state admit/dismiss a defect.
- Deduplicate by defect while retaining relevant witnesses and refs; distinguish experimental evaluation from normal hook eligibility.
- Add equivalent PHP/JavaScript normalized claims with equally eligible capabilities and assert equal admission; add an unsupported-origin control whose coverage/eligibility differs without changing core scores.
- Done when the policy rejects an entailed but unsupported claim and admits fully supported synthetic controls equivalently across origins and advisory/blocking modes; real capabilities remain disabled pending 8.3.
- Verified 2026-09-13: independent review APPROVED after operation/context-binding repair; 1,207 tests passed, nine skipped; default real-evidence controls remain diagnostic and synthetic PHP/JavaScript admission is equivalent. See
implementation-group-4.md. - Boundary: Delta and Actionability Policy
- Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 5.3, 5.4, 8.1
-
4.3 Render compact results and preserve complete artifacts
- Render up to three actionable summaries plus remaining count; emit at most one no-finding status and one aggregated incomplete notice with a useful next action.
- Persist all defects, candidate dispositions, exact scope, identities, capability/ranker provenance and timings with separate finding/coverage/push status.
- Done when the display cap does not change the push decision, incomplete analysis cannot read as secure, and artifact-write failure produces a bounded notice while retaining the real decision.
- Verified 2026-09-13: manual Kiro review fallback APPROVED after dispatch thread limit; 1,222 tests passed, nine skipped. Packaged comparison/admission/report smoke and CLI boot passed; full records retained with bounded output/write failure handling. See
implementation-group-4.md. - Boundary: Result and Hook Adapter
- Requirements: 3.1, 3.2, 3.3, 5.1, 5.2, 5.5, 7.3
-
5. Prove the first end-to-end experiment before cache optimization
-
5.1 Wire explicit base/head replay through the full transaction
- Connect immutable snapshots, ranker decisions, existing arbiter, targeted comparison, admission diagnostics and compact artifacts through an explicit local replay command.
- Apply the shared budget to preparation and both revisions; no model/service or installed hook is required. All runtime errors preserve coverage state.
- Done when a controlled security change and fixed twin traverse the production runner through the existing ranker and produce replayable results with truthful deadlines.
- Verified 2026-09-13: manual Kiro fallback review APPROVED; 1,236 tests passed, nine skipped; final packaged security replay and fixed/deadline controls verified. See
implementation-group-5.md. - Boundary: Push runner, CLI, Snapshot Adapter, engine and policy integration
- Depends: 1.3, 3.1, 3.4, 4.3
- Requirements: 1.1, 2.1, 3.1, 3.4, 4.1, 4.2, 5.5, 7.1, 7.3
-
5.2 Measure PMPro and frozen-core Node transfer
- Prerequisite repaired 2026-09-13: 3.1/5.1 deadline regression verified in the packaged Node replay; re-freeze the experiment after this core identity change.
- Replay each prepared security/fixed/benign case, first using the declared hook deadline; record timeouts and stage costs. A separately named extended lab run may obtain witnesses but cannot satisfy the hook latency gate.
- Freeze the core after PMPro, run Node with existing facts, and classify instrument, mapping, fact, scope and domain gaps without tuning on an untouched holdout.
- Done when one joint artifact reports actual evidence and outcomes for both workloads, with unresolved cases retained and an explicit feasibility decision; no normal capability is enabled here.
- Verified 2026-09-13: manual Kiro review APPROVED; six final CLI cases measured under unchanged core, all timed out in 30.55–30.64 seconds with artifacts retained. Full 11-case population retained; feasibility NO-GO, no eligibility enabled. 1,242 tests passed, nine skipped. See
implementation-group-5.md. - Boundary: Evaluation harness, full-run integration
- Depends: 1.2, 1.3, 5.1
- Requirements: 6.2, 6.3, 6.4, 6.5, 8.1, 8.2, 8.3, 8.4
-
6. Reuse valid graphs and results without stale conclusions
-
6.1 Validate graph artifact identity and leases
- Have the backend own source/declaration/exclusion digests, frontend/engine/overlay options, graph census and complete/partial identity.
- Permit validated graph leases in the generic driver while preserving the fresh-build path; isolate potentially mutating loads and retain bounded cleanup.
- Done when identical valid inputs can reuse a graph while any changed graph input or failed census rejects the lease; original cached artifacts survive query loading unchanged.
- Boundary: Cache and Engine Boundary, generic driver integration
- Requirements: 4.2, 4.3, 4.4, 7.3
-
6.2 Publish and evict local artifacts safely
- Add private temporary publication, per-key bounded locking, atomic completed manifests, cache size enforcement and unleased eviction.
- Retain partial records only as diagnostics; never interpret a directory or failed write as a complete result. Do not rebuild graphs incrementally by mutating old node identities.
- Done when concurrent writers, corrupt entries and interrupted publication yield either a validated complete artifact or an explicit miss without exceeding the remaining lock budget.
- Boundary: Cache and Engine Boundary, push cache integration
- Depends: 6.1
- Requirements: 4.1, 4.3, 4.4, 7.3
-
6.3 Key query and comparison results by all relevant semantics
- Include graph identity, question/context/depth, facts/sanitizers/query versions and configuration; include ranking/admission/model provenance for final results and both revision identities for comparisons.
- Keep incomplete query/comparison results out of complete lookups; preserve current eligibility checks after reuse.
- Done when source, context, fact, query, mode, policy and optional model changes invalidate the appropriate layer, while an identical compatible request returns the same evidence and provenance.
- Boundary: Push cache, policy integration
- Requirements: 3.4, 4.3, 4.4, 6.4, 7.2, 7.3
-
6.4 Integrate cache reuse into the shared push deadline
- Reuse complete preparation/discovery by immutable tree and discovery/fact/configuration identity, retaining readable snapshot and declaration/boundary records; measure its costs and hits separately from graph/query reuse.
- Reuse identical snapshots/unchanged declared units across refs without merging different comparisons; changed units rebuild and stale graph relationships remain hints only.
- Account for lookup, lock wait, lease, build/query and result persistence in the same deadline; preserve completed outcomes on cancellation.
- Done when cached and uncached replay produce equal evidence/admission for the same completed questions under compatible semantics, with full equivalence under a sufficiently large controlled budget. Under the hook deadline, verify truthful extra completion/deferred scope from cache reuse rather than requiring identical scope; no stale answer survives a dependency/configuration edit.
- Boundary: Push runner, cache and engine integration
- Depends: 5.1, 6.2, 6.3
- Requirements: 1.1, 2.2, 4.1, 4.2, 4.3, 4.4, 7.3
-
7. Connect the real push interface without adding noise
-
7.1 Execute Git's full push input and enforcement policy
- Extend the replay command to consume all update lines and remote arguments, aggregating per-ref comparisons under one deadline and preserving independent coverage and finding statuses.
- Apply advisory, opt-in blocking and separate incomplete-coverage policy; keep external services disabled by default and normal alerts limited to admitted capabilities.
- Done when controlled multi-ref push invocations exercise all allow/block/status combinations, including deletion-only, missing-base and no-work cases, without resetting budgets.
- Boundary: CLI/config, Push runner and Result and Hook Adapter integration
- Depends: 2.1, 4.2, 4.3, 6.4
- Requirements: 1.1, 1.3, 1.4, 4.1, 5.1, 5.2, 5.3, 5.4, 5.5, 7.1
-
7.2 Provide non-destructive hook integration and removal
- Supply an executable integration snippet/installation path with explicit invocation, preserving existing hooks, hook path configuration, remote arguments and stdin for other consumers.
- Refuse automatic overwrite; retain an existing hook's nonzero result even when the Safety Net runs advisory. Include concrete integration/removal instructions with the delivered hook interface.
- Done when a disposable Git remote accepts/rejects pushes correctly with an existing stdin-reading hook, and removal restores the preceding behavior without touching source or refs.
- Boundary: Result and Hook Adapter, CLI integration
- Requirements: 1.2, 5.3, 5.4, 7.4
-
7.3 Keep optional model assistance within existing evidence rules
- Reuse explicit endpoint configuration and redaction only when enabled; propagate the remaining deadline to model calls and record model/prompt/cost provenance.
- Restrict explanations to existing witnesses and preserve deterministic no-model behavior; do not implement a new learned ranker or prompt optimizer in this task.
- Done when a delayed or misleading model cannot extend the push budget, leak unredacted input in the tested path, raise a rung or bypass actionability admission.
- Boundary: Push runner, existing model and policy integration
- Requirements: 3.5, 4.1, 4.2, 7.1, 7.2, 7.3
-
8. Validate usefulness, runtime and cross-language admission
-
8.1 Exercise complete push failure and regression paths
- Combine dirty/non-HEAD/multi-ref/new-branch/force/tag/deletion pushes with unusual paths, external content boundaries, failed census, missing comparisons and interrupted persistence.
- Verify no orphan children, no overwritten live state, no vendor code in graphs, exact scope reports and no false clean result from skipped work.
- Done when the integration suite covers the critical flows through the real CLI/backend seams and preserves ordinary scan behavior.
- Boundary: Push runner integration tests
- Requirements: 1.1, 1.2, 1.3, 1.4, 2.2, 2.3, 4.2, 4.4, 5.2, 5.5, 7.1, 7.4, 8.2
-
8.2 Measure realistic cache and deadline feasibility
- Measure identical-tip reuse, a warm one-function edit, dependency/configuration edits, cold starts, repository growth and multi-ref workloads on declared hardware.
- If reuse correctness passes but changed-code checks time out, identify preparation, frontend/overlay/census and query costs separately and route the remaining optimization to its owning boundary before rollout GO. Preserve the declared gates and do not substitute identical-tip latency.
- Publish p50/p95, timeouts, cancellation allowance, completed/deferred population and stage costs; the denominator is fixed before ranker selection.
- Done when the versioned scorecard evaluates the accepted latency/coverage gates without hiding timeouts or calling repeated-identical-tip hits representative warm edits; a failed gate remains NO-GO for rollout.
- Boundary: Evaluation harness, runtime integration
- Requirements: 4.1, 4.2, 6.2, 6.3, 6.5, 7.3
-
8.3 Evaluate independent transfer and admit only passing capabilities
- Run the frozen candidate on reserved Node changes and benign/fixed controls; preserve PHP/Python regressions, including a verified VAmPI transitive target, and report libpng envelope/unsupported bounds separately.
- Score actual actionable alerts, reviewed precision with counts/intervals, supported recall, benign interruptions and fixed-side silence alongside latency and coverage. Record adapter changes separately; a core retune requires a new untouched evaluation.
- Generate versioned capability eligibility only when all predeclared gates pass; no positive controls, no reviewed alerts, no untouched workload, wrong family semantics or unsupported properties prevent admission.
- Require untouched evaluation for the particular language/framework/family being admitted. Node results cannot qualify PHP/WordPress or Python; capabilities without their own untouched evidence remain experimental with an explicit disposition.
- Done when the policy consumes a reproducible PASS/NO-GO capability artifact tied to exact semantics and rejects stale eligibility; a NO-GO result never silently enables advisory alerts.
- Boundary: Evaluation harness, Delta and Actionability Policy integration
- Depends: 1.2, 1.3, 5.2, 7.3, 8.2
- Requirements: 3.1, 3.2, 6.1, 6.2, 6.3, 6.4, 6.5, 8.1, 8.2, 8.3, 8.4
- Blocked 2026-09-13: Eligibility implementation and reproducible NO-GO artifact delivered; independent reviewed Node/PHP/Python populations and verified VAmPI transitive witness remain missing. Runtime/coverage gates fail. See
implementation-group-8.mdand contributor-scan owner follow-up.
-
8.4 Verify the packaged end-to-end outcome and acceptance coverage
- Run the required regression checks and a local packaged smoke over supported security/fixed/benign pushes; verify no-model startup, preservation of hook behavior and truthful partial results.
- Reconcile every acceptance criterion with fresh evidence and record unresolved capability/performance blockers separately from completed plumbing.
- Done when a reproducible validation artifact states GO only with all mandatory integration and joint quality/latency gates satisfied; otherwise it names the remaining NO-GO conditions without marking the feature complete.
- Boundary: Packaged runtime, full feature validation integration
- Requirements: 3.1, 3.4, 5.1, 5.2, 5.3, 5.4, 5.5, 6.5, 7.1, 7.3, 7.4, 8.3, 8.4
-
9. Diagnose the first useful detection with vetoes recorded (release milestone M1a)
-
9.1 Add the task-local recorded-veto evaluation flag
- Add an explicit experimental evaluation path that records each comparison and admission veto per raw finding instead of applying it. Keep it task-local, off by default, and available only on explicit
--base/--headreplay; refuse it on the Git stdin path so it can never become a hook capability. - Lift only recognized completeness vetoes over a copy of the evidence: answered-then-demoted question outcomes, the vendor exclusion completion veto, and the diagnosed context/correspondence boundary kinds. Never lift failures: deadline exhaustion, unanswered questions, unreadable input, empty graphs and unparsed files stay applied, because silence from failure is not a quiet fixed twin.
- Retain the production novelty, reason and admission reasons beside the recorded ones for every finding, plus the query-to-witness identity difference, base-only findings and readable census receipts. Every payload carries an explicit experimental label; the recorded path admits no defect and enables no capability.
- Done when the flag is off by default, produces a labeled artifact section and terminal line under explicit replay, is refused in Git stdin mode, and unit controls prove recognized vetoes are recorded while failures and unrecognized boundaries remain applied.
- Boundary: Delta and Actionability Policy, Push runner, Result and Hook Adapter, CLI integration
- Requirements: 3.1, 3.4, 5.5, 6.3, 7.3
- Verified 2026-09-16: 1,330 tests passed, nine skipped; global Ruff/format and touched mypy passed. Flag off by default, refused in Git stdin mode, labeled artifact section and terminal line under explicit replay. Controls prove recognized vetoes are recorded while deadline exhaustion, unanswered questions, empty graphs and unrecognized boundaries stay applied, and that the recorded path emits no defect even when a lifted candidate would otherwise admit. Regenerated the committed module audit for the added module.
- Add an explicit experimental evaluation path that records each comparison and admission veto per raw finding instead of applying it. Keep it task-local, off by default, and available only on explicit
-
9.2 Run the three NodeGoat revisions and produce the per-finding veto list
- Record exact base/head revisions, expected operation and expected outcome before running. Exercise the shipped replay path, existing ranker and physically vendor-free graph; do not substitute a hand-selected slice or hard-coded target function.
- Replay the pinned vulnerable, fixed and unchanged revisions under the flag with an explicitly named development budget. That budget cannot satisfy or measure the hook latency gate.
- Deliverable: the raw finding on the vulnerable revision with location and witness, the fixed twin genuinely analyzed and quiet, the unchanged revision not reported as a new regression, and for every finding the exact list of vetoes that fired and why, with source and census receipts.
- Done when one replayable three-revision record states each expectation against its observed outcome and names the vetoes that actually fired. A fixed twin that timed out or was not analyzed fails M1a and blocks M1b until diagnosed.
- Boundary: Evaluation harness, full-run integration
- Depends: 9.1
- Requirements: 1.1, 2.1, 2.4, 3.4, 6.3, 7.1, 7.3
- Verified 2026-09-16: all three pinned revisions replayed through the production CLI under a named 900-second development budget; expectations frozen on disk first. Raw
<lambda>2witnesses present at lines 32–34; fixed twin analyzed and quiet (target question answered with zero rows, no deadline reason, three base-only findings); unchanged revision analyzed and recordedunchanged. Five vetoes fired and were recorded; the remaining blocker isoperation_correspondence_unresolvedfor an operation on a changed line. Zero admitted defects. Evidence: M1a record andbenchmarks/measurements/2026-09-16-nodegoat-m1a-recorded-vetoes.json. Diagnosis only; no capability enabled and no latency claim.
-
10. Repair only the vetoes M1a recorded (release milestone M1b)
-
10.1 Establish operation correspondence for an operation on a changed line
- M1a measured this as the blocker that survives every other repair: a newly introduced operation sits on a changed line, has no base line anchor, and therefore cannot be classified even when every completeness veto is lifted.
- Bind the required base/head correspondence to the specific operation identity rather than to a line anchor. A complete base answer for the counterpart question that contains no operation with the same operation and source establishes absence; a matching operation elsewhere in that answer is movement, not novelty.
- Keep the existing conservative rules: an incomplete or failed base stays unknown, benign movement and renames stay unchanged, and lexical alignment is never semantic proof.
- Add absent, renamed, removed-guard, moved-operation and repeated-identical-operation controls. Done when a newly introduced operation on a changed line is classified new, while movement, renames and an incomplete base remain unchanged or unknown.
- Boundary: Delta and Actionability Policy
- Requirements: 2.4, 3.1, 3.4
- Verified 2026-09-16: 1,337 tests passed, nine skipped; Ruff/format/mypy passed. Correspondence now reports
anchored,ambiguous,changed_line,unmappedorunchanged_path, and onlychanged_linesupports an introduced operation. Movement keys on the operation spelling, which the existing benign-rename control proved necessary. One superseded control was updated with its reason; movement, ambiguity, unmapped operations, discharged or unentailed claims and an incomplete base all remain unknown.
-
10.2 Establish the required comparison scope explicitly
- Comparison currently requires every selected base question to complete with no base degradation and no non-tier-zero deferred question, so one unsupported family anywhere prevents an otherwise complete target comparison. The owner research names this and requires the amendment to state the required scope rather than relax the bar.
- Define the comparison's required scope as the counterpart question plus the questions its evidence actually depends on, and require completeness over exactly that scope. Retain every other gap in aggregate coverage; a question outside the required scope stays reported and unresolved, never silently discharged.
- A missing raw finding in an incomplete base still cannot establish novelty. Unknown dependency remains inside the required scope.
- Done when an unrelated unsupported family no longer prevents a complete target comparison, while an incomplete counterpart, an incomplete dependency of it, or unknown dependency ownership still yields unknown.
- Boundary: Delta and Actionability Policy
- Depends: 10.1
- Requirements: 2.2, 2.4, 3.4, 6.4
- Verified 2026-09-16: 1,342 tests passed, nine skipped; Ruff/format/mypy passed. Completeness is now demanded over the counterpart plus its recorded dependencies. Controls prove an unrelated unsupported family no longer blocks a complete target comparison, while an incomplete counterpart, an incomplete recorded dependency, an unknown-ownership boundary and a truncated base scan all still yield unknown. Every other gap stays in aggregate coverage.
-
10.3 Scope correspondence ambiguity to the operations it actually affects
ambiguous_line_correspondenceis recorded file-wide and inherited by every question, so repeated unchanged lines invalidate distinct, uniquely mapped operations elsewhere.- Retain ambiguous mappings as unavailable and record which regions are ambiguous, so only an operation whose required correspondence needs an ambiguous line is vetoed. Do not delete the ambiguity check.
- Done when a uniquely mapped operation is comparable in a file that also contains repeated unchanged lines, while an operation needing an ambiguous line stays unresolved.
- Boundary: Snapshot Adapter, Delta and Actionability Policy
- Depends: 10.2
- Requirements: 2.4, 3.4
- Verified 2026-09-16: 1,347 tests passed, nine skipped; Ruff/format/mypy passed. Ambiguity is no longer inherited as a context gap by every question, and no longer blocks the transaction comparison; it is decided per operation by the correspondence classifier. Controls prove a uniquely mapped or newly introduced operation stays comparable in a file with repeated unchanged lines, an operation needing an ambiguous anchor stays unresolved, and any other transaction gap still blocks. The mapping is still withheld and still reported in aggregate coverage.
-
10.4 Export the enumerated operation inventory so absence is provable
- Prerequisite discovered 2026-09-16, not in the original plan. The 10.1-10.3 rerun showed the remaining two vetoes cannot be scoped or relaxed soundly as written. A taint answer reports flows, so an absent row means either that the operation does not exist in the base or that no traced flow reached it.
dynamic_external_or_depth_context_unresolvedexists precisely to stop the second being read as the first, and on a real Express application it is always true, because ordinary framework and dependency calls have unresolved destinations. It therefore carries no discrimination and can never be discharged, while removing it would make an unreached operation look absent. - Export the operations the query actually enumerated in the question's scope, separately from the flows it traced, so absence of an operation is established by the inventory rather than by the absence of a flow. Sanitizer and source facts are unchanged; this adds no detector, alias resolver or second IR.
- Done when a base answer distinguishes "this operation does not exist here" from "no traced flow reached it", with an unenumerated scope remaining explicitly unresolved.
- Boundary: contributor-scan query/evidence contract, Delta and Actionability Policy
- Depends: 10.1
- Requirements: 2.1, 2.2, 3.4, 8.1, 8.2
- Verified 2026-09-16: 1,354 tests passed, nine skipped; Ruff/format/mypy passed. The taint query exports one row per enumerated operation plus a completion marker, which is what makes an empty inventory meaningful. Comparison now separates three outcomes: operation present in the base without a traced flow, operation absent from an enumerated base, and a base scope never enumerated, which stays explicitly unresolved along with any malformed inventory. The arbiter skips every row carrying a kind, so an inventory row cannot become a flow with an empty source. Real-engine smoke on an authored two-revision JavaScript control confirmed emission, the zero-operation marker and unchanged answer integrity:
benchmarks/measurements/2026-09-16-operation-inventory-smoke.json. Pre-inventory fixtures were given an enumerated base, since an unenumerated one can no longer establish absence.
- Prerequisite discovered 2026-09-16, not in the original plan. The 10.1-10.3 rerun showed the remaining two vetoes cannot be scoped or relaxed soundly as written. A taint answer reports flows, so an absent row means either that the operation does not exist in the base or that no traced flow reached it.
-
10.5 Make the external and dynamic context boundary question-owned
- M1a recorded
dynamic_external_or_depth_context_unresolvedon the target's own question. The flag is computed over every in-scope method, so one unresolved call anywhere vetoes every question. - With 10.4 supplying inventory evidence, bind the boundary to what it actually limits: reachability, not enumeration. A question establishing absence from the inventory no longer needs complete reachability; a question asserting a traced flow still does. Unknown ownership remains a blocker; absent evidence of independence is not proof of independence.
- Done when an absence established from the inventory survives an unrelated unresolved destination, while a reachability claim under a bounded scope stays unresolved.
- Boundary: contributor-scan query/context evidence, Ranker Scope Contract
- Depends: 10.4
- Requirements: 2.1, 2.2, 8.1, 8.2
- Verified 2026-09-16: 1,362 tests passed, nine skipped; Ruff/format/mypy passed. The boundary is now bound to what it limits. At scan level it is recorded against its owning question without demoting that question's answer, while an unavailable projection, an empty scope and an unreadable extent still demote. In comparison it no longer blocks the head's own traced flow, and no longer blocks an absence read off a structural enumeration, but still blocks every claim resting on a flow not having been traced. The query declares its enumeration basis, because a scope followed through the call graph can be shrunk by the same unresolved destination; such an inventory cannot establish absence while reachability is bounded. Controls cover both directions and both bases; every other gap kind still blocks.
- M1a recorded
-
10.6 Stop vendor exclusion acting as a global completion veto
vendor_semantics_unresolveddegrades every otherwise complete outcome tograph_incomplete, including on a complete 44-file first-party census with successful query receipts.- Keep the exclusion physical and keep the coverage note. Let the inventory and reachability evidence from 10.4 and 10.5 carry whether a specific answer needed excluded semantics; a recorded dependency on an excluded path still blocks that question.
- Done when an unrelated vendor exclusion no longer invalidates a first-party answer, while a question with evidence of dependency on excluded code remains unresolved and the exclusion stays out of targets and graphs.
- Boundary: contributor-scan graph/scope evidence, Cache and Engine Boundary
- Depends: 10.5
- Requirements: 2.2, 2.3, 8.2, 8.4
- Verified 2026-09-16: 1,365 tests passed, nine skipped; Ruff/format/mypy passed. Graph boundaries are split into integrity failures, which still demote a completed answer, and declared exclusions, which do not. Whether a particular answer needed the excluded semantics is carried by that question's own unresolved call destinations from 10.5, not assumed for every answer. Controls exercise the real partition logic rather than an injected reason: a vendored directory leaves the answer completed with the exclusion still reported, an unresolved symlink still demotes, and the two together still demote. The exclusion stays physical and out of targets and graphs. The NodeGoat rerun showed the same distinction was still missing inside comparison, where any degradation and any boundary blocked: comparison now filters declared exclusions and correspondence ambiguity on both sides, keeping integrity degradations and every other boundary blocking.
-
10.7 Bind candidate dependency gaps to the candidate
- Discovered 2026-09-16 in the M1b production rerun. The runner passes every coverage reason as every candidate's
dependency_gaps, so admission always answersdependency_unresolved. That is not "an unresolved dependency that undermines the claim"; it is every gap anywhere in the transaction. - Supply the gaps that pertain to this candidate's required comparison scope, the same scope 10.2 established. Unknown ownership still counts against the candidate.
- Not an M1b exit blocker, since eligibility stays disabled and no alert is emitted either way. It would prevent admission from ever passing after qualification, so it belongs before M4.
- Done when an unrelated transaction gap no longer appears as this candidate's unresolved dependency, while a gap in its required scope still does.
- Boundary: Push runner, Delta and Actionability Policy
- Depends: 10.2
- Requirements: 3.1, 3.3, 3.4
- Verified 2026-09-16: 1,371 tests passed, nine skipped; Ruff/format/mypy passed. Gaps are now attributed: a boundary carrying a question id counts only when that question is in this candidate's required scope, an unattributed gap always counts because unknown ownership is not evidence of independence, and integrity degradations count. A declared exclusion, correspondence ambiguity and bounded reachability do not, because each was already weighed against this specific claim. The runner no longer hands every candidate the whole transaction's reason list, which had included other candidates' comparison outcomes.
- Discovered 2026-09-16 in the M1b production rerun. The runner passes every coverage reason as every candidate's
-
10.8 Carry query-to-operation provenance into the reported evidence
- The target is selected through a file-fallback question with
function=Nonewhile rows report method<lambda>2. M1a showed this did not block the comparison once the question completed, so it is provenance quality rather than the blocker. - Record verified query-to-operation provenance on the evidence; do not inject benchmark function names into regions or count coincident sites.
- Done when a reported operation names the question that produced it and the method the engine reported, with unresolved provenance explicit.
- Boundary: Delta and Actionability Policy, contributor-scan normalization
- Requirements: 3.1, 3.4, 6.3
- Verified 2026-09-16: 1,371 tests passed, nine skipped; Ruff/format/mypy passed. Evidence now carries the method the engine reported beside the question that produced it, so the link is inspectable rather than inferred or dug out of a detail blob. Provenance is recorded as
exact,file_scopewhen the question named no function and the engine supplied one,unresolvedwhen the two disagree, andmethod_unreportedwhen the engine named none. Controls cover all four. No region is renamed, no benchmark function name is injected and no coincident site is counted; the state is recorded, not applied.
- The target is selected through a file-fallback question with
-
10.9 Rerun the three revisions through the production rules
- Replay the same pinned vulnerable, fixed and unchanged revisions without the recorded-veto flag under the named development budget. Demonstrate a developer-readable evaluation finding with a supported consequence, exact location, witness, change attribution and repair direction.
- Verify the fixed twin is analyzed and produces no actionable defect and the unchanged revision is not reported as a new regression. Keep qualification eligibility disabled; experimental evaluation output cannot advertise an unqualified hook alert.
- Done when one replayable record states which vetoes were repaired and which remain, with completed required checks and readable source and census receipts.
- Boundary: Evaluation harness, full-run integration
- Depends: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8
- Requirements: 1.1, 2.1, 2.4, 3.1, 3.4, 6.3, 7.1, 7.3
- Verified 2026-09-16 with one named exception: all three revisions replayed through the production rules with exact pins and reopened source receipts. Vulnerable: three sites new with
operation_absent_from_comparable_base, admission rejecting oncapability_unavailablealone. Fixed twin: analyzed and quiet with three base-only findings. Unchanged: three sitesunchangedwith base locations resolved to the same lines. Every finding carries its question, the engine-reported method and its witness; the head census is the complete 44 files; all 436 base questions complete. Zero admitted defects. The repaired and remaining veto lists are in the M1b record. Exception: the supported security consequence and concrete repair direction are not demonstrated, because both are owned by an evaluated capability declaration and the registry is deliberately empty. M1b's comparison half is verified; its explanation half waits on M4.
-
10.10 Render the experimental evaluation finding with its consequence and repair
- The remaining half of M1b's exit, named by the 10.9 run. The comparison is verified, but the milestone also asks for a developer-readable finding carrying a supported security consequence and a concrete repair direction. Both come from an evaluated capability declaration and the registry is deliberately empty, so nothing renders them.
- Generate an explicitly unreviewed, experimental declaration for this capability with the existing registry tool, and render the finding through the same grounded-template path the admitted case uses. Keep eligibility disabled: an experimental declaration is not enabled and not PASS, so admission must still reject it and no normal hook alert may be advertised.
- Do not write consequence or repair text that no declaration supports. Refusing generic advice without a supported context and operation is the rule admission exists to enforce, and the experimental path must not become a way around it.
- Done when the experimental output shows family, exact location, witness, change attribution, consequence and repair direction for the vulnerable revision, is labeled experimental throughout, and the production result still reports no actionable defect with zero admitted defects.
- Boundary: Result and Hook Adapter, Delta and Actionability Policy, Evaluation harness
- Depends: 10.9
- Requirements: 3.1, 3.2, 3.3, 5.1, 5.5, 6.3
- Verified 2026-09-16: 1,386 tests passed, nine skipped; Ruff/format/mypy passed. Three evaluation findings rendered on the vulnerable revision with exact location, witness, change attribution, provenance, consequence and repair, all from the declaration's templates through the grounded-template path. The declaration reports verdict experimental and enabled false, the production result still reports no actionable defect, and all three artifacts carry zero admitted defects. Controls prove an ungrounded template is refused, an uncovered operation symbol is stated, no matching declaration leaves both sentences absent with the reason, and a declaration claiming PASS still admits nothing because the rendering path is outside admission. Evidence:
benchmarks/measurements/2026-09-16-nodegoat-m1b-explained.json.
-
11. Fit the same useful analysis into the push budget (release milestone M2)
-
11.1 Give the backend a transaction-owned engine session
- The measured cost is JVM startup, not analysis. In the M1b runs each disposable
joern --scriptinvocation cost 13 to 23 seconds while the work itself took under a second, and the 2026-09-14 lifecycle probe measured one session at 12.8 s to start, 3.6 s to load a graph and 0.22 to 0.47 s per warm request. - Own the session inside the existing backend for the length of one transaction: loopback only on an ephemeral port with no published port, a private working directory, one shared deadline, serialized requests, explicit graph identity and epoch, and kill-and-reap of the whole process group on cancellation, failure or exit.
- An HTTP status is not an answer contract. Require a per-request receipt written to a fresh private file and verified before the answer is accepted, the way the probe's repaired instrument did after its first attempt read an empty stdout as a census.
- Poison the session on any crash or malformed response so a failed session can never answer a later request, and fall back to the disposable path rather than reporting a zero.
- Done when a controlled hanging in-session request is cancelled inside the allowance with no surviving process group, a crashed session answers nothing afterwards, and the disposable path still works unchanged.
- Boundary: contributor-scan runtime, Cache and Engine Boundary
- Requirements: 4.1, 4.2, 7.3
- Verified 2026-09-16: 1,391 tests passed, nine skipped; Ruff/format/mypy passed. Sixteen session tests drive a real fake-server process: receipt verified before an answer is accepted, a mismatched receipt or crash poisons the session so nothing answers afterwards, an empty body with its receipt is a valid empty answer, a hanging request is cancelled and the process group is gone inside the allowance, and a launch failure is recorded rather than raised. The probe took four iterations, each failing loudly:
runScriptis absent from the server REPL; the server answers success even for a compile error, so the receipt is the only proof; a shipped@mainscript cannot be nested in a block, so definitions go to the top level unwrapped; and the REPL shadowsConsole, so capture must qualifyscala.Console.withOut. Evidence:benchmarks/measurements/2026-09-16-session-script-execution.json.
- The measured cost is JVM startup, not analysis. In the M1b runs each disposable
-
11.2 Prove every shipped query kind answers identically through both transports
- Run taint, dominance, configuration and census over the same graph through the session and through the disposable path and require identical payloads, including empty answers and the distinction between an empty answer and an unavailable one.
- Cover a malformed or absent response, a switched graph, a second graph loaded in the same session, and a request arriving after cancellation. Equivalence on a census alone does not establish equivalence for security queries.
- Done when every shipped query kind matches byte for byte on a real graph, and every failure shape is reported as unavailable rather than empty.
- Boundary: contributor-scan runtime, engine integration tests
- Depends: 11.1
- Requirements: 4.1, 4.2, 6.4, 7.3
- Verified 2026-09-16: taint, dominance and configuration all return byte-identical payloads through both transports on a real NodeGoat graph, with parameters built by each arbiter's own request function through the driver's family lookup. Failure shapes are correct: an empty answer stays present and distinguishable from an unavailable one, an unknown query and an absent graph are both unavailable, and two graphs in one session each answer about the graph the request names. Amortized over five taint requests the session took 22.48 s against 71.96 s disposable, all answered and identical; a single request per session is its worst case and is recorded as such. Evidence:
benchmarks/measurements/2026-09-16-session-transport-equivalence.json.
-
11.3 Route the batch path through the session behind an explicit setting
- Consume the session from the existing batch and census paths, keeping the disposable path as the default and as the fallback. No capability, ranking decision or evidence semantics changes; only the transport does.
- Account for session startup, load, requests and teardown inside the one shared deadline, and record them as separate stage costs so the next decision is made on measurements rather than estimates.
- Done when a replay produces the same evidence and admission through either transport under a sufficient budget, with per-stage costs recorded for both.
- Boundary: Cache and Engine Boundary, Push runner
- Depends: 11.2
- Requirements: 4.1, 4.3, 6.4, 7.3
-
11.4 Remove the startup cost from the critical path where the measurement says it pays
- With 11.3's stage costs in hand, decide what else to change. The candidates the M1b runs point at are the overlay pass, 15.8 s in a separate invocation, and session startup, 12.8 s, which may overlap the frontend build rather than follow it.
- Change only what the measurement implicates, and keep each change's correctness evidence separate from its timing evidence. Do not substitute a faster census for a completed security check.
- Done when the recorded stage costs after the change are compared against the same profile before it, with any regression in completed checks reported rather than traded away.
- Boundary: contributor-scan runtime
- Depends: 11.3
- Requirements: 4.1, 4.2, 6.2, 6.5
- Verified 2026-09-17: 1,399 tests passed, nine skipped; Ruff/format/mypy passed. The overlay, the last fixed cost bypassing the session, now runs in-session and the saved graph is verified rather than assumed. The first session-enabled replay then showed correct detections and no speedup, and a targeted probe disproved the sequence hypothesis before finding the real defect: a build runs on a copy of the backend, so the session it started was never closed, leaking a JVM holding the configured heap, twice per transaction, enough to exhaust the container. With that fixed the session stays alive for the whole transaction and no
session_query_failedappears. Measured on the same pins and budget, the vulnerable revision went from 326.4 s to 243.3 s, head query time from 63.6 s to 32.0 s and base from 85.1 s to 51.2 s, with dominance and configuration each falling from about 14 s to about 4 s. Detections unchanged. Evidence:benchmarks/measurements/2026-09-17-nodegoat-session-transport.json.
-
11.5 Rerun the frozen seven-class runtime profile and decide
- Rerun the unchanged seven-class profile: identical-tip, function edit, dependency edit, configuration edit, cold, growth and multi-ref, three samples each, retaining every timeout and unresolved check. Identical-tip reuse cannot substitute for warm changed-code work.
- Verify the M1 outcome still holds on the same three revisions through the production rules, so speed is not bought with a lost detection.
- Apply the existing gates without alteration: representative warm changed-code p95 at most 30 s, cancellation and reporting at most 2 s, supported-check completion at least 95%. A failed gate stays NO-GO and names the dominant remaining cost.
- Done when one versioned scorecard reports p50 and p95 with sample counts beside the M1 correctness outcome, and states GO or NO-GO against the unchanged gates.
- Boundary: Evaluation harness, runtime integration
- Depends: 11.4
- Requirements: 4.1, 4.2, 6.2, 6.3, 6.5, 7.3
- Measured 2026-09-17, verdict NO-GO: the unchanged seven-class profile, three samples each, session enabled. Every changed-code workload still times out at the 30-second deadline, and identical-tip got worse, p50 rising from 18.5 s to 24.2 s. The cause is recorded in the artifacts rather than inferred: 13 transactions report
session_unavailable:session_startup_timeout, so under the real deadline the session usually never starts and everything falls back. Startup is 9 to 13 seconds, about a third of the whole budget, spent before any analysis. Where a session did start, on cached-graph transactions with little remaining work, it cost more than it saved. Correctness is unchanged: the same three revisions through the production rules with the session on reproduce the M1 verdict. Evidence:benchmarks/measurements/2026-09-17-session-runtime-profile.json. Dominant remaining cost: engine startup itself, which a transaction-owned session relocates but cannot remove.
-
12. Security capability work found during M1a and M1b
- Group 11 established that M2 needs graph reuse rather than more runtime tuning, and that decision is recorded there. This group returns to what the tool can actually establish about a codebase, which is where the remaining release risk sits: two of the three shipped families cannot complete their change context at all, and admission rejects genuine findings on a mis-extracted operation symbol.
-
12.1 Extract the operation symbol from the operation, not from the statement
- Found 2026-09-16 in the M1b explanation run. Admission takes the text before the first parenthesis as the operation symbol, so
const preTax = eval(req.body.preTax)yieldsconst preTax = eval. The demonstration declaration had to enumerate each assignment spelling for its coverage check to pass. - A qualified declaration cannot enumerate every assignment a codebase might write, so admission would reject genuine findings on
operation_semantics_mismatch. Take the symbol from the called operation the engine reported, preserving receiver spelling, and leave an unresolved or dynamic spelling ineligible as it is today. - Done when the symbol for an assigned call is the call's own name, a receiver-qualified call keeps its receiver, a dynamic spelling stays ineligible, and a declaration naming the plain symbol covers the assigned form.
- Boundary: Delta and Actionability Policy
- Requirements: 3.1, 3.2, 3.4
- Verified 2026-09-17: 1,403 tests passed, nine skipped; Ruff/format/mypy passed. The symbol is now the trailing qualified callee rather than everything before the first parenthesis, so
const preTax = eval(...)yieldsevaland a declaration naming the call covers the assigned form. Receiver spelling is preserved, so$wpdb->prepareandprepareremain different operations. A spelling that is not a qualified name, such as a dynamichandlers[name](x), yields the statement text, matches no declaration and stays ineligible exactly as before. The recorded-veto report reads the same helper rather than repeating the extraction.
- Found 2026-09-16 in the M1b explanation run. Admission takes the text before the first parenthesis as the operation symbol, so
-
12.2 Supply per-family context producers for dominance and configuration
- The M1b exit run left 231 of 436 head questions reporting
change_context_incompletefromcontext_projection_unavailableandcontext_scope_empty, because those families' queries export no context. They did not block the Node injection case, so M1b correctly left them alone, but their families stay experimental until they can answer. - Give dominance and configuration their own context producers describing the actual scope of the query they accompany. Do not copy taint context into them: dominance evaluates sibling operations across a file and configuration has different scope and obligations.
- Done when a dominance and a configuration question complete their change context on a real repository, with missing extents and unsupported reasoning still explicit.
- Boundary: contributor-scan query/context evidence
- Requirements: 2.1, 2.2, 8.1, 8.2
- Verified 2026-09-17: 1,405 tests passed, nine skipped; Ruff/format/mypy passed. The gap was larger than a missing query block: the evidence pass dispatches taint only, because only taint has an evidence vector, so these families were never asked for context at all. Each query now describes its own scope, dominance the file's methods labelled operation, guard or sibling because its claim is about a file contradicting itself, and configuration the file's methods labelled setting or sibling. Neither copies taint's reachability projection, which would describe work they never do, and both report an empty scope explicitly. A new context pass asks each family with the flag set; a family whose query answers nothing keeps no context and stays unresolved, which a control asserts so no family can complete on another's context. Confirmed on a real NodeGoat graph: both produce a context summary and seven scoped methods. Evidence:
benchmarks/measurements/2026-09-17-per-family-context.json.
- The M1b exit run left 231 of 436 head questions reporting
-
11.6 Decide what to do about engine startup, which the session cannot remove
- Named by the 11.5 measurement. A transaction-owned session amortizes engine startup across the invocations inside one transaction, and a warm changed-code push does not have enough of them: startup is 9 to 13 seconds of a 30-second budget, so the session either fails to start or costs more than it saves. Group 11 as designed cannot reach the gate.
- The options are a decision for the maintainer, not an implementation detail, because two of them change the product's shape. A session shared across transactions is a resident local service, which the accepted milestone spec explicitly does not imply. A smaller budget for what a push analyses is a scope change, not a speed change. A faster cold path would mean attacking the frontend and overlay costs directly, which the session does not touch.
- Record the measured cost of each candidate before choosing, and keep the 30-second and 2-second gates unchanged; a gate met by analysing less is not the same result.
- Done when one record states the candidates, their measured or estimated costs, their effect on the product's shape, and the maintainer's choice.
- Boundary: contributor-scan runtime, release direction
- Depends: 11.5
- Requirements: 4.1, 4.2, 6.5
- Decided 2026-09-17 by the maintainer: do not rebuild a whole graph per push. Both startup attacks were measured and neither reaches the gate. The session helps a long transaction, 326 s to 243 s, and cannot help a 30-second one: startup is 9 to 13 seconds of the budget, so it usually declines. Removing JavaScript's second engine start is real, 11.0 s against 21.0 s on identical graphs, but its census gate was asymmetric and it was reverted after costing two engine starts on 12 profile transactions. What the profile names is not startup at all: a changed-code transaction reaches its deadline during the build, head build 25.2 s of 30 s with query time 0.0 s. The remaining cost is building a fresh whole-repository graph for every pushed revision. Direction: reuse the graph across revisions instead of rebuilding it, rather than further optimising the rebuild. Not scheduled here; M2 stays unmet and the 30-second and 2-second gates stay unchanged.
-
12.3 Resolve function-scoped questions whose function the query cannot locate
- Named by the 12.2 measurement. With per-family context in place, head completion rose from 205 to 298 of 436 and
context_projection_unavailablefell to zero. Every remaining unresolved question reportscontext_scope_empty, and they sit in three files:app/routes/index.js(114),app/routes/tutorial.js(12) andserver.js(12). None is access control. - These are function-scoped questions whose named function the query cannot locate in the graph, so the scope it describes is honestly empty. It is the same query-to-operation identity problem M1a recorded at the witness end, appearing at the scope end.
- Carry verified provenance from the region's declared function to the method the engine actually holds, and leave an unresolvable name explicit. Do not widen a question to the whole file to make it complete, and do not inject a declared name into a region.
- Done when a question whose function the graph holds completes, a question whose function it does not hold stays unresolved with the name recorded, and no question completes by widening its scope.
- Boundary: contributor-scan query/context evidence, Ranker Scope Contract
- Depends: 12.2
- Requirements: 2.1, 2.2, 6.3, 8.1
- Verified 2026-09-17: 1,406 tests passed, nine skipped; Ruff/format/mypy passed. The cause is recorded rather than guessed:
app/routes/index.jsrequires seven handler modules and defines none of them, so a question naminghandleContributionsUpdatethere names a function that file genuinely does not hold and the empty scope is correct. Taint and configuration now separate the two causes, reportingcontext_function_unresolvedwith the name when a question names a function the file does not define, andcontext_scope_emptyonly when the file has no methods at all. The driver no longer adds its generic empty-scope gap on top of a query that already explained itself, and the new gap is question-owned so it cannot block another question's comparison. Completion is unchanged by design: a question whose function the graph does not hold stays unresolved, and nothing completes by widening its scope to the whole file.
- Named by the 12.2 measurement. With per-family context in place, head completion rose from 205 to 298 of 436 and
-
12.4 Attribute a region to the file that defines its handler
- The region-attribution half of 12.3. A route-registration module registers handlers other modules define.
app/routes/index.jsrequires seven handler modules and defines none, yet regions there were scoped tohandleContributionsUpdateand its siblings, so the graph rightly held no such method and every question in those regions was unresolvable: 138 across six families. - The entry record still names the handler, because the authorization question is which middleware guards which handler and that is cross-file by nature. It now also records whether the file defines what it names, and a region built from an entry that does not is scoped to the file rather than to a method the file lacks.
- Verified 2026-09-17: 1,407 tests passed, nine skipped; Ruff/format/mypy passed. A first attempt changed the mapper to stop naming cross-file handlers and was reverted: an existing control proved the name carries the authorization attribution, so the fix belongs in region construction, not in the mapper. Controls assert the registering file asks about itself with no function-scoped region, while the defining module keeps its function-scoped region.
- Boundary: Ranker Scope Contract, contributor-scan entry mapping
- Depends: 12.3
- Requirements: 2.1, 2.2, 8.1
- The region-attribution half of 12.3. A route-registration module registers handlers other modules define.
-
13. Show the shared design transfers across languages (release milestone M3)
- Status 2026-09-17: Python established, PHP blocked by runtime on a large repository. M3 is not met. Its exit asks for Node, PHP and Python together, and two of the three are in hand.
-
13.1 Replay the declared Python family through the shared core
- Generalize the replay instrument so a case family is an argument rather than a hard-coded target: a harness that only knows one language cannot say whether the design transfers. Targets, pins and expectations come from the committed inputs.
- VAmPI is a regression snapshot with the same revision on both sides, so it can establish detection and completion but never novelty. The mode asserts that too: with nothing changed, a target reported new would be a comparison defect rather than a finding.
- Verified 2026-09-17: all three declared targets detected at their declared lines with their questions answered, 35 of 35 questions complete on every case, nothing reported new, zero admitted defects. The SQL injection at
models/user_model.py:73is the evidence the milestone names as previously unresolved. Two of the three are access control, the family that could not complete a question anywhere before group 12, so this is the first time broken object-level authorization has been asked on Python. Evidence:benchmarks/measurements/2026-09-17-m3-python-transfer.json. - Limits: identical revisions establish detection, not novelty; VAmPI is inspected teaching data and cannot qualify production Python support.
- Boundary: Evaluation harness, full-run integration
- Requirements: 6.2, 6.3, 8.1, 8.2
-
13.2 Replay the declared PHP pair through the shared core
- PMPro 2.9.7 against 2.9.8 with its upstream prepare fix, plus a comment-only benign variant. A different language, a different mechanism and a fix this project did not author.
- The first attempt selected one case of three, because the manifest labels a case by what it is while the case id says what the comparison does; the harness now maps label to role explicitly. That run also exhausted a 900-second budget on the single case it ran.
- Done when all three comparisons complete with their declared target evidence recorded, or the budget they need is stated as a finding rather than reported as a pass.
- Measured 2026-09-17, PHP transfer NOT established. All three comparisons exhausted a 3000-second deadline with a 1200-second query ceiling and produced no artifact, so there is no target evidence at all. The engine reports
cpg batch taint failedtwice per case on the 1,274-file PMPro tree: first an exception whose trace runs through ujson array transformation, then a timeout on the retry. Budgets tried: 900 s, 1200 s and 3000 s deadlines against 300 s and 1200 s query ceilings. Pins, source receipts and the declared target are all correct, so the question was asked properly and the engine could not answer it. This is a runtime blocker on a large repository, not evidence about whether the design transfers to PHP. Evidence:benchmarks/measurements/2026-09-17-m3-php-transfer.json. - Two harness defects found and fixed on the way, each of which had made an earlier run look like a transfer failure: the manifest labels a case by what it is while the id says what the comparison does, and filtering on one while keying expectations by the other ran a single case; and the per-query ceiling is taken as the minimum with the remaining transaction time, so raising only the deadline could never have helped.
- Retried 2026-09-18 with batch splitting, still not established. A batch that fails whole is now retried in eight parts, which a probe made look promising: the evidence pass answers all 4,425 PMPro regions in one batch in 29.7 s with a 681 KB response, and cost is fixed rather than per request, so neither count nor size is near a limit. That probe sent
evidenceOnly=trueand therefore measured the structural pass, not the failing one; recorded because those numbers would otherwise read as evidence that PMPro's taint work is cheap. With a 4500-second deadline and a 900-second query ceiling the run was stopped after 26 minutes with no case completed. Splitting also makes an unrecoverable batch slower to abandon, since each part burns its own share of the ceiling. - Correction 2026-09-20: the malformed-CPG bug is fixed and was never what these runs were hitting. joernio/joern#6281, this project's own report, is a PHP
globalinside a closure producing a node with two AST parents, which made the dataflow overlay throw and cost the WHOLE graph. It was filed against 4.0.623 and fixed by #6270 in v4.0.625, which is the version the image has pinned since before any measurement here. Verified today on the real trigger: PMPro'sincludes/fields.php, the file the original bisection found, builds and applies the default overlays cleanly on 4.0.625 and on 4.0.631. So what remains under this task is a RUNTIME failure and must not be re-diagnosed as the graph bug. - Quantified 2026-09-20, and it is a runtime failure. A taint batch of 2,526 questions over the 673-file checkout does not complete in thirty minutes. Raising the query ceiling six-fold, from 300 to 1,800 seconds, changed nothing but how long the failure took: query time went from 363 s to 1,874 s and zero questions completed in both. The eight-way split retry did not rescue it, finishing its parts in about three minutes against a deadline with 3,300 seconds still on it, which is worth its own look. Joern 4.0.625 and 4.0.631 are indistinguishable here, 627.7 s against 619.1 s. The engine pin stays at 4.0.625: the newer release was built and measured and is equivalent on this subject, so there is no benefit to weigh against re-verifying every JavaScript and Python measurement. Evidence:
benchmarks/measurements/2026-09-20-php-runtime-after-graph-fix.json. - Corrected 2026-09-20: PHP is not silent, and this project's own instrument is why it looked silent. The census harness ordered its verdict completion-first, so a family with eighteen findings and no completed question reported "asked but no question completed" — and only the 500-region runs, the ones that time out, had been looked at. Fixed: findings are checked first, because a question-outcome count is bookkeeping and cannot hide the thing being censused. NodeGoat and VAmPI were never affected, since they complete every question.
- What PHP actually detects on PMPro. At 25 regions, 19 findings in seven minutes with no timeout; at 100 regions, 43 findings in twelve minutes with no timeout. Among them a true positive at
adminpages/discountcodes.php:749:sanitize_text_field($_REQUEST["s"])reaching$wpdb->get_results($sqlQuery, OBJECT)through$sqlQuery .= "WHERE code LIKE '%$s%' "with noprepare. That is the class of CVE-2023-23488, whichphp.toml's own comment names as the reason those WordPress sanitizers are excluded — the prediction and the measurement agree. - And a measured precision cost beside it. Output encoding reports eighteen findings at 25 regions and the three sampled are all properly escaped, two with
esc_attrand one coerced withintval. Those escapers are deliberately absent from a sanitizer list that is flat across families, because none of them stops SQL injection; keeping the injection finding above costs this family its precision, and that is the documented tension's price. - The scale limit, quantified. Roughly one to two seconds per question. 25 and 100 regions complete cleanly; 500 regions asks 2,526 questions, needs more than the 1,800-second ceiling and is killed. Nothing pathological, and the retry's fast failure is still unexplained.
- What is NOT the problem, each measured rather than assumed: the malformed-CPG bug (fixed in 4.0.625, verified on the real trigger file); the dataflow overlay (42 s on the full 637-file graph); the engine version (4.0.625 and 4.0.631 indistinguishable); and the bundled Stripe, Braintree and Twocheckout SDKs under
includes/lib, which the vendored list does not cover — that was the hypothesis and the overlay costs 42 s with them and 37 s without, so it is refuted. - The remaining real gap is bookkeeping, not detection:
questions_completedis zero in every PHP run including the ones that find things, withcross_partition_semantics_unresolvedandpartition_file_census_incompletepresent throughout. Findings and question outcomes disagree in public, and the admission policy reads the outcomes. Group 12 raised completion for JavaScript and Python and PHP was never measured. Evidence:benchmarks/measurements/2026-09-20-php-detection-corrected.json. - IDENTIFIED 2026-09-22. The cost is the number of CALL NODES a family's sink names match in the repository. Not the request count, not the declared sink count, not any request parameter.
reachableByFlowsis asked per matched sink, and PHP's output-encoding sinks areechoandprint, which a WordPress plugin writes thousands of times. - Three controlled sweeps on one graph, built once. Batch size is exonerated: 400 requests answer in 170 s and cost per request FALLS with size, because roughly 70 s is fixed JVM start and graph load. Request parameters are exonerated: at 200 requests the full production shape, call depth 3 with parameter sources, costs 221 s against a 150 s baseline. The family is the variable: at the same 100 requests, SQL injection costs 147 s with six declared sinks, path costs 194 s with sixteen, and output encoding costs 704 s with seven.
- And the driver named its own expensive step, once it was asked to.
taint: 2435 request(s) in 45.5s, answeredfor the evidence pass, thentaint: 315 request(s) in 1200.1s, unansweredfor arbitration across all families, then a split abandoned for want of time. - Why it survived since September: every reproduction outside the driver carried the SQL sinks, because that is the family the declared target belongs to. The one family that is cheap on this subject was the one being re-measured, and nine hypotheses were eliminated that way.
- Why splitting never rescued it: a failed batch is split into eight parts BY REQUEST COUNT, and each part carries the same everywhere-sinks, so the whole is no cheaper. Splitting by request count cannot help when the cost is per matched sink. Evidence:
benchmarks/measurements/2026-09-22-php-query-cost-identified.json. - Blocker identified; the fix is contributor-scan 7.7. Six hypotheses were measured and eliminated: batch size (17,700 requests answer in 44 s with a 2.6 MB response), dataflow cost (50 execution requests in 24 s), call depth 3 with parameter sources (24.7 s for 20), argument length (a 16 KB hook table against a 128 KB cap), hook callback resolution (1 s difference with 233 hooks), and build or sharding (79 s, no shards). Preparation measures 273.5 s. Every reproduction of the failing 22,125-request batch outside the push path answers in under a minute, and the difference from the production invocation is not known. The exception has never been read, because the backend logged a stack trace's tail and the corrected extractor reports its head, which on this engine is JVM startup noise. Evidence:
benchmarks/measurements/2026-09-19-php-blocker-open.json. - Boundary: Evaluation harness, full-run integration
- Depends: 13.1
- Requirements: 6.2, 6.3, 8.1, 8.2
-
14. Ask every declared family whether it establishes anything (precondition for M4)
- Seven families are declared and two were proven end to end. A declared family that has never been asked whether it finds anything is advertised coverage, and M4 cannot qualify a capability that establishes nothing.
-
14.1 Census the seven declared families on NodeGoat and VAmPI
- Run production discovery and scan over each whole checkout and report, per family, the regions that offered it, the questions asked, the questions completed, and what was found with the witness and rung. No comparison, no novelty, no admission: detection only.
- A family that answered and found nothing must be reported differently from one that was never asked, and neither may read as clean.
- Verified 2026-09-19: 1,411 tests passed, ten skipped; the six Joern-gated query tests pass in the image. Four families establish findings across the two subjects: injection (three
evalflows on NodeGoat, one SQL flow on VAmPI), access_control (two sibling contradictions on VAmPI), config_secrets (one on each) and untrusted_destination (one open redirect on NodeGoat). All 299 NodeGoat and 35 VAmPI questions completed. Evidence:benchmarks/measurements/2026-09-19-family-detection-census.json. - The census found one false positive and it is fixed. NodeGoat's only prototype report was the word
setinside the comment// set these up in case we have an error case: the matched node is the<operator>.assignmentholding the whole 1,000-character signup handler, and its text was searched entire.sinkMatchesnow confines its text clause to the callee and excludes operator nodes, which is the fourth instance of this bug class afterfgets/gets,resolve/resolveUrlanduser/users. Measured on NodeGoat's non-vendor calls: injection sinks 28 to 3, prototype 13 to 2, untrusted_destination 26 to 7, with no true positive lost.req.queryhad been an injection sink, becausequeryis word-bounded inside it. - Boundary: contributor-scan detection, Evaluation harness
- Requirements: 6.2, 6.3, 8.1
-
14.2 Give untrusted destination the sinks a Node server actually calls
- Named by the 14.1 census. NodeGoat's documented SSRF is
needle.get(req.query.url + req.query.symbol)atapp/routes/research.js:16, andneedle.getis absent from a vocabulary holding axios, fetch, got,http.getand request. The family works and found the open redirect in the same repository; it could not ask about the request. - This is a vocabulary gap, so fix it as data rather than as a query change, and state which client libraries the declared set now covers.
- Done when the research handler's request is asked about, with a witness naming the call, and no benign client call is reported.
- Verified 2026-09-19: 1,411 tests passed, eleven skipped; seven Joern-gated query tests pass in the image; Ruff/format/mypy passed. One data change, no query change. The declared set now covers axios, got, needle, request, superagent, unirest, undici and fetch as bare module tokens, plus
http.get,http.request,https.getandhttps.requestdotted. Bare is the documented call form of each module and also covers its verb methods, because a token matches word-bounded against the callee andaxiosis bounded insideaxios.get;axios.getandaxios.postwere therefore dropped as subsumed.httpandhttpsstay dotted because their namespace also holdscreateServer, and a bare token would have claimed every server a repository starts. - The research handler's request is now reported at
app/routes/research.js:16, model_entailed, witnessreq.query.url + req.query.symbol -> needle.get(url, ...). This family goes from one finding to two on NodeGoat and every other family is unchanged, with 299 of 299 questions still completing. Measured on that graph,needlematches exactly one call and the eleven other clients match none, in first-party or vendored code, so NodeGoat shows the gap closing and cannot show those clients' noise. - A control asserts both halves rather than only the recall half: a handler calling
axios.getwith a literal andneedle.getwithreq.query.urlreports the tainted client alone, and the control reads the shipped vocabulary rather than a copy so removing the client fails it. The CWE-601 redirect entry is untouched; a Location-header redirect is value-shaped and stays outside what this family asks. Evidence:benchmarks/measurements/2026-09-19-untrusted-destination-vocabulary.json. - Boundary: shipped semantic facts
- Depends: 14.1
- Requirements: 6.2, 8.1
- Named by the 14.1 census. NodeGoat's documented SSRF is
-
14.3 Decide what output encoding claims for server-rendered JavaScript
- Named by the 14.1 census. All six declared JavaScript sinks are DOM sinks. NodeGoat's XSS is server-side, rendered into Swig templates and written with
res.write, so the 44 questions this family completed could not have found it whatever the code did. - Either the family gains server-side sinks and template-rendering sinks, or it declares itself client-side only. Both are honest; silence while declaring XSS coverage is not.
- Done when one record states the decision, and the family either asks about
res.writeand template rendering or says in its declaration what it does not cover. - Verified 2026-09-19: 1,415 tests passed, thirteen skipped; nine Joern-gated query tests pass in the image; Ruff/format/mypy passed. Decision: the family asks about a value reaching a rendering call and states in its own declaration that it cannot decide template-mediated escaping. Whether
res.renderis safe depends on the engine's autoescape setting and on the template's markup, neither of which is at the call, sores.renderis not a sink and the family'slimitsnow says so. The setting is claimed where its evidence is instead, as a configuration defect. - It now establishes a finding:
app/routes/research.js:25, model_entailed,req.query.url + req.query.symbol -> res.write(body). The handler fetches a URL the caller chooses and writes the response body into a text/html response, so the document served from this origin is the attacker's. The record states how coarse the step is: the path fromurltobodyruns through the client's callback parameter, which the graph treats as reachable from the call's arguments, so the conclusion rests on the call's semantics rather than on what the graph established. Five of seven families now establish findings. Evidence:benchmarks/measurements/2026-09-19-output-encoding-family.json. - Four defects found on the way, three of them false positives this change would otherwise have shipped. The permissive-value set was shared across every setting while a boolean's polarity belongs to the setting, so
swig.setDefaults({autoescape: true})would have been reported as the defect on the line that repairs it;ConfigSpecnow carries per-setting values and Jinja2'sEnvironmentwas moved out of Python's shared list for the same latent reason.config.scmatched a dotted setting on its trailing segment alone, somarked.setOptionswould have claimed an editor'ssetOptions.taint.scasked its flow query of every argument including the receiver, sores.end()— which takes no data — was reported exactly like theres.write(body)two lines above it. And the arbiter sorted a file's settings by line number as text, so which setting a file reported depended on the spelling of its line numbers. - Boundary: shipped semantic facts, family taxonomy
- Depends: 14.1
- Requirements: 6.2, 8.1
- Named by the 14.1 census. All six declared JavaScript sinks are DOM sinks. NodeGoat's XSS is server-side, rendered into Swig templates and written with
-
14.6 Report every permissive setting in a file, not the first one
- Named by the 14.3 measurement. The configuration arbiter returns one verdict per question. NodeGoat's
server.jsholds a permissive session at line 78 and template auto-escaping disabled at line 135 in the same function, so the escaping defect is detected — measured model_entailed against the real query and the real arbiter — and never reported. A contributor is shown one of two defects and told nothing about the other. - This is the arbiter's contract, not a data gap:
verdict()returnsVerdict | Noneand the scan collects one per question. Changing it touches every family that returns a verdict, so it is scheduled rather than folded into a vocabulary change. - Done when a file with two permissive settings reports both, with their own sites and witnesses, and no family reports a defect twice.
- Verified 2026-09-20: 1,417 tests passed, seventeen skipped; thirteen Joern-gated query tests pass in the image; Ruff/format/mypy passed. Configuration now returns one verdict per setting whose value the model could read. The pipeline already emitted one finding per entailed answer, so only this arbiter was still folding them to one. NodeGoat's
server.jsreports both defects with their own sites,server.js:78for the permissive session andserver.js:135for the disabled template auto-escaping, which is the defect task 14.3 could detect and never report. Its applied fix at line 126 stays silent. VAmPI is unchanged and no other family moved. Evidence:benchmarks/measurements/2026-09-20-every-permissive-setting.json. - Three boundaries drawn rather than assumed. A setting whose value is COMPUTED stays one verdict per region: that band is a question for the judge, and returning every undecided setting would multiply model calls for claims the graph did not establish. A call carrying two permissive literals is one finding, because
cors({origin: true, credentials: true})is one misconfiguration. And config verdicts now carry apath:line:functionlocation, without which the plural answer collapsed straight back to one in the deduplicator, which is the same gap dominance carried until its findings stopped arriving with no line. - Dominance is left answering once per region, and that is settled rather than pending. Its claim is that a region contradicts itself, so the region is the unit and a second verdict would restate the same inconsistency. The pipeline says so where it used to call this a known gap.
- Boundary: contributor-scan arbitration, Rung ladder
- Depends: 14.3
- Requirements: 6.3, 8.1
- Named by the 14.3 measurement. The configuration arbiter returns one verdict per question. NodeGoat's
-
14.4 Raise the access-control obligation where the application splits route from query
- Named by the 14.1 census and measured with the real dominance query. Every modelled data operation in NodeGoat lives in
app/data/*-dao.js; none lives in a route handler file. Operations are scoped to the region's own file and access_control regions are the handler files, so no obligation is raised where the question is asked, and inside the DAO files every operation is equally unguarded so the sibling rung has nothing to contradict. The family answers correctly on VAmPI, which keeps handler and query in one file. - The file scope was chosen deliberately, because unscoped rows attributed a model's unguarded query to a handler of the same name. Widening it is not the fix; carrying the obligation along the call the handler actually makes might be.
- NodeGoat's two documented defects are also outside what the model can claim: an authenticated handler reading
userIdfromreq.params(app/routes/allocations.js:17), and a commented-outisAdminin the route table (app/routes/index.js:58). Record which of the two this task is for, and do not silently widen the claim to the other. - Done when a route-and-DAO split raises the obligation at the handler with a witness naming both sites, and a same-name model function is still not attributed to a handler.
- Verified 2026-09-20: 1,415 tests passed, fifteen skipped; eleven Joern-gated query tests pass in the image; Ruff/format/mypy passed. This task is for the first defect, the route-and-DAO split itself. The obligation now follows the one named call the handler writes, out of the region's file and no further. Attribution is by the graph's own provenance, not by name: jssrc2cpg leaves the callee an external stub, and that stub's fullName records the file and member where the implementation was written, so a module the handler never imports is never consulted. A row is attributed to the region's DECLARED function, which is what lets a question about
displayAllocationsmatch a graph that holds a<lambda>. A carried row is also checked for a guard at the CALL, because that is where a handler establishes who is asking, and the method holding the carried operation joins the question's context scope so the query's own module cannot change unattributed. - Measured on NodeGoat: 15 handlers asked, obligations raised at zero before and at six now, three of which reach a verdict. All three are corroborated and none is reported without a judge. VAmPI is unchanged at two entailed findings, because it keeps handler and query in one file and carries nothing. Evidence:
benchmarks/measurements/2026-09-20-access-control-module-split.json. - A carried obligation corroborates, it does not entail, and that was measured rather than assumed. Entailing it reported
handleSignup, which looks a user up by name to check the name is free during registration and is public by design, because two authenticated siblings in the same file passreq.session.userIdinto the same module. The entailed rung's justification is that one FILE contradicts itself, same file and same trust context; carried, the compared operations live in a data-access module that handlers of different trust contexts share. The corroborated band already asks the right residual: does this operation require a check, or is the route legitimately public? - What this does not reach, recorded rather than implied: NodeGoat's documented IDOR at
app/routes/allocations.js:17still raises nothing, because its module reads withallocationsCol.find(...)andfindis not a modelled operation (task 14.8). The commented-outisAdminatapp/routes/index.js:58is not claimable by this abstraction at all, since the evidence that/benefitsneeds a stronger guard than its siblings exists only in a comment. - Boundary: contributor-scan dominance, Ranker Scope Contract
- Depends: 14.1
- Requirements: 6.2, 6.3, 8.1
- Named by the 14.1 census and measured with the real dominance query. Every modelled data operation in NodeGoat lives in
-
14.7 Decide the sibling scope, and see identity that a destructuring spelled away
- Named by the 14.4 measurement. Two of NodeGoat's three corroborated claims are correct code:
const { userId } = req.sessionfollowed bydao.getByUserId(userId, ...)is identity-constrained, and the destructuring erases thesession.userIdspelling the token list holds, so the guard is invisible at the call. A judge would be asked about handlers that are not defects. - The contradiction that would make NodeGoat's IDOR entailed is cross-file:
allocationspasses a URL parameter whilecontributionsandprofilepass session identity. The sibling set is one file, and widening it is a decision, not an implementation detail: measured, a route file mixes public with authenticated handlers, so application-wide siblings would contradict every login and signup handler there is. - Record the measured cost of each candidate before choosing, and do not widen the scope to make a finding appear.
- Done when one record states the candidates and the choice, a destructured identity read discharges the obligation it actually discharges, and no public route is reported.
- Verified 2026-09-20: 1,415 tests passed, sixteen skipped; thirteen Joern-gated query tests pass in the image; Ruff/format/mypy passed. A destructured identity read now discharges what it reaches, through a bounded three-hop walk back over the method's own assignments from the names the operation is GIVEN.
req.sessionjoins the identity sources and is only safe because of that requirement: what discharges an object-level obligation is that the value PASSED to the operation comes from the authenticated context, not that a session is mentioned nearby. On NodeGoatdisplayContributionsanddisplayProfilego silent, which is correct code no longer put to a judge, and VAmPI is unchanged. - Two exclusions the measurement forced, each caught by a page whose whole purpose is the leak it demonstrates. Taking identifiers from the whole argument subtree credited NodeGoat's memo listing with a discharge, because
memosDAO.getAllMemos((err, docs) => ... userId ...)reads every memo and only RENDERS the session's user; a callback is not a constraint, so method references are excluded. Then the session read still dominated the query and the dominating-guard clause credited that instead; an assignment is a read of the authenticated context rather than a check applied to it, so operator calls are excluded from that clause, which is the same exclusion the sink matcher needed. The leak stays reported. - The sibling scope is decided by measurement, and the choice is to keep the file. On NodeGoat the region's own file entails two handlers, both public routes and both false, and no true defect at all. Every handler in the application entails six: four true, and the same two public routes. Both scopes carry the same false positives, because neither can tell a public route from a protected one — which is a semantic question and already the corroborated band's residual. With the carried rung capped at corroborated the scope changes no verdict, only the witness, so widening it would buy a repository-wide walk per question and no precision. What would change the decision is a discriminator for a public route; with one, the wider scope is worth four true entailments on this subject. Evidence:
benchmarks/measurements/2026-09-20-sibling-scope-and-identity.json. - Boundary: contributor-scan dominance, shipped obligation facts
- Depends: 14.4
- Requirements: 6.2, 6.3, 8.1
- Named by the 14.4 measurement. Two of NodeGoat's three corroborated claims are correct code:
-
14.8 Model the collection read an application actually writes
- Named by the 14.4 measurement. NodeGoat's documented insecure direct object reference reads with
allocationsCol.find(searchCriteria()), andfindis absent from an operation vocabulary holdingfindOne,findAllandfindById. It is the commonest read there is, so the family's obligation is missing at exactly the shape it exists for. - It is absent for a reason:
Array.prototype.findis everywhere in JavaScript and jQuery's.findis everywhere in a vendored bundle. A collection read takes a query document where those take a closure or a selector, so the discriminator is the argument's shape rather than the name. - Done when a collection read raises the obligation, an array search and a selector do not, and the false-positive count is measured on a repository with vendored bundles rather than argued from the shape alone.
- Verified 2026-09-20: 1,415 tests passed, sixteen skipped; twelve Joern-gated query tests pass in the image; Ruff/format/mypy passed. The obligation is claimed where the first argument has the shape of a query document, which the operation fact states as
query_document_argand the arbiter decides: no arguments at all, an object literal, or a builder call. Not claimed: a method reference, a literal, an operator call, and a bare identifier. - The false-positive count is measured, on wp-statistics, a WordPress plugin that ships jQuery. Its assets hold 88 calls named
find— 70 jQuery selectors, 13 array predicates and 5 selectors built at run time by a template literal or a concatenation, and no collection read at all. Zero became an obligated operation. The zero is a measured one and not an empty graph: the same run's census reports 60 files and 4,552 methods, and the files holding those 88 calls are in it. - On NodeGoat all three
findcalls are collection reads and all three are claimed, which raises the obligation at four more handlers includingapp/routes/allocations.js:displayAllocations— the documented insecure direct object reference, which had raised nothing at any point in the pipeline before. Seven of fifteen handlers now reach a verdict, all corroborated, none reported without a judge, and the census is unchanged on both subjects. Evidence:benchmarks/measurements/2026-09-20-collection-read-obligation.json. - A bare identifier is deliberately not claimed.
col.find(query)andarr.find(pred)are the same shape and neither repository measured holds one, so this is a stated false negative rather than an unmeasured false-positive risk. - Boundary: shipped obligation facts, contributor-scan dominance
- Depends: 14.4
- Requirements: 6.2, 8.1
- Named by the 14.4 measurement. NodeGoat's documented insecure direct object reference reads with
-
14.5 Ask the prototype and deserialization families somewhere they can answer
- Named by the 14.1 census. Prototype pollution has been asked on one repository and its only report was a false positive; deserialization was never asked at all, because VAmPI holds no such sink and NodeGoat is not a language where the family is declared. Neither has established anything anywhere.
- Find or record a subject where the family has a sink, and report what it does. A negative on a subject that contains the mechanism is a result; silence on a subject that does not contain it is not.
- Verified 2026-09-20: 1,417 tests passed, eighteen skipped; Ruff/format/mypy passed. Neither family has a subject that can answer, and each for its own reason, which is the result rather than a failure to produce one.
- Deserialization: the mechanism exists on exactly one subject and that subject cannot be queried. Across four WordPress plugins and 1,164 PHP files there is one deserialization site,
maybe_unserializeatincludes/fields.php:1111in Paid Memberships Pro. The vocabulary held only the bareunserialize, which never reached it and correctly so: the matcher is word-bounded and the character before the token insidemaybe_unserializeis a word character. That spelling is now declared, so the family has a sink to ask about — and the census asked 487 deserialization questions on that plugin and completed none, because no query over that repository completes at all. That is task 13.2's runtime failure, now quantified there, not a statement about this family. - Prototype pollution: no available subject contains the mechanism. The only first-party matches on the Node subject are
app.set("view engine", "html")and its sibling, Express settings whose arguments are literals, and the other JavaScript available is minified browser bundles with no request source. So the family's ability to answer is established on a control instead: a merge of the request body into an object is reported, and the Express setting beside it is not. Declared and never demonstrated has become demonstrated on a control and never on a real subject, which is a different and honest claim. - Boundary: Evaluation harness
- Depends: 14.1
- Requirements: 6.2, 8.1
-
13.3 Re-run the declared PHP transfer at a scale whose questions complete
- Named by the 2026-09-20 correction. The not-established record came from two instrument defects, not from the analyser: the census verdict checked question completion before findings, and a 500-region budget asks 2,526 questions, which no ceiling this project sets can hold. Bounded to 25 and 100 regions, PMPro completes with no timeout and establishes a true-positive SQL injection at
adminpages/discountcodes.php:749. - Replay the three declared comparisons with the corrected harness at a region budget that completes, and record the declared target's evidence or its absence. State the budget as a limit of the run, not as a property of the repository: a bounded population establishes transfer, never coverage.
- Done when all three comparisons complete with their declared target evidence recorded, or the budget they need is stated as a finding.
- Attempted 2026-09-21, and the attempt named its own blocker. At 25 regions the first case now runs to completion and writes a full artifact, in 1,706 s, where at 100 it had exhausted a 2,400 s deadline with nothing. But 25 regions of 4,461 is 0.6% of the population, and the declared target at
classes/class.memberorder.php:936was not among them: zero findings at the target, because the question was never asked. A bounded population establishes transfer only if it CONTAINS the target, and this one cannot. - So this task depends on contributor-scan 7.3, not on a larger budget. The population that holds the target is affordable only if arbitration stops being paid for questions whose evidence says there is nothing to find. Raising the budget is the thing already measured to fail; making most of the budget unnecessary is the thing that has not been tried.
- Two further observations from that run, recorded because they cost nothing to keep. The head scan cost 1,178 s against the base's 219 s, which is worth explaining before M2 designs around either number. And every coverage reason was
ambiguous_line_correspondence, one per file — so in the PUSH path, unlike the census, correspondence is what stands between this subject and a completed question. The partition work removed one blocker and revealed the next. - Sized and attempted 2026-09-22. One whole-repository scan at 500 regions with a 3,600 s deadline puts the declared target in scope and finds it: 271 of 406 questions completed, 213 findings, 2,950 s. The three-case replay at that budget then exhausted a 7,200 s case deadline with no artifact, and the failure is mine to own twice over: the container mounts
srclive and I edited the backend heavily during the run, including a version that could hang on a timed-out pipe; and the push's EVIDENCE pass is one unportioned 22,125-request batch protected only by the ceiling, which it exceeded under the CPU and memory contention my concurrent test runs created. Neither is the algorithm. The evidence pass is now portioned, streaming keeps what a killed portion finished, and the rerun uses a frozen export of the committed source with per-invocation logging so the record names its own expensive step. - Attempted again 2026-09-23 on frozen
cda679c, after contributor-scan 7.9, and it failed at a new wall. Case 1 (introduce) spent its whole 7,200 s in the HEAD side's evidence pass and saved no artifact; cases 2 and 3 were stopped on the same path. The evidence portions took 13, 31 and 51 minutes each. A push asks evidence of the WHOLE population with change context, 22,125 requests, where the census asks about 2,400 without it. - Diagnosed by probe, not by timestamp. On one PMPro graph from the production request builder, context costs ~20% more engine time per request -- not the wall. The wall is OUTPUT: each request itemised every method its scope reached, up to 1,845 rows; 200 requests into the bundled Braintree SDK returned 52,831 rows, 9.4 MB, against 211 without context.
affected_contextonly ever attaches a location in a CHANGED file and discards every other row after checking it, so nearly all of it was shipped to be thrown away. Fixed in3c195c9: the engine itemises the changed files and summarises the rest into one row carrying what the check reads. On PMPro the same 2,440 relationships and zero gaps are decided from both forms, 52,831 rows down to 3,241 with the most-referenced file as the change. Probe:benchmarks/push/context_cost_probe.py. The replay is re-running on frozen3c195c9. - Corrected the same day: that was real but it was not the wall. The re-run on
3c195c9showed the same 13 → 31 → 51-minute portions. Sampled in flight, no engine was running for minutes at a time while thepre-pushPython process sat at 83% CPU. The cause was the reuse cache: every publication listed and stat'ed the WHOLE cache to total its size, and a push publishes one answer per question, so the pass was quadratic. Measured on the live cache, one scan took 0.68 s at 12,238 entries, ~70 minutes of scanning by then and ~2.3 hours to finish the pass. The growing portion times were the quadratic, not the engine. Fixed in7cf256b: the scan is remembered, keyed by the cache root's modification stamp, so another writer still forces a rescan and the size limit stays exact. The probe did not catch it because it called the engine directly, without the cache -- the instrument lesson again: a probe that skips a layer cannot clear that layer. The output fix stays: it is still 16× less data per push. The replay is re-running on frozen7cf256b. - The loop, named and broken, 2026-09-23. Walls kept appearing one per two-hour run -- the cache rescan, then the harness losing a killed case's output (
982cb14), then a linear-list relationship check that also overran the deadline by >60 s (e7e2941) -- because every one was Python bookkeeping between engine calls, every one grew with the repository, and the only full-scale instrument was a Joern replay. Two instruments replace that: an in-process sampling profiler (OUSAST_SAMPLE_PROFILE, on for every replay case) andbenchmarks/push/python_overhead.py, which runs the realreplay()on the real PMPro pins with an instant stub engine answering real recorded rows. Its first pass found the remaining two walls at once: discovery re-parsed the ruleset per region (89,127 TOML parses, 339 s → 7 s, output digest identical) and change attribution decoded every line anchor per row (78% of a stubbed push; now indexed). The whole stubbed PMPro push now costs 58.5 s of Python; per-entry cache fsync is gone too (b4b9308,6b85a60). - The budget, stated correctly. The push's head scan can select 2,500 questions (500 regions × 5 families) where change context leaves them incomplete, because tier-zero pruning never prunes an incomplete question -- ~6 hours of engine at ~9 s per PHP question. That does NOT make a 7,200 s case infeasible: contributor-scan 7.7–7.9 portion the arbitration phase in ranked order and hold time back for arbitration, so a case answers what fits and reports the rest as
query_budget_reserved. What defeated every earlier case was the phases BEFORE portioning -- discovery, the cache, change attribution -- each spending the deadline before arbitration began. With those fixed (preparation 290 s → 19.5 s in the real run), the three cases re-run at 7,200 s on frozen6b85a60; completion is expected to be partial and stated, and the declared target's file ranks 19th of 86. - Boundary: Evaluation harness, full-run integration
- Depends: 13.2, contributor-scan 7.3, contributor-scan 7.8
- Requirements: 6.2, 6.3, 8.1, 8.2
- Named by the 2026-09-20 correction. The not-established record came from two instrument defects, not from the analyser: the census verdict checked question completion before findings, and a 500-region budget asks 2,526 questions, which no ceiling this project sets can hold. Bounded to 25 and 100 regions, PMPro completes with no timeout and establishes a true-positive SQL injection at
-
13.4 Let a question inside one partition complete, whatever else the repository holds
- Diagnosed 2026-09-21, and PHP was never the variable. A repository holding TWO languages can never complete a question, so it can never admit a finding.
partitions.pyraisescross_partition_semantics_unresolvedwhenever more than one language partition exists; that reason is a member ofGRAPH_INTEGRITY_GAPS; and a graph-integrity gap demotes every completed outcome in the scan. - The evidence is one column wide: NodeGoat is JavaScript alone and completes 299 of 299, VAmPI is Python alone and completes 35 of 35, PMPro is PHP with 36 regions of bundled JavaScript and completes 0 of 127 and 0 of 520 while establishing 19 and 43 findings. NodeGoat's own
vendor_semantics_unresolvedis a DECLARED exclusion rather than an integrity gap, which is why it does not demote. - This is larger than the PHP runtime limit and it lands on the declared v0.1 target. A WordPress plugin is PHP plus its admin JavaScript, so every one of them is multi-language, so on the target this tool was built for it can detect defects and never admit one. Neither subject that currently passes could reveal it.
- Recording the boundary is right: a flow from PHP into JavaScript is semantics the partitioned graph cannot follow. Demoting every question is not, including one wholly inside a single PHP file. Task 12.3 already fixed this bug class one level down, making an empty-scope gap question-owned "so it cannot block another question's comparison"; the same principle applies to a partition boundary.
- Do not reclassify the reason as a declared exclusion. That would claim cross-language flows are out of scope when they are unresolved, which is the opposite of what this project's degradation vocabulary exists to say.
- Done when a question whose scope lies inside one partition completes, a question whose scope could cross a partition boundary stays unresolved with the boundary named, and no question completes by widening its scope. Evidence:
benchmarks/measurements/2026-09-21-two-languages-defeat-completion.json. - Both halves implemented 2026-09-21. A language boundary is now a declared limit: it can hide a finding and never invent one, so it stays published and stops demoting. And an integrity gap raised by a partition census now carries the partition it came from and reaches only that partition's questions, because a missing JavaScript file cannot make a PHP answer wrong. Two controls exercise the real boundaries rather than injecting a reason, and both were confirmed to FAIL with the classification restored. The rule behind both is now written in the design as an invariant, with all five instances listed, because this is the fifth time it has been found and fixed one instance at a time.
- Verified on the subject 2026-09-21: PMPro completes 120 of 127 questions where it completed none, with its 19 findings unchanged. The seven still unresolved are the JavaScript partition's own questions, which is correct: the four missing files are its files, so its answers are the ones that could be wrong. 1,419 tests pass, eighteen skipped; Ruff/format/mypy pass.
- Three causes, not one, and the third was hidden by the record rather than by the code. The same census gap is raised in TWO passes, arbitration and evidence, and carrying the partition in one but not the other left an unscoped copy that kept demoting everything, so the fix looked applied and was not. It was found by making the census print each outcome's reason instead of only its status, after which three separate causes stopped looking identical.
- Admission is scoped too.
_blocking_degradationsalready exempted declared exclusions; it now also ignores a gap another language partition raised, so a JavaScript build artefact cannot block a PHP finding. A gap naming no partition still reaches everything, and so does every gap when the claim's own language is unknown, because unknown scope is not evidence of independence. Evidence:benchmarks/measurements/2026-09-21-partition-gap-blast-radius.json. - Boundary: contributor-scan query/context evidence, Ranker Scope Contract
- Checked 2026-10-02 per the operational-readiness audit: implemented and verified on PMPro (120 of 127, above). 13.3 stays open on its own blocker; this box was left unchecked only because it waits on it.
- Depends: 13.3
- Requirements: 2.1, 2.2, 6.3, 8.1
- Diagnosed 2026-09-21, and PHP was never the variable. A repository holding TWO languages can never complete a question, so it can never admit a finding.
-
15. Prepare the independent population and declare the shippable scope (M4 preparation)
- Runs in parallel with M2 and M3. M4 already permits population preparation before the frozen candidate exists, and it is the longest lead-time item on the path: nothing can be qualified without it however good the analyser gets.
-
15.1 Reserve and review an untouched evaluation population
- Freeze reviewed vulnerable, fixed and benign changes from projects not used to develop or tune this analyser. NodeGoat, VAmPI, PMPro, WP Statistics, MW WP Form and libpng are all development subjects and none of them may qualify anything.
- Reserve the cases untouched: do not examine their scan results before the population is frozen, and record the reservation so a later run cannot be mistaken for a blind one.
- The population MUST hold a multi-language repository. Task 13.4 found that a second language in a repository cost every question its completion, and so cost every finding its admission — on the declared v0.1 target, which is PHP plus admin JavaScript. Neither subject that passes today could reveal it, because both hold exactly one language. A population of single-language repositories would qualify a capability that fails on the product's own target.
- Done when a frozen population exists with its provenance and review recorded, at least one member holds more than one language, and nothing in it has been scanned.
- Reserved 2026-09-24, not yet frozen. Eleven cases selected by the maintainer from fifteen researched by advisory and fix diff alone (no clone, no scan), none among the 262 repositories this project's benchmarks reference, plus the earlier Ghost reservation: 4 PHP with JavaScript (Ultimate Member and LearnPress WordPress plugins, phpMyFAQ, YesWiki; all SQL injection), 4 Node/TypeScript (Signal K SSRF, Saltcorn SQLi, mongo-express code injection, Mockoon permissive CORS), 3 Python (alerta SQLi, wger open redirect, OpenCVE SSRF). Record:
benchmarks/independent/population-v1.toml; research and rejected alternates:research-2026-09-24.md. A guard test fails if any reserved repository is referenced elsewhere in the tree. Remaining to FREEZE: per case, verify the pins and the sink by reading source, and choose a benign change from the same history without scanning; the OpenCVE licence must be checked before any result is published. - Frozen 2026-09-24.
benchmarks/independent/freeze.pyverified every case with git and file reads only -- no analyzer, query or tooling: both pins present and ordered, the sink text present at the vulnerable pin (LearnPress's fix is source-side and recorded asfix_site; Mockoon's sink is recorded assink_evidence), the fix touching the sink or its declared fix site, and SHA-256 of the sink file at both pins. Benign controls were proposed mechanically and REVIEWED by diff: the keyword filter alone let through an access-restriction, a filter check, API-token scopes, a login change and a TLS-verification option, all rejected; Ultimate Member's and alerta's controls were replaced by hand (a coding-standards reformat; a dependency-plumbing change with both sides still vulnerable). Record:freeze-v1.json. The guard test now requires a benign pair for every case of a frozen population. Nothing in it has been scanned; the first scan is the evaluation. - Boundary: Evaluation harness, corpus provenance
- Requirements: 8.1, 8.2
-
15.2 Declare the v1 advisory scope as the families whose claims can be admitted
- Admission requires the entailed rung. Injection, untrusted destination and configuration entail with witnesses on the measured subjects. Access control does not: on an application that splits its route from its data access the obligation is carried, and a carried obligation corroborates by design, so shipping the family would ship silence. Output encoding, path, prototype and deserialization have not earned a place.
- Record the scope, the reason each excluded family is excluded, and what a contributor is therefore NOT told. M4 does not require every language or family to become eligible, and a published limitation is the honest form of a narrow release.
- Done when one record states the declared scope and every exclusion's reason, and the packaged advisory output names the limits it ships with.
- Boundary: pre-push-safety-net admission, release validation
- Depends: 15.1
- Requirements: 3.1, 5.1, 5.6, 7.1
-
16. Make a harness report the cause of its own failure (robustness)
- Named by a week in which three separate causes looked identical. The census reported 19 findings and 0 completed questions side by side for days without complaint; a stale image measured the engine a bug had been diagnosed against; and a partition fix looked applied while an unscoped copy of the same gap kept demoting everything. Each was found by reading code, and each would have been visible in a record that carried its own causes.
- Every harness result should carry the counts that disambiguate its failure modes, the degradations whole rather than by reason, and the engine and ruleset identities actually used. Two of these were added reactively during that week and should be the standard shape rather than a reaction.
- Add a self-consistency assertion to each harness. Findings above zero with completion at zero is a contradiction, and an instrument that says so loudly is the difference between an hour and a week.
- Done when each harness record carries its provenance and per-cause counts, and a deliberately contradictory result is reported as a contradiction rather than as a number.
- Boundary: Evaluation harness
- Requirements: 8.1, 8.2
-
17. Operational readiness (2026-10-02)
- Named by the 2026-10-02 operational-readiness audit, maintainer-approved as the first increment. The maintainer's definition: a user installs the hook on ANY repository and gets honest, advisory-only results; nothing ever blocks a push. The audit found the plumbing sound (snapshots, resolution, deadline accounting, advisory exit 0, the JSON artifact) and five things in the way: the hook can block a push, quick rules are not part of pre-push, the engine cannot finish a 26-file repository inside 30 s, the terminal hides every skip reason, and there is no supported install path without the source checkout. Before numbers for every item are the audit's live runs on records (Python), serve-static (JavaScript) and unrolled/secure (Go), each with one planted commit.
- This group is plumbing and honesty. It enables no capability, edits no rule for a planted sample, and leaves graph reuse (the engine speed work decided in 11.6) as a later task.
- Requirements: 9.1–9.6
-
17.1 Never block a push the user did not choose to block
- The installed hook exits 0 with one line when
ousastis missing (the check did not run, and how to install). In hook (stdin) mode, an argument or usage error (an invalidOUSAST_PUSH_DEADLINE, a cache directory inside the repository) is advisory: one line and exit 0.--mode blockingstays the explicit opt-in that can reject a push, and keeps its usage errors. The prior-hook input-retention failure keeps exit 1, because it is the chained hook's verdict that cannot be obtained, not ours. - Files:
ops/pre-push,src/openultrasast/cli.py. Tests:tests/test_push_hook.py(a push withousastabsent from PATH succeeds and prints the line; a bad deadline in hook mode exits 0; blocking mode keeps exit 2). - Evidence: the audit's
hook.shpush 1 (rejected before) andfail.sh(rc=2 before) rerun with exit 0. - Done 2026-10-02: the hook prints one line and exits 0 (chaining a prior hook if present) when
ousast(orOUSAST_COMMAND) is missing; in hook mode without--mode blocking, argparse/usage errors and unexpected exceptions print one line and exit 0. Tests:test_missing_ousast_never_blocks_the_push,test_hook_mode_usage_errors_are_advisory_unless_blocking(blocking keeps exit 2), andtest_cli_refuses_the_flag_outside_explicit_replayupdated (the experimental flag is still refused in hook mode, now advisory). Gates: 1987 passed, 42 skipped; ruff, format, mypy clean. - Requirements: 9.1, 5.3, 5.4
- The installed hook exits 0 with one line when
-
17.2 Name every skipped check in plain words
- One terminal line per distinct coverage reason: hex-encoded paths decoded, sha256-suffixed reasons grouped with a count, JSON unchanged. "language not covered: ( files)" for a language with no engine frontend and no quick rules, in both
pre-pushandscan. A first push of a new branch says there is no base and what was checked instead.reports.pygains lines for the eight scan degradationscli.pyrecords and the report did not render (learning_endpoint_unavailable,cpg_unavailable,facts_unavailable,policy_invalid,model_withheld_execution_budget,max_findings_exceeded,hunter_model_unavailable,sandbox_unavailable). - Files:
src/openultrasast/push/report.py,src/openultrasast/push/explain.py(new),src/openultrasast/reports.py,src/openultrasast/cli.py. Tests:tests/test_push_report.py,tests/test_reports.py. - Evidence: the three repositories' terminal output names engine-missing, Go not covered and the new branch.
- Done 2026-10-02: new
push/explain.pyturns every raw coverage reason into one plainSkipped:line per distinct kind (hex paths decoded, sha256 tokens dropped and counted, unknown reasons still printed); the existing one-lineNotice:keeps its next action and gains an engine-missing variant.language_not_covered:<lang>:<changed>:<total>is recorded for a changed language with no engine frontend and no quick rules;ousast scanprintsnotice: language not covered: go (N files)and its report says so.reports.pyrenders the eight previously dropped degradations and prints any reason it has no sentence for. The hook passesOUSAST_COMPARISON_BASE. The new-branch line names the base it lacks; what was checked instead lands with the quick tier (17.3). Tests:tests/test_push_explain.py(6), two intests/test_reports.py; module-audit manifest regenerated for the new module. Gates: 1995 passed, 42 skipped; ruff, format, mypy clean. - Requirements: 9.2, 5.2, 8.2
- One terminal line per distinct coverage reason: hex-encoded paths decoded, sha256-suffixed reasons grouped with a count, JSON unchanged. "language not covered: ( files)" for a language with no engine frontend and no quick rules, in both
-
17.3 Run the quick rules as the fast first tier
- Inside the deadline and before the engine, the shipped quick rules run over the head side of the changed files and keep the findings on changed lines or in changed functions. They print as advisory pattern matches, labeled apart from admitted alerts, recorded in the artifact's
quick_tier, and never block. - Files:
src/openultrasast/push/quick.py(new),src/openultrasast/push/runner.py,src/openultrasast/push/report.py. Tests:tests/test_push_quick.py(new). - Evidence: serve-static's planted
evalin the newdebug.jsis shown; whether records' planted%-formatted SQL is shown is recorded as is, with no rule edits for it. - Done 2026-10-02: new
push/quick.pyruns the shipped quick rules (no shadow rules, no model) over the changed head files inside_prepareunder the shared deadline, before the engine, keeping matches on a changed line or inside a changed function (entry-point ranges); it recordsfiles/bytes_readas proof of input. The terminal shows them under "Quick rules: N pattern match(es) on changed code ... Advisory: not verified by the engine."; the artifact keepsquick_tier; admission and push disposition are untouched. A new branch runs the quick tier over its last commit's changes and says so._analyzegainedengine="off". On the audit's serve-static planted commit theevalin the newdebug.jsis now shown (js-eval, js-xss-response at debug.js:2, ~1.1 s on the host); the planted%-formatted SQL in records is NOT matched by any quick rule (coverage gap, no rule edited). Tests:tests/test_push_quick.py(3). Gates: 1998 passed, 42 skipped; ruff, format, mypy clean. - Requirements: 9.3
- Inside the deadline and before the engine, the shipped quick rules run over the head side of the changed files and keep the findings on changed lines or in changed functions. They print as advisory pattern matches, labeled apart from admitted alerts, recorded in the artifact's
-
17.4 Classify a file new in head as new, not unknown
ChangeContextrecords the paths Git reports as added. An operation in an added file, when no add/delete pairing is unresolved and its head evidence is complete, isnewwith reasonfile_added_in_head, and admission accepts that reason as change evidence. An engine finding with established novelty whose only vetoes are capability vetoes renders as an advisory engine finding, not an alert. Every other unknown comparison keeps its veto.- Files:
src/openultrasast/model/contracts.py,src/openultrasast/push/snapshot.py,src/openultrasast/push/policy.py,src/openultrasast/push/report.py. Tests:tests/test_push_policy.py,tests/test_push_snapshot.py,tests/test_push_report.py. - Evidence: unit tests on the comparison and admission; a live engine run if the host allows one (free memory >= 3 GB).
- Done 2026-10-02:
ChangeContext.added_pathsrecords Git status-A paths (rename targets excluded). In_compare_evidence, after every existing context/head/deadline check, an operation in an added file isnew/file_added_in_headwhen the claim is entailed, undischarged and has change evidence, and no base answer holds the same operation (elseoperation_moved_within_change); an unpaired add/delete still blocks aschange_context_unresolved. Admission accepts(new, file_added_in_head); with the empty registry such a candidate's only veto iscapability_unavailable, and the terminal now shows it under "Engine: N finding(s) this push introduced. Advisory ... not an alert." Any comparison, evidence or location veto keeps a candidate out of that line. Tests:tests/test_push_new_file.py(9). Live engine evidence depends on host memory (17.7). Gates: 2007 passed, 42 skipped; ruff, format, mypy clean. - Requirements: 9.4, 3.4
-
17.5 Say when the engine cannot finish, and offer it in the background
- One line when the engine did not finish (deadline, engine not installed).
--engine backgroundstarts the engine detached after the quick tier, writes its result under the artifact directory and, on the next run, prints "engine result for <head> from the previous push". One background engine at a time;--engine offskips it with a stated line. No speed work. - Files:
src/openultrasast/push/background.py(new),src/openultrasast/push/runner.py,src/openultrasast/cli.py,ops/pre-push. Tests:tests/test_push_background.py(new). - Evidence: a hook push with
OUSAST_PUSH_ENGINE=backgroundfollowed by a second push showing the previous result. - Requirements: 9.5, 4.1, 4.2
- One line when the engine did not finish (deadline, engine not installed).
-
17.6 Install, Docker and documentation for a first-time user
ousast pre-push install|uninstall [--force] [--docker] [REPO]ships the hook as package data, honourscore.hooksPath, refuses an existing hook unless--force(which chains it aspre-push.before-ousast), and works from a pip install.ops/ousast-dockeris a hook-callable script (not a shell function) that passes stdin and the artifact path through. Theopenultrasast:devimage is rebuilt only ifdf -h /shows at least 4 GB free; otherwise the rebuild command is documented and the record says it was not rebuilt.docs/scanning.mdgains a first-run guide, a glossary of skip reasons, a GitHub Actions recipe and full--helptext; README tiers 1 and 2 updated, pullingghcr.io/norandom/openultrasast:2.0.1where the wrapper allows.- Files:
src/openultrasast/push/hook/pre-push,src/openultrasast/push/install.py(new),src/openultrasast/cli.py,ops/ousast-docker,docs/scanning.md,README.md,ops/README.md. Tests:tests/test_push_install.py(new),mkdocs build --strict. - Evidence: install from the package into a scratch clone, push through it, uninstall restores the prior hook.
- Requirements: 9.6, 7.4
-
17.7 Prove it on the audit's three repositories
- Rerun records, serve-static and unrolled/secure (same planted commits) with the new build in quick mode and with the hook installed, plus one Docker engine run on serve-static if free memory >= 3 GB and free disk >= 1 GB. Prove each run read its input (files, bytes).
- Files:
benchmarks/measurements/2026-10-02-pre-push-operational-1/record.json. Evidence: before (the audit) and after: exit codes, wall time against the deadline, findings shown, every skip line printed. A planted sample still missed is a coverage gap, recorded, never a rule edit. - Requirements: 9.1–9.6, 6.3
Implementation Notes
-
Task5.2 measured PMPro timing out after both snapshots were materialized but before change context/graph analysis. Group6 must account for compatible snapshot/discovery reuse as well as graph reuse; identical-tip hits cannot substitute for the later representative changed-code latency gate. No new detector, scope heuristic or incremental CPG mutation is implied.
-
Cancellation must cover semantic planning and reporting, not only engine subprocesses:
_collectand_scope_worknow consume the shared deadline, and replay instantiates the budget-aware backend once. Preserve the 400-question failure-census and real Node artifact-retention regressions. -
Group 9 (M1a) corrected the owner diagnosis: all four named vetoes fire on the NodeGoat case, and a fifth,
operation_correspondence_unresolved, is the actual remaining blocker because a newly introduced operation sits on a changed line and has no base anchor. The unchanged revision resolves its base location on the same lines and is classifiedunchanged, which isolates correspondence as the mechanism. M1b repairs context producers, question-owned vendor relevance, operation correspondence, file-wide ambiguity scope and query-to-operation provenance; vetoes that did not fire stay unchanged. -
Group 6: complete discovery/graph/query reuse is explicit via
--cache-dir; source/declaration edits invalidate affected units. Installed frontend/adaptation bytes now bind graph and admission provenance. A valid cached census cannot qualify a fresh answer that omitted its own census. Identical-revision timing is not the changed-code rollout gate.