Skip to content

Appendix B: the schemas#

Store protocol and schema#

Chapter 06's step four names the finding schema and the evidence contract behind it, leaning on the same store contract chapter 02 first called the narrow waist; step five names the governor and the record a change of severity earns. Neither chapter stops to list every field, because a chapter arguing for an invariant is not the place to also carry a data dictionary, and a data dictionary with no reasoning beside each field is not worth writing at all. This appendix is that list, for three shapes: the store contract every tool call's effects are written through, the finding that contract persists, and the governance record a severity change earns on the way. For each field below: whether it is required or conditional, what it is for, and what breaks when it is missing or wrong. Read this beside chapter 06's step four and step five rather than instead of them; where this appendix restates something either step already argued, it does so because a schema reference that sends a reader chasing a chapter for every field is not a reference, and each restatement below was checked again against the tree rather than copied from the chapter's own prose.

The store contract every tool call is written through#

What this schema is: the one surface every side effect eventually reaches, declared structurally rather than nominally, and left deliberately unable to prove at runtime that anything satisfies it. store_protocol.py:FindingStore is a typing.Protocol under core/, and every member on it ends in an ellipsis: nothing is implemented, FindingStore() raises TypeError: Protocols cannot be instantiated, and the class is not decorated runtime_checkable, so isinstance(x, FindingStore) does not return False for a bad fit, it raises TypeError: Instance and class checks can only be used with @runtime_checkable protocols: both raised the same way whether the caller is careless or careful, verified directly against the class rather than taken from its docstring. The module's own reasoning for the shape is worth carrying rather than paraphrasing: a Protocol is structural, so a caller needs no import of a concrete store class and a double needs no inheritance, and even a decorated, runtime-checkable Protocol would only ever prove that the member names an object carries are present, never their signatures and never their semantics: an isinstance pass would be a check worth less than it looks like, which is the module's own stated reason for refusing to offer it at all. tests/test_consolidator.py and tests/test_result_processor.py each keep their own partial double for exactly the members the module under test touches, in tests/ where the docstring says a double belongs; the one implementation living outside tests/ altogether, WalkthroughStore in walkthrough/store.py, carries every member name and the same asymmetry described below on purpose, and sits outside core/ for the same reason the Protocol's own docstring gives for keeping a double out of it: a plausible-looking store sitting beside the modules that call it invites exactly the misreading this whole contract exists to close off. Chapter 06's step zero walks through what a persist-only stand-in does differently when handed the same finding; this section is the field list underneath that demonstration, not a second telling of it.

scan_id is a plain attribute, not an accessor, because that is what its callers actually do: the consolidator and the result processor both reach for store.scan_id directly rather than asking for it through a method. A store that instead exposes it only as a method or a property satisfies no caller here, since neither reads it that way; the failure is not a crash so much as a silent mismatch, because Python does not care that an attribute access and a method call look different until the exact line that assumed one and got the other.

add_finding persists a finding and returns its string id, and the return value is the part a caller cannot skip: a store may derive the id itself from the finding's own content when the caller supplies none, so code that assumes the id it sent is the id that was stored can end up addressing the wrong row on every later read. The docstring's load-bearing sentence is about what happens before the row is written at all: a conforming store normalises the finding's evidence and governs its severity before it persists the row, so a store that skips that step silently ships the ungoverned severity it was handed, because nothing upstream of this call sets a severity of its own to fall back on. A store that appends the row first and governs it after has already let the ungoverned number reach anything that reads the table between those two steps.

get_findings pages over a store's own findings by severity, finding_type, validated_only, exclude_fp, limit and offset, and the last two are load-bearing rather than a convenience a substitute can shrug off: severity_governor.py:fetch_all_findings, the governor's own scan-wide helper, asks for successive windows until a short one comes back, so a substitute that accepts limit and offset but ignores them returns a full window on every call, and that loop never sees the short batch that would tell it to stop. The failure this produces is not a truncated result a reader would notice; it is a pager that keeps asking forever, which is a slower and uglier failure to diagnose than a wrong answer would have been.

update_finding_governed persists the revised severity, governance record and false-positive flag. It is synchronous because callers invoke it without await. An asynchronous substitute would return a coroutine without performing the write. The named test checks this method and two asynchronous methods. Source inspection establishes that the Protocol's five remaining methods are asynchronous too; the test alone does not establish that broader claim.

add_tool_result and add_tool_execution both record a tool call and neither can stand in for the other. add_tool_result appends the wide record (the tool name, its arguments and the whole result payload) to the timeline; add_tool_execution writes the narrow coverage row, indexed by scan and by the url-and-tool pair, and returns that row's id so the caller can hold onto it. A tool call that produced no finding still needs the narrow row recorded, because that row, not the finding table, is what a coverage matrix reads back from; a store that only calls the first of the two on every invocation leaves a scan whose coverage view cannot distinguish a url nothing ever touched from one that was tried and came back empty.

get_coverage returns the coverage matrix for the store's own scan and carries a coverage_pct a live status read consults, and that figure is allowed to be computed lazily: the docstring calls the read best-effort at the call site, so a store is not in violation of the contract for computing it on demand rather than maintaining it continuously. rollup_scan_stats recomputes the scan's finding totals and is required to come back as a mapping and never as None, because the caller reads findings_count off the return value with .get, and .get on None raises at the call site rather than failing quietly; a store that returns None on an empty scan rather than an empty-shaped mapping turns an uneventful scan into a crash.

update_status writes the lightweight status row a dashboard polls, and its own docstring specifies a calling pattern rather than a single call: it is written twice per tool result, once with a placeholder count and again with the rolled-up findings_count, so a live view settles on the authoritative total once the rollup has actually run. A caller that writes it only once, on the placeholder pass, leaves a dashboard reporting a count that was already known to be provisional when it was written, with nothing about the row itself to say so.

The Protocol's own member list is held to a rule that keeps it from padding itself: test_no_member_is_declared_that_nothing_under_core_calls, in the same test file, derives every member FindingStore declares straight off the class and separately collects every store.X attribute access anywhere under core/, then fails if a declared member has no such call and is not named in PENDING_CONSUMERS, which is empty today, so every declared member here has a real caller under core/. The docstring on _member_names_read_on_a_store, the helper that collects those accesses, names the floor rather than letting a reader assume more than the check delivers: the match on store.X is syntactic and name-shaped, blind to what the object actually is, so it can both miss a real orphan (an unrelated local dict named store can make a dead update member look used) and falsely accuse a live one reached through an alias such as self._store. What it does catch is real, and that same helper's docstring names the precedent rather than a hypothetical: the test's own docstring carries the defect class in a more general form, and the detail is the helper's: elsewhere in this repository, not on this Protocol, a documented parameter's accessor turned out to live on a different class from the one its annotation, its docstring and its usage example all named, and nothing had ever executed that parameter, so nothing had ever found out, until test_recommend_runs_on_both_of_its_documented_signatures in tests/test_chapter_claims.py was written to exercise it directly. A Protocol whose whole purpose is to document a contract is exactly the place that same defect class would be worst to repeat, which is what this check is for.

The finding#

There is no declared Finding class anywhere under core/ to point at (checked directly, by name, across the package) so the schema below is measured off walkthrough/artifacts/07-findings.json and off what the functions that build and read a finding actually do with it, the same way chapter 06's step four measures the evidence contract. That artifact holds an entry per committed exchange fixture, named f0000 through f0006; the field list below is scoped to that artifact and re-derived from it directly rather than carried over from memory, because an appendix that states "every finding" and means only the seven it happened to read is exactly the overreach this whole project keeps catching in itself.

evidence, id, raw_data, severity, title, type and url are the fields present on every entry in that artifact. type and url matter beyond their own content, because they are among the fields severity_governor.py:match_rules reads to decide whether a semantic rule fires at all: a rule's type list is matched case-folded, and matched as a substring on either side, not only exactly, so a rule declared against type: ["wildcard"] (the shipped cors-wildcard rule's own match block) fires on any finding whose type merely contains that word. I built a finding typed dns_wildcard_record, nothing to do with cross-origin headers, and ran it through the governor directly: it matched cors-wildcard and its critical severity was capped to medium, which is not a hypothetical, it is what the matching code in this tree does today with a type field an author picked for an unrelated reason. title and the finding's own evidence string are read too, by title_regex and evidence_regex, and the failure runs in both directions: read-via-post-bounce's title pattern needs a title that reads like an unauthenticated write, so a real bounce-page false positive titled some other way is never caught by it, while source-map-disclosure's own title pattern, (?i)source ?map|sourcemap|\.map exposed, fires on any of those shapes appearing anywhere in a title regardless of what the finding is actually about. severity is the entering grade the governor reconciles against everything else, and it enters as whatever string the fixture declared, stripped and lowercased only once it reaches severity_governor.py:rank and severity_governor.py:govern_finding internally: a caller comparing a finding's own severity field against a governance record's original_severity byte for byte can see a spurious difference that is only ever casing or a stray space, never a real change, because the record's copy has already been normalised and the finding's own field, before governance runs, has not.

raw_data is not a flat bag; in this artifact every entry's raw_data carries source_fixture and rule_id, naming the fixture and the rule the walkthrough driver associated with that finding before governance ever ran, and both are driver bookkeeping that severity_governor.py:govern_finding itself never reads or writes. Two further raw_data keys are conditional on facts the finding schema keeps separate on purpose. raw_data.evidence (the captured request/response pair, not the top-level evidence string) is present exactly on the entries whose source fixture declared expected.attach_exchange_evidence: true; f0000, sourced from 01-cors-wildcard.json, declares it true and carries the pair, while f0006, sourced from 07-evidence-ceiling.json, declares it false and carries no evidence key under raw_data at all, confirmed by reading both fixtures and both finding entries directly rather than assuming chapter 06's own account of the same two fixtures. raw_data.governance is the second conditional key, and it is not a second fact: every entry that carries a top-level governance_record carries the identical mapping again under raw_data.governance (checked for equality entry by entry rather than assumed from one example) because, as the governor's own docstring states, the store's governed write persists raw_data and nothing else, so a copy that lived only at the top level would not survive being written to a real store and read back. A consumer who reads a finding back out of an actual store and looks only at the top-level governance_record key is reading the copy this codebase does not promise to keep; raw_data.governance is the one a real round trip guarantees.

The top-level evidence field and raw_data.evidence are not the same fact wearing two names, and treating them as interchangeable is its own failure mode. The top-level field is the one-line prose a fixture declares (f0000's reads as a sentence about a static wildcard header) and severity_governor.py:evidence_grade, the function that decides thin, moderate or strong, never reads it: grading walks raw_data's own request, response, poc_curl and poc_output keys, plus a string sitting directly under raw_data.evidence, and nothing under the finding's own top-level evidence key ever reaches that decision. Writing a longer, more convincing top-level evidence sentence does not improve a finding's evidence grade by one letter. The same top-level string is not idle, though: severity_governor.py:match_rules's evidence_regex search reads it too, joined with raw_data.evidence and a description field this artifact never populates, so a finding with empty raw_data can still trip an evidence-pattern rule purely off its own top-level sentence. A direct governor call confirmed this with a finding with raw_data: {} and a top-level evidence string reading like a session-expired bounce page, which read-via-post-bounce still caught and marked false positive.

governance_record and rules_fired are the first conditional pair, present together on every entry but f0005, absent together only there: the fixture built to match no rule, discussed in full in the next section. Read that absence the way the governing code means it and not the way an absent field usually reads elsewhere: it says the pass looked at this finding and found nothing to change, not that nothing ever evaluated it. test_a_governance_record_is_written_only_when_something_changed, in tests/test_severity_governor.py, is the narrow proof behind that reading; narrow because it checks exactly one finding, a low-severity, strong-evidence entry matching no shipped rule, and asserts both keys come back absent on it; it does not sweep a wider space, and its name should not be read as though it did. A monitor built on the opposite assumption (that a missing governance_record means a check that never ran) will alarm on every finding this system ever left alone precisely because nothing was wrong with it.

false_positive and fp_reason are the second conditional pair, present together on f0003 and f0004 and absent together everywhere else in this artifact. Both are written by exactly one action in the ruleset, mark_fp: severity_governor.py:govern_finding sets false_positive to true and fp_reason to the firing rule's own id in the same branch, which is why the two never appear apart in this artifact and why fp_reason is never a free-text explanation, only ever one of the ids named in core/severity_rules.json. The ids are read-via-post-bounce for f0003 and spa-fallback-api-200 for f0004, matching each entry's raw_data.rule_id. A consumer that triages on false_positive alone and never reads fp_reason throws away exactly the sentence the rules file exists to keep: its own stated purpose is that the rationale beside a rule is the operator correction that would otherwise have to be remembered and re-argued the next time the same shape of finding shows up.

The governance record#

Two shapes carry this name and they are not the same shape, and an appendix that specifies one while pointing a reader at the other hands them something that will not match what they diff it against. I called severity_governor.py:govern_finding directly this session and read back exactly what it built; I also read walkthrough/artifacts/08-governance.json directly, entry by entry. What follows specifies the first as the governance record, because it is the shape the enforcing code itself constructs, and the artifact's own accounting is described afterward under its own name rather than folded into the same table.

Key govern_finding's own record The artifact's entry for an acted-on finding The artifact's entry for f0005, the one nothing acted on
original_severity present present present
final_severity present present present, value null
evidence_grade present present present
rules_fired present present absent
skipped present present absent
cvss_source present present absent
environment present present absent
ceiling_enforced present present absent
finding_id absent present present
source absent present present
note absent absent present

What govern_finding itself builds#

A record is built at all only when something moved (original_severity != final_severity, or the false-positive flag flipped) which is why original_severity and final_severity matching each other inside an existing record is not proof that nothing happened: an info-severity finding that matches a mark_fp rule stays info on both sides of the record, verified directly by governing exactly that shape, and the record still exists, still names the firing rule in rules_fired, because the false-positive flag is what moved even though the band did not. Both values are read off the finding only after severity_governor.py:rank's own normalisation (stripped of surrounding whitespace and case-folded) so a difference between a finding's raw, caller-supplied severity and the record's original_severity can be nothing more than casing or a stray trailing space, never a real disagreement; rank's own docstring is direct about why that stripping is load-bearing rather than cosmetic, since without it a severity arriving as "critical " ranks at the floor of the scale, the same rank as "info", and bypasses every cap, the evidence ceiling and the record itself, silently.

rules_fired inside the record is a trace of the mechanisms that took their action on the way to its final value, in the order each one ran (an authored CVSS vector first, the semantic ruleset second, the evidence ceiling last) and not a single pointer to one cause. I governed a finding carrying both an authored vector that would lower a critical to high and a type matching the shipped cors-wildcard rule, which caps lower still: the returned rules_fired named cvss-reconcile and then cors-wildcard, in that order, and the final severity was the second mechanism's target, not the first's. Read it as what acted rather than as what moved the severity, because those are not the same set: the capping actions are rank-gated, so a cap_at or downgrade_to id is appended only when its target ranked below the severity already in hand, which is to say only when it moved it, while mark_fp carries no such gate and appends its id whenever it matches. On a finding an earlier mark_fp rule has already marked, the later one re-executes onto the state it is already in and its id still lands. I built a finding matching both shipped mark_fp rules (a title read-via-post-bounce accepts, an /api/ url, and an evidence body carrying both rules' triggers) and rules_fired came back naming read-via-post-bounce and then spa-fallback-api-200, while the same finding with /api/ taken out of its url reaches an identical severity and flag on read-via-post-bounce alone: the later id is in the list having changed nothing. The gate shows in the other direction on a rule that matches without acting: a secret_exposure finding carrying the tokenization rule's evidence matches tokenization-key-public whether it enters at critical or at low, and match_rules returns the rule in both, but its id lands only in the first: the uat target medium ranks below critical and does not rank below low, so on the second run the rule matched, its branch ran, and rules_fired was never written at all. fp_reason is overwritten on each match as well, so it names the last mark_fp rule to match rather than the one that caused the transition. A reader who assumes this list never holds more than a single name, or that whichever name comes first is the operative cause, or that each name in it moved something, is right only on the finding shapes where just one mechanism actually fires.

skipped inside the record is not the only place a skipped reconciliation is ever recorded, and confusing the two is a real trap. The record's own skipped key holds a CVSS-reconciliation skip only when the record already exists for some other reason, because a governed pass appends to it purely inside the CVSS step, and a record is built at all only when original_severity and final_severity differ or the false-positive flag moved. A vector that would have lowered a finding, carrying no attributable cvss_source, is skipped for exactly that reason and fails closed rather than lowering anything on unattributed evidence, but if nothing else about the finding changed, that skip is reported on the finding's own top-level reconcile_skipped key instead, with no governance_record anywhere and no raw_data.governance either. test_cvss_reconcile_fails_closed_on_an_unlabelled_vector, in tests/test_severity_governor.py, is the test that pins exactly this: it asserts reconcile_skipped present and governance_record absent on the same finding, in that combination. A consumer that only ever checks the record's own skipped list for an unattributed-vector event will miss every case where nothing else about the finding happened to move.

evidence_grade inside the record is the grade severity_governor.py:evidence_grade computed for that call, not a value read off anything the finding itself claims; the module's own reasoning is that a finding asserting evidence_grade: strong over nothing but a title still grades thin, because the grading function never consults a finding's own opinion of its evidence at all. cvss_source defaults to the literal string none whenever neither the finding nor its raw_data carries one, and that default is the ordinary case for the majority of records in this artifact: a record built by a matched rule or by the evidence ceiling has nothing to do with CVSS at all, so none here is not itself a sign that CVSS reconciliation is missing or broken, only that this particular record's move did not come from it.

environment always resolves to one of exactly two words, uat or prod, and never anything else. Every return path in severity_governor.py:resolve_environment and in the caller that wraps it has been checked; neither function has a third branch. So a cap_at rule with an environment split always finds a key to read and a split cap always fires with one of its two values. That totality is a deliberate fix for a fail-open the module's own docstring names directly: a caller passing PROD in the wrong case used to find neither severity_PROD nor a plain severity on a split rule, so the cap silently did not apply at all. The direction the fallback resolves in matters as much as its existence: an unrecognised hostname segment resolves to prod, not uat, so a real non-production host whose name uses a token this module does not recognise is treated as production and gets the higher side of any environment split rather than the lower side. resolve_environment on a URL whose host is beta.example.com returns prod, verified directly, because beta is not among the tokens severity_governor.py:resolve_environment treats as non-production; a tokenization-key leak on that host, with no environment argument supplied, governs to high rather than medium for exactly that reason. Also verified directly. test_every_environment_spelling_resolves_and_the_cap_always_fires, in tests/test_severity_governor.py, is named as though it swept every spelling; its body checks a fixed, curated set of spellings plus a blank string, which is a sample of the totality above and not a proof of it by itself: the proof is in resolve_environment and its caller having no branch that returns anything else, and the test is what would catch a regression in that sample rather than what established the property in the first place. ceiling_enforced records only whether the evidence-ceiling pass was switched on for that call, mirroring the enforce_evidence_ceiling argument; it says nothing about whether the ceiling actually lowered anything on this call, which is why ceiling_enforced: true and evidence-ceiling absent from rules_fired on the same record is the ordinary case for a call where the ceiling ran and found nothing to cap, not a contradiction.

What the artifact publishes instead#

walkthrough/artifacts/08-governance.json is built by walkthrough/run.py's own write stage, and it adds finding_id and source to every entry, acted-on or not, naming the finding and the fixture file each entry came from: both driver bookkeeping severity_governor.py:govern_finding never sets on its own return value, confirmed by reading that return value directly with nothing else in the loop. Where the governor did produce a record, the driver copies it through unchanged and layers finding_id and source on top, which is why an acted-on entry in this artifact carries both the record's own fields and the driver's additions together. Where the governor produced no record (f0005, sourced from the fixture built to match no shipped rule) the driver does not drop the finding from the ledger; it writes an entry of its own instead, carrying the severity that entered, a final_severity of null, the evidence grade, and a note reading "no rule matched and no ceiling applied," a sentence that does not exist anywhere in core/severity_governor.py, checked directly. A consumer who treats this artifact as though it were shaped like govern_finding's own contract will get a KeyError reaching for ceiling_enforced on that entry for f0005, and will silently read the wrong thing entirely if they reach for final_severity expecting a severity band back and get null instead; null here means the governor was never credited with moving this finding, not that it moved it to nothing.

The pairing nothing declares#

governance_record and the finding's own top-level rules_fired sit behind two different conditions in the source, not one: the record is built inside an if changed: block, and the top-level key is set inside a further if fired: check nested one level inside that same block. Nothing in severity_governor.py:govern_finding ties those two conditions to each other on purpose, and yet no input has been found, by anyone who has gone looking on this branch, where either one shows up without the other. Stated at the strength the evidence actually supports: the pairing is emergent, not declared anywhere as a rule, and it holds today because of a fact one level down from both conditions. Every branch inside govern_finding that can move current away from its entering value, or flip the false-positive flag, appends the mechanism's own id to fired in the very same statement that performs the move. The CVSS reconciliation returns a fired id exactly when it returns a changed severity and never otherwise; mark_fp, cap_at and downgrade_to each append their rule's id in the same branch that reassigns current or sets the false-positive flag; the evidence ceiling appends its own name in the same conditional that lowers current to the ceiling. Because changed can only become true through one of those same branches, fired is never empty when changed is true, in every branch that exists today, which is a fact about how those branches happen to be written, not a property either guard enforces on the other.

I ran my own sweep of that claim rather than resting on the reasoning alone: one probe built to match each shipped rule, plus one built to match none, crossed against every severity, every evidence grade, both environments, three CVSS vector positions and three provenance labels, calling severity_governor.py:govern_finding directly on the unmodified, currently-installed module: a standalone script, not part of this repository, printed zero mismatches across every combination. That sweep varies type, title and evidence far more than it varies url, which stays fixed except where a rule's own match block requires a path fragment, so it does not close every axis either; a search that also holds url fixed is a limit of that run, not a property this appendix is claiming about the tree. Chapter 06's step five reports a further check I did not repeat here because repeating it means editing core/severity_governor.py itself, which this appendix does not do: mutating a scratch copy of the module with one additional branch that moves current down without appending to fired, then running the whole committed suite against that mutated copy, green throughout. I reproduced the part of that claim available without touching core/ at all: an isolated copy under a scratch path, never imported as core.severity_governor and never on this repository's own sys.path, carrying the same one-line addition, and calling govern_finding from that copy on a finding built to hit the new branch returned a governance_record whose own rules_fired was empty and no top-level rules_fired on the finding at all, splitting the pairing exactly as the reasoning above predicts. Both halves point the same way: the co-occurrence holds because of what every existing branch happens to do, not because anything checks that a new branch keeps doing it.