design

package
v0.18.17 Latest Latest
Warning

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

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

Documentation

Overview

Package design defines the design workspace directory contract: the design/ tree layout, the manifest scaffold, and parsers for the machine-readable manifest blocks. See roadmap/SP-140-1-formats.md.

Index

Constants

View Source
const (
	// BriefDepthSummary is the default depth (§5g): a condensed brief — counts
	// and headings rather than the full annotation/detail text.
	BriefDepthSummary = "summary"
	// BriefDepthFull is the detailed brief: annotation notes, per-edge detail,
	// and the flow file list.
	BriefDepthFull = "full"
)

Design↔code sync, screen-brief half (SP-140-5 §5g).

This file is the pure, read-only core of the `design_brief` tool: given a screen name (a wireframe stem) and the workspace root, it reads the design/ tree *for that screen* and returns a structured brief the implementing agent reads before building it:

  • purpose — the screen's purpose, from the README Screens listing;
  • wireframe path — design/wireframes/<stem>.svg;
  • flows in/out — every flow edge touching the screen, with its trigger (the mermaid edge label), direction, and flow file;
  • token paths — the DTCG token paths the wireframe refers to (its `{group.token}` comments), plus the token groups available to consume;
  • open feedback — pending §4d feedback annotations for the screen;
  • status — the README status marker (draft/review/ready).

The brief is a *contract, not a generator* (§5g): it writes nothing and it produces no component code. It is purely advisory context returned in the ToolResult. Determinism is a property of the core — the same tree must yield the same brief byte for byte — so every slice is sorted by a canonical key and nothing is ordered by map iteration.

View Source
const (
	// DirName is the design workspace root directory, relative to the
	// project root.
	DirName = "design"

	// ManifestName is the inventory manifest at the design root.
	ManifestName = "README.md"

	// SlugPattern is the name rule for screens, flows, and icons.
	SlugPattern = `^[a-z0-9]+(-[a-z0-9]+)*$`
)
View Source
const (
	// TokenSubdir holds the W3C DTCG *.tokens.json files.
	TokenSubdir = "tokens"

	// FlowSubdir holds the mermaid *.mmd flow sources.
	FlowSubdir = "flows"

	// RuntimeSubdir holds the SP-143 screen-kit fixed assets: the screen
	// runtime (sprout-screens.js), device chrome (chrome.css), and the base
	// documents screens start from (base/*.html).
	// It is NOT in Subdirs — it is scaffolded tool-owned content, not a user
	// authoring tier, so it never appears in the manifest's directory
	// contract. The validator (SP-143 §143.5) registers it separately.
	RuntimeSubdir = "runtime"
)

Canonical subdirectory names, named so callers can test membership and subtree relationships without repeating string literals.

View Source
const (
	// DriftDirectionDesignAhead is design/ ahead of generated/code.
	DriftDirectionDesignAhead = "design-ahead"
	// DriftDirectionCodeAhead is implementation ahead of the semantic layer.
	DriftDirectionCodeAhead = "code-ahead"
	// DriftDirectionSynced is the balanced row: neither side is ahead. Both
	// direction rows still report, with Synced=true and an empty remedy, so a
	// consumer reads a stable two-row shape and "no drift" is explicit.
	DriftDirectionSynced = "synced"
)

Drift direction ids, fixed by §5c. These are the stable row identifiers both design_assets and design_validate emit.

View Source
const (
	// DriftSeverityInfo marks a row with no remedy (in sync, or ahead with
	// nothing countable to point at).
	DriftSeverityInfo = "info"
	// DriftSeverityWarn marks a row that carries an actionable remedy.
	DriftSeverityWarn = "warn"
)

Drift severities. Drift is signal, never an error (§5c): both rows are advisory. info = "nothing to do", warn = "there is a remedy to run".

View Source
const (
	// DriftRowDesignAhead is the design-ahead row.
	DriftRowDesignAhead = "design-ahead"
	// DriftRowCodeAhead is the code-ahead row.
	DriftRowCodeAhead = "code-ahead"
)

Drift rows (stable row order). Every DriftReport carries exactly these two, in this order.

View Source
const (
	// DriftDesignAheadNextStep is the design-ahead remedy pointer: regenerate
	// the generated theme from the token sources (§5a). The count of screens
	// ahead of the build is reported in the row's Count/Summary.
	DriftDesignAheadNextStep = "" /* 144-byte string literal not displayed */
	// DriftCodeAheadNextStep is the code-ahead remedy pointer: import the
	// semantic deltas the implementation introduced (§5b).
	DriftCodeAheadNextStep = "" /* 127-byte string literal not displayed */
)

Drift remedy next-steps: the run-me pointers §5c fixes. They are stable strings the handler rows and the tests assert, so the two surfaces cannot drift apart in wording either.

View Source
const (
	ExportTargetCSS      = "css"
	ExportTargetTS       = "ts"
	ExportTargetJSON     = "json"
	ExportTargetTailwind = "tailwind"
	ExportTargetSwift    = "swift"
	ExportTargetKotlin   = "kotlin"
)

Export target identifiers. These are the exact strings the design_export_tokens `targets` argument accepts.

View Source
const (
	// GitAttributesRule fires when the workspace .gitattributes is missing
	// or lacks the design SVG diff rule; its finding is a fix carrying the
	// exact line to append (GitAttributesDiffHTMLLine).
	GitAttributesRule = "gitattributes_diff_html"

	// GitIgnoreCacheRule fires when the workspace .gitignore does not
	// ignore design/.cache/; its finding is a fix carrying the exact line
	// to append (GitIgnoreCacheLine).
	GitIgnoreCacheRule = "gitignore_design_cache"

	// GitIgnoreGeneratedRule is advisory: the workspace .gitignore also
	// ignores design/generated/, which SP-140-1 §1h deliberately leaves
	// un-ignored (committing generated artifacts is the project's choice,
	// and the provenance hashes keep either choice consistent).
	GitIgnoreGeneratedRule = "gitignore_design_generated"

	// DataURISizeRule is advisory (warn): an embedded data: URI inside a
	// design SVG exceeds DataURISizeWarnThreshold bytes.
	DataURISizeRule = "svg_data_uri_size"

	// FixGitAttributesRule is the machine-applicable rule id for the
	// .gitattributes fix.
	FixGitAttributesRule = "gitattributes_diff_html_fix"

	// FixGitIgnoreCacheRule is the machine-applicable rule id for the
	// .gitignore fix.
	FixGitIgnoreCacheRule = "gitignore_design_cache_fix"
)

Rule ids for the design-tree git contract, SP-140-1 §1h. GitAttributesRule and GitIgnoreCacheRule name the contract clauses (they are the finding rule ids); the machine-applicable fix findings carry the FixRule ids so callers can tell a report from a patch instruction.

View Source
const (
	KindManifest  = "manifest"
	KindToken     = "token"
	KindBrand     = "brand"
	KindIcon      = "icon"
	KindWireframe = "wireframe"
	KindComponent = "component"
	KindScreen    = "screen"
	KindFlow      = "flow"
	KindFeedback  = "feedback"
	// KindRuntime lists the SP-143 screen-kit fixed assets under
	// design/runtime/ (the runtime, device chrome, base documents). They are
	// tool-owned, not an authoring tier, but design_assets lists the tier so
	// a model sees what is actually in the tree (SP-143 §143.5).
	KindRuntime = "runtime"
	KindUnkn    = "unknown"
)

inventoryAssetKinds are the per-asset `kind` values the inventory rows carry, one per canonical design asset class (SP-140-2 §2c).

View Source
const (
	// DeltaBasisLiteral: a token variable was renamed/revalued in code and
	// maps 1:1 to a DTCG entry. Safe to apply mechanically.
	DeltaBasisLiteral deltaBasis = "literal"
	// DeltaBasisStructural: a new route/screen/component/nav target —
	// structure, not value. Maps to wireframe/flow changes.
	DeltaBasisStructural deltaBasis = "structural"
	// DeltaBasisInferred: styling with no token counterpart. A proposal.
	DeltaBasisInferred deltaBasis = "inferred"
)

Basis values, fixed by SP-140-5 §5b.

View Source
const (
	UtilityGroupColor  = "color"
	UtilityGroupSpace  = "space"
	UtilityGroupFont   = "font"
	UtilityGroupRadius = "radius"
	UtilityGroupShadow = "shadow"
)

The SP-143 §143.1 utility vocabulary: a fixed set of known token groups that map to utility classes in the css export target, so a screen styles with `.bg-*`/`.p-*` instead of re-deriving CSS from scratch. Unknown groups pass through as variables only — the vocabulary is closed on purpose (SP-143 Premise §1), so a project tier named `sizing` never generates surprise classes.

View Source
const DataURISizeWarnThreshold = 1 << 20

DataURISizeWarnThreshold is the byte size above which an embedded data: URI is a validator warn (SP-140-1 §1h, "binary hygiene"). 1 MiB keeps diffs reviewable: past it the escape hatch — move the raster to brand/ and reference it — is the better trade.

View Source
const ExportTargetAll = "all"

ExportTargetAll is the `targets` value that runs every exporter.

View Source
const ExportTargetFlows = "flows"

ExportTargetFlows is the SP-140-9 §9b derived-flow export target: it regenerates design/flows/<name>.mmd from each flow source .json plus the touched screens. Like the screens index it is explicit-only — never part of `all` — because it derives from the flow sources, not the token sources, so a token re-theme must not rewrite the flow exports.

View Source
const ExportTargetScreens = "screens"

ExportTargetScreens is the design_export_tokens target that writes the derived screens.json. It is deliberately not part of ExportTargets (the `all` set): the token targets render from the token sources, while the screens index derives from design/screens/*.html, so a plain re-theme run (`all`) never rewrites the screen graph — it is an explicit regeneration.

View Source
const FeedbackStatusChangesRequested = "changes-requested"

FeedbackStatusChangesRequested is the §4d status a human-written annotation carries while the agent still has to address it. The feedback affordance writes it (webui/src/design/feedbackWrite.ts PENDING_FEEDBACK_STATUS) and the design-system skill's loop starts any target carrying it with a read of its feedback file.

View Source
const FeedbackSubdir = "feedback"

Feedback-subdirectory name under design/. Named so the validator, the design_assets inventory, and the parser all agree on one literal.

View Source
const FlowDraftStatus = "draft"

FlowDraftStatus is the status a §5b-created wireframe/flow edge carries: draft, so the next design turn reviews it rather than trusting it.

View Source
const FlowSourceHashLabel = "flow-source-hash"

FlowSourceHashLabel is the header key on the derived .mmd carrying the §9b input hash. The value renders with the token-export digest label (fnv1a64:<16 hex>), so every derived artifact in the tree verifies the same way.

View Source
const GeneratedSubdir = "generated"

GeneratedSubdir is the design subdirectory export writes to, under DirName (SP-140-5 §5a: "targets design/generated/"). It is deliberately not part of Subdirs — it is a generated output location, not a scaffolded source tier, and SP-140-1 §1h leaves it un-ignored.

View Source
const GitAttributesDiffHTMLLine = "design/**/*.svg diff=html"

GitAttributesDiffHTMLLine is the exact line the design tree's .gitattributes contract requires (SP-140-1 §1h): readable markup diffs for wireframe, icon, and logo SVGs. Repos already carrying text/binary rules keep them — the line is appended, never clobbered.

View Source
const GitContractFile = ".gitattributes"

GitContractFile is the pseudo design-asset path the §1h git-contract findings attach to. It is repository-level, not a file inside design/, so single-file validation runs never emit these findings — only whole-tree runs (design_validate with no path) check them.

View Source
const GitIgnoreCacheLine = "design/.cache/"

GitIgnoreCacheLine is the exact line the design tree's ignore policy requires (SP-140-1 §1h): render PNGs and critique scratch live in design/.cache/ and are never sources. design/generated/ is deliberately *not* covered — committing generated artifacts stays the project's choice.

View Source
const GitIgnoreFile = ".gitignore"

GitIgnoreFile is the repository-level file the ignore-policy findings (design/.cache/, design/generated/) attach to.

View Source
const GitIgnoreGeneratedPattern = "design/generated/"

GitIgnoreGeneratedPattern is the pattern the validator checks against the workspace .gitignore to emit its advisory design/generated/ finding.

View Source
const RuntimeFilename = "sprout-screens.js"

RuntimeFilename is the screen runtime's fixed asset name under design/runtime/ (SP-143 §2).

View Source
const ScreensIndexFilename = "screens.json"

ScreensIndexFilename is the screens.json artifact name under design/generated/ (SP-143 §4).

View Source
const StatusFindingsCap = 200

StatusFindingsCap is the maximum number of findings the status payload carries. The tallies stay authoritative (they count everything); the list is capped so one broken tree cannot flood a browser. The cap keeps the validator's canonical order (first N in sorted order).

View Source
const SyncModeApplyConst = "apply"

SyncModeApplyConst mirrors the handler's mode constant without importing the handler (the pure core must not depend on pkg/agent_tools).

View Source
const TokenExportSourceHashLabel = "fnv1a64"

TokenExportSourceHash is the label prefix on every content hash this package emits. It names the algorithm in-band so a consumer reading a header knows how to recompute the digest without reading the implementation.

Variables

View Source
var ErrDirtyAliasGraph = errors.New("dirty alias graph: token aliases do not resolve")

ErrDirtyAliasGraph is returned when the token tree contains at least one dangling or cyclic whole-value alias (SP-140-5 §5a: export "refuses dirty alias graphs"). It is a distinct sentinel so the handler can report the refusal as a structured, actionable failure and name the offending token — see DirtyAliasError for the per-token detail. Export never writes a partial or broken theme on a dirty graph.

View Source
var ErrManifestExists = errors.New("design manifest already exists")

ErrManifestExists is returned by Scaffold when the design manifest already exists, so callers can detect that a scaffold would be a no-op rather than clobbering user work.

View Source
var ErrManifestIsDirectory = errors.New("design manifest path is a directory")

ErrManifestIsDirectory is returned by Scaffold when the manifest path exists but is a directory, which Scaffold cannot replace with a file.

View Source
var ErrNoFlowSources = errors.New("no flow sources found under design/flows")

ErrNoFlowSources is returned when the `flows` export target is requested but design/flows/ holds no flow source documents — there is nothing to derive, and an empty export would be misleading. A distinct sentinel so the handler reports a usage error, mirroring ErrNoScreensForIndex.

View Source
var ErrNoScreensForIndex = errors.New("no design screens found to index")

ErrNoScreensForIndex is returned when the screens index is requested but design/screens/ holds no screens — there is nothing to derive, and an empty index would be misleading. A distinct sentinel so the handler reports a usage error, mirroring ErrNoTokensForExport.

View Source
var ErrNoTokensForExport = errors.New("no design tokens found to export")

ErrNoTokensForExport is returned when no token leaves exist under the workspace's design/tokens/ directory — there is nothing to export, and emitting target boilerplate with no tokens would be misleading. It is a distinct sentinel so the handler can report a usage error rather than a crash. (Exporting requires tokens/, which is also how the handler refuses a workspace with no design/ tree at all.)

View Source
var ExportFilenames = map[string]string{
	ExportTargetCSS:      "tokens.css",
	ExportTargetTS:       "tokens.ts",
	ExportTargetJSON:     "tokens.json",
	ExportTargetTailwind: "tailwind.theme.css",
	ExportTargetSwift:    "tokens.swift",
	ExportTargetKotlin:   "tokens.kt",
}

ExportFilenames maps each target to the file it writes under design/generated/. The names are fixed by SP-140-5 §5a so generated output is predictable to consumers (a webui build can hardcode tokens.css).

ExportTargets is the canonical target list, in the deterministic order exports are generated and reported (all → css, ts, json, tailwind, swift, kotlin). Callers must not rely on map order, so this slice is the one ordering authority.

View Source
var ProprietaryNames = []string{
	"figma",
	"penpot",
	"sketch",
	"illustrator",
	"adobe",
	"photoshop",
}

ProprietaryNames is the hardcoded word list SP-140-1's Acceptance Criteria fixes for the design tier: the six proprietary design-tool product names.

The design tree's formats are deliberately open (W3C DTCG tokens, SVG, mermaid, self-contained HTML — see SP-140-1 §1), so the design-tier code must never couple itself to a specific vendor. The list is versioned here, not in the guard test, so any future design-tier file (Go or webui) can be scanned with the same vocabulary without duplicating it.

One entry is load-bearing: the "sketch" product name collides with SP-140-2's `design_import_sketch` tool, which takes its name from the *activity* of sketching, not the product. This list is therefore scoped to the SP-140-1 design-tier files only (pkg/design and its design_validate handler); SP-140-2's tool files are covered by their own spelling convention (`import_sketch`) rather than this rule.

The literals below are the vocabulary itself — the one sanctioned place the names may appear (see providerScanExemptBasenames).

View Source
var RuntimeAssets = []string{
	"sprout-screens.js",
	"chrome.css",
	"base/phone.html",
	"base/desktop.html",
}

RuntimeAssets are the SP-143 fixed screen-kit assets the scaffold copies into design/runtime/, in copy order: the runtime itself, device chrome, then the base documents new screens start from. They are versioned assets, not user content — Scaffold never overwrites an existing file, so a tree may pin its own copy without the scaffold fighting it.

View Source
var Subdirs = []string{
	"tokens",
	"brand",
	"icons",
	"wireframes",
	"components",
	"screens",
	"flows",
	"feedback",
}

Subdirs are the canonical design workspace subdirectories, in contract order.

Functions

func AppendGitContract

func AppendGitContract(dir string) error

AppendGitContract appends the SP-140-1 §1h lines to the workspace .gitattributes and .gitignore when absent, creating either file when missing. It is append-only: existing lines are preserved verbatim, and a contract already satisfied (directly or via a broader ignore rule) is left alone. For example, appending .gitattributes to:

  • text=auto eol=lf *.go text eol=lf

yields the same rules plus "design/**/*.svg diff=html" at the end — the repo's text/binary rules are never clobbered. design/generated/ is deliberately never added (SP-140-1 §1h leaves that choice to the project).

func CompareDeltas

func CompareDeltas(a, b SyncDelta) bool

CompareDeltas is the canonical delta ordering: by basis, then kind, then code file, then the delta text. It is the determinism guarantee for the report — two runs over the same inputs emit the same Deltas slice.

func DriftDirections

func DriftDirections() []string

driftDirectionOrder exposes the fixed direction order to consumers that build their own view of the report (the design_assets output rows).

func DriftRuleID

func DriftRuleID(direction string) string

DriftRuleID is the design_validate rule id for one drift direction row. The ids are the direction names (design-ahead / code-ahead), prefixed with the shared `drift_` namespace so a consumer can tell a drift row from a validator rule at a glance.

func DriftSummaryLine

func DriftSummaryLine(report *DriftReport) string

DriftSummaryLine renders the report as one human/agent-readable sentence for the design_assets summary line: both directions named with their counts, so the two-row shape is visible in the text as well as the JSON.

func FileExists

func FileExists(root string) bool

FileExists reports whether the workspace at root has a design/ directory. The design_validate handler uses it to distinguish "clean tree" from "no design/ at all" when a run yields zero findings.

func FlowMMDFilename added in v0.18.17

func FlowMMDFilename(name string) string

FlowMMDFilename returns the derived export path for a flow name.

func FlowMMDInputHash added in v0.18.17

func FlowMMDInputHash(srcRaw []byte, srcRelPath string, allScreens []TokenExportSource, touched []string) string

FlowMMDInputHash computes the §9b provenance hash over one flow's inputs in the documented order: the flow source bytes first, then the touched screens' bytes in stem order. allScreens are the raw screen documents (as ScreensIndexSources read them); screens the flow touches but the tree does not carry are skipped (unknown stems are the screen validator's finding).

func FlowSourcePaths added in v0.18.17

func FlowSourcePaths(root string) ([]string, error)

FlowSourcePaths globs the flow source documents under root (design/flows/*.json, the §7d .layout.json sidecars excluded), in sorted order. A missing or empty flows directory yields an empty slice.

func FlowSourceRelPath added in v0.18.17

func FlowSourceRelPath(name string) string

FlowSourceRelPath returns the canonical flow source path for a flow name: design/flows/<name>.json (SP-140-9 §9b).

func FlowTouchedScreens added in v0.18.17

func FlowTouchedScreens(src *FlowSource, screens *ScreenIndexDoc) []string

FlowTouchedScreens returns the flow's transitive data-nav closure: the step screens plus every screen reachable from them. Sorted; the off-path cross check and the hash-input assembly share this one closure definition.

func IsGitContractFile

func IsGitContractFile(p string) bool

IsGitContractFile reports whether path is a repository-level git-contract file (.gitattributes or .gitignore). They are valid design_validate path arguments even though they live outside design/.

func ManifestTemplate

func ManifestTemplate() ([]byte, error)

ManifestTemplate returns the embedded starter manifest content, for callers that need the template itself (for example to repair a manifest or to preview what a scaffold would write).

func PendingFeedbackCount

func PendingFeedbackCount(states []FeedbackFileState) int

PendingFeedbackCount returns the number of pending feedback files: those whose status is "changes-requested" or that carry at least one unresolved annotation (§4d). It is the single predicate the design_assets summary uses and the skill loop keys on.

func RenderExport

func RenderExport(target string, tokens *TokenExport) ([]byte, error)

RenderExport renders the token set for one target, returning the exact bytes to write. The output opens with the target's provenance header (item 5.2) and ends with a single trailing newline; it is byte-identical across runs for identical inputs.

func RenderFlowMMD added in v0.18.17

func RenderFlowMMD(doc *FlowExportDoc, hash string) []byte

RenderFlowMMD renders the derived .mmd bytes: the provenance banner, the declaration, node definitions (walk then off-path), then edges (walk then off-path), triggers as |label| edge labels. Deterministic by construction — the doc's slices are ordered by DeriveFlowMMD, the hash is an input.

func RenderScreenBrief

func RenderScreenBrief(b *ScreenBrief) string

RenderScreenBrief builds the human/agent-readable one-block summary of the brief, so a model reads the shape from the text without parsing JSON. The summary contracts to counts at summary depth and includes the annotation notes at full depth.

func RenderScreensIndex added in v0.18.16

func RenderScreensIndex(doc *ScreenIndexDoc) ([]byte, error)

RenderScreensIndex renders the derived screens.json bytes. Deterministic: the doc's slices are sorted by construction, encoding/json emits struct fields in declaration order, and there is no map anywhere in the shape, so identical screen bytes render identical bytes on every run and platform.

func RenderSyncSummary

func RenderSyncSummary(r *SyncReport) string

RenderSyncSummary is the human/agent-readable one-liner for a report: the touched-file count and the delta split by basis, plus a pointer to the safe subset apply can write. Aimed at the model reading the ToolResult text.

func Scaffold

func Scaffold(dir string) error

Scaffold creates the design workspace tree under dir: the design/ root, each canonical subdirectory with a .gitkeep placeholder (git does not track empty directories), the manifest from the embedded template, and the workspace git contract (SP-140-1 §1h).

The git contract appends GitAttributesDiffHTMLLine to the workspace .gitattributes and GitIgnoreCacheLine to the workspace .gitignore when the line is absent, creating either file when missing. Existing rules are never clobbered — the lines are appended, and an already-satisfied contract (including one covered by a broader rule) is a no-op.

Scaffold never overwrites user work: if design/README.md exists with content it returns ErrManifestExists (wrapped with the path) and leaves the file untouched. A 0-byte manifest is treated as an empty placeholder — the greenfield flow creates the tree before filling the manifest — and is replaced with the template. If the manifest path is a directory it returns ErrManifestIsDirectory. Subdirectory and .gitkeep creation is idempotent — existing entries are skipped, missing ones are created.

func ScaffoldRuntimeAssets added in v0.18.16

func ScaffoldRuntimeAssets(designRoot string) error

ScaffoldRuntimeAssets copies the SP-143 screen-kit assets (device chrome + base templates) from the embedded templates into root/design/runtime/, creating the directory. Idempotent and overwrite-free: an existing asset is left byte-for-byte alone, so a scaffold over a brownfield tree never clobbers user work — the same posture as the manifest.

func ScreenRelPath added in v0.18.17

func ScreenRelPath(stem string) string

ScreenRelPath returns the canonical primary-screen path for a stem: design/screens/<stem>.html (SP-140-9 §9a). Shared by the deprecation findings and the flow tooling, which name screens by this path.

func SerializeScreenBrief

func SerializeScreenBrief(b *ScreenBrief) ([]byte, error)

SerializeScreenBrief renders the brief to deterministic, 2-space-indented JSON. The bytes are stable for identical trees, so a caller can hash or diff the output (the tests rely on this for the determinism assertion).

func SlugMatches

func SlugMatches(name string) bool

SlugMatches reports whether a name satisfies the shared slug rule (SlugPattern, SP-140-1). It is exported so the sync handler/tests can assert a proposed stem is a legal wireframe name without duplicating the regexp.

func SubdirByName

func SubdirByName(name string) (string, bool)

SubdirByName returns the canonical subdirectory path under DirName for the given name (for example "wireframes" yields "design/wireframes"). The second return value is false for a name not in Subdirs.

func SubdirsJoined

func SubdirsJoined() []string

SubdirsJoined returns each canonical subdirectory joined under DirName (for example "design/tokens"), in contract order. The result is always non-nil, even when there are no canonical subdirectories.

func TokenExportInputHash

func TokenExportInputHash(docs []TokenExportSource) string

TokenExportInputHash hashes the *token inputs* — the DTCG source documents the export read, not the rendered artifact. It is the authoritative provenance hash recorded in every generated artifact's header (SP-140 invariant 2, SP-140-5 §5f "provenance at any ref").

Why inputs and not artifact bytes: the header must be identical across the five targets (only the rendering differs, the semantic content does not), and it has to be recomputable without re-running the export. A consumer at an arbitrary checkout recomputes it with:

exportTokenSourceHash(dir(os.ReadFile(design/tokens/*.tokens.json)))

— i.e. concat the raw bytes of design/tokens/*.tokens.json sorted by basename, hash that. Nothing else (no timestamps, no paths, no env) feeds the digest, so it is deterministic across machines and checkouts.

Each document is folded in as "len(bytes)\n bytes" so two different file partitions cannot concatenate to the same byte stream and collide.

func WireframeExists

func WireframeExists(exists bool) bool

WireframeExists is a tiny convenience so the token-reference call reads clearly; it mirrors the boolean field.

func WriteExportedArtifacts

func WriteExportedArtifacts(root string, artifacts []ExportedArtifact) error

WriteExportedArtifacts writes each artifact (already rendered and carrying its final design-relative slash path in RelPath) under root, creating parent directories as needed. It returns an error on the first failure.

Every RelPath must be confined to design/ — the handler asserts this before calling (relocateArtifacts + resolveExportOutDir), and this function re-checks it so a future caller cannot use it to escape the tree. Writes are plain file writes; the handler has prechecked every target path already.

func WriteExportedArtifactsAt

func WriteExportedArtifactsAt(root string, artifacts []ExportedArtifact) error

WriteExportedArtifactsAt is WriteExportedArtifacts under its explicit name: it writes artifacts at their own RelPath, which the handler has already relocated to the requested output directory. Keeping both names lets the handler read naturally while the default-path helper stays for tests.

Types

type AliasViolation

type AliasViolation struct {
	// Token is the dotted path of the token whose alias is broken
	// (e.g. "color.brand.text").
	Token string
	// Path is the workspace-relative slash path of the source document the
	// token lives in (design/tokens/color.tokens.json).
	Path string
	// Line is the 1-based source line of the token leaf's value.
	Line int
	// Rule is the validator rule id (token_alias_dangling or
	// token_alias_cycle), so the refusal uses the same vocabulary as
	// design_validate.
	Rule string
	// Kind is a short human label: "dangling" or "cycle".
	Kind string
	// Message is the validator's finding message (already names the token
	// and, for cycles, the member loop).
	Message string
}

AliasViolation is one dangling or cyclic alias in the token tree.

type AssetRow

type AssetRow struct {
	Path    string `json:"path"`
	Kind    string `json:"kind"`
	Name    string `json:"name"`
	Status  string `json:"status,omitempty"`
	Summary string `json:"summary,omitempty"`
}

AssetRow is one inventory entry `{path, kind, name, status?, summary?}` (SP-140-2 §2c). Path is the workspace-relative slash path; Name is the asset's stem (the screen/flow/icon name, `README` for the manifest, or the file stem otherwise). Status is populated from the manifest's status markers when one is declared for the asset; Summary is a short, best-effort description parsed from the manifest listing when present.

type BriefFeedback

type BriefFeedback struct {
	// Path is the feedback file (workspace-relative) when one exists.
	Path string `json:"path,omitempty"`
	// Status is the §4d top-level status ("changes-requested", "resolved", …).
	Status string `json:"status,omitempty"`
	// Open is the number of annotations with `resolved: false`.
	Open int `json:"open"`
	// Total is the total annotation count in the file.
	Total int `json:"total"`
	// Pending reports whether the file still needs the agent's attention
	// (status changes-requested or at least one unresolved annotation, §4d).
	Pending bool `json:"pending"`
	// Resolution is the agent's closing note, when present.
	Resolution string `json:"resolution,omitempty"`
	// Notes are the open annotations' notes ("[area] note"), in file order.
	// Populated at full depth only.
	Notes []string `json:"notes,omitempty"`
}

BriefFeedback is the §4d feedback view for the screen: the feedback file (when one exists) plus the open (unresolved) annotation notes. Notes carries the annotation text at full depth and is empty at summary depth.

type BriefFlowEdge

type BriefFlowEdge struct {
	// Flow is the flow file (workspace-relative, e.g. design/flows/sign-up.mmd).
	Flow string `json:"flow"`
	// FlowName is the flow's stem (e.g. "sign-up").
	FlowName string `json:"flowName"`
	// Source / Target are the edge endpoints (node ids == wireframe stems).
	Source string `json:"source"`
	Target string `json:"target"`
	// Direction is "in" (the screen is the target), "out" (the screen is the
	// source), or "both" (a self-edge). It is the §5g "in/out" split.
	Direction string `json:"direction"`
	// Trigger is the edge label, i.e. what causes the transition (§5g
	// "triggers"). "" when the edge carries no label.
	Trigger string `json:"trigger,omitempty"`
	// OtherStem is the far endpoint of the edge (the node the screen moves to,
	// or comes from). "" for a self-edge.
	OtherStem string `json:"otherStem,omitempty"`
	// OtherLabel is the far endpoint's node label, when the flow declared one.
	OtherLabel string `json:"otherLabel,omitempty"`
}

BriefFlowEdge is one flow edge touching the briefed screen (§5g "flows in/out with triggers"). Trigger is the mermaid edge label — the `-- "tap Submit" -->` form carries the trigger semantics per the design manifest contract; a labelled edge whose label is empty reports "".

type BriefTokenRef

type BriefTokenRef struct {
	// Path is the dotted token path (e.g. "color.brand.primary").
	Path string `json:"path"`
	// Known is true when the reference resolves to a DTCG leaf in
	// design/tokens/.
	Known bool `json:"known"`
}

BriefTokenRef is one DTCG token a wireframe refers to, resolved from the wireframe's `{group.token}` comments (the SP-140-4 §4b convention). Known reports whether the reference resolves to a real DTCG leaf.

type Confidence

type Confidence = string

Confidence is a coarse automation gate on a delta. Literal and structural deltas are safe to apply; inferred deltas are proposals and carry a lower confidence.

const (
	ConfidenceHigh   Confidence = "high"
	ConfidenceMedium Confidence = "medium"
	ConfidenceLow    Confidence = "low"
)

Confidence values. High means "safe to apply mechanically", medium means "mapped to a known design artifact with reasonable certainty", low means "a proposal a human or the agent should confirm".

type ContentReader

type ContentReader interface {
	ReadFile(path string) ([]byte, error)
}

ContentReader is the optional companion to Scanner: when implemented, the scan reads file bodies through it instead of os.ReadFile. FSScanner does not implement it (os.ReadFile is correct for real paths); the test fake does.

type DeltaBasis

type DeltaBasis = deltaBasis

DeltaBasis is the basis of one semantic delta (SP-140-5 §5b). It governs automation: literal and structural deltas are the safe-to-apply subset; inferred deltas are proposals.

type DeltaKind

type DeltaKind = string

DeltaKind is the design-tier class a delta belongs to (SP-140-5 §5b: token|wireframe|flow|feedback).

const (
	DeltaKindToken     DeltaKind = "token"
	DeltaKindWireframe DeltaKind = "wireframe"
	DeltaKindFlow      DeltaKind = "flow"
	DeltaKindFeedback  DeltaKind = "feedback"
)

Delta kinds, fixed by SP-140-5 §5b.

type DesignStatus added in v0.18.9

type DesignStatus struct {
	Exists     bool             `json:"exists"`
	Validation StatusValidation `json:"validation"`
	// Drift carries the two §5c rows in fixed order plus the overall synced
	// flag (true when neither direction is ahead).
	Drift struct {
		DesignAhead StatusDriftRow `json:"designAhead"`
		CodeAhead   StatusDriftRow `json:"codeAhead"`
		Synced      bool           `json:"synced"`
	} `json:"drift"`
	Feedback StatusFeedback `json:"feedback"`
	// TokenRefs maps a dotted token path ({color.brand.primary}) to the number
	// of lexical alias references to it across design/tokens/*.tokens.json —
	// the SP-140-7 §7c alias-warning input ("N tokens reference this").
	// Unreferenced tokens are absent. Deterministic via JSON map marshalling.
	TokenRefs map[string]int `json:"tokenRefs,omitempty"`
	// Summary is the one-line human/agent-readable rollup for logs and titles.
	Summary string `json:"summary"`
}

DesignStatus is the full GET /api/design/status payload. Exists=false (no design/ directory) is reported with a 200 and zeroed sections — the health strip renders "no design tree", not an error (§6b).

func BuildDesignStatus added in v0.18.9

func BuildDesignStatus(root string, touched []SyncFileInput) *DesignStatus

BuildDesignStatus computes the §6b status for the workspace at root. touched is the code-ahead evidence set: an HTTP read has none (pass nil), and the code-ahead row then reports synced/not-assessable with its evidence — the honest tree-only answer (§5c). The call never fails: scanner errors become zeroed sections or synthetic findings, never an error return, so the endpoint has one shape.

type DirtyAliasError

type DirtyAliasError struct {
	// Violations are the offending aliases, one per (rule, token) pair.
	Violations []AliasViolation
}

DirtyAliasError is the structured refusal returned by ResolveExportTokens (wrapping ErrDirtyAliasGraph) when the alias graph is dirty. Violations are sorted by token path so the message and the structured detail are deterministic across runs and file read orders.

func (*DirtyAliasError) Error

func (e *DirtyAliasError) Error() string

Error renders the refusal so a model reads which token is dirty without parsing structured data. It lists every offending token in path order.

func (*DirtyAliasError) Unwrap

func (e *DirtyAliasError) Unwrap() error

Unwrap lets errors.Is(err, ErrDirtyAliasGraph) recognize the structured refusal, so the handler can branch on the sentinel without a type assertion.

type DriftDirectionRow

type DriftDirectionRow struct {
	// Direction is design-ahead | code-ahead (the stable row id).
	Direction string `json:"direction"`
	// Ahead is true when this direction actually holds (there is drift here).
	Ahead bool `json:"ahead"`
	// Synced is true when this direction has nothing ahead — the complement of
	// Ahead, explicit so a model reads "no drift" without a negation.
	Synced bool `json:"synced"`
	// Count is the concrete magnitude of the drift: screens ahead of the build
	// (design-ahead) or semantic deltas to import (code-ahead). 0 when Synced
	// or when nothing countable was found.
	Count int `json:"count"`
	// Summary is the one-line description of what is ahead ("2 screens ahead
	// of the exported theme"). Empty when Synced.
	Summary string `json:"summary,omitempty"`
	// Remedy is the run-me pointer for this direction (§5c): regenerate the
	// theme (design-ahead) or run design_sync (code-ahead). Empty when Synced.
	Remedy string `json:"remedy,omitempty"`
	// NextStep is the tool-level pointer the remedy names (the design_export_tokens
	// or design_sync call), stable so a consumer can route on it. Empty when Synced.
	NextStep string `json:"nextStep,omitempty"`
	// Advisory is the severity class: info (nothing to do) or warn (a remedy to
	// run). Drift is signal, never an error (§5c), so this is never "error".
	Advisory string `json:"advisory"`
}

DriftDirectionRow is one direction row of the drift report (§5c). Both directions always produce a row; a row with nothing to report reads Synced=true with an empty Count and Remedy, so the two-row shape is stable.

func DriftDirectionRowFor

func DriftDirectionRowFor(report *DriftReport, direction string) DriftDirectionRow

DriftDirectionRowFor returns the report's row for one direction, or a zero row when the direction is unknown. Both handlers use it so a consumer reads a direction by name rather than by slice index.

type DriftReport

type DriftReport struct {
	// Rows are the two direction rows, design-ahead then code-ahead.
	Rows []DriftDirectionRow `json:"rows"`
	// DesignAheadCount / CodeAheadCount mirror the rows for a reader that does
	// not want to index Rows.
	DesignAheadCount int `json:"designAheadCount"`
	CodeAheadCount   int `json:"codeAheadCount"`
	// Synced is true when neither direction is ahead.
	Synced bool `json:"synced"`
	// Evidence carries how each count was computed (the generated provenance
	// hash basis, the touched-file basis, or the offline fallback), so a model
	// can judge the confidence of a count. Deterministic order.
	Evidence []string `json:"evidence,omitempty"`
}

DriftReport is the §5c drift-direction view: the two direction rows (always both, always in DriftRowDesignAhead, DriftRowCodeAhead order) plus the mapping of each row onto a design_validate finding, so both reporting surfaces share one vocabulary. It is deterministic: the rows are fixed in order and every count is derived from a sorted walk.

func AnalyzeDrift

func AnalyzeDrift(root string, touched []SyncFileInput) *DriftReport

AnalyzeDrift computes the §5c drift report for one workspace.

It is deliberately cheap and deterministic — it is called by both design_assets and design_validate, tools a turn runs freely — and it never fails a caller: a workspace with no design/ tree yields a synced report rather than an error (the design_assets handler's {exists:false} path is the scaffold signal; drift is only meaningful once a tree exists).

The two signals:

  • design-ahead — the design/ tree versus the exported provenance. When design/generated/ carries the §5a source-hash provenance header, the current token-input hash is recomputed and compared: a difference means the tokens moved after the last export, so the generated theme is behind. Screens/wireframes give the count ("N screens ahead of the build"). Without a generated/ tree the whole tree counts as ahead — the design side exists and nothing has been exported from it yet.

  • code-ahead — the implementation versus the semantic layer. When the caller knows the touched code files (design_validate can ask the ChangeTracker; a standalone design_assets run passes none), the §5b analysis is reused verbatim: the delta count is exactly what design_sync would import, restricted to the literal/structural subset design_sync can actually adopt. Without a touched set, whether the implementation is ahead is simply not knowable from the tree alone, so the row reports synced and Evidence says so — the remedy direction is unchanged, the count is just not claimable. This is core-owned deliberately: the pkg/agent_tools layer supplies the touched files, the semantics live here.

type ExportTarget

type ExportTarget struct {
	Name string
	File string
}

ExportTarget is one resolved export target: its identifier and its design-relative file name.

func ResolveExportTargets

func ResolveExportTargets(raw string) ([]ExportTarget, error)

ResolveExportTargets maps a `targets` argument value to the ordered target list to run. An empty value or "all" selects every target in ExportTargets order. A value may be a single target ("css") or a comma-separated list ("css, ts"); each entry is trimmed and lowercased. Unknown targets are an error naming the accepted set, so a typo never silently exports nothing.

type ExportedArtifact

type ExportedArtifact struct {
	// Target is the exporter that produced it (css, ts, ...).
	Target string
	// RelPath is the workspace-relative slash path (design/generated/<file>).
	RelPath string
	// Content is the exact byte content written.
	Content []byte
	// Hash is the deterministic content hash (see tokenExportArtifactHash).
	Hash string
}

ExportedArtifact is one written (or to-be-written) generated file.

func RenderArtifacts

func RenderArtifacts(tokens *TokenExport, targets []ExportTarget) ([]ExportedArtifact, error)

RenderArtifacts renders every target into memory without touching the filesystem, in canonical target order. The handler uses this to precheck and write each file; tests use it to assert byte-identical output.

func RenderScreensIndexArtifact added in v0.18.16

func RenderScreensIndexArtifact(root string) (ExportedArtifact, error)

RenderScreensIndexArtifact derives, renders, and hashes the screens.json artifact for the export tool's `screens` target: the deterministic bytes plus the content hash the handler reports per file. The RelPath is the canonical design/generated/ location; the handler's relocation logic owns any out_dir override.

type ExportedToken

type ExportedToken struct {
	// Name is the dotted token path (e.g. "color.brand.primary").
	Name string
	// Type is the leaf's $type (one of the nine DTCG types).
	Type string
	// Value is the raw (unresolved) $value.
	Value any
	// Resolved is the transitive alias resolution of Value. For a whole-value
	// alias it is the final non-alias $value; for a plain value it is Value
	// itself; for a value with no final scalar (an alias to a group, an alias
	// chain that cannot end at a string, or a non-string structured value) it
	// is nil and ResolvedOK is false.
	Resolved any
	// ResolvedOK reports whether Resolved is usable.
	ResolvedOK bool
	// HasAlias is true when Value (or any link in its alias chain) is a
	// whole-value alias reference. Targets use it to decide between a literal
	// and a var()/token() reference.
	HasAlias bool
	// AliasPath is the dotted path of the first whole-value alias in the
	// chain (from Value itself, or the deepest resolvable link), used for
	// cross-token references. "" when HasAlias is false.
	AliasPath string
}

ExportedToken is one DTCG token leaf prepared for export.

type FSScanner

type FSScanner struct{}

FSScanner is the production Scanner backed by os.Stat and filepath.WalkDir.

func (FSScanner) HasAnyDir

func (FSScanner) HasAnyDir(roots ...string) bool

HasAnyDir reports whether any root exists and is a directory.

func (FSScanner) HasAnyFile

func (FSScanner) HasAnyFile(paths ...string) bool

HasAnyFile reports whether any path exists and is a regular file.

func (FSScanner) WalkDir

func (FSScanner) WalkDir(root string, fn func(path string, d os.DirEntry, err error) error) error

WalkDir delegates to filepath.WalkDir; missing roots surface the usual *fs.PathError to the callback.

type FeedbackFileState

type FeedbackFileState struct {
	// Path is the workspace-relative slash path of the feedback file
	// (e.g. "design/feedback/login.json").
	Path string `json:"path"`
	// Target is the §4d target (e.g. "design/wireframes/login.svg"); empty
	// when the file is invalid or omits it.
	Target string `json:"target,omitempty"`
	// Status is the §4d top-level status ("changes-requested", "resolved",
	// …); empty when absent.
	Status string `json:"status,omitempty"`
	// Resolution is the agent's closing note; empty while the loop is open.
	Resolution string `json:"resolution,omitempty"`
	// Annotations is the total annotation count.
	Annotations int `json:"annotations"`
	// Resolved is the count of annotations with `resolved: true`.
	Resolved int `json:"resolved"`
	// Pending is the count of annotations with `resolved: false` (i.e.
	// Annotations - Resolved); it is the count the agent must address.
	Pending int `json:"pending"`
	// Valid reports whether the file parsed as the §4d document shape.
	Valid bool `json:"valid"`
}

FeedbackFileState is the parsed, reporting-oriented view of one design/feedback/*.json file: the §4d fields plus the pending/resolved annotation counts the design_assets pending-feedback section needs.

Parse is lenient by design — a malformed document yields Valid=false and the zero counts rather than an error, so a reporting run never fails on one bad file (the validator, not the reader, is where a bad file becomes a finding).

func ScanFeedbackDir

func ScanFeedbackDir(root string) ([]FeedbackFileState, error)

ScanFeedbackDir reads every design/feedback/*.json file under root and returns the parsed §4d state for each, sorted by path. A missing feedback directory yields an empty, non-nil slice; a real I/O failure reading the directory is returned as an error. Individual unreadable files are skipped rather than failing the walk: reporting must not break on one bad file.

func (FeedbackFileState) IsPending

func (f FeedbackFileState) IsPending() bool

IsPending reports whether this feedback file still needs the agent's attention: its status is "changes-requested" OR it carries at least one annotation with `resolved: false` (§4d — the two conditions the design-system skill's loop starts on). A file that is not valid is never pending.

type Finding

type Finding struct {
	// File is the design workspace path the finding applies to.
	File string

	// Line is the 1-based source line; 0 means the line was not
	// derivable.
	Line int

	Severity Severity
	Message  string
	Rule     string
}

Finding is one structured validator result for a single design file ({file, line?, severity, message, rule}, SP-140-1 §1g). Every design validator rule emits it: the token rules per §1a, and the later SVG/mermaid/README rules per §1g.

func DriftDirectionFindings

func DriftDirectionFindings(report *DriftReport) []Finding

DriftDirectionFindings maps a drift report onto design_validate findings: one advisory finding per direction row that is AHEAD (§5c: both directions are reported, and the tools share the same rows). A synced direction emits no finding — the always-present two-row shape lives in the structured drift section, and a "nothing to do" finding on every clean run would be noise rather than signal. When a direction is ahead its finding names the count and the remedy, so the validator surface carries the run-me pointer too.

Severity is advisory by construction: ahead rows are warns (an actionable remedy), never errors. Drift is signal, not guilt (§5c).

func ValidateBrandDir

func ValidateBrandDir(root string) []Finding

ValidateBrandDir validates design/brand/brand.md under root, SP-140-1 §1d. A missing brand directory or brand.md yields no findings (not an error) — a whole-tree run must not fail on workspaces without a brand file.

func ValidateBrandFile

func ValidateBrandFile(relPath string, content []byte) []Finding

ValidateBrandFile validates one brand.md file. A raw hex color is a warn finding (reference a token instead); a brand file with no token references is advisory (info). The result is never nil.

func ValidateComponent added in v0.18.12

func ValidateComponent(relPath string, content []byte) []Finding

ValidateComponent validates one design/components/*.svg component spec, SP-140-1 §1b applied to the component tier: a self-contained, low-fidelity SVG that shows one reusable UI component in its key variants and states. relPath is the design-relative path (e.g. "design/components/button.svg").

Components are the composable layer beneath screens: a screen wireframe is a composition of components, so a component spec carries no navigation semantics (no data-nav check) and its viewBox is the component's bounding box, not a device frame (no frame-match check). Everything else the wireframe contract requires applies unchanged: well-formed XML, a root <svg> with an integer viewBox, self-containment, the slug name rule, <text> labels (advisory), data-URI size (advisory), and token-usage comments (advisory).

Hard checks (SeverityError): well-formedness, root/viewBox, self- containment, slug name. Advisory (info/warn): text usage, data-URI size, token usage. The result is never nil.

func ValidateComponentInventory added in v0.18.12

func ValidateComponentInventory(root string) []Finding

ValidateComponentInventory runs the component-inventory pack over the whole design tree under root, SP-140-4 §4b "Component inventory": every component stem must appear in the README's Components listing. A component that appears there is orphaned — it exists on disk but the manifest does not track it — and is surfaced as `info`. Components are referenced by screen compositions (README Composition table, data-component attributes) rather than by flows, so the README listing is their only reference channel; unlike screens, a flow reference does not rescue a component.

A workspace with no components yields no findings; the result is sorted and never nil.

func ValidateComponentsDir added in v0.18.12

func ValidateComponentsDir(root string) ([]Finding, error)

ValidateComponentsDir validates every design/components/*.svg under root. A missing or empty components directory yields no findings, not an error — a whole-tree validator run must not fail on workspaces without a component tier. Findings are sorted by file, line, rule, message; errors are I/O failures only.

func ValidateConsistency

func ValidateConsistency(root string) []Finding

ValidateConsistency runs the cross-artifact consistency packs over the whole design tree under root, SP-140-4 §4b. It covers the cross-artifact checks the per-artifact validators cannot see:

  • ruleConsistencyFlowEdgeWireframe: every non-terminal node referenced by a flow edge has a wireframe counterpart (a terminal state is exempt);
  • ruleManifestLinkDangling: every artifact named by the README manifest's Screens/Flows listings maps to a real file (a Screens entry to a wireframe or screen, a Flows entry to a flow or wireframe).

The third bullet of §4b ("every data-nav target exists") is the existing ruleSVGDataNavDangling hard rule (svg.go); it is emitted through ValidateWireframesDir/ValidateWireframe and is covered by this pack's tests rather than re-implemented here.

It also runs the screen-inventory and naming packs of §4b (orphan screens, screens/ ↔ wireframes/ name mismatches, duplicate screen names), which live in inventory_rules.go alongside this pack because they share the same cross-artifact dispatch point and finding schema. A workspace with no design/ tree yields no findings. Findings are sorted by file, line, rule, message; the result is never nil.

func ValidateDataURISizes

func ValidateDataURISizes(relPath string, content []byte) []Finding

ValidateDataURISizes scans one SVG body for data: URIs above DataURISizeWarnThreshold and returns one warn finding per oversized URI (SP-140-1 §1h binary hygiene). lineHint, when positive, is the 1-based line of the first data: URI occurrence in the document, used to point the finding at the embedded raster. The result is never nil.

func ValidateFeedbackDir

func ValidateFeedbackDir(root string) ([]Finding, error)

ValidateFeedbackDir validates all design/feedback/*.json files under root, SP-140-4 §4d: the file parses as a feedback JSON object (hard `feedback_json` when it does not), target is non-empty (hard `feedback_target_ref`), each annotation id is non-empty and unique (`feedback_annotation_id`, advisory), each annotation note is non-empty (`feedback_annotation_note`, advisory), and the target resolves to a known wireframe/screen/flow stem (`feedback_target_dangling`, advisory). A missing feedback directory yields no findings. Findings are sorted by file, line, rule, message.

func ValidateFile

func ValidateFile(root, relPath string) ([]Finding, error)

ValidateFile validates one design asset under root, SP-140-1 §1g. relPath is workspace-relative (e.g. "design/wireframes/login.svg"); a path that does not start with design/ is treated as relative to design/ (so "wireframes/login.svg" also resolves). The path must stay under design/ and the file must exist.

Dispatch is by location + extension: tokens/*.tokens.json, wireframes/*.svg, components/*.svg, icons/*.svg, flows/*.mmd, screens/*.html, README.md, brand/brand.md, and the repository-level git-contract files .gitattributes and .gitignore (§1h). Anything else is an error, not a silent pass. The result is never nil.

func ValidateFlowSource added in v0.18.17

func ValidateFlowSource(relPath string, content []byte, stems []string) []Finding

ValidateFlowSource validates one flow source document (SP-140-9 §9b). relPath is the workspace-relative slash path; stems is the transitional screen-stem union from 9.1 (wireframe OR screen stems) that step screens resolve against. Hard checks (SeverityError): the document parses strictly, the name is a slug matching the file stem, step ids are unique, every non-empty next targets an existing step, a step is terminal exactly when its next is empty, and every screen resolves to a known stem. The result is never nil and sorted.

func ValidateFlows

func ValidateFlows(root, relPath string, content []byte, wireframeStems []string, screenStems ...string) []Finding

ValidateFlows validates one mermaid flow file per SP-140-1 §1c/§1g, adjusted by SP-140-9 §9a: screen-flow node ids may equal either a wireframe stem or a delivered-screen stem during the format migration (9.4 collapses the tier; post-migration only screens remain). Hard checks (SeverityError): exactly one flowchart declaration, unparseable statement lines, and screen-flow node ids matching neither tier. The result is never nil.

func ValidateFlowsDir

func ValidateFlowsDir(root string) ([]Finding, error)

ValidateFlowsDir validates every design/flows/*.mmd under root, SP-140-1 §1c/§1g. Wireframe stems are gathered from design/wireframes/*.svg so the node-id == stem rule resolves. A missing or empty flows directory yields no findings, not an error. Findings are sorted by file, line, rule, message.

func ValidateFlowsTree added in v0.18.17

func ValidateFlowsTree(root string) []Finding

ValidateFlowsTree runs the §9b whole-tree flow checks on top of the per-file flow rules: for every flow source document, the .json schema rules; for every flows/*.mmd, the derived-drift check (a source + a header-less or stale .mmd) or the legacy notice; and the off-path cross-check for .json-bearing trees. Called from ValidateTree after the per-artifact passes. The result is never nil and sorted.

func ValidateGitAttributesContent

func ValidateGitAttributesContent(relPath string, content []byte) []Finding

ValidateGitAttributesContent checks one .gitattributes body against the SP-140-1 §1h contract, for callers (and single-file runs) that already hold the content. The result is never nil.

func ValidateGitContract

func ValidateGitContract(root string) []Finding

ValidateGitContract runs the SP-140-1 §1h git-contract checks for the workspace at root:

  • .gitattributes must carry the exact GitAttributesDiffHTMLLine; a missing file or a missing line yields a fix finding with the line to append (appending never clobbers the repo's existing text/binary rules). A line already present, however it is qualified (e.g. "design/**/*.svg text diff=html"), satisfies the contract.
  • .gitignore must ignore design/.cache/ only; a missing file or line yields a fix finding with the line to append, and ignoring design/generated/ yields an advisory info finding.

A workspace with no design/ tree yields no findings: the contract belongs to the design tree, so a workspace without one is not a violation (matches the "missing design/ is not an error" rule of ValidateTree). Findings are sorted and the result is never nil.

func ValidateGitIgnoreContent

func ValidateGitIgnoreContent(relPath string, content []byte) []Finding

ValidateGitIgnoreContent checks one .gitignore body against the SP-140-1 §1h ignore policy: design/.cache/ is required (fix), design/generated/ is advisory (info). The result is never nil.

func ValidateIconsDir

func ValidateIconsDir(root string) ([]Finding, error)

ValidateIconsDir validates every design/icons/*.svg under root, SP-140-1 §1f: self-containment (no <script>, no external/local refs), the slug name rule, and — for the optional sprite.svg — <symbol> entries whose ids follow the slug rule. A missing or empty icons directory yields no findings, not an error. Findings are sorted by file, line, rule, message.

func ValidateInventory

func ValidateInventory(root string) []Finding

ValidateInventory runs the screen-inventory pack over the whole design tree under root, SP-140-4 §4b "Screen inventory": every wireframe stem must appear in either a flow (as a node) or the README (as a Screens listing). A stem that appears in neither is an orphan screen, surfaced as `info` — the screen exists on disk but nothing navigates to it or tracks it.

The tree's per-artifact rules stay with their own validators; this pack only reads the stems and the cross-artifact references. A workspace with no design/ tree yields no findings; the result is sorted and never nil.

func ValidateManifest

func ValidateManifest(root string) []Finding

ValidateManifest validates the design README (design/README.md) under root, SP-140-1 §1e/§1g: the frames: block parses as name → WxH (a missing, empty, or malformed block is a hard finding), every relative link resolves to a real design asset (hard finding), and the draft/review/ready status markers are present (advisory). A missing README yields no findings (not an error). Findings are sorted and the result is never nil.

func ValidateScreensDir

func ValidateScreensDir(root string) ([]Finding, error)

ValidateScreensDir validates every design/screens/*.html under root, SP-140-1 §1i/§1g. Device frames are read from the design README (when present) to enable the advisory width check. A missing or empty screens directory yields no findings, not an error. Findings are sorted by file, line, rule, message.

func ValidateScreensIndex added in v0.18.16

func ValidateScreensIndex(root string) ([]Finding, error)

ValidateScreensIndex runs the SP-143 §143.5 index/runtime checks: the screens.json drift (rule d) and the runtime source-hash (rule c). It is a whole-tree check (the index is one artifact spanning every screen), called from ValidateTree; a tree with no screens tier contributes nothing.

func ValidateTokens

func ValidateTokens(relPath string, content []byte) []Finding

ValidateTokens validates one DTCG token file per SP-140-1 §1a: JSON parse, leaf/group structure, $type membership, and within-file alias resolution. Alias checks are per-file by design: tiers are self-contained, and cross-file aliasing is not specified.

The returned findings carry File=relPath and SeverityError — every token rule is a hard violation — sorted by file, line, rule, message. The result is never nil.

func ValidateTokensDir

func ValidateTokensDir(root string) ([]Finding, error)

ValidateTokensDir validates every design/tokens/*.tokens.json under root, SP-140-1 §1a. A missing or empty tokens directory yields no findings, not an error — a whole-tree validator run must not fail on workspaces without tokens (or without design/ at all).

Files are validated in glob order (filepath.Glob is lexical); the combined findings are sorted by file, line, rule, message. Errors are I/O failures only — per-file rule violations are findings, not errors.

func ValidateTree

func ValidateTree(root string) ([]Finding, error)

ValidateTree validates the whole design/ tree under root, SP-140-1 §1g: every §1.x whole-tree validator runs (tokens, wireframes, flows, screens, icons, brand, README manifest), plus the §1h git contract (.gitattributes diff rule, .gitignore cache policy) and the §4d human feedback channel (design/feedback/*.json), and the combined findings are sorted by file, line, rule, message. root is the workspace root (the parent of design/).

A missing or partial design/ tree yields empty findings, not an error — a whole-tree run must not fail on workspaces without design assets. Errors are real I/O failures only (an unreadable file that matches a glob, for example); rule violations are findings, never errors. The result is never nil.

func ValidateWireframe

func ValidateWireframe(relPath string, content []byte, knownStems []string, frames []Frame) []Finding

ValidateWireframe validates one SVG wireframe per SP-140-1 §1b. relPath is the design-relative path (e.g. "design/wireframes/login.svg"); knownStems is the set of all wireframe file stems (without .svg) used to resolve data-nav targets; frames (optional) enables the advisory frame-match check and is nil for runs without declared device frames.

Hard checks (SeverityError): XML well-formedness, a root <svg> with an integer viewBox, self-containment (no <script>, no external/local resource refs), the slug name rule, and data-nav targets resolving to a wireframe stem. Advisory checks (SeverityInfo): frame match, <text> usage, and stable ids on interactive elements. The result is never nil.

func ValidateWireframesDir

func ValidateWireframesDir(root string) ([]Finding, error)

ValidateWireframesDir validates every design/wireframes/*.svg under root, SP-140-1 §1b/§1g. All wireframe stems are gathered first so data-nav targets resolve across files; device frames are read from the design README (when present) to enable the advisory frame-match check. A missing or empty wireframes directory yields no findings, not an error — a whole-tree validator run must not fail on workspaces without wireframes.

Files are validated in glob order; the combined findings are sorted by file, line, rule, message. Errors are I/O failures only — per-file rule violations are findings, not errors.

type FlowCounts

type FlowCounts struct {
	Path  string `json:"path"`
	Name  string `json:"name"`
	Nodes int    `json:"nodes"`
	Edges int    `json:"edges"`
}

FlowCounts is the node/edge count of one design/flows/*.mmd file (SP-140-2 §2c "flow node/edge counts").

type FlowEdge

type FlowEdge struct {
	Source string
	Target string
	// Label is the |trigger| text on a labeled edge (A -->|trigger| B);
	// "" on a label-free edge. SP-140-9 §9b renders the derived .mmd's
	// triggers as edge labels, so the parser surfaces them instead of
	// dropping them.
	Label    string
	HasArrow bool
}

FlowEdge is a single directed/undirected edge extracted from a flowchart.

type FlowExportDoc added in v0.18.17

type FlowExportDoc struct {
	// Name is the flow name (= the source document stem).
	Name string
	// Direction is the mermaid layout direction (always defaultFlowDirection
	// today).
	Direction string
	// Nodes are the graph nodes: the walk steps in source order first, then
	// the off-path screens sorted by stem.
	Nodes []FlowExportNode
	// Edges are the graph edges: the step-to-step walk edges in step order
	// first, then the off-path nav edges sorted by (source, trigger, target).
	Edges []FlowExportEdge
}

FlowExportDoc is the derived graph of one flow: the walk (the steps) plus every data-nav-reachable screen marked off-path. It is the render model of the .mmd — everything RenderFlowMMD emits, nothing else.

func DeriveFlowMMD added in v0.18.17

func DeriveFlowMMD(src *FlowSource, screens *ScreenIndexDoc) *FlowExportDoc

DeriveFlowMMD derives the export doc for one flow source against the screens' data-nav graph (the parsed screens index). A step screen unknown to the index contributes no nav edges — the screen validator reports the unknown stem, and the export still renders the walk. Self-loop navs (a screen navigating to itself) are suppressed: they are state navigation inside one screen, not graph edges.

type FlowExportEdge added in v0.18.17

type FlowExportEdge struct {
	Source string
	// Target is the target node id (a step id or an off-path screen stem).
	Target string
	// Trigger is the edge label: the step's trigger for walk edges, the
	// data-nav trigger for off-path edges; "" renders an unlabeled edge.
	Trigger string
	// OffPath marks an edge the flow steps do not account for (a data-nav
	// edge reached by closure, not by a step).
	OffPath bool
}

FlowExportEdge is one edge of the derived graph.

type FlowExportNode added in v0.18.17

type FlowExportNode struct {
	// ID is the §9c stable node id: the step id for walk nodes, the screen
	// stem for off-path nodes.
	ID string
	// Label is the rendered label: the step's label text, or the screen stem.
	Label string
	// Screen is the stem of the screen the node stands for; "" for a
	// non-screen flow's step (agent-turn walks no surface) and for pure
	// walk nodes whose screen is only implied by the step.
	Screen string
	// OffPath marks a data-nav-reachable screen that is not a step.
	OffPath bool
}

FlowExportNode is one node of the derived graph.

type FlowExportedArtifact added in v0.18.17

type FlowExportedArtifact struct {
	// Name is the flow name (the .json stem).
	Name string
	// RelPath is the workspace-relative slash path (design/flows/<name>.mmd).
	RelPath string
	// Content is the exact byte content to write.
	Content []byte
	// Hash is the §9b input hash carried in the provenance header.
	Hash string
}

FlowExportedArtifact is one rendered derived .mmd ready to write.

func RenderAllFlowMDMArtifacts added in v0.18.17

func RenderAllFlowMDMArtifacts(root string) ([]FlowExportedArtifact, error)

RenderAllFlowMDMArtifacts derives the export artifact for every flow source under root, in flow-name order.

func RenderFlowMDMArtifact added in v0.18.17

func RenderFlowMDMArtifact(root string, name string) (*FlowExportedArtifact, error)

RenderFlowMDMArtifact derives one flow's export artifact under root: read the flow source, read the screens, derive, render, hash.

type FlowNode

type FlowNode struct {
	ID    string
	Label string
}

FlowNode is a single node extracted from a mermaid flowchart.

type FlowSource added in v0.18.17

type FlowSource struct {
	// Name is the flow's slug; it must equal the document's file stem
	// (design/flows/sign-up.json names itself "sign-up"), the same
	// identity-by-location rule the rest of the tree uses.
	Name string `json:"name"`
	// Steps are the process steps in walk order. The first step is the
	// entry; a step with an empty Next is terminal.
	Steps []FlowStep `json:"steps"`
}

FlowSource is the parsed v1 flow source document.

func ParseFlowSource added in v0.18.17

func ParseFlowSource(relPath string, content []byte) (*FlowSource, error)

ParseFlowSource parses one flow source document strictly: it must be a JSON object with exactly the v1 fields, a slug name, and at least one step. Structural problems (bad JSON, unknown fields, empty required fields) come back as a single error whose message names the fault — the validator renders it as a finding; the export refuses on it. Name-vs-filename is the caller's check (it owns the path).

type FlowSourceFile added in v0.18.17

type FlowSourceFile struct {
	RelPath string
	Name    string
	Raw     []byte
	Source  *FlowSource
}

FlowSourceFile is one flow source document as read from disk: its workspace-relative slash path, its stem (the flow name), its exact bytes, and the parsed source. Produced by ReadFlowSources.

func ReadFlowSources added in v0.18.17

func ReadFlowSources(root string) ([]FlowSourceFile, error)

ReadFlowSources reads and parses every flow source under root, sorted by flow name. A malformed document is an error naming the file — the export refuses on it; the validator reports it as a finding instead (via ValidateFlowSource) and never needs this strictness.

type FlowStep added in v0.18.17

type FlowStep struct {
	// ID is the step's stable node id (§9c): the derived .mmd and the layout
	// sidecar key on it, and a regeneration must not change it.
	ID string `json:"id"`
	// Label is the human step name rendered as the node label.
	Label string `json:"label"`
	// Screen is the stem of the screen the step happens on. It resolves to
	// a screen stem or — during the §9a migration — a wireframe stem.
	// Optional: a non-screen flow's steps carry no screen (SP-140-9 §9b's
	// agent-turn example walks plan → tools → review with no surface).
	Screen string `json:"screen,omitempty"`
	// Trigger is the label on the edge to the next step; "" on a terminal
	// step (and rendered as a plain unlabeled edge when Next is set without
	// a trigger).
	Trigger string `json:"trigger,omitempty"`
	// Next is the id of the following step; "" marks the step terminal.
	Next string `json:"next,omitempty"`
}

FlowStep is one step of a FlowSource.

type Flowchart

type Flowchart struct {
	Direction    string
	Declarations int // number of flowchart/graph declaration lines
	Nodes        map[string]FlowNode
	Edges        []FlowEdge
	NodeOrder    []string // first-seen order of node ids
	BadLines     []int    // 1-based line numbers of statements that could not be parsed
}

Flowchart is the result of parsing a mermaid flowchart file (the flowchart subset — node/edge extraction, not full syntax fidelity, SP-140-1 §1c).

func ParseFlowchart

func ParseFlowchart(content string) Flowchart

ParseFlowchart parses a mermaid flowchart document and extracts its nodes and edges. It is a subset parser: it handles flowchart/graph declarations, node definitions (id plus any shape/label), and edge statements (including chained A --> B --> C forms), while skipping comments, subgraph/end, and styling keywords (classDef/class/style/linkStyle/click). Full syntax fidelity is intentionally not required (SP-140-1 §1c).

type Frame

type Frame struct {
	Name   string
	Width  int
	Height int
}

Frame is a device frame declared in the manifest's frames block.

func ParseFrames

func ParseFrames(content string) ([]Frame, error)

ParseFrames extracts device frames from manifest content. The frames block starts at a non-indented `frames:` line and continues through indented `name: WxH` entries until the first non-indented, non-blank line (or end of input). Blank lines inside the block are ignored, and any indentation depth counts as an entry line. Frames are returned in declaration order.

A manifest without a frames block yields an empty slice and no error; frame presence is validated separately. A frames block with no entries, duplicate names (checked across all blocks in the content), a frame name not matching the slug rule, a non-integer width or height, a non-lowercase-x separator, or non-positive dimensions is an error carrying the 1-based line number.

type Inventory

type Inventory struct {
	Assets      []AssetRow        `json:"assets"`
	TokenGroups []TokenGroupCount `json:"tokenGroups"`
	Flows       []FlowCounts      `json:"flows"`
	Manifest    ManifestSummary   `json:"manifest"`
	Findings    []Finding         `json:"findings"`
	BySeverity  map[string]int    `json:"bySeverity"`
}

Inventory is the structured `design_assets` result for a workspace that has a design/ tree (SP-140-2 §2c). Assets holds one row per discovered asset, sorted by path. TokenGroups holds per-group leaf-token counts sorted by group name. Flows holds per-flow node/edge counts sorted by path. Findings holds the (uncached, always-fresh) validator results for the tree, and BySeverity tallies them.

func Scan

func Scan(root string) (*Inventory, error)

Scan walks the design/ tree under root and builds its inventory (SP-140-2 §2c). root is the workspace root (the parent of design/); it must already contain a design/ directory — callers check FileExists first.

The walk is bounded to the canonical subdirectories; unknown files inside design/ are reported with KindUnkn rather than skipped, so the model sees what is actually there. Findings come from ValidateTree and are advisory: an error-severity finding never makes Scan fail. Only a real I/O failure (an unreadable matching file) returns an error.

type ManifestSummary

type ManifestSummary struct {
	Exists bool     `json:"exists"`
	Frames []Frame  `json:"frames"`
	Status []string `json:"status"`
}

ManifestSummary is the high-level manifest view: whether design/README.md exists, the device frames it declares, and the status markers it documents (SP-140-2 §2c "manifest summary").

type ProviderWordHit

type ProviderWordHit struct {
	File string
	Line int
	Text string
}

ProviderWordHit is one occurrence of a proprietary product name in a scanned file. Line is 1-based; Text is the trimmed offending line so a failure message can point at the exact coupling.

func ScanProviderNames

func ScanProviderNames(root, lang string, fs Scanner) ([]ProviderWordHit, bool, error)

ScanProviderNames walks the design-tier surface for lang (relative to root) — the directory trees plus the explicit files in designTier — and returns every occurrence of a name in ProprietaryNames, sorted by file then line. Absent roots are skipped silently (the webui design directories do not exist until SP-140-3 lands). The second return value reports whether any root for lang existed, so a caller can fail loudly on a vacuous scan instead of passing green over nothing.

type Scanner

type Scanner interface {
	// HasAnyDir reports whether any of the given filesystem paths exists and
	// is a directory.
	HasAnyDir(roots ...string) bool
	// HasAnyFile reports whether any of the given filesystem paths exists and
	// is a regular file.
	HasAnyFile(paths ...string) bool
	// WalkDir visits every regular, non-hidden file under root, honoring
	// SkipDir results. The callback receives the full path and the directory
	// entry so the caller can inspect the mode and control descent.
	WalkDir(root string, fn func(path string, d os.DirEntry, err error) error) error
}

Scanner is the read-only file surface ScanProviderNames needs. Tests inject a fake so the scan contract can be exercised without touching disk; production callers use FSScanner.

type ScreenBrief

type ScreenBrief struct {
	// ScreenName is the requested wireframe stem.
	ScreenName string `json:"screenName"`
	// Depth is the depth the brief was rendered at (summary|full).
	Depth string `json:"depth"`
	// Found reports whether the screen exists in the design tree (a wireframe
	// with that stem, or a feedback file targeting it). An unknown screen
	// yields Found=false with Guidance, never an error.
	Found bool `json:"found"`

	// Purpose is the screen's purpose from the README Screens listing (§5g
	// "README purpose"). "" when the README lists no entry for the screen.
	Purpose string `json:"purpose,omitempty"`
	// Status is the README status marker (draft|review|ready). "" when the
	// README declares none.
	Status string `json:"status,omitempty"`
	// ListedInReadme reports whether the README Screens listing names the
	// screen (the §4b inventory signal: an unlisted wireframe is an orphan).
	ListedInReadme bool `json:"listedInReadme"`

	// Wireframe is the wireframe path (design/wireframes/<stem>.svg). It is
	// always set: the brief names where the wireframe *would* live, so a
	// not-found brief still points the agent at the right file.
	Wireframe string `json:"wireframe"`
	// WireframeExists reports whether that file exists.
	WireframeExists bool `json:"wireframeExists"`

	// FlowsIn / FlowsOut are the flow edges touching the screen, split by
	// direction (§5g "flows in/out with triggers"). A self-edge appears in
	// both. Both are always non-nil.
	FlowsIn  []BriefFlowEdge `json:"flowsIn"`
	FlowsOut []BriefFlowEdge `json:"flowsOut"`

	// TokenPaths are the DTCG tokens the wireframe refers to, sorted by path
	// (§5g "token paths to consume").
	TokenPaths []BriefTokenRef `json:"tokenPaths"`
	// TokenGroups are the top-level token groups available to consume, sorted
	// by group — the tree's token vocabulary, so the agent knows what exists
	// even when the wireframe refers to nothing yet.
	TokenGroups []TokenGroupCount `json:"tokenGroups"`

	// Feedback is the §4d pending-feedback view for the screen (§5g "open
	// feedback annotations"). The matching feedback file is the one whose §4d
	// target resolves to the screen's stem, or — when no target matches — the
	// one whose own file name (stem) equals the screen; the first match by
	// path order wins (ScanFeedbackDir sorts by path).
	Feedback BriefFeedback `json:"feedback"`

	// ScreenFile is the delivered design/screens/<stem>.html path, when one
	// exists (full depth only when it differs from the wireframe).
	ScreenFile string `json:"screenFile,omitempty"`

	// Guidance is a not-found helper: what to do when the screen is unknown
	// ("" when the screen exists).
	Guidance string `json:"guidance,omitempty"`

	// Notes carry run-level caveats (e.g. a tree with no README).
	Notes []string `json:"notes,omitempty"`

	// WritesNothing states the §5g contract explicitly: the brief is read-only
	// advisory context, never a generator. Always true.
	WritesNothing bool `json:"writesNothing"`
}

ScreenBrief is the structured §5g brief for one screen.

func BuildScreenBrief

func BuildScreenBrief(in ScreenBriefInput) (*ScreenBrief, error)

BuildScreenBrief reads the design/ tree under root and returns the §5g brief for the screen. It never writes and never errors on a missing screen or a missing design/ tree: an unknown screen yields Found=false with guidance, and a workspace with no design/ yields the same plus a note. Only a real I/O failure reading a file it must read is returned as an error.

type ScreenBriefInput

type ScreenBriefInput struct {
	// Root is the workspace root (the parent of design/).
	Root string
	// ScreenName is the wireframe stem to brief.
	ScreenName string
	// Depth is summary|full; "" defaults to summary.
	Depth string
}

ScreenBriefInput is the read-only input for BuildScreenBrief.

type ScreenIndexDoc added in v0.18.16

type ScreenIndexDoc struct {
	Provenance    string             `json:"provenance"`
	SourceHash    string             `json:"source-hash"`
	Source        string             `json:"source"`
	FormatVersion int                `json:"format-version"`
	Screens       []ScreenIndexEntry `json:"screens"`
}

ScreenIndexDoc is the whole design/screens/ graph derived from the screens' data-attributes. Screens are sorted by stem; Nav is sorted by (trigger, to) and deduplicated. Everything is derived, so the JSON is byte-identical for identical screen bytes — the determinism the drift check depends on.

func DeriveScreensIndex added in v0.18.16

func DeriveScreensIndex(root string) (*ScreenIndexDoc, error)

DeriveScreensIndex reads design/screens/*.html under root and derives the screens.json document (screen graph + provenance over the input bytes). An empty screens tier is not an error: it yields an empty-document index; the export tool refuses to write an empty one and ValidateScreensIndex treats an absent index as clean for an empty tier, so trees without screens (or without the runtime kit at all) validate clean.

func ReadScreensIndex added in v0.18.16

func ReadScreensIndex(data []byte) (*ScreenIndexDoc, error)

ReadScreensIndex parses the design/generated/screens.json bytes into a ScreenIndexDoc. A file that does not parse yields an error with the JSON fault included, so the drift finding can name the real problem (a hand-edit that broke the JSON, not just a stale index).

type ScreenIndexEntry added in v0.18.16

type ScreenIndexEntry struct {
	// Stem is the screen's file stem (the slug nav targets and the README
	// Screens listing use).
	Stem string `json:"stem"`
	// Device is the screen's data-device attribute, or "" when absent (the
	// desktop default; chrome.css draws no chrome for it).
	Device string `json:"device"`
	// Frame is the px width a container on this screen is sized to when it
	// matches a README-declared frame (advisory per the existing frame rule),
	// else 0. Derived from the same width scan the frame validator runs.
	Frame int `json:"frame"`
	// States are the screen's declared state names (html[data-states]),
	// in declaration order, deduplicated.
	States []string `json:"states"`
	// Nav is the screen's data-nav edges, sorted by (trigger, to).
	Nav []ScreenIndexNav `json:"nav"`
}

ScreenIndexEntry is one screen's derived metadata.

func ParseScreenHTML added in v0.18.16

func ParseScreenHTML(content []byte, frames []Frame) ScreenIndexEntry

ParseScreenHTML derives the screens-index view of one screen document: the html element's data-device / data-states, the body's data-nav edges, and the declared frame a container width matches. It reads data-attributes only — the point of the index is that other tooling never parses HTML at read time, so the one place that does is here.

frames is the README-declared device frame list (may be nil). A container width matching a declared frame width is reported as Frame (the first match in frame order); no match or no frames yields 0.

type ScreenIndexNav added in v0.18.16

type ScreenIndexNav struct {
	To string `json:"to"`
	// Trigger is the authored trigger label; "" when the attribute carries
	// only to:.
	Trigger string `json:"trigger,omitempty"`
}

ScreenIndexNav is one data-nav edge: the target stem and the authored trigger label (`to:<stem>;trigger:<label>`).

type Severity

type Severity string

Severity classifies a validator finding. error marks a hard violation; warn and info are advisory; fix marks a machine-applicable fix (warn and fix are added by the advisory pass, SP-140-1 §1h).

const (
	// SeverityError is a hard violation that fails validation.
	SeverityError Severity = "error"

	// SeverityWarn is an advisory finding added by SP-140-1 §1h.
	SeverityWarn Severity = "warn"

	// SeverityInfo is an advisory finding.
	SeverityInfo Severity = "info"

	// SeverityFix is a machine-applicable fix added by SP-140-1 §1h.
	SeverityFix Severity = "fix"
)

func (Severity) String

func (s Severity) String() string

String returns the canonical lowercase name of the severity, matching the JSON representation in validator output.

type StatusDriftRow added in v0.18.9

type StatusDriftRow struct {
	Ahead    bool   `json:"ahead"`
	Synced   bool   `json:"synced"`
	Count    int    `json:"count"`
	Summary  string `json:"summary,omitempty"`
	Remedy   string `json:"remedy,omitempty"`
	NextStep string `json:"nextStep,omitempty"`
}

StatusDriftRow is one §5c drift direction for the status payload: the fields the health strip renders (count + remedy, §6c) plus NextStep so the strip can route a click (design-ahead → Tokens tab; code-ahead → agent-panel prefill, SP-140-6 §6c). Shape-mirrors DriftDirectionRow without exposing the full evidence machinery.

type StatusFeedback added in v0.18.9

type StatusFeedback struct {
	PendingCount int                   `json:"pendingCount"`
	Pending      []StatusFeedbackEntry `json:"pending"`
}

StatusFeedback is the feedback section: the pending count plus the sorted pending targets.

type StatusFeedbackEntry added in v0.18.9

type StatusFeedbackEntry struct {
	Target     string `json:"target"`
	Unresolved int    `json:"unresolved"`
	Status     string `json:"status,omitempty"`
}

StatusFeedbackEntry is one pending feedback target in the status payload: the §4d target and how many annotations are still open on it.

type StatusFinding added in v0.18.9

type StatusFinding struct {
	File     string `json:"file"`
	Line     int    `json:"line"`
	Severity string `json:"severity"`
	Message  string `json:"message"`
	Rule     string `json:"rule"`
}

StatusFinding is one validator finding in the status payload — the §1g finding shape ({file, line?, severity, message, rule}) with JSON tags for the HTTP surface.

type StatusValidation added in v0.18.9

type StatusValidation struct {
	Errors   int             `json:"errors"`
	Warnings int             `json:"warnings"`
	Infos    int             `json:"infos"`
	Findings []StatusFinding `json:"findings"`
}

StatusValidation is the validation section: authoritative tallies plus the capped finding list. Warnings count SeverityWarn AND SeverityFix — both are advisory-and-actionable classes, and the strip renders one "warnings" chip.

type SyncApplyPlan

type SyncApplyPlan struct {
	// Mode is always "apply" (this is the apply half's plan).
	Mode string `json:"mode"`
	// Report is the analyze report the plan was derived from, so a consumer can
	// see the full delta set behind a partial apply.
	Report *SyncReport `json:"report"`
	// Writes is the ordered safe-subset write set (deterministic, design/-only).
	Writes []SyncApplyWrite `json:"writes"`
	// Proposals are the deltas apply deliberately left unwritten (inferred
	// proposals, plus any unsafe/unplannable delta).
	Proposals []SyncApplyProposal `json:"proposals"`
	// Refused are design-file writes that were suppressed because a write would
	// have escaped design/ (§5e). Always empty in a plan produced by
	// PlanSyncApply, which drops such writes; kept so a consumer can distinguish
	// "nothing to apply" from "something was refused".
	Refused []string `json:"refused,omitempty"`
	// AppliedCount / ProposalCount are the write and proposal tallies.
	AppliedCount  int `json:"appliedCount"`
	ProposalCount int `json:"proposalCount"`
	// WritePaths is the sorted set of distinct design/ paths the plan writes,
	// surfaced so a caller can assert design/-confinement without walking Writes.
	WritePaths []string `json:"writePaths"`
	// Notes carries run-level caveats (e.g. a malformed token file a write
	// could not be planned from).
	Notes []string `json:"notes,omitempty"`
}

SyncApplyPlan is the pure result of planning a report's safe subset. The handler performs the Writes; nothing here touches the filesystem.

func PlanSyncApply

func PlanSyncApply(report *SyncReport, read SyncFileReader) *SyncApplyPlan

PlanSyncApply is the pure apply core: given a report and a reader for the current design-file bytes, it returns the plan of safe-subset writes plus the proposals apply leaves alone. It performs no writes and never plans a write outside design/.

A nil report (or one with no deltas) yields an empty, valid plan — apply on an already-synced tree is a no-op.

func (*SyncApplyPlan) IsConfinedToDesign

func (p *SyncApplyPlan) IsConfinedToDesign() bool

IsConfinedToDesign reports whether every write in the plan targets a file inside design/ (§5e hard assertion). An empty plan is trivially confined.

type SyncApplyProposal

type SyncApplyProposal struct {
	// Delta is the semantic delta description.
	Delta string `json:"delta"`
	// Kind is the delta kind.
	Kind DeltaKind `json:"kind"`
	// Basis is literal|structural|inferred.
	Basis deltaBasis `json:"basis"`
	// Reason is why apply left it alone: "inferred" (§5b proposal),
	// "outside-design" (§5e refusal), or a specific refusal message.
	Reason string `json:"reason"`
	// ProposalKind / Proposed carry the §5b proposal fields when the delta is
	// an inferred proposal.
	ProposalKind string `json:"proposalKind,omitempty"`
	Proposed     string `json:"proposed,omitempty"`
	// CodeFiles are the touched code files the delta came from.
	CodeFiles []string `json:"codeFiles"`
}

SyncApplyProposal records a delta the apply half deliberately did NOT write: an inferred delta (a proposal §5b leaves to the agent/user via normal file edits), or a malformed/unsafe delta that could not be applied mechanically.

type SyncApplyWrite

type SyncApplyWrite struct {
	// Path is the design/ file to write, workspace-relative.
	Path string `json:"path"`
	// Op is the planning operation that produced this write.
	Op SyncPlanningOp `json:"op"`
	// Kind is the delta kind (token|wireframe|flow) this write serves, so a
	// consumer can group the run by the §5b vocabulary.
	Kind DeltaKind `json:"kind"`
	// Delta is the semantic delta description this write applies.
	Delta string `json:"delta"`
	// Token is the DTCG token path the write concerns ("" otherwise).
	Token string `json:"token,omitempty"`
	// Stem is the wireframe stem the write concerns ("" otherwise).
	Stem string `json:"stem,omitempty"`
	// Edge is the flow edge the write adds ("" otherwise).
	Edge string `json:"edge,omitempty"`
	// Status is the status marker the artifact carries (draft for §5b-created
	// artifacts; "" for a token revalue).
	Status string `json:"status,omitempty"`
	// Created is true when the write creates a file that did not exist.
	Created bool `json:"created"`
	// Content is the full new file bytes. It is deliberately JSON-omitted:
	// the plan's JSON shape names *what* will be written, and the handler is
	// the one that writes it; a consumer needing the bytes reads the file
	// after apply.
	Content []byte `json:"-"`
}

SyncApplyWrite is one design-file write the apply half will perform. Path is workspace-relative, slash-separated, and always confined to design/ (§5e). Content is the full new file bytes (the write is a whole-file replace, which is what makes the ChangeTracker see an ordinary workspace edit).

type SyncDelta

type SyncDelta struct {
	// Delta is the one-line description of the detected change.
	Delta string `json:"delta"`
	// Kind is token|wireframe|flow|feedback.
	Kind DeltaKind `json:"kind"`
	// Basis is literal|structural|inferred.
	Basis deltaBasis `json:"basis"`
	// Confidence is high|medium|low.
	Confidence Confidence `json:"confidence"`
	// DesignFiles are the design/ paths this delta would touch, sorted and
	// slash-separated, workspace-relative. Always non-nil (possibly empty) so
	// the JSON shape is stable; empty means "the semantic layer has no
	// counterpart yet" (apply's job to create one, subject to basis).
	DesignFiles []string `json:"designFiles"`
	// SafeToApply is the automation gate: true for literal and structural
	// deltas, false for inferred (and for any delta whose basis could not be
	// established). Redundant with Basis but explicit for a model reading the
	// report.
	SafeToApply bool `json:"safeToApply"`
	// Proposal marks an inferred delta: a proposal (new token, or switch to an
	// existing one), never auto-applied.
	Proposal bool `json:"proposal,omitempty"`
	// ProposalKind is new-token|switch-to-existing for an inferred delta.
	ProposalKind string `json:"proposalKind,omitempty"`
	// Proposed is the proposed new token path (inferred, new token) or the
	// design/ path proposal ("" otherwise).
	Proposed string `json:"proposed,omitempty"`
	// Candidate is the existing DTCG token path an inferred delta could switch
	// to, when one matches the literal value ("" otherwise).
	Candidate string `json:"candidate,omitempty"`
	// Token is the DTCG token path a token delta concerns ("" otherwise).
	Token string `json:"token,omitempty"`
	// TokenFile is the design/tokens/*.tokens.json the token lives in
	// ("" otherwise), from the token export machinery.
	TokenFile string `json:"tokenFile,omitempty"`
	// TokenEntry is the DTCG entry key as it appears in TokenFile: the
	// dotted token path for new/revalued entries, or the pre-change alias
	// target for a literal rename ("" otherwise).
	TokenEntry string `json:"tokenEntry,omitempty"`
	// WireframeStem is the proposed wireframe stem for a structural delta
	// ("" otherwise).
	WireframeStem string `json:"wireframeStem,omitempty"`
	// FlowEdge is the proposed flow edge ("login --> home") for a structural
	// delta ("" otherwise).
	FlowEdge string `json:"flowEdge,omitempty"`
	// FlowFile is the design/flows/*.mmd the edge would be added to
	// ("" otherwise).
	FlowFile string `json:"flowFile,omitempty"`
	// Status is the status a created artifact would carry (draft for
	// §5b-created wireframes/edges).
	Status string `json:"status,omitempty"`
	// CodeFiles are the touched code files this delta was detected in, sorted
	// and slash-separated.
	CodeFiles []string `json:"codeFiles"`
	// Evidence is the offending code snippet/line, trimmed (best-effort).
	Evidence string `json:"evidence,omitempty"`
}

SyncDelta is one detected semantic delta (SP-140-5 §5b). Delta is the human/agent-readable description; Kind/Basis/Confidence govern what apply may do with it; DesignFiles names the design/ paths it would touch.

type SyncFileInput

type SyncFileInput struct {
	Path    string
	Content []byte
}

SyncFileInput is one touched code file: its path as the caller named it and its current content.

type SyncFileReader

type SyncFileReader func(rel string) ([]byte, bool)

SyncFileReader returns the current bytes of an existing design file (path is workspace-relative, slash-separated). It returns (nil, false) when the file does not exist (a create) or cannot be read (a note, not a failure). The handler supplies a workspace-confined reader; the pure core never does I/O.

type SyncInput

type SyncInput struct {
	// Root is the workspace root (the parent of design/).
	Root string
	// Touched are the code files to analyse. Paths may be absolute or
	// workspace-relative; Contents maps each path (as given) to its bytes.
	// A path with no content entry is skipped (advisory, named in Skipped).
	Touched []SyncFileInput
}

SyncInput is the analyze-mode input: the touched code files and their current contents, plus the workspace root so the design/ tree can be read.

type SyncPlanningOp

type SyncPlanningOp = string

SyncPlanningOp is the kind of write a plan step performs.

const (
	// SyncPlanningUpdateToken rewrites an existing DTCG entry's value in place
	// (a literal token revalue). Delta is the token path.
	SyncPlanningUpdateToken SyncPlanningOp = "update-token"
	// SyncPlanningAddToken adds a DTCG entry that does not yet exist (a literal
	// token rename/addition the code introduced). Delta is the token path.
	SyncPlanningAddToken SyncPlanningOp = "add-token"
	// SyncPlanningAddWireframe creates a skeleton wireframe SVG with draft
	// status. Delta is the wireframe stem.
	SyncPlanningAddWireframe SyncPlanningOp = "add-wireframe"
	// SyncPlanningAddFlowEdge appends the proposed edge to a flow file. Delta is
	// the edge ("login --> home").
	SyncPlanningAddFlowEdge SyncPlanningOp = "add-flow-edge"
)

Planning ops, fixed by the apply half.

type SyncReport

type SyncReport struct {
	// Mode is the mode the report was produced in ("" for a bare analyze
	// call; apply mode is item 5.4). It is advisory metadata for a stored
	// report.
	Mode string `json:"mode,omitempty"`
	// TokensPath / WireframeDir / FlowsDir are the design-tier directories the
	// run cross-referenced, slash-separated and workspace-relative. They are
	// always present so the report reads as a design-tree lookup rather than a
	// bag of deltas.
	TokensPath   string `json:"tokensPath"`
	WireframeDir string `json:"wireframesPath"`
	FlowsDir     string `json:"flowsPath"`
	// TouchedCount is the number of touched code files analysed.
	TouchedCount int `json:"touchedCount"`
	// DeltaCount is the number of deltas; also broken out by Basis and Kind
	// so a model reads the automation split without counting.
	DeltaCount int            `json:"deltaCount"`
	ByBasis    map[string]int `json:"byBasis"`
	ByKind     map[string]int `json:"byKind"`
	// Deltas is the ordered semantic-delta work list (deterministic).
	Deltas []SyncDelta `json:"deltas"`
	// WritesNothing states the §5e invariant explicitly: analyze mode writes
	// no files, and apply (5.4) is confined to design/. Always true here.
	WritesNothing bool `json:"writesNothing"`
	// NextStep is a one-line pointer to the apply half / the skill loop, so a
	// model knows analyze is the first half.
	NextStep string `json:"nextStep"`
	// Skipped are touched paths the analysis could not read (missing on disk,
	// a directory, or not a recognised UI file). Advisory.
	Skipped []string `json:"skipped,omitempty"`
	// Notes carries human-readable caveats (e.g. "no design/ tree, so
	// literal deltas have no DTCG counterpart"). Deterministic order.
	Notes []string `json:"notes,omitempty"`
}

SyncReport is the structured result of one `design_sync` analyze run (SP-140-5 §5b). It is deliberately the shape apply mode consumes: Deltas is the ordered work list, each delta naming the design files it would touch.

func AnalyzeTouchedFiles

func AnalyzeTouchedFiles(in SyncInput) (*SyncReport, error)

AnalyzeTouchedFiles is the pure analyze core (no I/O beyond the design/ tree read): given the touched code files and the workspace root, it returns the §5b sync report. It never writes anything.

The analysis is convention-based, in three passes over the touched files:

literal    — CSS custom properties and token references, cross-referenced
             against the DTCG export projection.
structural — router routes/screens, component file names, and nav targets.
inferred   — raw hex colours and magic spacing values with no token
             counterpart, emitted as proposals.

The result is deterministic: touched paths and every emitted slice are sorted, and the delta order is fixed by CompareDeltas.

type TokenExport

type TokenExport struct {
	Leaves []ExportedToken
	// InputHash is the §5f provenance hash of the token source documents this
	// projection was built from (see TokenExportInputHash). Every generated
	// artifact's header records it, so the artifact can be verified against
	// design/tokens/ at any checkout, offline.
	InputHash string
}

TokenExport is the resolved, target-neutral projection of every DTCG token leaf in a tier set. Leaves is sorted by Name; the validators have already established that each leaf has a well-formed $type, so the projection can assume one.

func ResolveExportTokens

func ResolveExportTokens(root string) (*TokenExport, error)

ResolveExportTokens reads every design/tokens/*.tokens.json under root and projects each DTCG leaf into an ExportedToken. Files are read in glob order (filepath.Glob is lexical) and the leaves are sorted by dotted name, so the result is identical for identical inputs.

A malformed tokens file is an error: the validator is the reporting surface (design_validate), but export must not silently emit a partial theme from a file it could not parse. A missing or empty tokens/ directory yields ErrNoTokensForExport.

A *dirty alias graph* — any dangling or cyclic whole-value alias in any source file — is also an error (ErrDirtyAliasGraph, item 5.2): export refuses rather than emit a theme whose var()/*token* references cannot be resolved. The check reuses the validator's own alias machinery (checkAliases), so export and design_validate agree by construction on what "dirty" means.

type TokenExportSource

type TokenExportSource struct {
	// Path is the workspace-relative slash path of the source document.
	Path string
	// Name is the source document's base name (color.tokens.json); it is the
	// stable sort key, so the hash does not depend on the workspace root.
	Name string
	// Content is the document's exact bytes.
	Content []byte
}

TokenExportSource is one DTCG token source document as read from disk: its workspace-relative slash path (design/tokens/<name>.tokens.json) and its exact bytes. Collected by ResolveExportTokens and retained on the TokenExport so the provenance header can be built from the same read that produced the leaves (no second filesystem pass, no chance of drift).

func ScreensIndexSources added in v0.18.16

func ScreensIndexSources(root string) ([]TokenExportSource, error)

ScreensIndexSources reads every design/screens/*.html under root into the §5f hash-input shape (workspace-relative slash path, stem, exact bytes), sorted by stem. A missing screens directory yields an empty slice and no error.

type TokenGroupCount

type TokenGroupCount struct {
	Group  string `json:"group"`
	Tokens int    `json:"tokens"`
}

TokenGroupCount is the number of leaf tokens in one top-level token group across every design/tokens/*.tokens.json file (SP-140-2 §2c "token group counts"). Counting is per group name, summed across files, because tokens are split one file per tier and consumers glob.

Jump to

Keyboard shortcuts

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