triage

package
v0.13.2 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package triage reduces public GitHub issue bodies to validated, enumerated data before any model holding tools can act on them.

Zalaras/muster went public on 2026-09-10, so an issue body is attacker-controlled text. /triage used to read one straight into the main Claude session, which holds Bash and Edit, and wrote model-authored prose into TODO.md — a file every later /orchestrate, /plan-work and /triage --audit session reads with full tools. A payload did not have to fire during triage; it could lie dormant and fire in a more capable session later. That dormancy is the risk this package exists to remove.

The design rule is that nothing between GitHub and the TODO.md diff is a model with tools. Everything reaching this package is untrusted, including the snapshot JSON: an author may edit their own issue body forever, and nothing binds that JSON to anything musterd produced. The schema buys a strict parser, never trust.

Transform order is load-bearing

Sanitize applies six transforms in a fixed order. Each swap below is a real defect, and each has a test:

  1. reject bidi overrides
  2. truncate the input at a rune boundary
  3. strip image then link syntax down to its text
  4. defang autolinks and bare URLs
  5. HTML-escape, ampersand FIRST, then the angle brackets
  6. escape every remaining non-ASCII or control rune to a visible marker

Running 5 before 4 hides an autolink's delimiters from the defanger, so the URL survives. Running 6 before 5 rewrites this package's own markers into entity text. Escaping the angle brackets before the ampersand is the worst of the three: the body text "&lt;script&gt;" becomes a live "<script>" tag, because the entities the author typed are decoded rather than escaped.

What survives, and why that is not enough

Escaping the three HTML characters makes a tag impossible by construction, which a tag denylist never achieves. Markdown headings, lists, emphasis, code fences and tables survive untouched, because none of them use those characters — which is the point, and also a hazard: a body may still contain a line reading "## Reported issues (pre-v1 release)". Spliced into TODO.md that would forge a section heading. Containing it is RenderEntry's job, not this file's.

See docs/history/design/triage-hardening.md for the threat model and the residual risks.

Index

Constants

View Source
const DispatchFile = "dispatch.json"

DispatchFile is what the SKILL reads. It deliberately carries no body, no title and no flag text — only what the main session needs to spawn one proposer per issue.

The split exists because index.json holds the sanitised bodies, and the main session reading those would undo the whole point: the body reaches the proposer by path, never through the context of a session holding Bash and Edit. The nonce is safe to print because this package minted it; it is not attacker text.

View Source
const HistoryFile = "docs/history/todo-done.md"

HistoryFile is where a TODO.md entry goes once it is ticked. Entries there still own their issues/N link, so it is part of what "already triaged" means — but this package only ever reads it.

View Source
const IndexFile = "index.json"

IndexFile carries the artifacts between the fetch and apply runs, which are separate processes with on-disk state in between.

View Source
const TodoFile = "TODO.md"

TodoFile is the only file this package ever writes in the repository. Ticked entries leave it for HistoryFile, which is read alongside it (ReadTracked) and never written.

View Source
const TruncationMarker = "\n[truncated]"

TruncationMarker is appended when a cap bites. A fixed constant: a marker assembled from the body would be attacker text in a position the reader trusts.

Variables

View Source
var (
	Components = []string{"dashboard", "daemon", "tmux", "installer", "update", "issue-capture", "hooks", "docs", "unknown"}
	Symptoms   = []string{"crash", "hang", "wrong-output", "visual", "missing-feature", "perf", "install", "unknown"}
	Sections   = []string{
		"Pre-v1 Cleanup",
		"Reported issues (pre-v1 release)",
		"M5+ (v1.x, re-rank when reached)",
	}
)

The closed vocabularies the proposer chooses from on the facts-only path. They exist so that no model-authored prose reaches TODO.md: an enum cannot carry an instruction.

View Source
var ErrBidi = errors.New("bidirectional override present")

ErrBidi rejects a body outright rather than escaping it. No honest bug report carries a bidirectional override, and the whole point of one is to make rendered text disagree with its bytes — the exact property every other transform here exists to remove.

View Source
var Schema = []Field{
	{Path: "capturedAt", Kind: KindString, Check: CheckRFC3339},
	{Path: "scope", Kind: KindString, Check: CheckEnum("session", "dashboard")},

	{Path: "musterd.version", Kind: KindString, Check: CheckVersion},

	{Path: "claudeCode.installed", Kind: KindString, Check: CheckVersion},
	{Path: "claudeCode.floor", Kind: KindString, Check: CheckVersion},
	{Path: "claudeCode.verified", Kind: KindString, Check: CheckVersion},
	{Path: "claudeCode.status", Kind: KindString, Check: CheckToken(32)},
	{Path: "claudeCode.pinned", Kind: KindString, Check: CheckVersion, Retired: true},
	{Path: "claudeCode.drift", Kind: KindBool, Check: CheckBool, Retired: true},

	{Path: "host.os", Kind: KindString, Check: CheckEnum("darwin", "linux", "windows")},
	{Path: "host.arch", Kind: KindString, Check: CheckEnum("amd64", "arm64", "386", "arm")},

	{Path: "dashboard.sessionsTotal", Kind: KindInt, Check: CheckIntRange(0, 1e6)},
	{Path: "dashboard.sessionsAlive", Kind: KindInt, Check: CheckIntRange(0, 1e6)},
	{Path: "dashboard.view", Kind: KindString, Check: CheckToken(32)},
	{Path: "dashboard.density", Kind: KindString, Check: CheckToken(32)},
	{Path: "dashboard.railSort", Kind: KindString, Check: CheckToken(32)},

	{Path: "session.state", Kind: KindString, Check: CheckToken(32)},
	{Path: "session.stateSince", Kind: KindString, Check: CheckRFC3339},
	{Path: "session.alive", Kind: KindBool, Check: CheckBool},
	{Path: "session.endedAt", Kind: KindString, Check: CheckRFC3339},
	{Path: "session.attention.reason", Kind: KindString, Check: CheckToken(64)},
	{Path: "session.attention.since", Kind: KindString, Check: CheckRFC3339},
	{Path: "session.failure.error", Kind: KindString, Check: CheckToken(128)},
	{Path: "session.model.id", Kind: KindString, Check: CheckToken(64)},
	{Path: "session.permissionMode.value", Kind: KindString, Check: CheckToken(32)},
	{Path: "session.permissionMode.source", Kind: KindString, Check: CheckToken(32)},
	{Path: "session.context.usedPct", Kind: KindFloat, Check: CheckFloatRange(0, 100)},
	{Path: "session.context.totalInputTokens", Kind: KindInt, Check: CheckIntRange(0, 1e15)},
	{Path: "session.context.windowSize", Kind: KindInt, Check: CheckIntRange(0, 1e15)},
	{Path: "session.compactions", Kind: KindInt, Check: CheckIntRange(0, 1e6)},
	{Path: "session.tmuxTarget", Kind: KindString, Check: CheckToken(64)},
	{Path: "session.createdAt", Kind: KindString, Check: CheckRFC3339},
	{Path: "session.claudeSessionIdBound", Kind: KindBool, Check: CheckBool},
	{Path: "session.events.firstSeq", Kind: KindInt, Check: CheckIntRange(0, 1e12)},
	{Path: "session.events.lastSeq", Kind: KindInt, Check: CheckIntRange(0, 1e12)},
	{Path: "session.events.count", Kind: KindInt, Check: CheckIntRange(0, 1e12)},
	{Path: "session.events.lastReceivedAt", Kind: KindString, Check: CheckRFC3339},
	{Path: "session.events.recentTypes", Kind: KindStringSlice, Check: CheckTokenSlice(32, 48)},
}

Schema is the allowlist of snapshot leaves, one row per dotted JSON path. Nothing here confers trust: an author may edit their own issue body forever, so a snapshot is only untrusted text that happens to have a shape we own. What the shape buys is a strict parser — every value that survives is a bounded scalar, and a bounded scalar cannot carry a payload.

The schema is a UNION of every shape musterd has ever emitted, not a mirror of the current struct. Issue #9 carries claudeCode.{pinned,drift}; internal/server/issue.go now emits claudeCode.{floor,verified,status}. Rows for shapes no longer emitted are marked Retired so a future tidy-up cannot quietly delete them and start dropping fields on every old issue. internal/server/issue_snapshot_drift_test.go asserts the other direction: every path the server can emit has a row here.

An ordered slice rather than a map, because RenderSnapshotTable must be deterministic.

View Source
var TripwirePhrases = []string{
	"ignore previous instructions",
	"ignore all previous",
	"ignore prior instructions",
	"disregard the above",
	"disregard previous",
	"disregard all prior",
	"forget everything above",
	"new instructions",
	"system prompt",
	"you are now",
	"act as though",
	"override your instructions",
}

TripwirePhrases are strings that essentially never appear in an honest bug report but are the opening move of a careless injection.

Precision is excellent and recall is terrible: paraphrase, translation or encoding walks straight past this list. It is a tripwire, not a filter, and nothing in this package depends on it — a hit routes an issue to a human, and a miss still lands in the facts-only path if anything else about the body was irregular.

View Source
var TrustedAssociations = map[string]bool{"OWNER": true, "MEMBER": true}

TrustedAssociations is the set that may take the normal path.

On this repo OWNER and MEMBER mean "filed from Damian's own dashboard button", so the normal path is effectively self-filed-only. Nothing may fall back to comparing the login against a name: the login is not the check, and an account can be renamed.

Functions

func Apply

func Apply(ctx context.Context, run RunFunc, root, repo string, decisions []Decision, arts map[int]Artifact, props map[int]Proposal) (string, error)

Apply splices every decided entry into TODO.md and makes one commit.

One commit, not one per issue: the existing pass produces a single "docs(triage): file #12 and #14 into the pre-v1 backlog", and a commit per issue would change a settled convention for no reason.

The commit subject is built from integers and constants only, so an issue title cannot reach it — which is what makes it impossible for a triage commit to carry a closing keyword and close the very issues it just filed.

func CheckBool

func CheckBool(v Value) bool

CheckBool accepts any boolean; the decoded type is the whole check.

func CheckEntryShape

func CheckEntryShape(entry string) error

CheckEntryShape verifies a rendered entry cannot forge structure in TODO.md.

The first line is exempt: it is the house-style checkbox, which must start with "- ". Every other line has to be indented, which is what makes it part of this entry rather than a new block in the document.

func CheckRFC3339

func CheckRFC3339(v Value) bool

CheckRFC3339 accepts a timestamp musterd could actually have written.

func CheckVersion

func CheckVersion(v Value) bool

CheckVersion accepts a version string.

func EntryState

func EntryState(todo string, n int) (checked, found bool)

EntryState reads the checkbox of the entry that OWNS issue n.

Entries are split on a "- [ ]" or "- [x]" at column 0 and the owning entry is the one whose own block carries the issues/N link. Reading the checkbox off any block that merely mentions the issue would attribute another item's state to it — the file is full of cross-references, and getting this wrong is how an audit reports a fix that never landed.

func ExtractSnapshotJSON

func ExtractSnapshotJSON(body string) (raw string, present, ambiguous bool)

ExtractSnapshotJSON finds the single fenced JSON block in a raw issue body.

Fence matching is line-based and length-aware because musterd writes a four-backtick fence (the payload itself contains three-backtick text), and Go's regexp has no backreference to match an opening fence to its own closing one.

func HasBidi

func HasBidi(s string) bool

HasBidi reports whether text carries a bidirectional override.

Exported so a caller can decide to hold an issue before sanitising rather than reading a rejection back out of an error — a hold is a routing outcome, not a failure.

func HasIssue

func HasIssue(todo string, n int) bool

HasIssue reports whether TODO.md already owns an entry for issue n.

Matching is on the issues/N URL, never a bare #N. Measured on the real file, a bare #N has two false-positive sources: "#343a4a" is a hex colour in a design note, and a cross-reference like "same seam as #4" makes #4 look triaged when it has no entry of its own. The boundary check stops issues/4 matching inside issues/42.

func IssueURL

func IssueURL(repo string, n int) string

IssueURL builds the only URL an entry ever carries, from the integer GitHub asserted. Nothing model-authored reaches a URL position.

func NewNonce

func NewNonce() (string, error)

NewNonce mints the per-issue framing token. Random rather than derived, so an author cannot compute it from anything they can see and write a matching delimiter into their own body.

func ReadTracked

func ReadTracked(root string) (todo, tracked string, err error)

ReadTracked returns TODO.md's text and the text of every tracked entry: TODO.md followed by HistoryFile when it exists. Decide "is this issue already filed" against tracked; read-modify-write TODO.md through todo. Keeping the two apart is what stops an archived issue being re-filed and what keeps the write path a single file.

func RenderEntry

func RenderEntry(a Artifact, p Proposal, repo string) string

RenderEntry produces the TODO.md entry for an issue on the facts-only path.

Every token is a closed-set enum, an integer GitHub asserted, or one quoted substring already checked against the sanitised body. The house style is TODO.md's: the title line carries the full markdown link, and continuation lines are indented two spaces.

The guarantee that matters is structural, not textual. Markdown survives sanitisation by design, so a body can still contain a line reading "## Reported issues (pre-v1 release)"; spliced in raw that would forge a section heading and break the next run's scanner. Containing that is this function's job: no continuation line it emits begins with a markdown structural character at column 0. CheckEntryShape asserts it.

func RenderSnapshotTable

func RenderSnapshotTable(s Snapshot) string

RenderSnapshotTable regenerates the fact table from validated values.

The rendered "## Snapshot" table in the body is discarded rather than kept, because it duplicates this data as pre-formatted prose ("4% · 41783 / 1000000 tokens") that would need its own parsing. One validated source beats two. No value here can break the table: the token charset admits no pipe.

func ResolveRepo

func ResolveRepo(ctx context.Context, run RunFunc, modulePath string) (string, error)

ResolveRepo derives the repository slug from the Go module path and refuses unless the checkout's own origin agrees.

The slug is not a fourth copy of a constant that already lives in go.mod: it is read from there. The cross-check exists because gh resolves the repo from the working directory's origin remote, so inside a worktree of a fork, or after a remote rename, the tool would triage one repository and splice issue URLs pointing at another.

func SectionNames

func SectionNames(todo string) []string

SectionNames lists the "## " headings, skipping fenced regions.

Fence awareness is not decoration. TODO.md has no code fences today, but an entry is the one thing that can introduce one, and a "## " line inside a fence is text rather than a heading — a scanner that cannot tell them apart can be steered into splicing into a section an author invented.

func Splice

func Splice(todo, section, entry string) (string, error)

Splice inserts an entry at the end of a section, preserving the blank line between entries. Appending rather than sorting is deliberate: TODO.md's "Reported issues" preamble records that open entries sit in Damian's priority order, and re-sorting would silently discard a ranking he set by hand.

A pure insertion — the result is the original with one block added, and nothing else touched. TestSpliceIsPureInsertion holds that.

func StripSnapshotRegions

func StripSnapshotRegions(body string) string

StripSnapshotRegions removes the machine-written regions from a raw body so the artifact carries the author's prose rather than a copy of the JSON.

Presentation only. Every region boundary here is attacker-controlled, so nothing security-relevant may depend on a match: whatever survives still goes through Sanitize like the rest of the body.

func Tripwire

func Tripwire(clean string) (string, bool)

Tripwire reports the first listed phrase present in already-sanitised text.

Matching normalises a copy; the stored text is never altered. Note the deliberate gap: a phrase split by a zero-width character survives normalisation intact and does NOT hit here — but the zero width itself is a flag, so the issue still leaves the normal path. TestRouteZeroWidthCoversTripwireGap pins that interaction.

func WriteArtifacts

func WriteArtifacts(dir string, arts []Artifact) error

WriteArtifacts writes one file per issue into dir, which the caller made with os.MkdirTemp. Permissions are tight because the bodies are quoted prompt text from a public tracker and the repo's own rule is never to leave that world-readable.

func WriteDispatch

func WriteDispatch(dir string, arts []Artifact) error

WriteDispatch records how to reach each artifact that a proposer should read. Held issues are excluded: they never reach a model at all.

func WriteIndex

func WriteIndex(dir string, arts []Artifact) error

WriteIndex records every artifact, including its sanitised body — apply needs that to check a quote's provenance, and re-deriving it would mean re-fetching a body that may have been edited in the meantime.

Types

type Artifact

type Artifact struct {
	Number            int
	CreatedAt         string
	UserLogin         string
	AuthorAssociation string
	Route             Path
	Flags             Flags
	Nonce             string
	Title             string // sanitised
	Body              string // sanitised
	SnapshotTable     string
}

Artifact is one issue after the program has finished with it: bounded, escaped, enumerated, and safe to hand to a model that holds no tools.

func Build

func Build(iss Issue, l Limits) (Artifact, error)

Build runs one issue through the whole pipeline in the order the package doc fixes.

func ReadIndex

func ReadIndex(dir string) ([]Artifact, error)

ReadIndex loads what WriteIndex wrote.

func (Artifact) Render

func (a Artifact) Render() string

Render writes the file the proposer subagent reads.

The instruction is repeated after the body as well as before it. Recency helps a little and costs nothing — but it is the weakest layer here by a distance, and nothing depends on it. The boundary is that the proposer holds no tools; this is a reminder, not a wall.

type CheckFunc

type CheckFunc func(Value) bool

CheckFunc reports whether a value is well-formed enough to keep. A false is always a drop-and-count, never an error: the point is to survive a body that is partly garbage, not to reject it wholesale.

func CheckEnum

func CheckEnum(allowed ...string) CheckFunc

CheckEnum accepts one of a closed set. Used only where the set is genuinely stable — an enum that drifts drops valid fields.

func CheckFloatRange

func CheckFloatRange(lo, hi float64) CheckFunc

CheckFloatRange accepts a number within bounds. Integer JSON satisfies it.

func CheckIntRange

func CheckIntRange(lo, hi int64) CheckFunc

CheckIntRange accepts an integer within bounds.

func CheckToken

func CheckToken(limit int) CheckFunc

CheckToken accepts a bounded token.

func CheckTokenSlice

func CheckTokenSlice(maxElems, elemLen int) CheckFunc

CheckTokenSlice bounds both the element count and each element, so a recentTypes array of ten thousand entries, or one entry a megabyte long, is dropped rather than rendered.

type Counts

type Counts struct {
	HTMLEscaped   int
	LinksStripped int
	URLsDefanged  int
	NonASCII      int
	ZeroWidth     int
	Truncated     bool
}

Counts feeds routing. Every field is a count of something removed or rewritten, so a non-zero total means the body was not plain prose.

func Sanitize

func Sanitize(s string, l Limits) (string, Counts, error)

Sanitize applies the six transforms in the order package doc fixes. It returns the cleaned text, what it had to do, and ErrBidi if the body must be held instead.

The result contains no '<' or '>' other than this package's own escape markers, which is what makes the artifact's nonce framing hold: an author's angle brackets have become entities by the time any marker is written.

func SanitizeTitle

func SanitizeTitle(s string, limit int) (string, Counts, error)

SanitizeTitle is Sanitize with newlines folded to spaces. A title is rendered on one line, so a newline in one is only ever an attempt to forge structure around it.

func (Counts) Clean

func (c Counts) Clean() bool

Clean reports whether the body needed no intervention at all.

type Decision

type Decision struct {
	Number  int
	Section string
}

Decision pairs an issue with the section Damian chose for it. The section choice stays a human one: where an item ranks is a judgement, and the skill still asks.

type Dispatch

type Dispatch struct {
	Number int    `json:"number"`
	Path   string `json:"path"`
	Ack    string `json:"ack"`
	Route  string `json:"route"`
}

Dispatch is one row of DispatchFile.

type Fetcher

type Fetcher struct {
	Run  RunFunc
	Repo string // "owner/name"
}

Fetcher reads open issues through the gh CLI. docs/conventions.md settles the choice: os/exec plus the gh CLI, never a Go GitHub library.

func (*Fetcher) List

func (f *Fetcher) List(ctx context.Context, state string) ([]Issue, error)

List returns issues in the given state ("open", "closed" or "all"), pull requests excluded. The audit needs closed ones to spot an entry ticked while its issue is open.

func (*Fetcher) ListOpen

func (f *Fetcher) ListOpen(ctx context.Context) ([]Issue, error)

ListOpen returns every open issue, pull requests excluded.

type Field

type Field struct {
	Path    string
	Kind    Kind
	Check   CheckFunc
	Retired bool
}

Field is one allowlisted leaf.

func LookupField

func LookupField(path string) (Field, bool)

LookupField finds the row for a dotted path.

type Flags

type Flags struct {
	Counts
	SnapshotUnknownFields int
	SnapshotAmbiguous     bool
	Tripwire              string
	Bidi                  bool
}

Flags is everything mechanically observed about one body. Every field is derived by a program; none is a judgement.

func (Flags) Any

func (f Flags) Any() bool

Any reports whether anything at all was irregular.

func (Flags) Strings

func (f Flags) Strings() []string

Strings renders the flags for the run report. Only counts, fixed names and the constant tripwire phrase — never a slice of the body, because this output lands in the main session's context, which is the place all of this exists to keep clean.

type Issue

type Issue struct {
	Number            int
	CreatedAt         string
	UserLogin         string
	AuthorAssociation string
	Title             string
	Body              string
	State             string
}

Issue is the allowlisted view of a GitHub issue. Assembled by explicit copy rather than by unmarshalling a whole object and removing keys — the same discipline internal/server/issue.go applies to the outbound snapshot, for the same reason: subtraction silently admits every field added upstream later.

Every string field is attacker-controlled. Number and the association are the only values here that GitHub itself asserts.

func Untriaged

func Untriaged(todo string, issues []Issue) []Issue

Untriaged returns the issues with no entry in TODO.md.

The triaged set is computed here rather than by a model running grep — one less reason for anything holding Edit to open that file.

type KV

type KV struct {
	Path  string
	Value string
}

KV is one validated leaf, ready to render.

type Kind

type Kind int

Kind is the JSON shape a schema row accepts. A value arriving as the wrong kind is dropped, never coerced: coercion is where a string starts pretending to be a number.

const (
	KindString Kind = iota
	KindInt
	KindFloat
	KindBool
	KindStringSlice
)

func (Kind) String

func (k Kind) String() string

String makes drift-test failures readable.

type Limits

type Limits struct {
	MaxInputBytes  int
	MaxOutputBytes int
}

Limits bound both ends. The output cap is not redundant: escaping is expansive, and a body of nothing but zero-width runes grows roughly eightfold under it.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits is what tools/triage runs with.

type Path

type Path int

Path is where an issue goes after sanitising.

const (
	// PathNormal keeps today's richer prose entry. Reached only by a clean body from a
	// trusted association — in practice, an issue Damian filed from his own dashboard.
	PathNormal Path = iota
	// PathFactsOnly renders from enums and one quoted substring, with no model-authored
	// prose reaching TODO.md.
	PathFactsOnly
	// PathHeld never reaches a model at all and is reported for review.
	PathHeld
)

func Route

func Route(association string, f Flags) Path

Route decides where an issue goes. Flags dominate association: a trusted author whose body needed intervention still takes the facts-only path, because the question is what the text does, not who sent it.

func (Path) String

func (p Path) String() string

type Proposal

type Proposal struct {
	Number      int    `json:"number"`
	Ack         string `json:"ack"`
	Component   string `json:"component"`
	Symptom     string `json:"symptom"`
	ErrorString string `json:"error_string"`
	SectionHint string `json:"section_hint"`
}

Proposal is everything the proposer is allowed to say about one issue.

func ValidateProposal

func ValidateProposal(raw []byte, a Artifact) (Proposal, error)

ValidateProposal checks one proposer reply against the artifact it was given.

Every failure is a hold, never a repair. The proposer read attacker-controlled text, so its reply is attacker-influenced by construction; what makes that survivable is that nothing here is taken on trust — the ack proves which artifact it read, the enums are closed sets, and the one quoted field has to be found verbatim in the sanitised body.

type RunFunc

type RunFunc func(ctx context.Context, name string, args ...string) (stdout, stderr []byte, err error)

RunFunc is the injectable subprocess seam (docs/conventions.md §Testing: "never a $PATH shim"), so every case below is unit-testable with canned JSON.

It keeps stdout and stderr apart, unlike tools/versions' CombinedOutput equivalent: gh writes progress and warnings to stderr, and merging them would splice non-JSON into the document being decoded.

type Snapshot

type Snapshot struct {
	Present   bool
	Ambiguous bool
	Dropped   int
	Fields    []KV
}

Snapshot is the result of parsing an issue's raw JSON block.

Present is false when the body carried no block. Ambiguous is true when it carried more than one candidate: a body may contain any number of <details> blocks and fenced JSON, and taking the first match would let an author shadow the genuine snapshot with fabricated version facts — which is exactly the data that decides whether a report is still true against the current tree. More than one candidate therefore discards the snapshot entirely rather than picking a winner.

func ValidateSnapshot

func ValidateSnapshot(raw string) (Snapshot, error)

ValidateSnapshot parses a raw block field by field against Schema.

Version-tolerant by construction: an unknown path, a wrong JSON type, or a value that fails its format check is dropped and counted, never a reason to reject the whole block. The schema has already drifted once — issue #9 predates the current claudeCode shape — and a strict validator would have held every issue filed before it.

type Value

type Value struct {
	Kind  Kind
	Str   string
	Num   json.Number
	Bool  bool
	Slice []string
}

Value is one decoded leaf. Numbers stay as json.Number because usedPct arrives as the integer 4 rather than 4.0, and totalInputTokens through a float64 would lose precision at nine figures.

Jump to

Keyboard shortcuts

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