spec: name the fact tables an ontology, and commit to a general-purpose hook

The source, sink, sanitizer, operation, discharger and layout rows are not a pattern list. They are an
ontology of program elements and their security relations, and nearly all of this tool accuracy lives
there: the family census found five of seven families establishing nothing, every one for an ontology
reason with the flow engine ready and nothing to aim at.

The hook is general-purpose, not application-specific. The ontology describes libraries, frameworks
and language constructs; it does not describe repositories. An entry names an API and is justified by
what that API does, carries provenance and a weakness class, and may not encode a path or identifier
peculiar to one project -- a plugin bundled library tree was measured and declined on exactly that
ground. A measured gap is a candidate, never an entry: accretion by subject is how a general tool
becomes a fixture of its benchmark.

contributor-scan owns the ontology, so its design carries that statement and a new group 7 makes
growth measurable rather than anecdotal: report what a repository calls that nothing models, key
sanitizers to the weakness they prevent, skip arbitration where the evidence says there is nothing to
find, share one matcher across the queries, require a degradation to declare its scope, and commit
golden findings so a silent change cannot pass.

pre-push-safety-net carries the general-purpose test for its own reviews, and a task for harnesses
that report the cause of their own failure -- named by a week in which three separate causes looked
identical as one count.

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-21 16:29:23 +02:00
co-authored by Claude Opus 5
parent 3a18d025f4
commit e245040855
3 changed files with 94 additions and 0 deletions
+32
View File
@@ -51,6 +51,38 @@ for someone who does not have Joern.
- Building a sandbox. Req 10 of the predecessor stands: adopt, never build.
- Changing the gates, the taxonomy, or the fact-table schema.
## The fact tables are an ontology, and it is general-purpose — 2026-09-21
The `[[source]]`, `[[sink]]`, `[[sanitizer]]`, `[[operation]]`, `[[discharger]]` and `[[layout]]` rows
are not a pattern list. They are an ontology of program elements and their security relations: what
carries untrusted input, what is dangerous under which weakness class, what discharges which
obligation, what a value's shape has to be for a call to mean what the table says. The engine reasons
over that ontology; it is where nearly all of this tool's accuracy lives, and the 2026-09 family
census measured that directly. Five of seven families established nothing, and every one of them
failed for an ontology reason with the flow engine ready and nothing to aim at: no HTTP client for a
server-side request forgery, no collection read for an object-reference defect, no WordPress spelling
for a deserialization, and only browser sinks for a server-rendered injection.
**This is a general-purpose pre-commit hook, not an application-specific one.** The ontology describes
LIBRARIES, FRAMEWORKS and LANGUAGE CONSTRUCTS. It does not describe repositories. That distinction is
the difference between a tool that transfers and a set of rules that happen to fit whatever was last
measured, and it constrains how the ontology may grow:
- An entry names an API and is justified by what that API DOES. `needle.get` is admissible because
needle is an HTTP client and the argument is a destination, not because NodeGoat calls it.
- An entry carries provenance and a weakness class, so a reader can check the claim without trusting
the person who added it.
- An entry must not encode a path, directory name or identifier peculiar to one project. A plugin's
`includes/lib/` was measured and declined on exactly this ground: it would have fixed one subject and
claimed something false about every other.
- A measured gap is a CANDIDATE, never an answer. The coverage instrument below reports what a
repository calls that nothing models; each candidate must then be resolved to a library API with
provenance before it enters the ontology. Accretion by subject is how a general tool becomes a
fixture of its benchmark.
The corollary is that ontology growth should be driven by measurement rather than by whatever
repository is in front of the maintainer, which is what task 7.1 exists to make possible.
## Boundary Commitments
### This Spec Owns
+47
View File
@@ -1210,3 +1210,50 @@ checkout that can actually demonstrate detection.
pre-push-safety-net task 3.4: pinned JavaScript frontend filters remove declared
first-party tests. The maintainer approved its dependency adaptation and acceptance checks;
task 2.18 now implements it before downstream partition completion.
## Group 7 — Grow the ontology by measurement, not by subject (2026-09-21)
Named by the family detection census and the week of repairs after it. Five of seven families
established nothing and every one failed for an ontology reason, so this group is where detection
efficiency actually lives. The design amendment above states the constraint these tasks work under:
the ontology describes libraries, frameworks and language constructs, never repositories.
- [ ] 7.1 Report what a repository calls that the ontology does not model
- Census the call names, and the receiver-qualified spellings, that appear in a repository's first-party graph; subtract everything any fact table names; rank the remainder by how often it is called and by how close it sits to a modelled source. The output is a measured worklist of what this tool is blind to here.
- It needs no dataflow, which is why it is cheap: the same structural pass that answers 17,700 requests in 44 s already walks every call.
- **It reports candidates, not entries.** A candidate becomes ontology only when someone resolves it to a library API, states the weakness class and records provenance. Nothing may be added because a subject called it.
- Three defects this would have named in one run rather than three investigations: `needle.get` absent while the family found the redirect two files away, `find` absent so the documented object-reference defect raised no obligation, and `maybe_unserialize` absent so the only deserialization site in 1,164 PHP files was invisible.
- Done when the instrument reports a ranked gap list for each measured subject, every entry carries how often it is called, and adding nothing to the ontology changes no result.
- _Boundary: shipped semantic facts, evaluation harness_
- _Requirements: 6.2, 8.1_
- [ ] 7.2 Key sanitizers to the weakness they actually prevent
- The sanitizer list is flat across families, and that forces a false choice which is now measured: adding WordPress's `esc_attr` removes eighteen output-encoding false positives on one plugin and simultaneously hides a real SQL injection, because no HTML escaper stops SQL injection. `php.toml` has carried a comment predicting exactly this since before the measurement existed.
- Key each sanitizer to the weakness class it discharges, so a family consults the cleansers that apply to it. Do not solve it by choosing which defect to keep.
- Done when an HTML escaper clears an output-encoding flow and leaves an injection flow standing, on the measured plugin, with both outcomes recorded.
- _Boundary: shipped semantic facts_
- _Requirements: 6.2, 8.1_
- [ ] 7.3 Do not arbitrate a question whose evidence says there is nothing to find
- The structural pass answers 17,700 requests in 44 s; arbitration costs one to two seconds per question, and a 673-file plugin asks 2,526 of them. Gate arbitration on the evidence vector already computed: a question whose scope holds no modelled sink, or no reachable source, cannot produce a flow and does not need the expensive pass.
- This attacks the scale limit from the opposite side to graph reuse and can be sized today from data already on disk, by counting how many of those 2,526 questions had any sink in scope at all.
- Done when the arbitrated count falls without any finding being lost on the measured subjects, and the questions skipped are recorded as skipped rather than as answered.
- _Boundary: contributor-scan scan budget_
- _Requirements: 4.1, 6.3, 8.1_
- [ ] 7.4 One matcher, used by every query
- Four query files carry near-copies of the same token matcher and each acquired the same defect separately: the configuration matcher needed a receiver check the dominance matcher already had, and the sink matcher searched a whole node's text including its comments. Six instances of one bug class are listed in the pre-push design's invariants.
- Extract one matcher with callee confinement and word boundaries built in, and have taint, dominance and config use it. A rule in a document gets rediscovered; a rule in a constructor does not.
- Done when one implementation is shared, each caller's behaviour is unchanged on the measured subjects, and a new query cannot match text that merely contains a name.
- _Boundary: contributor-scan queries_
- _Requirements: 6.2, 8.1_
- [ ] 7.5 A degradation must declare the scope it reaches
- Five instances of one bug class are listed in the pre-push design's invariants, two of them found this week, and two of those cost the declared target its entire admissibility. A degradation is currently a bare dict, so nothing forces a new reason to say whether it reaches one question, one file, one partition or the whole read.
- Make it a record with a required scope, and let the demotion and the admission policy read that field rather than a hand-maintained membership set.
- Done when a degradation cannot be constructed without a scope, the existing reasons carry theirs, and the measured subjects' outcomes are unchanged.
- _Boundary: contributor-scan degradation vocabulary, pre-push admission_
- _Requirements: 6.3, 8.1_
- [ ] 7.6 Golden findings per subject, so a silent change cannot pass
- Nothing fails today when a change alters what NodeGoat or VAmPI report. Four changes in one week altered detection and each time the census had to be re-run from memory; one false positive was caught by reading a witness rather than by a test.
- Commit an expectation per subject at the finding level, site and family and rung, and diff against it. Keep it honest about what it is: a regression guard on development subjects, never evidence of qualification.
- Done when a deliberate change to a matcher or a fact table fails the diff, and the expectation records the ruleset and engine identities it was taken under.
- _Boundary: evaluation harness_
- _Requirements: 8.1, 8.2_
+15
View File
@@ -24,6 +24,21 @@ an irrelevant old defect, or an unactionable witness can still produce noise.
- Do not build a second ranker/scope heuristic, an incremental CPG engine, a daemon, an automatic fixer,
or a mandatory model service. Do not scan intermediate pushed history in this feature.
## What this hook is, and is not — 2026-09-21
A general-purpose pre-commit hook, not an application-specific one. It may be measured on WordPress
plugins and Node applications, and it may not become a tool that only works on them. The practical
test at review time: would this change still be correct on a repository nobody here has seen? A path,
a directory name or an identifier peculiar to one project fails that test, and one was declined on
exactly this ground when a plugin's bundled library tree would have fixed a single subject while
claiming something false about every other.
The fact tables are an ontology of libraries, frameworks and language constructs, and
`contributor-scan` owns it. Its design records how it may grow and its group 7 makes growth
measurable rather than anecdotal. Nearly all of this tool's accuracy lives there: the 2026-09 family
census found five of seven families establishing nothing, every one of them for an ontology reason
with the flow engine ready and nothing to aim at.
## Two invariants this project keeps rediscovering
Added 2026-09-21 after the same two defects appeared eleven times between them. Each instance was