Skip to content
Generated from the .d.ts files published in guardlink@2.2.0. Report problems against guardlink, not this page.

hypothesis

Everything exported by guardlink/hypothesis, from the .d.ts files published in guardlink@2.2.0. See the entry points for the other subpaths, and use guardlink as a library for worked examples.

Property Type
ledger HypothesesStatus
records BoundaryClaimRecord[]
summary BoundaryClaimSummary

Property Type Description
asset_a string -
asset_b string -
claim string -
description string -
directed boolean Written from <outer> to <inner>: asset_a is the outer side.
entry BoundaryEntry | null -
expired boolean The entry no longer applies because the code beneath the boundary changed.
file string -
id string The boundary’s #id, without the #; ‘’ when it has none.
key string -
line number -
location SourceLocation -
previous HypothesisOutcomeRecord<BoundaryOutcome> | null -
state BoundaryClaimState -

Property Type
contradicted number
retest number
supported number
unverified number

Property Type
anchor_hash_version number
entries LedgerEntry[]
schema "guardlink.hypotheses/v1"

Property Type
error? string
ledger HypothesesLedger | null
status HypothesesStatus

Property Type
hash string
scope AnchorScope
symbol string | null

Property Type
ledger HypothesesStatus
records HypothesisRecord[]
summary HypothesisSummary

Type Parameter Default type
O extends LedgerOutcome HypothesisOutcome
Property Type Description Inherited from
anchor HypothesisAnchor | null The claim’s anchor when the outcome was recorded; null when the claim had none. HypothesisOutcomeRecord.anchor
at string ISO 8601, UTC. HypothesisOutcomeRecord.at
by string human:<name> or cxg:<template>. HypothesisOutcomeRecord.by
claim string Display only; never matched on. -
evidence string - HypothesisOutcomeRecord.evidence
file string - -
history HypothesisOutcomeRecord<O>[] Earlier outcomes for the same key, newest first. -
key string Stable claim key from relationRecords. -
line number - -
outcome O - HypothesisOutcomeRecord.outcome
source HypothesisSource - HypothesisOutcomeRecord.source

Type Parameter Default type
O extends LedgerOutcome HypothesisOutcome
Property Type Description
anchor HypothesisAnchor | null The claim’s anchor when the outcome was recorded; null when the claim had none.
at string ISO 8601, UTC.
by string human:<name> or cxg:<template>.
evidence string -
outcome O -
source HypothesisSource -

Property Type Description
asset string -
claim string -
entry HypothesisEntry<HypothesisOutcome> | null -
expired boolean The entry no longer applies because the code beneath the claim changed.
file string -
key string -
line number -
location SourceLocation -
previous HypothesisOutcomeRecord<HypothesisOutcome> | null The lapsed outcome, when expired.
refs string[] The exposure’s external references (cwe:, owasp:), for the offered Confirmed line.
severity string -
state HypothesisState -
threat string -
verb "exposes" | "confirmed" -

Property Type
confirmed number
refuted number
retest number
untested number

Property Type Description
ambiguous object[] -
boundaries object[] Outcomes recorded against a declared boundary’s claim key. Never written into source.
confirmed object[] -
malformed object[] A stamp arrived under one of our names that is not a claim key. Reported, never joined: it says nothing about which claim was tested, so guessing from the coarse tiers would be a confirmation resting on a value we rejected.
scanId string -
stale object[] The finding’s claim key names no claim in the model: the claim it was tested against is gone. No candidates are offered — whatever the coarse tiers would have matched is a different claim, and recording this evidence against it by hand is the exact confirmation the key just refused.
unmatched ScanFinding[] -

Property Type Description
at string ISO 8601. The caller supplies it so the ledger stays deterministic under test.
by string -
evidence string -
source? HypothesisSource -

Property Type Description Inherited from
asset string - HypothesisRecord.asset
claim string - HypothesisRecord.claim
entry HypothesisEntry<HypothesisOutcome> | null - HypothesisRecord.entry
expired boolean The entry no longer applies because the code beneath the claim changed. HypothesisRecord.expired
file string - HypothesisRecord.file
key string - HypothesisRecord.key
line number - HypothesisRecord.line
location SourceLocation - HypothesisRecord.location
onPath boolean The asset sits on an undefended source-to-sink path. -
previous HypothesisOutcomeRecord<HypothesisOutcome> | null The lapsed outcome, when expired. HypothesisRecord.previous
rank number - -
refs string[] The exposure’s external references (cwe:, owasp:), for the offered Confirmed line. HypothesisRecord.refs
severity string - HypothesisRecord.severity
state HypothesisState - HypothesisRecord.state
threat string - HypothesisRecord.threat
unowned boolean No Owns names the asset. -
verb "exposes" | "confirmed" - HypothesisRecord.verb

Property Type Description
annotation { file: string; line: number; } | null -
asset string | null -
boundary_claim_key string | null The claim key of a declared boundary this finding tested, read under every name in BOUNDARY_CLAIM_KEY_NAMES at the finding’s top level, in its annotation, or in either one’s properties. A probe can test a boundary and an exposure at once, so this sits beside claim_key rather than replacing it. Null when absent, and when every value found is unshaped (boundary_malformed).
boundary_malformed ScanStamp | null -
boundary_outcome BoundaryOutcome What the probe found about the boundary: contradicted unless the finding says supported.
claim_key string | null The claim key the export stamped onto the finding, when it carries one, read under every name in CLAIM_KEY_NAMES and from every container a consumer puts it in — see findingStamp. A stamp guardlink fails to recognise is worse than no stamp: it takes the weaker join, and the report that carried the discriminator gets told to start sending one. Null when the finding carried no stamp AND when EVERY stamp it carried is unshaped — the latter lands in malformed_stamp instead, so the two are never confused for each other.
confidence string | number | null -
cwe_ids string[] -
evidence object -
evidence.data Record<string, unknown> -
evidence.matched_patterns string[] -
evidence.request string | null -
evidence.response string | null -
id string -
junk_stamps ScanStamp[] Unshaped candidates found ALONGSIDE a valid key, which was used. Reported as a note and nothing more: selecting the good key must not silently hide that something is emitting garbage under a guardlink name, but it is not grounds to refuse a join the report identified precisely.
malformed_stamp ScanStamp | null Candidates were present under our names and NONE was shaped like a claim key. Its own state: a producer emitting something else under our name is neither “unstamped” nor “the claim is gone”, and folding it into either loses the one thing worth knowing about it.
rival_stamps ScanStamp[] Well-formed keys that DISAGREE with the one selected — the report claiming two different exposures at once. Deterministic precedence still picks the winner, because a widening must not change what an already-accepted report resolves to; what changes is that the disagreement is no longer discarded. A conflicting VALID extra is likelier to be trusted than an obviously malformed one, so hiding it is worse than hiding junk, not equally bad.
severity string -
template_id string -
threat string | null -
title string -

BoundaryClaimState = "unverified" | "supported" | "contradicted" | "retest"


BoundaryEntry = HypothesisEntry<BoundaryOutcome>

An outcome recorded against a declared boundary’s claim key.


BoundaryOutcome = "supported" | "contradicted"

What testing a declared boundary found: the line held, or it did not.


HypothesesStatus = "present" | "absent" | "corrupt"


HypothesisOutcome = "confirmed" | "refuted"


HypothesisSource = { kind: "manual"; } | { confidence: number | string | null; joined_by?: JoinedBy; kind: "scan"; scan_id: string; template_id: string; }

{ kind: "manual"; }


{ confidence: number | string | null; joined_by?: JoinedBy; kind: "scan"; scan_id: string; template_id: string; }

joined_by is absent on entries written before it existed; treat that as unknown, not as verified.


HypothesisState = "untested" | "confirmed" | "refuted" | "retest"


JoinedBy = "claim-key" | "claim-key-contested" | "location" | "asset-threat" | "cwe"

Which identity joined a scan finding to its claim. claim-key is the only one that names the claim itself; location, asset-threat and cwe match something the claim merely sits at or near, so a claim that moved onto the tested line satisfies them. Recorded so a reader can tell a key-verified confirmation from one taken on the weaker match — an unmarked pair of those is the failure GAP-58 describes.

claim-key-contested is a key-verified join whose report carried MORE THAN ONE well-formed key, naming different claims: the winner came from a deterministic precedence tiebreak, not from agreement. It is its own value rather than a flag beside claim-key because “key-verified” is a claim about what happened, and a tiebreak establishes less than an uncontested stamp. A label that overstates the verification actually performed is the same species as every other false claim this subsystem exists to prevent.


LedgerEntry = HypothesisEntry | BoundaryEntry


LedgerOutcome = HypothesisOutcome | BoundaryOutcome

const BOUNDARY_CLAIM_KEY_NAMES: readonly string[]

Every name a scan report may carry a boundary’s claim key under. Separate from CLAIM_KEY_NAMES because a finding stamped with both is reporting on two claims, an exposure and the boundary in front of it, not contradicting itself.


const HYPOTHESES_FILE: ".guardlink/hypotheses.json" = ".guardlink/hypotheses.json"

GuardLink Hypotheses — barrel.

– “The tested state of every exposure and declared boundary: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue”


const HYPOTHESES_SCHEMA: "guardlink.hypotheses/v1" = "guardlink.hypotheses/v1"

GuardLink Hypotheses — barrel.

– “The tested state of every exposure and declared boundary: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue”

attachHypotheses(model, c): void

Stamp the state onto each exposure so surfaces that only see the model can read it.

Parameter Type
model ThreatModel
c HypothesisClassification

void


classifyBoundaryClaims(model, read): BoundaryClaimClassification

Parameter Type
model ThreatModel
read HypothesesRead

BoundaryClaimClassification


classifyHypotheses(model, read): HypothesisClassification

Parameter Type
model ThreatModel
read HypothesesRead

HypothesisClassification


confirmedLine(record, entry): string

The @confirmed annotation that reflects a confirmation, ready to paste.

Parameter Type
record HypothesisRecord
entry HypothesisEntry

string


emptyHypotheses(): HypothesesLedger

GuardLink Hypotheses — barrel.

HypothesesLedger

– “The tested state of every exposure and declared boundary: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue”


formatBoundaryList(c, state?): string

Parameter Type
c BoundaryClaimClassification
state? string

string


formatBoundaryOutcome(record, entry): string

One boundary outcome, as a block. Like formatOutcome, it names the claim, not a position.

Parameter Type
record BoundaryClaimRecord
entry BoundaryEntry

string


formatHypothesisList(c, state?): string

Parameter Type
c HypothesisClassification
state? string

string


formatImport(r): string

Parameter Type
r ImportResult

string


formatIntake(q, project, total): string

The queue as a brief bugb intake can take. total is the queue length BEFORE -n bounded it.

Parameter Type
q RankedHypothesis[]
project string
total number

string

– “A bounded brief that does not say it is bounded is a misleading brief. This one closes by telling an operator to hand it to bugb intake, so a 10-item page of a 142-claim queue reads as the whole of what there is to test and the 132 it never names read as nothing left to do. Both numbers are stated above the list and again on that closing line, since either can be the one an operator reads, along with the flag that asks for the rest. An untruncated brief is unchanged, down to the byte. The notice COUNTS the remainder and deliberately names no state for it: rankUntested queues untested and retest together, so a state noun here can misdescribe what is hidden — calling a claim that was confirmed, and whose code has since moved, untested, in the very line an operator hands to bugb intake. Membership is decided by that filter and is free to change without anyone reading this string; a line that only counts cannot fall out of step with what it counts, and the per-item labels below keep the composition legible where it belongs. The remainder clause agrees in number, because a remainder of exactly one is an ordinary count and The other 1 are is the kind of seam that tells a reader the sentence was never run at that value. The page size stays a numeral even when it is one: a bounded notice has to name both numbers, so it is not dropped for grammar. total is REQUIRED here for the same reason as in formatQueue — a default would return the permissive answer to a caller who forgot it, in the one renderer an operator hands to bugb intake”


formatOutcome(record, entry, offered?): string

One outcome, as a block.

The header names the CLAIM and not a position. record.line is a pre-write coordinate — this runs before --write splices anything — and the authoritative post-write positions are the wrote and already confirmed lines the CLI prints once the writes have landed. Printing a second coordinate for the same thing asks the reader to hold two and pick correctly, and the two WILL disagree in the run that moves one of them; the asset, threat and key identify the claim without one. offered is the exception and is safe: it is shown only when nothing is being written, so no insertion can move it.

Parameter Type
record HypothesisRecord
entry HypothesisEntry
offered? string

string


formatQueue(q, total): string

The queue as a table. total is the queue length BEFORE -n bounded it.

Parameter Type
q RankedHypothesis[]
total number

string

– “The header counted the rows it was handed, so once -n bounded every renderer a 142-claim queue printed 10 to test: not a smaller answer to the same question but a wrong answer to it, because that line is the only place the table says how much there is. It names both numbers when the page is short of the queue, and reads exactly as it always did when it is not — a run that hides nothing must not grow a notice about hiding. total is REQUIRED and deliberately carries no default: defaulting it to q.length hands a caller who forgets it the permissive answer — truncated false, and a 10-of-142 page reporting 10 to test — which is the wrong number this renderer exists to stop, reinstalled as a silent default. Required, an omission is a build failure instead of a confident wrong answer”

– “total decides EVERY branch here, the empty page included. The early return is the one sentence in this function that names a state rather than counting — it says every exposure has an outcome that still holds — so it is owed to total === 0 and not to an empty page: formatQueue([], 142) asked the queue for nothing and got nothing back, which says where the page ended, never that there is nothing outstanding. That branch was the last place in this file where a required total was accepted and then not read, and an empty page over a non-empty queue now falls through to the ordinary header and renders 0 of 142 to test, which counts and claims nothing”


importScan(root, model, scanPath, input): ImportResult

Join each finding to a claim and record the confirmed ones.

A claim key RESOLVES the join; it does not narrow one. A finding that carries a key is looked up against the whole record set, and that lookup is the whole answer: it hits exactly one claim (keys are unique within a model) or it hits none, in which case the claim it named is gone and the finding is stale. The coarse tiers — annotation location, then asset and threat, then CWE — run ONLY for a finding that carries no key, and a finding that fits more than one of them is reported as ambiguous, never guessed.

The order matters and is not an optimisation. Were the tiers to run first, the location tier would pick whatever claim now sits on the tested line and the key would only get to veto it — so a claim that merely DRIFTED (its file edited above it, moving its line) would be reported stale while it is alive and holding the stamped key, and a finding carrying nothing but a key — the most precise identifier in the system — would be reported unmatched. A wrong location match must never beat a correct key.

The key is the one the ledger already keys an entry on: a digest of the claim’s own words, so it survives every edit that is not the claim — a line move, and an edit to the code beneath it. That is what separates a finding from a sibling claim that landed on the line it was tested at: same file, same line, same asset, same threat, therefore the same threat id, and nothing else on the finding tells them apart.

The bound: two BYTE-IDENTICAL claims in one file — same verb, asset, threat, external refs and description — share a digest and are told apart only by an ordinal in document order. Delete the earlier one and the survivor inherits …:0, the deleted claim’s exact key, so a finding stamped against the first joins to the second. That is the shape this function exists to refuse, surviving at a strictly narrower population. Separately, rewording a claim’s own description re-keys it, so a stamp from before the rewording is refused.

Every confirmation records WHICH identity joined it (joined_by), because a key-verified confirmation and one taken on the coarse tiers must not be indistinguishable after the fact.

Parameter Type
root string
model ThreatModel
scanPath string
input { at: string; by: string; }
input.at string
input.by string

ImportResult


rankUntested(records, model, pathAssets?): RankedHypothesis[]

What to test next. retest first (a confirmed finding whose code changed), then untested; within each: severity, then on an undefended path, then unowned, then file and line. pathAssets is the set of asset refs on unmitigated paths (findUnmitigatedPaths(...).assetsOnPath), in any form.

Parameter Type
records HypothesisRecord[]
model ThreatModel
pathAssets? Set<string>

RankedHypothesis[]


readHypotheses(root): HypothesesRead

GuardLink Hypotheses — barrel.

Parameter Type
root string

HypothesesRead

– “The tested state of every exposure and declared boundary: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue”


recordBoundaryOutcome(root, model, target, outcome, input): object

Record one outcome for the boundary target names: its claim key, #id, or file:line.

Parameter Type
root string
model ThreatModel
target string
outcome BoundaryOutcome
input OutcomeInput

object

entry: BoundaryEntry

record: BoundaryClaimRecord


recordOutcome(root, model, target, outcome, input): object

Record one outcome for the exposure at target (file:line).

Parameter Type
root string
model ThreatModel
target string
outcome HypothesisOutcome
input OutcomeInput

object

entry: HypothesisEntry

record: HypothesisRecord


resolveBoundaryTarget(model, target): BoundaryClaimRecord

The boundary a target names: its claim key, its #id, or the file:line of its @boundary. Refuses anything that does not name exactly one.

Parameter Type
model ThreatModel
target string

BoundaryClaimRecord


resolveTarget(model, target): HypothesisRecord

The exposure at file:line, or an error that says what is there instead.

Parameter Type
model ThreatModel
target string

HypothesisRecord


scanEvidence(scanId, f): string

Evidence text for the ledger: what the template sent and what came back, redacted, plus the scan id.

EVERY value here comes out of a scan report, and this string can be written into someone’s source as an @confirmed description. So all of them go through one() — the single boundary they can enter through — rather than each interpolation remembering. A per-field helper is exactly how template_id, title and matched_patterns came to be interpolated raw while only request and response were collapsed, and a newline in any of them ends our line and puts report-controlled text on the next one, in the syntax the threat model is parsed from.

Parameter Type
scanId string
f ScanFinding

string


serializeHypotheses(ledger): string

Deterministic serialisation: entries by file, line, key.

Parameter Type
ledger HypothesesLedger

string


writeConfirmedLine(root, record, line): ConfirmedWrite

Insert the line directly beneath the @exposes, with the same comment prefix. Reports already-present when a

Parameter Type
root string
record HypothesisRecord
line string

ConfirmedWrite

for the pair already sits there, and refuses a line that does not parse back as one @confirmed.

The re-parse is the structural half of the same guard confirmedLine’s collapse gives: the description is assembled from a scan report, and a value that broke out of its quotes or its line would land in source as something the next parse reads differently from what we thought we wrote. Checking here rather than in the caller means no caller can skip it. Same reason, and the same pairing, as buildAcceptLines in src/review/index.ts.

record.line is resolved from a model parsed BEFORE this call, and this call splices a line into the file — so applying several of these to one file must go in DESCENDING line order. Ascending, each insertion shifts every later target down by one, and where the shifted line is itself an @exposes every guard below passes and the confirmation lands against a claim that was never tested. Re-resolving between writes would work too; the ordering is cheaper and cannot be forgotten halfway.


writeHypotheses(root, ledger): void

GuardLink Hypotheses — barrel.

Parameter Type
root string
ledger HypothesesLedger

void

– “The tested state of every exposure and declared boundary: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue”