Documentation
¶
Overview ¶
Package types holds the frozen domain types and the interfaces each package implements.
Layout:
cmd/content-parity CLI entry (check, benchmark, aggregate, scan, keys, dataset) deploy operator kit for publishing the key directory signing RFC 9421 and web-bot-auth (no project imports) internal/types this package internal/schema SQLite DDL internal/fixture local fixture servers internal/fetch default observation identities (honest UA) internal/extract semantic blocks and token estimates internal/compare block-level matching internal/sitecontext robots, llms.txt, alternate links internal/storage SQLite persistence internal/bodystore compressed response bodies (local evidence) internal/engine check, benchmark, aggregate, and scan internal/inject machine-only text on stored bodies internal/dataset Tranco-backed pilot and benchmark lists internal/config JSON config file internal/sitemap sitemap.org urlset and index
Package signing is importable by third parties. It does not import this package. RequestSigner is satisfied implicitly.
Index ¶
- Constants
- Variables
- func BaselineEligible(a, b Observation) bool
- func BaselineUnstable(unsigned, control Observation, diff Comparison) bool
- func BlockLess(a, b Block) bool
- func CanonicalURL(raw string) string
- func ChallengeAsymmetry(a, b Observation) bool
- func ChallengeFamilyNames(sigs []ChallengeSignal) []string
- func ContentComparable(o Outcome) bool
- func ContentDiffers(a, b Observation, diff Comparison) bool
- func CoreCoverage(core []Block, replicas ...[]Block) float64
- func CoreTooSmall(reps []Observation, core []Block) bool
- func DomainSHA256(list string) string
- func EstimateTokens(s string) int
- func HasStrongChallenge(sigs []ChallengeSignal) bool
- func IsChallenge(status int, h http.Header, body []byte) bool
- func LooksLikeHTML(contentType string) bool
- func LooksLikeMarkdown(contentType string) bool
- func MachineHasExtraText(diff Comparison) bool
- func MarkdownFault(md, html Observation, htmlVsMarkdown Comparison) bool
- func MarkdownLostBody(md, html Observation, htmlVsMarkdown Comparison) bool
- func MarkdownMissingVary(md, html Observation) bool
- func MarkdownVariant(md, html Observation) bool
- func NormalizeText(s string) string
- func PathLooksLikeMarkdown(path string) bool
- func RedirectDiffers(a, b Observation) bool
- func ReportedTokens(h http.Header) (n int, name string, ok bool)
- func ResolveAlternate(pageURL, href string) string
- func RobotsToken(k IdentityKind) string
- func SameBlocks(a, b []Block) bool
- func SentToOrigin(o Outcome) bool
- func ShuffleSeed() uint64
- func StripFrontmatter(src []byte) (body []byte, front string)
- func TokenSource(blocks []Block) string
- func TrioEligible(reps []Observation) bool
- func TrioUnstable(reps []Observation, core []Block) bool
- func UserAgent(k IdentityKind) string
- func VaryCoversAccept(h http.Header) bool
- type Aggregates
- type Alternate
- type AlternateMismatch
- type AlternateRef
- type AlternateSource
- type Block
- type BlockDiff
- type BlockKind
- type BodyEvidence
- type BodyStore
- type CacheState
- type ChallengeFamily
- type ChallengeSignal
- type Comparer
- type Comparison
- type ContextProbe
- type DiffOp
- type DirectoryChecker
- type DirectoryServer
- type Extracted
- type Extractor
- type FetchRequest
- type FetchResult
- type Fetcher
- type IdentityKind
- type LLMSLink
- type LLMSTxt
- type LinkTargetChange
- type Observation
- type Outcome
- type PageRole
- type PageSelection
- type Platform
- type RequestSigner
- type RobotsRule
- type Run
- type RunSettings
- type RunStatus
- type Share
- type SiteContext
- type SiteContextRecord
- type Store
Constants ¶
const ( ChallengeChannelHeader = "header" ChallengeChannelLocation = "location" ChallengeChannelBody = "body" ChallengeStrengthStrong = "strong" ChallengeStrengthWeak = "weak" )
const ( ProductName = "content-parity" ProductVersion = "0.1.0" ProductUA = "content-parity/0.1.0 (measurement; +https://github.com/Zulwatha/content-parity)" ProductToken = "content-parity" // ImpersonateUA is a published GPTBot token. Sending it is not // default behavior. Fetch sends it only for IdentityImpersonate // when ImpersonateCrawler is set. Benchmark mode rejects that option. ImpersonateUA = "Mozilla/5.0 AppleWebKit/537.36 (compatible; GPTBot/1.2; +https://openai.com/gptbot)" ImpersonateToken = "GPTBot" )
Product identification. Every default HTTP identity sends ProductUA. Browser uses Chrome's own UA. Impersonate is the only identity that may send ImpersonateUA, and only when the caller opts in.
const ( CrawlerUA = ImpersonateUA CrawlerToken = ImpersonateToken )
CrawlerUA and CrawlerToken are the historical names for the impersonation strings. They are not used by default identities.
const ( DefaultTimeout = 30 * time.Second DefaultRedirects = 10 DefaultRetries = 2 DefaultConcurrency = 4 DefaultRetryWait = 200 * time.Millisecond DefaultHostDelay = time.Second DefaultHeartbeatInterval = 10 * time.Second DefaultHeartbeatStale = 90 * time.Second DefaultBrowserTimeout = 45 * time.Second DefaultBrowserRecycle = 20 )
Timing defaults consumed by fetch and the benchmark command.
const ( PlatformCDN = "cdn" PlatformEdge = "edge" PlatformOrigin = "origin" )
const ( WellKnownPath = "/.well-known/http-message-signatures-directory" DirectoryMediaType = "application/http-message-signatures-directory+json" RequestTag = "web-bot-auth" DirectoryTag = "http-message-signatures-directory" SignatureAlgorithm = "ed25519" Algorithm = SignatureAlgorithm RequestLabel = "sig1" DirectoryLabel = "binding" )
Signing contract for package signing.
The package lives at github.com/Zulwatha/content-parity/signing and must import nothing under this module. Third parties may import it on its own.
Profile: RFC 9421 HTTP Message Signatures plus draft-meunier-webbotauth-httpsig-protocol-02.
Keys: crypto/ed25519 only. PKCS#8 PEM on disk. keyid is the base64url (no pad) SHA-256 JWK thumbprint of {"crv":"Ed25519","kty":"OKP","x":"..."} as in RFC 8037 A.3.
Request Sign:
- set Signature-Agent to `sig1="<origin>"` (dictionary form)
- cover "@authority" "@method" "@path" `signature-agent`;key="sig1"
- params: created, expires (now + SignatureLifetime), keyid, alg="ed25519", nonce (32 random bytes, standard base64), tag="web-bot-auth"
- label sig1
- Signature value is an sf-byte-sequence (`:base64:`)
Directory:
- path /.well-known/http-message-signatures-directory
- media type application/http-message-signatures-directory+json
- body: JWKS {keys:[{kty,crv,kid,x,use:"sig"}]}
- Cache-Control: max-age matching DirectoryCacheAge
- sign the response: tag="http-message-signatures-directory", label binding, cover `@authority`;req and content-digest (RFC 9530 sha-256), created, expires, keyid
Tests: RFC 9421 B.1.4 Ed25519 key and the EdDSA vectors in appendix E.2 of the protocol draft. No live network.
Constants the package should export:
WellKnownPath = "/.well-known/http-message-signatures-directory" DirectoryMediaType = "application/http-message-signatures-directory+json" RequestTag = "web-bot-auth" DirectoryTag = "http-message-signatures-directory" Algorithm = "ed25519" RequestLabel = "sig1" DirectoryLabel = "binding"
AgentOrigin is the public origin that hosts the well-known directory. Signature-Agent must use this value. https is required except for loopback (tests and serve-keys).
CheckPublished GETs {Origin}{WellKnownPath} and verifies the current public key is in the JWKS. A failed check must abort a signed-mode benchmark at startup. Do not sign requests against an origin that does not publish the key.
const AcceptHTML = "text/html, application/xhtml+xml, */*;q=0.1"
AcceptHTML is the Accept value for unsigned replicas and signed.
const AcceptMarkdown = "text/markdown, text/x-markdown, text/plain;q=0.8, */*;q=0.1"
AcceptMarkdown is the Accept value for IdentityMarkdown.
const AlterationFloor = 0.6
AlterationFloor is the minimum text similarity for an altered pair. Below this, blocks stay unmatched (missing/added).
const DefaultBodyCap = 2 << 20
DefaultBodyCap is the maximum number of body bytes stored for one observation. The cap is recorded on the sidecar when it is hit.
const DirectoryCacheAge = 24 * time.Hour
DirectoryCacheAge is Cache-Control max-age on the key directory.
const ModulePath = "github.com/Zulwatha/content-parity"
ModulePath is the import path of this module. It is the repository URL without a scheme, so go get and go install can resolve it.
const SignatureLifetime = 5 * time.Minute
SignatureLifetime is the created-to-expires window on signed requests.
Variables ¶
var ChallengeFamilies = []ChallengeFamily{
cloudflareFamily,
amazonWAFFamily,
datadomeFamily,
perimeterXFamily,
anubisFamily,
recaptchaFamily,
hcaptchaFamily,
httpAuthFamily,
}
ChallengeFamilies is the ordered registry. The classifier walks this list and records every match.
var SignatureHeaderNames = []string{
"Signature",
"Signature-Input",
"Signature-Agent",
}
SignatureHeaderNames are the only headers Sign may add to the unsigned request. The signed identity differs in nothing else.
var TokenEstimateHeaders = []string{
"X-Markdown-Tokens",
"Markdown-Tokens",
"X-Token-Count",
"X-LLM-Tokens",
}
TokenEstimateHeaders are names used by edge markdown converters. First present header with a non-negative integer wins.
Functions ¶
func BaselineEligible ¶
func BaselineEligible(a, b Observation) bool
BaselineEligible is true when both observations can sit in a comparison denominator. Empty is excluded. Uncertain is not.
func BaselineUnstable ¶
func BaselineUnstable(unsigned, control Observation, diff Comparison) bool
BaselineUnstable is true when the unsigned and control observations are not equivalent. The site then has no meaningful identity comparison. Both observations must exist; the caller decides that.
func BlockLess ¶
BlockLess is a side-independent total order on semantic fields. Index is ignored, as in Equal.
func CanonicalURL ¶
CanonicalURL compares final URLs for redirect differences. Scheme and host are lowercased, default ports stripped, empty path becomes /. Query is kept. Fragment is dropped.
func ChallengeAsymmetry ¶
func ChallengeAsymmetry(a, b Observation) bool
ChallengeAsymmetry is the primary finding: one identity was challenged and the other was served. Never a content difference.
func ChallengeFamilyNames ¶
func ChallengeFamilyNames(sigs []ChallengeSignal) []string
ChallengeFamilyNames returns the families that produced sigs, in first-seen order.
func ContentComparable ¶
ContentComparable is true when the response has usable page content. Uncertain is included: the challenge evidence was inconclusive, not a refusal to measure. Empty is not.
func ContentDiffers ¶
func ContentDiffers(a, b Observation, diff Comparison) bool
ContentDiffers is a block or representation difference between two served identities. Challenge pairs must not be passed here.
func CoreCoverage ¶
CoreCoverage is len(core) / max replica length. Zero when every replica is empty.
func CoreTooSmall ¶
func CoreTooSmall(reps []Observation, core []Block) bool
CoreTooSmall is true when all replicas were served and produced blocks, but the intersection is empty.
func DomainSHA256 ¶
func EstimateTokens ¶
EstimateTokens is ceil(runes/4) over s. Empty input is 0. Extract applies this to block texts joined by a single newline.
func HasStrongChallenge ¶
func HasStrongChallenge(sigs []ChallengeSignal) bool
HasStrongChallenge reports a signal that is enough to call the page an interstitial.
func IsChallenge ¶
IsChallenge reports whether the response has a strong interstitial signal. A captcha widget string on an ordinary page is not one.
func LooksLikeHTML ¶
LooksLikeHTML reports an HTML Content-Type.
func LooksLikeMarkdown ¶
LooksLikeMarkdown reports a markdown Content-Type.
func MachineHasExtraText ¶
func MachineHasExtraText(diff Comparison) bool
MachineHasExtraText is the third aggregate predicate: the machine sequence contains text that is not in the browser sequence (DiffAdded when left is browser and right is machine).
func MarkdownFault ¶
func MarkdownFault(md, html Observation, htmlVsMarkdown Comparison) bool
MarkdownFault is lost body content in a real markdown variant. Navigation and interface chrome do not count. Missing Vary is MarkdownMissingVary, not this predicate.
func MarkdownLostBody ¶
func MarkdownLostBody(md, html Observation, htmlVsMarkdown Comparison) bool
MarkdownLostBody is true when the HTML sequence has a body block the markdown sequence is missing. Chrome-only loss is false. Words that appear in the markdown under another block kind are not lost.
func MarkdownMissingVary ¶
func MarkdownMissingVary(md, html Observation) bool
MarkdownMissingVary is true when a real markdown variant is missing Vary: Accept on the markdown side, or on the HTML side when HTML was served.
func MarkdownVariant ¶
func MarkdownVariant(md, html Observation) bool
MarkdownVariant is true when the markdown identity was served a markdown-typed body, or a type that differs from the HTML reference.
func NormalizeText ¶
NormalizeText trims, then collapses every Unicode whitespace run to one ASCII space. Extract must run this on every block text.
func PathLooksLikeMarkdown ¶
PathLooksLikeMarkdown is true for .md and .markdown paths.
func RedirectDiffers ¶
func RedirectDiffers(a, b Observation) bool
RedirectDiffers reports different final URLs after both served.
func ReportedTokens ¶
ReportedTokens reads a converter token estimate from headers. ok is false when none is present or parseable. Neither this value nor EstimateTokens is authoritative.
func ResolveAlternate ¶
ResolveAlternate is the comparison key for a markdown alternate. href is resolved against pageURL, then CanonicalURL. A variant hosted on another host is a different resource.
func RobotsToken ¶
func RobotsToken(k IdentityKind) string
RobotsToken is the robots.txt user-agent token used for k. Every identity, including impersonate, uses ProductToken. The tool never claims an allowance granted to another operator.
func SameBlocks ¶
SameBlocks reports identical sequences by Block.Equal, in order.
func SentToOrigin ¶
SentToOrigin is true when the tool performed an HTTP exchange. Unavailable and disallowed are client-side skips.
func ShuffleSeed ¶
func ShuffleSeed() uint64
ShuffleSeed is a non-zero seed from the clock. Tests that need a fixed order call ShuffledIdentities with a constant.
func StripFrontmatter ¶
StripFrontmatter removes a leading YAML (---) or TOML (+++) fence from markdown. The fence body is returned as front. body is the rest, or the original input when no fence is present.
func TokenSource ¶
TokenSource joins block texts in order with newlines. Used as the input to EstimateTokens.
func TrioEligible ¶
func TrioEligible(reps []Observation) bool
TrioEligible is true when every unsigned replica can sit in the stable-core denominator. Empty replicas cannot. Uncertain can.
func TrioUnstable ¶
func TrioUnstable(reps []Observation, core []Block) bool
TrioUnstable is true when the three unsigned replicas do not support a meaningful core comparison: outcomes differ, final URLs differ, or the core is empty while at least one replica has blocks.
func UserAgent ¶
func UserAgent(k IdentityKind) string
UserAgent returns the User-Agent string fetch must send for k in default (honest) mode. Browser is empty: Chrome supplies its own. Impersonate also returns ProductUA here; fetch sends ImpersonateUA only when the caller has opted in.
func VaryCoversAccept ¶
VaryCoversAccept reports whether Vary lists Accept, or is "*".
Types ¶
type Aggregates ¶
type Aggregates struct {
Sites int `json:"sites"`
// PrimaryFlags is how many site-context rows were flagged
// primary before collapse. Larger than Sites when a host had
// more than one primary page.
PrimaryFlags int `json:"primary_flags,omitempty"`
// Outcomes is a count per identity per outcome.
Outcomes map[IdentityKind]map[Outcome]int `json:"outcomes"`
SignedVsUnsignedContentRaw Share `json:"signed_vs_unsigned_content_raw"`
SignedVsUnsignedContentNet Share `json:"signed_vs_unsigned_content_net"`
SignedVsUnsignedRedirectRaw Share `json:"signed_vs_unsigned_redirect_raw"`
SignedVsUnsignedRedirectNet Share `json:"signed_vs_unsigned_redirect_net"`
SignedVsUnsignedChallengeRaw Share `json:"signed_vs_unsigned_challenge_raw"`
SignedVsUnsignedChallengeNet Share `json:"signed_vs_unsigned_challenge_net"`
SignedVsUnsignedLinkTargetRaw Share `json:"signed_vs_unsigned_link_target_raw"`
SignedVsUnsignedLinkTargetNet Share `json:"signed_vs_unsigned_link_target_net"`
// Unstable: the three unsigned replicas do not support a
// meaningful core comparison on the primary page.
Unstable Share `json:"unstable"`
// CoreTooSmall: all three unsigned replicas were served with
// blocks, but the intersection is empty. Counted in Unstable
// and reported separately so it is not a silent drop.
CoreTooSmall Share `json:"core_too_small"`
// CoreCoverageMean is the mean of len(core)/max(replica
// lengths) over primary pages whose three unsigned replicas
// were served. N is that denominator.
CoreCoverageMean Share `json:"core_coverage_mean"`
// InteriorFound: an interior content URL was chosen (not a
// homepage fallback), over sites.
InteriorFound Share `json:"interior_found"`
// Empty: observations that reached the origin and extracted
// zero blocks, over observations sent to the origin.
Empty Share `json:"empty"`
// Uncertain: observations with weak challenge evidence, over
// observations sent to the origin. Counted in neither the
// served set nor the challenged set.
Uncertain Share `json:"uncertain"`
// MarkdownLostBodyContent: a real markdown variant is missing
// HTML body content. Chrome-only loss is not a hit.
MarkdownLostBodyContent Share `json:"markdown_lost_body_content"`
// MarkdownMissingVary: a real markdown variant is missing
// Vary: Accept. Separate from content loss.
MarkdownMissingVary Share `json:"markdown_missing_vary"`
// AlternateMismatch is a count of sites whose head and Link
// header markdown alternates disagree. A finding, not a score.
AlternateMismatch int `json:"alternate_mismatch"`
// DuplicatePrimaryHosts is how many hosts had more than one
// page flagged primary. Those hosts are collapsed to one page
// before any share is computed. Zero means the denominator is
// already one page per host.
DuplicatePrimaryHosts int `json:"duplicate_primary_hosts,omitempty"`
}
Aggregates is the publishable summary: counts, shares, and platform breakdowns. It contains no site identifiers.
type Alternate ¶
type Alternate struct {
Href string `json:"href"`
Resolved string `json:"resolved,omitempty"`
Type string `json:"type"`
Source AlternateSource `json:"source"`
}
Alternate is a markdown alternate from the document head or Link header.
type AlternateMismatch ¶
type AlternateMismatch struct {
OnlyHead []AlternateRef `json:"only_head,omitempty"`
OnlyHeader []AlternateRef `json:"only_header,omitempty"`
}
AlternateMismatch is a finding: head and Link header sets disagree. One-sided declarations count. Not a score.
func (AlternateMismatch) HasMismatch ¶
func (m AlternateMismatch) HasMismatch() bool
HasMismatch reports any href present in only one source.
type AlternateRef ¶
AlternateRef is one one-sided declaration: what the site wrote and the absolute URL it resolves to.
type AlternateSource ¶
type AlternateSource string
AlternateSource says where a markdown alternate was declared.
const ( AlternateHead AlternateSource = "head" AlternateHeader AlternateSource = "header" )
type Block ¶
type Block struct {
Kind BlockKind `json:"kind"`
Text string `json:"text"`
Target string `json:"target,omitempty"`
Level int `json:"level,omitempty"`
Index int `json:"index"`
// Chrome is navigation or interface chrome: nav, page header,
// page footer, complementary rails, and search widgets.
// Article headers and footers inside main or article are not.
// Comparison ignores this flag.
Chrome bool `json:"chrome,omitempty"`
}
Block is one item in the normalized sequence.
func ProjectOntoCore ¶
ProjectOntoCore keeps blocks from seq that appear in core, by Equal, preserving seq order and consuming each core key once.
func StableCore ¶
StableCore is the blocks that appear, by Equal, in every replica. Order and Index follow the first replica. Target is ignored, as in Equal: a changed href does not drop a block from the core.
type BlockDiff ¶
type BlockDiff struct {
Op DiffOp `json:"op"`
LeftIndex int `json:"left_index"`
RightIndex int `json:"right_index"`
Left *Block `json:"left,omitempty"`
Right *Block `json:"right,omitempty"`
Confidence float64 `json:"confidence"`
Evidence string `json:"evidence"`
}
BlockDiff is one explainable difference.
func MissingBodyBlocks ¶
func MissingBodyBlocks(diff Comparison, markdown []Block) []BlockDiff
MissingBodyBlocks are DiffMissing entries whose left block is body content that is not in the markdown sequence. Chrome, empty punctuation, short interface links, and the same words under a different block kind are omitted.
type BlockKind ¶
type BlockKind string
BlockKind is a semantic unit after extraction. Not a markup role.
type BodyEvidence ¶
type BodyEvidence struct {
RunID string
Domain string
PageURL string
Identity IdentityKind
Body []byte
Capped bool
Cap int
RawBytes int
}
BodyEvidence is one stored response body. It is local evidence and is never part of a publishable output.
type BodyStore ¶
type BodyStore interface {
Put(ctx context.Context, rec BodyEvidence) error
Get(ctx context.Context, runID, domain, pageURL string, id IdentityKind) (BodyEvidence, bool, error)
}
BodyStore persists compressed response bodies outside the metrics database. Implementations must not write bodies into Store.
type CacheState ¶
type CacheState struct {
CacheControl string `json:"cache_control,omitempty"`
Age string `json:"age,omitempty"`
Expires string `json:"expires,omitempty"`
XCache string `json:"x_cache,omitempty"`
CFCacheStatus string `json:"cf_cache_status,omitempty"`
XCacheHits string `json:"x_cache_hits,omitempty"`
}
CacheState is what the origin (or an edge) said about this representation. Recorded as observed. Not a score.
func CacheStateFromHeaders ¶
func CacheStateFromHeaders(h http.Header) CacheState
CacheStateFromHeaders reads the usual cache and edge headers.
func (CacheState) Present ¶
func (c CacheState) Present() bool
Present is true when any cache field was set on the response.
type ChallengeFamily ¶
type ChallengeFamily struct {
Name string
Headers []familyHeader
Locations []familyLocation
Body []familyBody
// Phrases, if set, replace the default interstitial-copy list
// when deciding whether a contextual or widget body marker is
// strong for this family. Empty means the default list.
Phrases []string
}
ChallengeFamily is a named detector with its own signals and evidence rules. Adding a family does not require changing the classifier: append it to ChallengeFamilies.
type ChallengeSignal ¶
type ChallengeSignal struct {
Family string `json:"family,omitempty"`
Channel string `json:"channel"`
Name string `json:"name"`
Evidence string `json:"evidence,omitempty"`
Offset int `json:"offset,omitempty"`
Strength string `json:"strength,omitempty"`
}
ChallengeSignal is one audited reason a response was classified as a challenge or left uncertain. Family names the detector that produced it. Channel is header, location, or body. Body signals include the byte offset of the marker and a snippet of surrounding text so the call can be checked.
func ChallengeSignals ¶
func ChallengeSignals(status int, h http.Header, body []byte) []ChallengeSignal
ChallengeSignals walks ChallengeFamilies. An empty slice means no challenge evidence. The classifier does not mention families by name.
func (ChallengeSignal) String ¶
func (s ChallengeSignal) String() string
func (ChallengeSignal) Strong ¶
func (s ChallengeSignal) Strong() bool
type Comparer ¶
type Comparer interface {
Compare(left, right []Block) Comparison
}
Comparer matches two normalized block sequences.
The comparison is symmetric: Compare(B, A) is the mirror of Compare(A, B). Missing and added swap. Altered pairs swap indexes. Matched and MatchRatio are unchanged. If a tie rule would break that, change the rule.
Deterministic procedure:
- Longest common subsequence over Block.Equal. Those pairs are matches. They do not appear in Diffs. When skip-left and skip-right have the same length, skip the block that is greater under BlockLess. That choice does not depend on which sequence is on the left. Matched links whose Target differs are recorded on LinkTargets, not in Diffs. Evidence: `link target text=%q left=%s right=%s`.
- Remaining same-Kind pairs: compute text similarity 1 - levenshtein(runes)/max(len). Walk candidate pairs by descending similarity, then BlockLess of the pair's lesser block, then the greater block. No left-index tiebreak. First pair at or above AlterationFloor wins; no reuse. Those are DiffAltered. Confidence is the similarity. Evidence: `altered kind=%s sim=%.4f left=%d right=%d`.
- Unpaired left: DiffMissing, Confidence 1, LeftIndex set, RightIndex -1. Evidence: `missing kind=%s left=%d`.
- Unpaired right: DiffAdded, Confidence 1, RightIndex set, LeftIndex -1. Evidence: `added kind=%s right=%d`.
Diffs are ordered: altered by left index, then missing by left index, then added by right index. Order is not part of symmetry.
Matched is the LCS length. MatchRatio is Matched / max(left, right), or 1 if both sequences are empty.
Empty vs empty is a perfect match. Never scores a site.
type Comparison ¶
type Comparison struct {
LeftIdentity IdentityKind `json:"left_identity,omitempty"`
RightIdentity IdentityKind `json:"right_identity,omitempty"`
Diffs []BlockDiff `json:"diffs"`
LinkTargets []LinkTargetChange `json:"link_targets,omitempty"`
Matched int `json:"matched"`
LeftCount int `json:"left_count"`
RightCount int `json:"right_count"`
MatchRatio float64 `json:"match_ratio"`
}
Comparison is the full match of two sequences. LeftIdentity and RightIdentity are filled by the caller.
func (Comparison) HasDiffs ¶
func (c Comparison) HasDiffs() bool
HasDiffs reports any missing, added, or altered text block. Link target changes are reported separately and do not count.
func (Comparison) HasLinkTargetChanges ¶
func (c Comparison) HasLinkTargetChanges() bool
HasLinkTargetChanges reports a matched link whose href moved.
type ContextProbe ¶
type ContextProbe interface {
Probe(ctx context.Context, pageURL string, html []byte, headers http.Header) (SiteContext, error)
FetchOrigin(ctx context.Context, pageURL string, pause func(context.Context) error) (SiteContext, error)
WithPage(sc SiteContext, pageURL string, html []byte, headers http.Header) SiteContext
}
ContextProbe gathers origin metadata for one page.
pageURL is the page under test. html is the already-fetched HTML (unsigned or signed body); it may be empty. headers are that response's headers and MUST be read.
Probe GETs {origin}/robots.txt and {origin}/llms.txt with ProductUA. Do not follow redirects off origin. A missing file is not an error.
Robots: one RobotsRule per AllIdentities(), using RobotsToken (always ProductToken). Evaluate the tool's own group and the wildcard group only. Never select a group for another operator's token. Longest matching Allow/Disallow for that agent group wins; a more specific User-agent group beats *. Empty robots.txt or fetch failure means allowed.
Each Alternate stores Href (as declared) and Resolved (ResolveAlternate). One-sided mismatch entries are AlternateRef with both fields.
llms.txt: parse markdown links and bare http(s) URLs. IsMarkdown is true when the path ends in .md or .markdown, or the type looks like markdown.
Alternates come from both:
- document head: link[rel~=alternate]
- HTTP Link headers (RFC 8288)
Keep those whose type is text/markdown or text/x-markdown, or whose href path ends in .md / .markdown. Set Source to head or header. Compare href sets with ResolveAlternate(pageURL, href). Keys are resolved absolute CanonicalURLs. A variant on another host is a different resource. A difference, including a one-sided declaration, is Mismatch.
This package exists only to explain outcomes.
FetchOrigin GETs robots.txt and llms.txt. pause, if non-nil, is called before each GET so the caller can apply the same per-host delay as identity fetches. WithPage attaches head and Link-header alternates from an already-fetched page and does not touch the network. Probe is FetchOrigin with a nil pause, then WithPage.
type DirectoryChecker ¶
DirectoryChecker confirms the published key set contains the current key. Implemented by signing.Agent.
type DirectoryServer ¶
type DirectoryServer interface {
ServeDirectory(http.ResponseWriter, *http.Request)
}
DirectoryServer serves GET /.well-known/http-message-signatures-directory. Implemented by package signing without importing this package.
type Extracted ¶
type Extracted struct {
Blocks []Block `json:"blocks"`
TokenCount int `json:"token_count"`
Frontmatter string `json:"frontmatter,omitempty"`
}
Extracted is the pipeline output for one response body.
type Extractor ¶
Extractor turns HTML or markdown into the same block sequence.
Markdown is rendered to HTML first, then both inputs use one walker.
Walker, document order, on the body (or the whole tree if no body):
- ignore script, style, noscript, template
- h1-h6: one heading (Level 1-6, text = visible text), then child links only
- p: one paragraph, then child links
- li: one list_item, then child links
- a not already emitted as a child link: one link
- other visible text (div, section, article, td, span at block scope, and similar): one text run, skipped if empty after NormalizeText
A child link is Kind=link, Text=anchor text, Target=href as written (trimmed, not resolved, not lowercased).
Chrome is set when the block is emitted from nav, search, menu, page-level header/footer/aside, role=banner/contentinfo, or (when the document has main or article) anything outside those regions. Header and footer inside main or article are body content unless they have a chrome landmark role. The flag is not part of Equal.
TokenCount is EstimateTokens(TokenSource(blocks)). Empty or unparseable bodies return zero blocks and a nil error. Unknown content types: if the body looks like HTML, parse as HTML; otherwise treat as markdown.
Markdown that begins with a YAML (---) or TOML (+++) fence is passed through StripFrontmatter first. The fence is Frontmatter, never a block, so it cannot appear as missing or added content.
type FetchRequest ¶
type FetchRequest struct {
URL string
Identity IdentityKind
}
FetchRequest is one identity against one URL.
type FetchResult ¶
type FetchResult struct {
Identity IdentityKind
URL string
FinalURL string
Outcome Outcome
StatusCode int
Headers http.Header
ContentType string
Body []byte
ByteSize int64
Duration time.Duration
Err string
Challenge []ChallengeSignal
Cache CacheState
Platform Platform
}
FetchResult is the raw observation before extraction.
type Fetcher ¶
type Fetcher interface {
Fetch(ctx context.Context, req FetchRequest) FetchResult
FetchAll(ctx context.Context, rawURL string) []FetchResult
Available(kind IdentityKind) (ready bool, reason string)
}
Fetcher retrieves a URL as one or all identities.
Fetch never panics.
Config values are used as written. Zero retries means no retries. Zero redirects means do not follow redirects (record the first Location as FinalURL if present). A concurrency below 1 is invalid for FetchAll and must run as 1 worker so the call cannot hang. Callers that want the documented constants use fetch.DefaultConfig.
Redirects: follow up to Config.Redirects, record FinalURL. Retry: StatusCode 429, 503, and transport errors, up to Config.Retries, honoring Retry-After when present, otherwise DefaultRetryWait then 4x. Do not retry 4xx other than 429. After retries, map the final response with ClassifyHTTP, passing the body so challenge markers can be found. Record Challenge signals, CacheStateFromHeaders, and PlatformFromHeaders. Transport and deadline errors are OutcomeTransport.
Browser: render with Chrome, return documentElement.outerHTML. If Chrome is missing, OutcomeUnavailable. Classify the browser result from the navigation status, headers, and body the same way as HTTP identities.
Unsigned replicas (unsigned, unsigned_b, unsigned_c): GET with ProductUA and AcceptHTML. They are the same request, fetched three times so a stable core can be taken. Historical identity "control" is the same request and is not in AllIdentities.
Markdown: GET with ProductUA and AcceptMarkdown.
Signed: build the unsigned request first (same method, URL, User-Agent, Accept, header names, values, and relative order), then RequestSigner.Sign. Sign adds only Signature, Signature-Input, and Signature-Agent. It must not change any other request property. The signature is the only variable. If the signer is missing or AgentOrigin is empty, signed is OutcomeUnavailable.
Impersonate: not in AllIdentities. Fetch runs it only when the caller sets ImpersonateCrawler. Then it sends ImpersonateUA. Benchmark mode must refuse that option. Robots for impersonate still uses ProductToken.
Every default request identifies the tool. Do not add headers that bypass access control. The engine applies robots before Fetch (OutcomeDisallowed is set there).
FetchAll runs AllIdentities with at most Config.Concurrency in flight. Order of the returned slice is AllIdentities(). The engine fetches a shuffled copy per site.
type IdentityKind ¶
type IdentityKind string
IdentityKind is one observation identity.
const ( // IdentityBrowser is a full Chrome profile with JavaScript. // Optional at runtime: missing Chrome is OutcomeUnavailable. IdentityBrowser IdentityKind = "browser" // IdentityUnsigned is a plain GET that sends ProductUA. // This is the tool's own unsigned request. Two more replicas // (unsigned_b, unsigned_c) are fetched so a stable core can // be taken across three identical requests. IdentityUnsigned IdentityKind = "unsigned" // IdentityUnsignedB is the second unsigned replica. IdentityUnsignedB IdentityKind = "unsigned_b" // IdentityUnsignedC is the third unsigned replica. IdentityUnsignedC IdentityKind = "unsigned_c" // IdentityControl is the historical name for a second unsigned // fetch. It is not in AllIdentities. Old databases may still // contain it. Fetch still accepts it as unsigned-like. IdentityControl IdentityKind = "control" // IdentityMarkdown is a plain GET that prefers markdown via Accept. IdentityMarkdown IdentityKind = "markdown" // IdentitySigned is a controlled variant of the unsigned identity: // the unsigned request plus signature headers. Nothing else changes. // That is the basis of the headline finding. IdentitySigned IdentityKind = "signed" // IdentityImpersonate sends a third-party crawler User-Agent. // It is not in AllIdentities. Fetch only runs it when the caller // opts in. Benchmark mode refuses that option. It is for testing // sites you own or have permission to test, never for published data. IdentityImpersonate IdentityKind = "impersonate" )
func AllIdentities ¶
func AllIdentities() []IdentityKind
AllIdentities is the default observation set. FetchAll returns this order. The engine shuffles a copy per site before fetching so cache warming is not mistaken for an identity effect. Impersonate is not included.
func ShuffledIdentities ¶
func ShuffledIdentities(seed uint64) []IdentityKind
ShuffledIdentities returns AllIdentities in a per-seed order.
func UnsignedReplicas ¶
func UnsignedReplicas() []IdentityKind
UnsignedReplicas are the three unsigned fetches used to build the stable core. They send the same request.
func (IdentityKind) Machine ¶
func (k IdentityKind) Machine() bool
Machine reports whether k is a non-browser identity.
func (IdentityKind) UnsignedLike ¶
func (k IdentityKind) UnsignedLike() bool
UnsignedLike is true for the unsigned replicas, historical control, and signed. Those send the same method, URL, User-Agent, and Accept. Signed then adds only the signature headers.
func (IdentityKind) UnsignedReplica ¶
func (k IdentityKind) UnsignedReplica() bool
UnsignedReplica is true for the three unsigned fetches that form the stable core.
func (IdentityKind) Valid ¶
func (k IdentityKind) Valid() bool
type LLMSTxt ¶
type LLMSTxt struct {
Present bool `json:"present"`
URL string `json:"url"`
Status int `json:"status"`
Links []LLMSLink `json:"links"`
}
LLMSTxt is the /llms.txt probe. Presence is HTTP 200 with a body.
type LinkTargetChange ¶
type LinkTargetChange struct {
LeftIndex int `json:"left_index"`
RightIndex int `json:"right_index"`
Text string `json:"text"`
LeftTarget string `json:"left_target"`
RightTarget string `json:"right_target"`
Evidence string `json:"evidence"`
}
LinkTargetChange is a matched link whose visible text is the same and whose href changed. It is not a content difference.
type Observation ¶
type Observation struct {
RunID string
Domain string
PageURL string
FinalURL string
Identity IdentityKind
Outcome Outcome
StatusCode int
Headers http.Header
ContentType string
ByteSize int64
TokenCount int
TokenCountReported int
TokenCountHeader string
Challenge []ChallengeSignal
Cache CacheState
Platform Platform
Blocks []Block
Duration time.Duration
Err string
FetchedAt time.Time
}
Observation is one identity's stored result. The raw body is kept in a BodyStore, not here, so the metrics database stays portable. TokenCount is our estimate. TokenCountReported is the converter header, if any. Neither is authoritative.
type Outcome ¶
type Outcome string
Outcome is the result of one identity's attempt. A completed HTTP exchange is not the same as a successful measurement.
const ( // OutcomeServed: the origin returned a representation of the // requested resource, and extraction produced at least one // block. Not a challenge, 429, or 401/403. OutcomeServed Outcome = "served" // OutcomeEmpty: the origin returned a finished exchange that // classified as served, but extraction produced zero blocks. // Distinct from served. Not a successful measurement. OutcomeEmpty Outcome = "empty" // OutcomeRateLimited: final status 429 after retries. OutcomeRateLimited Outcome = "rate_limited" // OutcomeBlocked: origin refused with 401/403 and no challenge // signal. Distinct from robots disallowed. OutcomeBlocked Outcome = "blocked" // OutcomeChallenged: interstitial or verification page. Often HTTP // 200. Detected from strong response signals, never from content // compare. OutcomeChallenged Outcome = "challenged" // OutcomeUncertain: a body marker is present but the surrounding // evidence is not enough to call the page a challenge. The // response is still measured. The outcome is a qualifier. OutcomeUncertain Outcome = "uncertain" // OutcomeRedirectLoop: redirect hop limit hit. OutcomeRedirectLoop Outcome = "redirect_loop" // OutcomeTransport: network failure, timeout, or 5xx after retries. OutcomeTransport Outcome = "transport" // (no Chrome, no signer, no agent origin). No request was sent. OutcomeUnavailable Outcome = "unavailable" // OutcomeDisallowed: robots.txt forbids this identity. No request // was sent. OutcomeDisallowed Outcome = "disallowed" )
func AllOutcomes ¶
func AllOutcomes() []Outcome
AllOutcomes is the stable print order for aggregate breakdowns.
func ClassifyHTTP ¶
ClassifyHTTP maps a finished HTTP exchange to an outcome. redirectLoop is true when the hop limit stopped the client. 5xx after retries is OutcomeTransport. A strong challenge wins over 401/403. Weak body evidence is OutcomeUncertain.
type PageRole ¶
type PageRole string
PageRole is whether a stored page is the homepage or the interior content URL chosen from it.
type PageSelection ¶
type PageSelection struct {
Role PageRole `json:"role"`
Primary bool `json:"primary"`
Rule string `json:"rule,omitempty"`
Evidence string `json:"evidence,omitempty"`
Source string `json:"source_url,omitempty"`
Href string `json:"href,omitempty"`
}
PageSelection records how a target URL was chosen. It is audit evidence, not a score.
type Platform ¶
type Platform struct {
Class string `json:"class"` // cdn, edge, or origin
Name string `json:"name,omitempty"`
MarkdownEdge bool `json:"markdown_edge,omitempty"`
}
Platform is inferred from response headers only. No extra fetch.
func PlatformFromHeaders ¶
PlatformFromHeaders classifies CDN, edge platform, or origin server from headers already on the response.
type RequestSigner ¶
RequestSigner attaches web-bot-auth headers to req. Implemented by package signing without importing this package.
type RobotsRule ¶
type RobotsRule struct {
Identity IdentityKind `json:"identity"`
Allowed bool `json:"allowed"`
Agent string `json:"agent"`
Pattern string `json:"pattern,omitempty"`
CrawlDelay time.Duration `json:"crawl_delay,omitempty"`
}
RobotsRule is the robots.txt decision for one identity.
type Run ¶
type Run struct {
ID string
StartedAt time.Time
FinishedAt *time.Time
Status RunStatus
DomainList string
Settings RunSettings
HeartbeatAt *time.Time
HeartbeatPID int
StopReason string
LastHost string
}
Run is one benchmark execution. Resume reuses an unfinished row after the heartbeat says the previous process is gone.
type RunSettings ¶
type RunSettings struct {
DomainSHA256 string `json:"domain_sha256"`
KeyID string `json:"key_id,omitempty"`
AgentOrigin string `json:"agent_origin,omitempty"`
Concurrency int `json:"concurrency"`
Delay string `json:"delay"`
Timeout string `json:"timeout"`
Retries int `json:"retries"`
Redirects int `json:"redirects"`
}
RunSettings is the parameter set that produced a run. Resume refuses to continue when these do not match the stored row.
func ParseRunSettings ¶
func ParseRunSettings(raw string) RunSettings
func (RunSettings) Empty ¶
func (s RunSettings) Empty() bool
func (RunSettings) Equal ¶
func (s RunSettings) Equal(o RunSettings) bool
func (RunSettings) Marshal ¶
func (s RunSettings) Marshal() string
func (RunSettings) Mismatch ¶
func (s RunSettings) Mismatch(o RunSettings) string
type Share ¶
type Share struct {
}
Share is one rate with its denominator. Rate is 0 when N is 0. ByPlatform breaks the same share down by Platform.Key. Not a grade.
type SiteContext ¶
type SiteContext struct {
Robots []RobotsRule `json:"robots"`
RobotsRaw []byte `json:"-"`
LLMSTxt LLMSTxt `json:"llms_txt"`
Alternates []Alternate `json:"alternates"`
Mismatch AlternateMismatch `json:"mismatch"`
Selection PageSelection `json:"selection"`
}
SiteContext explains fetch outcomes. It is never scored and must not be mixed into Comparison.
type SiteContextRecord ¶
type SiteContextRecord struct {
RunID string `json:"run_id"`
Domain string `json:"domain"`
PageURL string `json:"page_url"`
Context SiteContext `json:"context"`
}
SiteContextRecord is one persisted probe.
type Store ¶
type Store interface {
CreateRun(ctx context.Context, domainList string, settings RunSettings) (Run, error)
FinishRun(ctx context.Context, id string) error
StopRun(ctx context.Context, id, reason string) error
ResumeRun(ctx context.Context, id string, settings RunSettings) error
SetRunSettings(ctx context.Context, id string, settings RunSettings) error
TouchHeartbeat(ctx context.Context, id string, pid int, lastHost string) error
Run(ctx context.Context, id string) (Run, error)
LatestRunning(ctx context.Context) (Run, bool, error)
LatestUnfinished(ctx context.Context) (Run, bool, error)
SaveObservation(ctx context.Context, o Observation) error
HasObservation(ctx context.Context, runID, domain, pageURL string, id IdentityKind) (bool, error)
Observation(ctx context.Context, runID, domain, pageURL string, id IdentityKind) (Observation, bool, error)
ObservationsByRun(ctx context.Context, runID string) ([]Observation, error)
ObservationsByDomain(ctx context.Context, runID, domain string) ([]Observation, error)
SaveSiteContext(ctx context.Context, rec SiteContextRecord) error
SiteContext(ctx context.Context, runID, domain, pageURL string) (SiteContextRecord, bool, error)
SiteContextsByRun(ctx context.Context, runID string) ([]SiteContextRecord, error)
}
Store persists runs, observations, and site context. SaveObservation upserts on (run_id, domain, page_url, identity).