canaryscan

package
v0.3.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 21, 2026 License: Apache-2.0 Imports: 35 Imported by: 0

Documentation

Overview

CANARY: REQ=CP-268; FEATURE="MermaidRefs"; ASPECT=Engine; STATUS=TESTED; TEST=TestCANARY_CBIN_202_ExtractDiagramRefs,TestCANARY_CBIN_202_ScanDiagramRefsSkipsOversizedFile; UPDATED=2026-08-31

CANARY: REQ=ENG-4325; FEATURE="MigrateNotes"; ASPECT=Engine; STATUS=TESTED; TEST=TestCANARY_CBIN_301_ExtractMigrateNotes; UPDATED=2026-08-31

Instrumentation for the scan pipeline: one root span per RunContext call, a handful of named child spans around its major phases, and a small set of scan-level metrics. The tracer and meter come from the OpenTelemetry API only (go.opentelemetry.io/otel) -- both are no-ops until internal/telemetry.Setup installs real SDK providers (CANARY_OTEL=1), so this file never allocates an exporter or talks to a network itself.

Deliberately absent: any per-file or per-token span or metric attribute. A tree can carry thousands of files and tens of thousands of tokens; a span or attribute per one of those would explode cardinality on export. canary.scan.files/.tokens/.issues are therefore accumulated locally in the walk and recorded once per Scan/scanContext call.

CANARY: REQ=CP-277; FEATURE="StalenessConfig"; ASPECT=Engine; STATUS=TESTED; TEST=TestCANARY_CBIN_304_StaleDaysFromConfig,TestCANARY_CBIN_304_UpdateStaleReportsEvidenceCurrency,TestCANARY_CBIN_304_UpdateStaleMutatesNothing; UPDATED=2026-08-30

Index

Constants

View Source
const (
	ExclusionIgnored    = "ignored"
	ExclusionPathPolicy = "path_policy"
	ExclusionExtension  = "extension_filter"
)
View Source
const (
	MaxFileBytes  = 16 << 20 // 16 MiB per file
	MaxLineBytes  = 1 << 20  // 1 MiB per logical line
	MaxFieldBytes = 1024     // per token field value, UTF-8 bytes
	// ParserSchemaVersion identifies the token grammar/semantics. Bumped to
	// 2 when STATUS became a pure declaration (no lexical promotion).
	ParserSchemaVersion = 2
	// TokenSchemaVersion seeds SnapshotSeed (records.go): bump it whenever
	// the snapshot's parse semantics change, so a stored ScanDigest computed
	// under the old schema reads as stale even if project.yaml, .canaryignore,
	// and every token file are byte-identical.
	// Version 4 rejects unsupported JSDoc declaration markers instead of
	// silently treating their source as an empty successful scan.
	TokenSchemaVersion = 4
)

Scanner and token-grammar limits. These bound what a single scan will read and what a single token field may hold, so a hostile or accidental input (a multi-gigabyte artifact, a minified one-line bundle) cannot exhaust memory or wedge a scan.

View Source
const (
	IssueBinary            = "binary"
	IssueFileTooLarge      = "file_too_large"
	IssueLineTooLarge      = "line_too_large"
	IssueReadError         = "read_error"
	IssueParseError        = "parse_error"
	IssueUnsupportedFormat = "unsupported_format"
	IssueStatusConflict    = "status_conflict"
	IssueSubscanError      = "subscan_error"
)

Scan issue reasons.

View Source
const DeclaredLinksSchemaVersion = 1

DeclaredLinksSchemaVersion identifies list splitting and target normalization. These links carry syntax, not resolved entities or dependency satisfaction.

View Source
const DefaultStaleDays = config.DefaultStaleDays

DefaultStaleDays is the fallback staleness window (in days) when neither Config.StaleDays nor .canary/project.yaml's verification.staleness_days is set. It is pkg/config's constant under canaryscan's historical name.

View Source
const EvidenceFile = ".canary/evidence.json"

EvidenceFile is the evidence store's path relative to a project root.

View Source
const SourceSpanVersion = 1

SourceSpanVersion identifies the declaration-line interval convention.

Variables

View Source
var ErrScanBudgetExceeded = errors.New("canonical record scan budget exceeded")

ErrScanBudgetExceeded means no complete record projection was produced.

View Source
var ErrTokenMoved = errors.New("token not found at recorded location; run 'canary index'")

ErrTokenMoved reports that the recorded file:line no longer holds the token identity expects -- the source moved, changed, or was deleted since the caller's row was last read. The caller has no safe edit to make; it must tell the operator to reindex.

View Source
var ErrUnsupportedFormat = errors.New("JSDoc declaration markers are unsupported; use a single-line CANARY comment")

ErrUnsupportedFormat identifies a recognized declaration marker whose comment form is intentionally unsupported by the single-line grammar.

View Source
var UndeclaredKey = evidence.FeatureKey{Feature: "*", Aspect: "*"}

UndeclaredKey is the stand-in feature/aspect used for a claimed requirement that declares no tokens at all. Such a claim can never be satisfied by evidence — there is nothing for evidence to attest to — so it is reported as one missing entry rather than passing vacuously.

Functions

func AnnotateSources

func AnnotateSources(rep *Report, reg *sources.Registry)

AnnotateSources stamps each requirement with its source name and ticket URL.

func Claims added in v0.3.3

func Claims(gapPath string, reg *sources.Registry) ([]string, error)

Claims returns the requirement IDs claimed in gapPath, in first-seen order with duplicates removed. The claim grammar is reg.ClaimPattern(): "✅ <KEY>-<digits>"; a nil registry means the default (CBIN).

func ClaimsDigest added in v0.3.7

func ClaimsDigest(verified []string) string

ClaimsDigest returns "sha256:" + hex(sha256(canonical JSON of the sorted verified list)) -- a content digest of the actual claims a VerificationReceipt accompanies. verified is sorted before hashing so the digest is independent of slice order (VerifiedRequirements' own output order is a presentation detail, not part of the claims' identity).

Binding the receipt's signature to this digest (see SignReceipt) closes a gap a signature over the receipt's scalar fields alone would leave open: without it, a workspace adversary could keep a genuinely-signed receipt (same project/commit/policy/timestamp) but swap in a DIFFERENT "verified" array underneath it -- the signature would still check out, having never covered the list it is supposed to vouch for.

func ClaimsReader added in v0.3.9

func ClaimsReader(reader io.Reader, reg *sources.Registry) ([]string, error)

ClaimsReader parses an already-authorized stream using the canonical grammar. Reads are bounded to the canonical maximum source-file size.

func DeclaredFeatures added in v0.3.3

func DeclaredFeatures(rep Report, reqID string) []evidence.FeatureKey

DeclaredFeatures returns rep's declared feature/aspect keys for reqID, deduplicated. Two tokens differing only in OWNER or UPDATED describe one thing to prove, so the key is required once, not twice. An id with no declarations returns nil.

func DefaultSkipRegex

func DefaultSkipRegex() *regexp.Regexp

DefaultSkipRegex returns the default skip path regex.

func ExtractTokenRecordsContext added in v0.3.9

func ExtractTokenRecordsContext(ctx context.Context, path string, source []byte, expectedSHA256 string, reg *sources.Registry) ([]TokenRecord, []ScanIssue, error)

ExtractTokenRecordsContext parses the exact caller-owned source snapshot, never reopening path or walking a repository. The caller owns authorization, eligibility/ignore policy, registry snapshot and generation binding, and must not mutate source during this call. expectedSHA256 is the lowercase hex digest of the complete snapshot. A mismatch or incomplete read returns no records. Parse issues also return no records, so partial facts cannot masquerade as a complete extraction. The same located reader and grammar serve filesystem scans.

func GitMetadataContext added in v0.3.9

func GitMetadataContext(ctx context.Context, root string) (commit, branch string, err error)

GitMetadataContext observes HEAD and branch in one bounded Git invocation. Detached HEAD is reported as branch "HEAD", matching rev-parse semantics. This observation does not pin the repository against later changes.

func HeadCommit added in v0.3.3

func HeadCommit(root string) (string, error)

HeadCommit returns root's current commit SHA. Evidence binds to a commit, so a repository whose HEAD cannot be read cannot be verified.

func HeadCommitContext added in v0.3.9

func HeadCommitContext(ctx context.Context, root string) (string, error)

HeadCommitContext reads the target-root commit with bounded, cancellable Git execution and without ambient repository/config selection.

func LoadCanaryIgnore

func LoadCanaryIgnore(root string) (*ignore.GitIgnore, error)

LoadCanaryIgnore loads .canaryignore from root. Returns nil if file missing.

func RefTimeFromEnv

func RefTimeFromEnv() time.Time

RefTimeFromEnv returns time from CANARY_TEST_TIMESTAMP (RFC3339) for tests; zero if unset or invalid.

func ReportEvidenceCurrency added in v0.3.3

func ReportEvidenceCurrency(rep Report, staleDiags []string, recs []evidence.Record, projectID, commit string) []string

ReportEvidenceCurrency answers, for every requirement named by staleDiags, whether it has passing evidence at the current commit.

This replaces the old --update-stale behavior, which rewrote UPDATED= dates in source. Rewriting a date made a stale claim *look* fresh without any new proof — the exact failure mode evidence-backed verification exists to prevent. Nothing is mutated here: the report is the whole output, one line per requirement, sorted by ID:

CANARY_UPDATE_STALE req=<id> evidence=current
CANARY_UPDATE_STALE req=<id> evidence=missing

"current" means every feature/aspect the requirement declares has a PASS record for this project at this commit; anything less is "missing".

func RequiredFeatures added in v0.3.3

func RequiredFeatures(rep Report, claimed []string) map[string][]evidence.FeatureKey

RequiredFeatures maps each claimed requirement to the feature/aspect pairs it declares in rep. A claimed requirement rep knows nothing about maps to a single UndeclaredKey entry so it is still checked (and fails), never silently skipped.

func RewriteTokenFields added in v0.3.4

func RewriteTokenFields(root, filePath string, lineNumber int, identity, set map[string]string) error

RewriteTokenFields rewrites the CANARY token on one line of one file, preserving the line's comment prefix and terminator. identity keys (e.g. {"REQ": "CBIN-101"} or {"BUG": "BUG-API-001"}) are verified against the parsed token before any edit; a mismatch (moved/stale line) returns ErrTokenMoved. set replaces existing fields' values or appends new fields before the terminator. The result is round-trip validated with ParseTokenLine and written via safewrite (Force, Backup).

func RewriteTokensAtomic added in v0.3.5

func RewriteTokensAtomic(root string, ops []RewriteOp) (func() error, error)

RewriteTokensAtomic stages every op (read the file, compute its rewritten bytes via computeRewrite) before writing anything. If staging any op fails -- a missing file, a moved/stale token line -- nothing is written to disk at all: a `bug update` or `prioritize` touching several files must never leave some rewritten and others untouched because a later op in the batch turned out to be invalid.

If a write partway through the batch fails, writes 0..k-1 are rolled back (restored from their pre-images) before the error is returned, so a mid-batch I/O failure never leaves a partially-rewritten tree either.

On success it returns a restore func the caller can invoke if a LATER operation -- typically the corresponding database write -- fails, undoing every file this call wrote and putting the tree back exactly as it found it.

func Run

func Run(cfg Config, stdout, stderr io.Writer) (exitCode int)

Run runs the full scan pipeline: load config, scan, optional update-stale, write outputs, --strict. Verification is not a scan concern: `scan --verify` delegates wholly to the dedicated verifier (pkg/cmds/verify), never to this function. It writes to stdout/stderr via the given writers (use os.Stdout, os.Stderr from CLI). Returns exit code: 0 success, 2 --strict failure, 3 parse/IO error.

Run delegates to RunContext with context.Background(); callers that have a context available (e.g. a cobra command) should call RunContext directly so cancellation and future tracing spans thread through the scan.

func RunContext added in v0.3.8

func RunContext(ctx context.Context, cfg Config, stdout, stderr io.Writer) (exitCode int)

RunContext is Run's context-aware form: identical behavior, but ctx is threaded into the scan's directory walk so a caller can cancel a long scan early. An already-cancelled (or later-cancelled) ctx aborts the walk and returns a non-zero exit code without writing the stdout summary line -- stdout must never carry a partial answer, only a complete one or none.

func RunFromArgs

func RunFromArgs(root, out, csv, skipExpr string, strict, updateStale, projectOnly bool, staleDays int) int

RunFromArgs is a convenience that builds Config from flag-like args and runs with os.Stdout/os.Stderr. Used by tools/canary main. skipExpr is the regex string; if empty, default is used. staleDays overrides the staleness window in days; 0 means "use config/default". Verification is not a scan concern: tools/canary's --verify spelling delegates directly to pkg/cmds/verify.Run rather than passing through here.

func ScanDiagramRefs

func ScanDiagramRefs(root string, skip *regexp.Regexp, reg *sources.Registry, ignorePatterns *ignore.GitIgnore) ([]DiagramRef, []ScanIssue, error)

ScanDiagramRefs walks root for .md/.markdown/.mmd files and extracts all requirement-ID mentions inside mermaid diagrams. Paths in the result are root-relative with forward slashes. ignorePatterns (from LoadCanaryIgnore) is honored the same way Scan honors it: relative path matched first, dirs skipped via SkipDir; nil means no .canaryignore patterns apply.

A file that cannot be read is reported as a ScanIssue rather than silently skipped, so callers can tell an empty result from an incomplete one.

func ScanDiagramRefsContext added in v0.3.9

func ScanDiagramRefsContext(ctx context.Context, root string, skip *regexp.Regexp, reg *sources.Registry, ignorePatterns *ignore.GitIgnore) ([]DiagramRef, []ScanIssue, error)

ScanDiagramRefsContext checks cancellation before each file and before publishing results.

func ScanDigest added in v0.3.3

func ScanDigest(seed string, files []FileDigest) string

ScanDigest folds a seed and a file-digest set into one digest: the seed line first, then the sorted "path:hash" lines. Two scans of the same tree under the same seed produce the same value; any change to any indexed file, or to the seed itself (parser schema, project.yaml, .canaryignore -- see SnapshotSeed), changes it.

V4-04/P0.3: before the seed existed, this digest bound only token-file bytes -- editing .canary/project.yaml (id_pattern/source registry semantics), .canaryignore, or the parser's own schema left the digest unchanged, so a "fresh" index could silently disagree with how its tokens would now be parsed. Every caller must pass the same seed the index was built with (SnapshotSeed(root)); see ComputeScanDigest in pkg/cmds/index/index.go and its use in `canary next`'s freshness check.

func ScanMigrateNotes added in v0.3.0

func ScanMigrateNotes(root string, skip *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry) ([]MigrateNote, []ScanIssue, error)

ScanMigrateNotes walks root and extracts CANARY:MIGRATE guidance notes from every non-binary file the scanner would otherwise read. Paths in the result are root-relative with forward slashes. ignorePatterns (from LoadCanaryIgnore) is honored the same way Scan honors it: relative path matched first, dirs skipped via SkipDir; nil means no .canaryignore patterns apply. Files larger than 1MB produce an explicit incomplete-scan issue.

A file that cannot be read is reported as a ScanIssue rather than silently skipped, so callers can tell an empty result from an incomplete one.

func ScanMigrateNotesContext added in v0.3.9

func ScanMigrateNotesContext(ctx context.Context, root string, skip *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry) ([]MigrateNote, []ScanIssue, error)

ScanMigrateNotesContext checks cancellation before each file and before publishing results.

func ScanSummaryLine

func ScanSummaryLine(rep Report) string

ScanSummaryLine returns a single parseable line for rep so agents get metrics without reading status.json.

func ScanTokenRecords added in v0.3.3

func ScanTokenRecords(root string, skip *regexp.Regexp, projectFilter *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry) ([]TokenRecord, []FileDigest, []ScanIssue, error)

ScanTokenRecords walks root and returns every CANARY token it finds, one record per token, alongside the digest of every file that contributed at least one token and the issues the scan hit. skip, projectFilter, ignorePatterns and reg may be nil.

Issues are returned rather than swallowed: a caller that is building an index (as opposed to printing a report) can refuse to commit anything when the tree contains a token the parser rejects.

func ScanTokenRecordsContext added in v0.3.9

func ScanTokenRecordsContext(ctx context.Context, root string, skip *regexp.Regexp, projectFilter *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry) ([]TokenRecord, []FileDigest, []ScanIssue, error)

ScanTokenRecordsContext checks cancellation at each bounded file read and before publishing records. Cancelled scans never return a complete projection.

func ScanTokenRecordsWithLimitsContext added in v0.3.9

func ScanTokenRecordsWithLimitsContext(ctx context.Context, root string, skip *regexp.Regexp, projectFilter *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry, limits RecordScanLimits) ([]TokenRecord, []FileDigest, []ScanIssue, error)

ScanTokenRecordsWithLimitsContext permits smaller per-call budgets. Overflow returns ErrScanBudgetExceeded and no records, digests or partial diagnostics.

func SerializeToken added in v0.3.3

func SerializeToken(fields []Field) (string, error)

SerializeToken renders canonical form: `// CANARY: K=V; K="quoted"`. Values that could otherwise be misread are quoted with `\`→`\\` and `"`→`\"` escapes; `;` is legal only inside quotes. Control characters (C0 and DEL), oversized values, unknown ASPECT/STATUS enum members and malformed REQ ids are refused.

func SignReceipt added in v0.3.7

func SignReceipt(priv ed25519.PrivateKey, r VerificationReceipt) (producerIdentity, signature string, err error)

CANARY: REQ=ENG-3961; FEATURE="SignedPeerReceipt"; ASPECT=Security; STATUS=TESTED; TEST=TestSignReceiptRoundTrip,TestVerifyReceiptTamperedFails,TestVerifyReceiptWrongKeyFails,TestClaimsDigestOrderIndependent,TestVerifyReceiptMalformedSignatureFails,TestSignReceiptHasDomainPrefix,TestReceiptDomainPrefixDistinctFromAttestationPrefix; UPDATED=2026-09-01 SignReceipt signs r's signable fields (ProjectID, CommitSHA, PolicyHash, Degraded, GeneratedAt, SourceDigest, ClaimsDigest -- NOT ProducerIdentity or Signature themselves, which this call is computing) with priv, and returns the producer identity (attest.Fingerprint of priv's public half) and the base64 signature ready to stamp onto VerificationReceipt.ProducerIdentity / .Signature.

func SkipMatch added in v0.3.5

func SkipMatch(skip *regexp.Regexp, path string) bool

SkipMatch reports whether path matches skip, a slash-anchored pattern. path is normalized with filepath.ToSlash first so an OS-native separator (Windows' backslash) can never let a directory the pattern was written to exclude slip through unmatched. nil-safe: a nil skip regexp never matches.

This is the single canonical skip check for every scanner in this repository (records.go, mermaid.go, migrate_notes.go, pkg/cmds/onboard, pkg/upgrade) — no caller should call skip.MatchString directly on an OS-native path.

func SnapshotSeed added in v0.3.5

func SnapshotSeed(root string) string

SnapshotSeed binds a scan digest to everything besides token-file bytes that determines how those bytes are interpreted: the parser schema version, the registry/config semantics in <root>/.canary/project.yaml (id_pattern, source definitions), and the exclusion rules in <root>/.canaryignore. Two roots with byte-identical token files but different project.yaml or .canaryignore content -- or scanned by two binaries with different TokenSchemaVersion -- must never produce the same seed, or a "fresh" index could describe a tree it no longer accurately parses (V4-04/P0.3).

A missing file contributes the literal "absent"; a read error other than not-exist contributes the literal "unreadable" rather than failing here -- the index refuses to build on an unreadable .canaryignore elsewhere (LoadCanaryIgnore), so this function stays total and deterministic and lets that caller report the real error.

func SnapshotSeedContext added in v0.3.9

func SnapshotSeedContext(ctx context.Context, root string) (string, error)

SnapshotSeedContext is compatible with SnapshotSeed on valid inputs while refusing unreadable, oversized, nonregular or out-of-root policy files.

func Stale

func Stale(rep Report, maxAge time.Duration, refTime time.Time) []string

Stale returns diagnostics for TESTED/BENCHED tokens older than maxAge. If refTime is zero, time.Now().UTC() is used.

func StateSkipRegex added in v0.3.3

func StateSkipRegex() *regexp.Regexp

StateSkipRegex returns the default skip path regex extended with canary's own `.canary` state directory. See stateSkipPattern.

func ValidReqID added in v0.3.6

func ValidReqID(s string) bool

ValidReqID reports whether s is a well-formed requirement id: either the canonical single-segment shape ("CBIN-001") or a legacy padded multi-segment shape ("REQ-GQL-046", "BUG-API-001"). Exported so callers outside this package -- notably deps.BuildGraphIn's spec-directory ID extraction (C5-03) -- can recognize a full requirement id without duplicating or loosening these two regexes.

CANARY: REQ=CP-262; FEATURE="RequirementIDValidation"; ASPECT=API; STATUS=TESTED; TEST=TestValidReqID; UPDATED=2026-09-01

func ValidStatus added in v0.3.4

func ValidStatus(s string) bool

ValidStatus reports whether s is one of the token grammar's known STATUS enum members. Implementation-lifecycle statuses (STUB, IMPL, TESTED, BENCHED, ...) and bug-lifecycle statuses (OPEN, IN_PROGRESS, FIXED, ...) share one set, matching what SerializeToken itself enforces on a STATUS field -- callers validate against this instead of guessing a subset so a value that this package would refuse to serialize is never accepted.

func VerifiedRequirements added in v0.3.3

func VerifiedRequirements(rep Report, recs []evidence.Record, projectID, commit string) []string

VerifiedRequirements returns, sorted, the IDs of every requirement in rep whose declared features all have passing evidence for projectID at commit. It asks pkg/evidence.Complete -- the single completion function -- once per requirement, so a peer's export and this project's own `canary verify` answer the same question the same way.

The result is always non-nil (an empty slice when nothing is verified) so a caller can hand it straight to Report.Verified, where nil carries the distinct meaning "not checked".

func VerifyReceipt added in v0.3.7

func VerifyReceipt(pub ed25519.PublicKey, r VerificationReceipt) bool

VerifyReceipt reports whether r.Signature is a valid ed25519 signature, by pub, over r's own signable fields. Any failure (malformed signature, wrong key, or fields that don't match what was actually signed -- the same tamper-evidence property attest.Verify gives Attestation) reports false.

func WalkSourceTreeContext added in v0.3.9

func WalkSourceTreeContext(ctx context.Context, root string, limit int, visit fs.WalkDirFunc) error

WalkSourceTreeContext shares the canonical bounded directory traversal with read-only source analysis. The callback owns scope filtering and file reads. Limits may reduce, but not exceed, the canonical entry budget.

func WorkingTreeClean added in v0.3.4

func WorkingTreeClean(root string) (clean bool, status string, err error)

WorkingTreeClean reports whether root's git working tree matches HEAD -- including untracked files, which `git diff --exit-code` would miss. status carries the porcelain lines when dirty, for diagnostics.

func WorkingTreeCleanContext added in v0.3.9

func WorkingTreeCleanContext(ctx context.Context, root string) (clean bool, status string, err error)

WorkingTreeCleanContext checks target-root status with cancellation and bounded diagnostics, ignoring ambient Git repository/config overrides.

func WriteCSV

func WriteCSV(path string, rep Report) error

WriteCSV writes rep to path as CSV, atomically, one row per (requirement, feature, file). A feature's tests and benches are joined with "|" inside single cells rather than zipped column-wise against the file list: the old zip paired unrelated files, tests, and benches by shared row index, asserting adjacencies that never existed. Every cell is passed through csvGuard so no value can be interpreted as a spreadsheet formula, and encoding/csv quotes any value containing a comma, quote, or newline so the output is decodable by an independent reader.

func WriteJSON

func WriteJSON(path string, rep Report) error

WriteJSON writes rep to path as JSON, atomically. The encode targets an in-memory buffer (which cannot short-write), and the file is committed by writeOut, whose error -- a failed create, write, or close -- is returned: a partial or unwritable report never passes silently.

Types

type Config

type Config struct {
	Root        string
	Out         string
	CSV         string
	Strict      bool
	SkipRegex   *regexp.Regexp
	UpdateStale bool
	ProjectOnly bool
	// StaleDays overrides the staleness window (in days) used by --strict and
	// --update-stale. When <= 0, the effective threshold falls back to
	// .canary/project.yaml's verification.staleness_days, then to 30 days.
	StaleDays int
}

Config holds scanner run options. Used by both the standalone tools/canary binary and the canary scan CLI subcommand so scan works from any CWD.

type DeclaredLinks struct {
	Version   int      `json:"version" yaml:"version"`
	Tests     []string `json:"tests,omitempty" yaml:"tests,omitempty"`
	Benches   []string `json:"benches,omitempty" yaml:"benches,omitempty"`
	DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
	Blocks    []string `json:"blocks,omitempty" yaml:"blocks,omitempty"`
	RelatedTo []string `json:"related_to,omitempty" yaml:"related_to,omitempty"`
}

DeclaredLinks preserves the distinct kinds of references in one source token. Test/benchmark names do not prove execution; requirement targets need not exist.

type DiagramRef

type DiagramRef struct {
	ReqID string
	File  string
	Line  int // 1-based
}

DiagramRef records one requirement-ID mention inside a mermaid diagram.

func ExtractDiagramRefs

func ExtractDiagramRefs(relPath, content string, reg *sources.Registry) []DiagramRef

ExtractDiagramRefs finds requirement IDs inside ```mermaid fenced blocks. relPath ending in .mmd is treated as a whole-file mermaid diagram (no fences required). IDs are normalized through reg (flatfile padding); reg nil means sources.Default() (the legacy CBIN series). This compatibility helper does not impose scan budgets on caller-owned text; repository callers should use ScanDiagramRefsContext for bounded extraction.

type Feature

type Feature struct {
	Feature string   `json:"feature"`
	Aspect  string   `json:"aspect"`
	Status  string   `json:"status"`
	Files   []string `json:"files"`
	Tests   []string `json:"tests"`
	Benches []string `json:"benches"`
	Owner   string   `json:"owner,omitempty"`
	Updated string   `json:"updated"`
	// Priority carries the token's declared PRIORITY (1 = highest). It is a
	// pointer so "declared nothing" stays distinguishable from "declared 0":
	// a consumer that needs a number applies its own default (5, the one
	// `canary index` uses) only when this is nil, rather than inventing one
	// for a token that did declare a value. An unparsable PRIORITY reads as
	// absent, matching `canary index`'s leniency.
	//
	// Additive: absent from a token means absent here.
	Priority *int `json:"priority,omitempty"`
	// DependsOn carries the token's DEPENDS_ON requirement IDs verbatim, so
	// verification can resolve external/peer dependency state without
	// re-parsing the tree. Additive: absent from a token means absent here.
	DependsOn []string `json:"depends_on,omitempty"`
}

Feature is one feature/aspect/status aggregation.

type Field added in v0.3.3

type Field struct{ Key, Value string }

Field is one key/value pair of a CANARY token, in source order. Values are held decoded: quoting and escaping belong to the wire form only.

func ParseTokenLine added in v0.3.3

func ParseTokenLine(line string) (fields []Field, ok bool, err error)

ParseTokenLine parses one line; ok=false when the line holds no CANARY token. CANARY:MIGRATE guidance lines are free text, not KV tokens, and so report ok=false as well. A malformed or unsupported token returns ok=true with err set. Unsupported JSDoc markers return ErrUnsupportedFormat.

type FileDigest added in v0.3.3

type FileDigest struct {
	Path string
	Hash string
}

FileDigest pairs a scanned file with the digest of its contents.

type MigrateNote added in v0.3.0

type MigrateNote struct {
	File   string   `json:"file"`
	Line   int      `json:"line"` // 1-based
	Text   string   `json:"text"`
	ReqIDs []string `json:"req_ids,omitempty"` // registry-pattern matches inside Text, normalized
}

MigrateNote is one CANARY:MIGRATE guidance comment.

func ExtractMigrateNotes added in v0.3.0

func ExtractMigrateNotes(relPath, content string, reg *sources.Registry) []MigrateNote

ExtractMigrateNotes finds CANARY:MIGRATE guidance comments in content. relPath is recorded verbatim on each note. reg nil means sources.Default() (the legacy CBIN series). A MIGRATE line is free text, not a KV token, and is never fed to parseKV — this is what keeps a MIGRATE line from ever aborting a scan. This compatibility helper does not impose scan budgets on caller-owned text; repository callers should use ScanMigrateNotesContext for bounded extraction.

type PolicySnapshot added in v0.3.9

type PolicySnapshot struct {
	// Config is parsed from the same captured bytes as Registry and Fingerprint.
	// Callers must treat the snapshot fields as read-only observations.
	Config      *config.ProjectConfig
	Registry    *sources.Registry
	Ignore      *ignore.GitIgnore
	Fingerprint string
}

PolicySnapshot binds parsed registry/ignore rules to the exact bounded bytes used to parse them. It does not imply source-scan completeness.

func LoadPolicyContext added in v0.3.9

func LoadPolicyContext(ctx context.Context, root string) (*PolicySnapshot, error)

LoadPolicyContext reads only root-confined regular policy files. Parsing and fingerprinting share the same bytes; read failures cannot become a valid seed.

func (*PolicySnapshot) ValidateContext added in v0.3.9

func (p *PolicySnapshot) ValidateContext(ctx context.Context, root string) error

ValidateContext refuses policy changes observed after snapshot capture.

type RecordScanLimits added in v0.3.9

type RecordScanLimits struct {
	Entries      int
	Records      int
	Issues       int
	PayloadBytes int
}

RecordScanLimits bounds discovered entries and retained record/diagnostic payload. PayloadBytes counts each record/issue's JSON bytes, not process RSS. Per-file buffers and filesystem syscalls have separate limits/limitations.

func DefaultRecordScanLimits added in v0.3.9

func DefaultRecordScanLimits() RecordScanLimits

DefaultRecordScanLimits returns independent per-invocation limits.

type RecordScanOptions added in v0.3.13

type RecordScanOptions struct {
	Limits     RecordScanLimits
	Extensions []string
}

RecordScanOptions selects explicitly requested text-file scope and budgets. Nil/empty Extensions accepts all file extensions; nonempty entries narrow the scope using case-insensitive filepath.Ext matching. This is not a language support whitelist: the canonical declaration grammar applies to eligible text. Zero Limits uses DefaultRecordScanLimits.

type RecordScanReport added in v0.3.13

type RecordScanReport struct {
	Records    []TokenRecord   `json:"records"`
	Files      []FileDigest    `json:"files"`
	Issues     []ScanIssue     `json:"issues"`
	Exclusions []ScanExclusion `json:"exclusions"`
}

RecordScanReport separates incomplete reads/parses from intentional exclusions. Issues must still be checked before treating the requested scope as complete.

func ScanTokenRecordsReportContext added in v0.3.13

func ScanTokenRecordsReportContext(ctx context.Context, root string, skip *regexp.Regexp, projectFilter *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry, options RecordScanOptions) (*RecordScanReport, error)

ScanTokenRecordsReportContext uses the same traversal and parser as the legacy record APIs, retaining ignore/path-policy decisions. Exclusions and issues share the Issues and PayloadBytes budgets. Budget overflow or cancellation returns no report; callers cannot mistake truncated diagnostics for success.

type Report

type Report struct {
	GeneratedAt    string        `json:"generated_at"`
	Requirements   []Requirement `json:"requirements"`
	Summary        Summary       `json:"summary"`
	MigrationNotes []MigrateNote `json:"migration_notes,omitempty"`
	Issues         []ScanIssue   `json:"issues,omitempty"`
	// Verified lists, sorted, the requirement IDs every one of whose declared
	// features had passing evidence at the scanned commit. It is the export a
	// peer project reads to answer "is this requirement done?" -- declarations
	// alone (STATUS=TESTED) never answer that.
	//
	// It is a pointer so absence is distinguishable from emptiness: nil means
	// this scan could not check evidence at all (no store, or no commit to
	// bind it to) and the key is omitted, which a reader must treat as
	// unknown. A non-nil empty slice is emitted as "verified": [] and means
	// the check ran and nothing passed.
	Verified *[]string `json:"verified,omitempty"`
	// Verification binds Verified to the policy and commit it was judged
	// under. It is set exactly when Verified is set (never one without the
	// other) so a peer can distinguish "this scan verified nothing" (a
	// present, empty Verified plus a present Verification) from "this scan
	// cannot be trusted at all" (Verification absent or Degraded) -- a bare
	// "verified" array carries no such binding on its own (C5-02).
	Verification *VerificationReceipt `json:"verification,omitempty"`
}

Report is the scan output (JSON/CSV shape). Fields are only ever added: status.json is a cross-package wire contract.

func Scan

func Scan(root string, skip *regexp.Regexp, projectFilter *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry) (Report, error)

Scan walks root and returns a Report. skip, projectFilter, ignorePatterns and reg may be nil. Files the scanner cannot fully process are recorded in Report.Issues and skipped; only walk errors abort.

Scan delegates to scanContext with context.Background(); a caller that has a context available should prefer ScanContext so a long walk can be cancelled.

func ScanContext added in v0.3.9

func ScanContext(ctx context.Context, root string, skip *regexp.Regexp, projectFilter *regexp.Regexp, ignorePatterns *ignore.GitIgnore, reg *sources.Registry) (Report, error)

ScanContext is Scan with invocation cancellation across walks, reads and bounded reference subscans. It does not set a hard filesystem syscall deadline.

type Requirement

type Requirement struct {
	ID        string    `json:"id"`
	Source    string    `json:"source,omitempty"`
	TicketURL string    `json:"ticket_url,omitempty"`
	Diagrams  []string  `json:"diagrams,omitempty"` // filled by Task 4
	Features  []Feature `json:"features"`
}

Requirement groups features by requirement ID.

type RewriteOp added in v0.3.5

type RewriteOp struct {
	FilePath   string
	LineNumber int
	Identity   map[string]string
	Set        map[string]string
}

RewriteOp describes one token rewrite for RewriteTokensAtomic. FilePath is index-relative and is joined under the call's root, matching RewriteTokenFields.

type ScanExclusion added in v0.3.13

type ScanExclusion struct {
	Path      string `json:"path"`
	Reason    string `json:"reason"`
	Directory bool   `json:"directory"`
}

ScanExclusion records an intentional policy decision, not a failed read. Path is root-relative. A directory entry represents its entire pruned subtree; descendants are not enumerated and their contents are not inspected.

type ScanIssue added in v0.3.3

type ScanIssue struct {
	Path   string `json:"path"`
	Reason string `json:"reason"` // One of the Issue constants below.
	Detail string `json:"detail,omitempty"`
}

ScanIssue records a file the scanner could not fully process. Issues never abort a scan: the file is skipped and the rest of the tree still scans.

func ReadSourceFileContext added in v0.3.9

func ReadSourceFileContext(ctx context.Context, path string, entry os.DirEntry, limit int64) ([]byte, *ScanIssue)

ReadSourceFileContext shares the canonical observed-file identity checks and bounded reads with read-only analysis. A returned issue means no usable bytes. The caller may reduce, but not exceed, the canonical file-size budget.

type SourceSpan added in v0.3.9

type SourceSpan struct {
	Version   int `json:"version" yaml:"version"`
	StartByte int `json:"start_byte" yaml:"start_byte"`
	EndByte   int `json:"end_byte" yaml:"end_byte"`
}

SourceSpan is a zero-based, half-open byte interval in the exact hashed source. Offsets count UTF-8 bytes, not runes or editor columns. Version zero is unknown.

type Summary

type Summary struct {
	ByStatus           map[string]int `json:"by_status"`
	ByAspect           map[string]int `json:"by_aspect"`
	TotalTokens        int            `json:"total_tokens"`
	UniqueRequirements int            `json:"unique_requirements"`
}

Summary holds aggregate counts.

type TokenRecord added in v0.3.3

type TokenRecord struct {
	// SourceSpan selects the complete matched declaration line, including its
	// comment wrapper, indentation and optional BOM, but excluding CR/LF bytes.
	SourceSpan SourceSpan
	// File is the path the token was read from, relative to the scan root.
	File string
	// Line is the 1-based line the token sits on.
	Line int
	// Raw is the token body verbatim, without the "CANARY:" marker.
	Raw string
	// ReqID is the registry-normalized requirement ID.
	ReqID string
	// Fields holds every declared key, upper-cased, values decoded.
	Fields map[string]string
	// ContentHash is the hex SHA-256 of the whole file this token came from.
	ContentHash string
}

TokenRecord is one CANARY token exactly where it was found. Scan folds tokens into per-requirement aggregates, which is the right shape for a report and the wrong shape for an index: the index needs the line each token sits on, its verbatim text, and every field it declared. Both views come out of the same parser, so the index can never disagree with the scan about what a token says.

func (r TokenRecord) DeclaredLinks(reg *sources.Registry) DeclaredLinks

DeclaredLinks applies the canonical list grammar and source-ID normalization without changing the record's original fields or promoting its STATUS.

func (TokenRecord) Field added in v0.3.3

func (r TokenRecord) Field(key string) string

Field returns the named field, or "" when the token did not declare it. It never invents a value: an absent UPDATED reads as "" so a caller cannot mistake today's date for something the author wrote.

type VerificationReceipt added in v0.3.6

type VerificationReceipt struct {
	ProjectID  string `json:"project_id"`
	CommitSHA  string `json:"commit_sha"`
	PolicyHash string `json:"policy_hash"`
	// Degraded is true when the export was computed under a relaxed policy
	// (e.g. imported evidence allowed). The scan export never sets this --
	// it always uses evidence.StrictPolicy() -- but the field exists so a
	// future degraded producer, or a hand-authored receipt, can say so
	// honestly rather than by omission.
	Degraded        bool   `json:"degraded,omitempty"`
	GeneratedAt     string `json:"generated_at"`
	VerifierVersion string `json:"verifier_version,omitempty"`

	// ProducerIdentity, Signature, SourceDigest, and ClaimsDigest are Plan-8
	// Task 6 additions (C6-02 peer trust) -- add-only, like every other
	// field on this wire contract. They let `canary scan` cryptographically
	// sign its own export when config evidence.sign_key is set, and let a
	// peer configuring evidence.trusted_keys demand that signature (see
	// pkg/external.resolvePeer) instead of trusting a bare unsigned receipt
	// on the filesystem.
	//
	//   - ProducerIdentity is pkg/attest.Fingerprint of the signing key's
	//     public half: "sha256:" + hex(sha256(pub)).
	//   - Signature is base64(ed25519.Sign(priv, canonical-receipt-bytes)),
	//     covering ProjectID/CommitSHA/PolicyHash/Degraded/GeneratedAt/
	//     SourceDigest/ClaimsDigest -- see SignableFields.
	//   - SourceDigest is "sha256:"+hex(index.ComputeScanDigest(root)) at
	//     scan time -- binding the receipt to the exact source tree it was
	//     computed against, the same digest evidence.Record.SourceDigest
	//     uses.
	//   - ClaimsDigest is "sha256:"+hex(sha256(canonical verified-list
	//     bytes)): a digest of the actual `verified` array this receipt
	//     accompanies. Signing ONLY the receipt's own scalar fields (project/
	//     commit/policy/...) would let an adversary who can write the
	//     workspace swap in a DIFFERENT verified array under an otherwise-
	//     genuine signed receipt; ClaimsDigest binds the signature to the
	//     specific list it was computed for.
	//
	// All four are empty/omitted on an unsigned export (evidence.sign_key
	// not configured) -- the default, filesystem-trust posture this task
	// does not change.
	ProducerIdentity string `json:"producer_identity,omitempty"`
	Signature        string `json:"signature,omitempty"`
	SourceDigest     string `json:"source_digest,omitempty"`
	ClaimsDigest     string `json:"claims_digest,omitempty"`
}

VerificationReceipt binds a verified export to the state it judged: which project, at which commit, under which evidence policy, and whether that policy was strict or degraded. A peer consulting another project's status.json requires this receipt before trusting its Verified array at all -- a receiptless or degraded export resolves unknown, not satisfied (see pkg/external.resolvePeer).

Fields are only ever added: like Report, this is a cross-package wire contract.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL