Skip to content
internal/staleness
On this page

Detecting descriptions that no longer describe what they sit on, by hashing the page's raw body, the docstrings its directives name and the CLI schema.

#internal/staleness

#internal/staleness

Package staleness detects descriptions that no longer describe what they sit on, by hashing what a description is about and comparing that hash against the one recorded the last time the description was written.

Three inputs are tracked per page: the page's own raw body, the module docstrings of the sources its directives reference, and -- for a CLI page -- the schema slice of the command it documents. When one of those changes and the page's frontmatter description does not, the description is reported stale (the body) or drifted (the docstrings, the schema).

#The baseline hold

A page reported stale or drifted keeps its ENTIRE previous entry, so the error persists until the description is actually rewritten. Advancing the baseline on the first report would make the second run pass with nothing fixed, which is how a stale description used to become permanent.

#Two writers, one store

The store is written by two independent passes: gen owns each page's seed_hash, while build and check own content, description, source_docstring and schema_hash. UpdateHashes therefore MERGES into an existing entry instead of replacing it, so neither writer erases the other's fields.

#The empty string stands for an absent hash

Every hash this package stores is a 64-character hex digest, so no real value is ever the empty string and the Python surface's None is carried as "" throughout: an absent source-docstring hash, an absent schema hash, and an absent stored field are all the empty string. The distinctions the Python drew with is None and not in are the same distinctions here.

#HashVersion

Go go
const HashVersion = 3

HashVersion is the version of the on-disk hash store's schema. It is bumped when the meaning of a stored hash changes, so an older store is discarded and re-baselined on load rather than compared against.

v1 -> v2: the content hash switched from the resolved output to the raw template body. v2 -> v3: added the per-page seed_hash (written by gen) and canonicalized directive-marker attribute values before content hashing.

#BaselineAcceptHintTemplate

Go go
const BaselineAcceptHintTemplate = "after reviewing the page against the changed {source}, run " +

BaselineAcceptHintTemplate is the remediation hint appended to every DRIFT001 message, with "{source}" and "{page}" standing for what changed and which page reports it. BaselineAcceptHint renders it.

A drift error means the source moved while the description did not -- which is sometimes correct: the description can still be accurate. Without this hint the operator is told a problem exists but not how to close it honestly, and the only discoverable way out is to invent a description edit.

#Entry

Go go
type Entry struct

Entry is one page's recorded hashes. Every field is a hex digest, and the empty string means the field was never recorded -- which the drift checks read as "nothing to compare against yet" rather than as a mismatch.

#Store

Go go
type Store map[string]Entry

Store is the hash store: one Entry per page, keyed by the page's path relative to the docs directory and prefixed with its locale when the project declares more than one.

#Doc

Go go
type Doc struct

Doc is one resolved docs page in the shape this package reads it: the parsed frontmatter and the RAW template body.

The raw body is what the content hash covers, so a directive whose output changed -- a version bump, a regenerated table -- does not read as a page edit. It is the same tuple the docs resolution produces, narrowed to the two members the hash computation uses.

#PageDirective

Go go
type PageDirective struct

PageDirective is one resolved directive as the drift measurement reads it: the path its "path" attribute named, and the source entry that answered it.

It is the narrowed form of the check pass's resolved-directive record -- the two members the source-docstring hash needs -- so this package does not depend on the command layer that produces them.

#SourceFile

Go go
type SourceFile struct

SourceFile is one source path paired with the extractor that reads it.

#Warning

Go go
type Warning struct

Warning is one staleness or drift report: the page it is about and the message the lint carries.

#BaselineAcceptHint

Go go
func BaselineAcceptHint(pagePath, source string) string

BaselineAcceptHint renders the DRIFT001 remediation hint for pagePath.

source names what changed ("docstrings", "CLI schema") so the operator knows what to review the description against.

#ComputeContentHash

Go go
func ComputeContentHash(body string) string

ComputeContentHash computes the SHA-256 hash of a page's raw body, with the frontmatter stripped and the directive marker lines canonicalized.

It receives the pre-resolution template content, so a change in what a directive emits -- a version bump, a regenerated table -- is not a content change. See canonicalizeDirectiveMarkers for what a marker line contributes.

#ComputeDescriptionHash

Go go
func ComputeDescriptionHash(description string) string

ComputeDescriptionHash computes the SHA-256 hash of a frontmatter description string.

#ExtractModuleDocstring

Go go
func ExtractModuleDocstring(path string, extractor extractors.Extractor) (string, error)

ExtractModuleDocstring extracts the module-level documentation at path through its extractor.

#ComputeSourceDocstringHash

Go go
func ComputeSourceDocstringHash(sourceFiles []SourceFile) (string, error)

ComputeSourceDocstringHash computes the SHA-256 hash of the module docstrings of sourceFiles, joined with newlines in path order.

It returns "" when not one of the files carries any documentation: there is nothing to compare a description against, so drift is not measurable for that page.

#ComputeSchemaHash

Go go
func ComputeSchemaHash(schemaSlice any) (string, error)

ComputeSchemaHash computes the SHA-256 hash of a CLI command's schema slice.

The slice is serialized as canonical JSON with sorted keys, so the hash covers every field the schema carries for that one command or group -- its help text, its flags, its arguments and anything a later strictcli adds -- and is stable across runs.

#CheckStaleness

Go go
func CheckStaleness(pagePath, contentHash, descriptionHash string, stored Store) string

CheckStaleness reports whether a page's description is stale: its content changed while its description stayed the same.

It returns "" for a new page, for unchanged content, and for a description that was rewritten alongside the content.

#CheckDrift

Go go
func CheckDrift(pagePath, sourceDocstringHash, descriptionHash string, stored Store) string

CheckDrift reports whether a page's description drifted from the module docstrings of the sources it documents.

It returns "" when the page has no source docstrings, is new, has no recorded docstring hash yet, has unchanged sources, or had its description rewritten alongside them.

#CheckSchemaDrift

Go go
func CheckSchemaDrift(pagePath, schemaHash, descriptionHash string, stored Store) string

CheckSchemaDrift reports whether a CLI page's description drifted from the schema slice of the command it documents.

It returns "" when the page has no schema hash, is new, has no recorded schema hash yet, has an unchanged schema, or had its description rewritten alongside the schema.

#HashesPath

Go go
func HashesPath(baseDir string) string

HashesPath is where the store file sits under a project root.

#LoadHashes

Go go
func LoadHashes(baseDir string) (Store, error)

LoadHashes loads the hash store from .selfdoc/hashes/hashes.json.

It returns an empty store when the file does not exist, and discards an older store wholesale -- nothing a v1 or v2 file holds is reusable under v3's rules, so the project re-baselines rather than comparing against hashes that meant something else.

#SaveHashes

Go go
func SaveHashes(hashes Store, baseDir string, handle *effects.Handle) error

SaveHashes writes the hash store to .selfdoc/hashes/hashes.json, creating the directory when it is missing and stamping the current HashVersion.

The write is atomic, so an interrupted run can never leave a truncated store -- which would re-baseline every page on the next run.

#ComputeCurrentHashes

Go go
func ComputeCurrentHashes(

ComputeCurrentHashes computes the full current hash entry for every documentation page.

Every page whose frontmatter declares a description gets a content and a description hash; a page that declares none is skipped entirely, because there is no description to report stale. A page whose directives resolve to readable sources also gets a source-docstring hash, and a page named in schemaHashes gets its schema hash.

This is the baseline a page WOULD receive if it had no outstanding errors: nothing is read from or written to the store, and no comparison is made. Pass nil for pageDirectives or schemaHashes to leave that hash out.

#UpdateHashes

Go go
func UpdateHashes(

UpdateHashes computes the current hashes for every page, reports the pages whose descriptions are stale or drifted, and advances the baseline of every page that is not.

dryRun computes everything and writes nothing. Pass nil for pageDirectives to leave source-docstring drift unmeasured, and nil for schemaHashes to leave CLI schema drift unmeasured.

skeletonPages names the pages that are both generated and machine-seeded, keyed as allDocs is. Those pages cannot be hand-fixed, so a persistent staleness or drift report would hold their baseline in error forever and never advance: they are exempt, their baselines always advance, and neither warning list ever mentions them. Pass an empty set where no exemption applies -- the build and gen paths, which regenerate content and description together. There is deliberately no default, because a caller that has not decided has not thought about the deadlock.

A page reported stale or drifted keeps its ENTIRE previous entry, so the error persists until the description is rewritten. Every other page's freshly computed fields are MERGED into its existing entry, so the gen-owned seed_hash survives a check pass and vice versa.

#StripFrontmatter

Go go
func StripFrontmatter(content string) string

StripFrontmatter returns content with its frontmatter block removed.

This is the staleness pass's own reading of a frontmatter fence, kept separate from the frontmatter parser because it answers a narrower question: where the body begins. A document that opens with "---" and never closes the fence is returned whole.

Search