feat(pre-push-safety-net): export the enumerated operation inventory (M1b 10.4)

A taint answer reports traced flows, so an absent row means either that the
operation is not present or that no traced path reached it. Change comparison
needs that distinction to say a change introduced an operation, and reachability
is deliberately bounded, so the two can never be separated from flow rows alone.
That is why the veto guarding the confusion is permanently true on any real
Express application, and why it could be neither discharged nor safely removed.

The taint query now exports one row per operation it enumerated in scope, plus a
completion marker. The marker is what makes an empty inventory meaningful:
without it a scope that was never enumerated and a scope holding no such
operation are indistinguishable.

Comparison separates three outcomes on a changed line: the operation is present
in the base without a traced flow, which is a reachability statement and not
novelty; the operation is absent from an enumerated base, which establishes that
the change introduced it; or the base scope was never enumerated, which stays
explicitly unresolved. A malformed inventory row is not an empty inventory.

The arbiter now skips any row carrying a kind, so an inventory row cannot be
normalized into a flow with an empty source that names no attacker input.

Verified against the real engine on an authored two-revision JavaScript control:
the operation is enumerated on both sides, the base carries it with zero traced
flows, the zero-operation marker appears for an empty scope, and answers and
findings are unchanged. No new sink inference, alias resolver or second source
IR. 1,354 tests passed, nine skipped. Rollout stays NO-GO.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-16 16:46:00 +02:00
co-authored by Claude Opus 5
parent 86980d8aa0
commit c73efd2bde
6 changed files with 220 additions and 7 deletions
+18 -1
View File
@@ -815,7 +815,24 @@
}
}
rows
// What this question ENUMERATED in scope, independent of what it traced. A flow row proves a
// path exists; the absence of one cannot distinguish an operation that is not present from an
// operation no traced path reached. Change comparison needs that distinction to establish that
// a change introduced an operation, and reachability is deliberately bounded, so the two can
// never be separated from flow rows alone. The completion marker is what makes an EMPTY
// inventory meaningful: without it, a scope that was never enumerated and a scope with no such
// operation look identical. No new sink inference, alias resolution or fact is introduced here.
val inventory = sinkCalls.l.map { sink =>
ujson.Obj(
"kind" -> "operation_inventory",
"inventory" -> sink.code.take(200),
"inventoryFile" -> sink.method.filename,
"inventoryLine" -> sink.lineNumber.getOrElse(-1).toString,
"inventoryMethod" -> sink.method.name
)
}
rows ++ inventory ++ List(ujson.Obj("kind" -> "operation_inventory_complete", "operations" -> inventory.size))
}
println("---OUSAST-CPG-BEGIN---")
+5
View File
@@ -171,6 +171,11 @@ def _flows(rows: object, *, function: str) -> list[dict[str, object]]:
for row in rows:
if not isinstance(row, Mapping):
continue
# Answer rows carry no `kind`. Anything that names one is a separate export -- the
# operation inventory, an evidence vector, a context location -- and normalizing it
# here would manufacture a flow with an empty source that names no attacker input.
if "kind" in row:
continue
if function and not _in_scope(row, function):
continue
kept.append(
+56 -1
View File
@@ -136,6 +136,50 @@ def _operations(scan: ModelScanResult, budget: ExecutionBudget | None = None) ->
return tuple(result)
INVENTORY_ROW = "operation_inventory"
INVENTORY_COMPLETE = "operation_inventory_complete"
@dataclass(frozen=True)
class OperationSite(Contract):
"""One operation the query enumerated in a question's scope, traced or not."""
question: QuestionIdentity
path: str
line: int
operation: str
def _inventory(scan: ModelScanResult, question: QuestionIdentity) -> tuple[OperationSite, ...] | None:
"""Operations enumerated for this question, or None when the scope was not enumerated.
None is the honest answer for an answer that carries no completion marker, including every
artifact produced before the inventory existed. An unenumerated scope stays unresolved: the
absence of a traced flow is not the absence of the operation. A malformed inventory row is
likewise not an empty inventory.
"""
answer = next((q for q in scan.question_outcomes if q.identity == question), None)
if answer is None or answer.raw_rows_json is None:
return None
sites: list[OperationSite] = []
complete = False
for row in json.loads(answer.raw_rows_json):
if not isinstance(row, dict):
continue
if row.get("kind") == INVENTORY_COMPLETE:
complete = True
continue
if row.get("kind") != INVENTORY_ROW:
continue
path, line, operation = row.get("inventoryFile"), row.get("inventoryLine"), row.get("inventory")
if not isinstance(path, str) or not path or not isinstance(operation, str) or not operation.strip():
return None
if not str(line).isdigit() or int(str(line)) < 1:
return None
sites.append(OperationSite(question, path, int(str(line)), operation))
return tuple(sites) if complete else None
def _correspondence(op: EvidenceOperation, context: ChangeContext) -> tuple[tuple[str, int] | None, str]:
"""Where this head operation maps in the base, and when it does not, why.
@@ -208,7 +252,9 @@ def _valid_answer(scan: ModelScanResult, question: QuestionIdentity, operations:
return False
rows = json.loads(answer.raw_rows_json)
# Query bodies are operation rows. An unrecognized/malformed row is not an empty answer.
return len(rows) == sum(op.question == question for op in operations)
# The enumerated inventory is a separate export and is counted by `_inventory`, not here.
body = [row for row in rows if not (isinstance(row, dict) and str(row.get("kind", "")).startswith(INVENTORY_ROW))]
return len(body) == sum(op.question == question for op in operations)
def _context_boundaries(scan: ModelScanResult, *questions: QuestionIdentity) -> tuple[str, ...]:
@@ -473,8 +519,17 @@ def _compare_evidence(
# The edit introduced these lines. A complete counterpart answer that
# contains this operation elsewhere is movement, never novelty; only its
# total absence establishes that the change introduced the flow.
inventory = _inventory(base, counterpart) if base is not None else None
if identical:
reason = "operation_moved_within_change"
elif inventory is None:
# The base scope was never enumerated, so absence cannot be established
# from the absence of a traced flow.
reason = "base_operation_inventory_unavailable"
elif any(site.operation == op.operation for site in inventory):
# The operation is in the base. No traced flow reached it there, which is
# a reachability statement, not evidence that the change introduced it.
reason = "operation_present_in_base_without_traced_flow"
elif finding.rung == Rung.ENTAILED and not op.discharged:
novelty, reason = "new", "operation_absent_from_comparable_base"
results.append(_candidate(finding, op, old, novelty, reason, change, context))