errors

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 27, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package errors defines Prism's typed application errors and the code catalog with fixup metadata.

AppError serializes to the Pulse-style JSON envelope (design/09-errors.md):

{
  "code":    "PRISM_SPEC_001",
  "message": "Field x not found in dataset y.",
  "fixups":  [...],
  "context": {...},
  "see_also": [...]
}

The plain Error() method renders a single-line text form suitable for stderr; the CLI prints the multi-line "ERROR / Fixups:" block by formatting AppError fields directly.

Pulse's *errors.CodedError values pass through verbatim — interceptors route them based on prefix.

Index

Constants

This section is empty.

Variables

View Source
var Codes = map[string]CodeMetadata{
	"PRISM_SPEC_001": {
		Code:    "PRISM_SPEC_001",
		Message: `Field {{.Field}} not in source schema for dataset {{.Dataset}}.`,
		Fixups: []string{
			`Check the field name spelling. Available fields: {{.Available}}`,
			`If the field comes from a transform, make sure the transform's "as" output name matches.`,
			`Run ` + "`prism inspect {{.Dataset}}`" + ` to list all fields in the source.`,
		},
		SeeAlso: []string{"PRISM_SPEC_002", "PRISM_SPEC_005"},
	},
	"PRISM_SPEC_002": {
		Code:    "PRISM_SPEC_002",
		Message: `Aggregate op {{.Op}} is not compatible with field {{.Field}} of type {{.FieldType}}.`,
		Fixups: []string{
			`Choose an aggregate compatible with {{.FieldType}}: {{.Compatible}}.`,
			`If you need a numeric aggregate on a {{.FieldType}} field, change the field's measure type or pre-cast it in a calculate transform.`,
		},
		SeeAlso: []string{"PRISM_SPEC_001"},
	},
	"PRISM_SPEC_003": {
		Code:    "PRISM_SPEC_003",
		Message: `Encoding channel {{.Channel}} is not valid for mark type {{.Mark}}.`,
		Fixups: []string{
			`Use a channel supported by {{.Mark}}: {{.Allowed}}.`,
			`If you want {{.Channel}} semantics, switch to a compatible mark type.`,
		},
		SeeAlso: []string{"PRISM_SPEC_008"},
	},
	"PRISM_SPEC_004": {
		Code:    "PRISM_SPEC_004",
		Message: `Selection reference {{.Selection}} does not resolve to a declared selection.`,
		Fixups: []string{
			`Declare the selection in the spec's "selection" block before referencing it.`,
			`Available selections: {{.Available}}.`,
		},
	},
	"PRISM_SPEC_005": {
		Code:    "PRISM_SPEC_005",
		Message: `Dataset reference {{.Dataset}} does not resolve to a declared dataset.`,
		Fixups: []string{
			`Declare the dataset in the spec's "datasets" block, register it via prism serve, or declare it page-side via <prism-dataset>.`,
			`Available datasets: {{.Available}}.`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_001"},
	},
	"PRISM_SPEC_006": {
		Code:    "PRISM_SPEC_006",
		Message: `Expression failed to parse: {{.Reason}}.`,
		Fixups: []string{
			`Check Pulse expression syntax. Expression: {{.Expression}}`,
			`Quote string literals with single quotes ('value'), not double quotes.`,
			`Use Pulse expression operators (and, or, not, ==, !=, <, <=, >, >=, +, -, *, /, %).`,
		},
	},
	"PRISM_SPEC_007": {
		Code:    "PRISM_SPEC_007",
		Message: `Scale type {{.ScaleType}} is not compatible with field {{.Field}} of type {{.FieldType}}.`,
		Fixups: []string{
			`Use a scale type compatible with {{.FieldType}}: {{.Compatible}}.`,
			`If you intended the field to be {{.ScaleFor}}, change the encoding's "type" to match.`,
		},
		SeeAlso: []string{"PRISM_SPEC_002"},
	},
	"PRISM_SPEC_008": {
		Code:    "PRISM_SPEC_008",
		Message: `Mark {{.Mark}} requires theta encoding (and typically color), not x/y.`,
		Fixups: []string{
			`Replace the x/y encodings with theta + color: { "theta": {"field": "...", "type": "quantitative"}, "color": {"field": "...", "type": "nominal"} }.`,
			`If you need x/y semantics, switch to a mark like bar or rect.`,
		},
		SeeAlso: []string{"PRISM_SPEC_003"},
	},
	"PRISM_SPEC_009": {
		Code:    "PRISM_SPEC_009",
		Message: `$schema value {{.Schema}} does not reference a known Prism schema.`,
		Fixups: []string{
			`Use the canonical URN: "$schema": "urn:prism:schema:v1:spec".`,
			`Or a relative path that ends in spec.schema.json (e.g. "./.prism/schemas/spec.schema.json").`,
		},
	},

	"PRISM_PLAN_001": {
		Code:    "PRISM_PLAN_001",
		Message: `Cyclic dataset reference detected (involving {{.Cycle}}; {{.Nodes}} nodes unscheduled).`,
		Fixups: []string{
			`Break the cycle by introducing an intermediate named alias.`,
			`Check transform "data" and "as" aliases for accidental loops.`,
			`Run ` + "`prism plan <spec> --format dot`" + ` to visualise the DAG and locate the cycle.`,
		},
	},
	"PRISM_PLAN_002": {
		Code:    "PRISM_PLAN_002",
		Message: `Unknown or unsupported plan kind {{.Kind}} (deferred to {{.Phase}}).`,
		Fixups: []string{
			`This spec uses a feature that is not yet implemented in the current Prism build.`,
			`Composition primitives (layer, concat, facet, repeat) land in P08/P09; selections land in P13.`,
			`Track the rollout in .planning/ROADMAP.md or run ` + "`prism errors lookup PRISM_PLAN_002`" + ` for the latest status.`,
		},
	},
	"PRISM_PLAN_003": {
		Code:    "PRISM_PLAN_003",
		Message: `Transform references undeclared dataset {{.Dataset}} (available: {{.Available}}).`,
		Fixups: []string{
			`Declare the dataset in "datasets" or earlier in the transform pipeline.`,
			`Check the spelling of the data/source reference.`,
			`If the dataset lives in another spec, hoist it into a top-level "datasets" entry.`,
		},
		SeeAlso: []string{"PRISM_SPEC_005", "PRISM_RESOLVE_001"},
	},
	"PRISM_COMPILE_001": {
		Code:    "PRISM_COMPILE_001",
		Message: `Node type {{.NodeType}} is not implemented yet (lands in {{.Phase}}).`,
		Fixups: []string{
			`This node is a P03 placeholder; the real Execute body ships in {{.Phase}}.`,
			`Until then the DAG builds and the rest of the pipeline runs — only this node fails.`,
			`Track progress: ` + "`prism errors lookup PRISM_COMPILE_001`" + ` or .planning/ROADMAP.md.`,
		},
	},
	"PRISM_COMPILE_002": {
		Code:    "PRISM_COMPILE_002",
		Message: `Expression failed at runtime: {{.Reason}}.`,
		Fixups: []string{
			`Expression: {{.Expression}} (site: {{.Site}}).`,
			`Run ` + "`prism validate`" + ` first — most parse errors surface as PRISM_SPEC_006 before they reach the compiler.`,
			`Check field references match the upstream schema and that arithmetic does not divide by a possibly-zero value.`,
		},
		SeeAlso: []string{"PRISM_SPEC_006"},
	},
	"PRISM_COMPILE_003": {
		Code:    "PRISM_COMPILE_003",
		Message: `Aggregate alias {{.Alias}} is not yet supported by backend {{.Backend}}.`,
		Fixups: []string{
			`Use a supported alias: count, sum, mean, median, min, max, stdev, variance, mode, distinct, q1, q3, ci0, ci1, wmean, ratio, lift, share.`,
			`If your spec relied on an upstream alias the planner forwarded, check ` + "`compile/aggregates.go`" + ` for the canonical alias-to-Pulse mapping.`,
			`File an issue with the alias name so it can be added to the next Pulse release.`,
		},
		SeeAlso: []string{"PRISM_SPEC_002"},
	},
	"PRISM_COMPILE_004": {
		Code:    "PRISM_COMPILE_004",
		Message: `Inline data is not supported by the Pulse backend for node {{.NodeType}}: {{.Reason}}.`,
		Fixups: []string{
			`The Pulse v0.8.4 facade does not expose an in-memory cohort constructor; inline data flows through the in-memory backend.`,
			`Materialise the inline values to a ` + "`.pulse`" + ` file via ` + "`prism import`" + ` (post-P02) and reference it as a source.`,
			`Track the upstream phase: in-memory Pulse cohorts land when Pulse exposes pulse.FromTable / pulse.NewMemory (no ETA).`,
		},
	},
	"PRISM_RESOLVE_001": {
		Code:    "PRISM_RESOLVE_001",
		Message: `Dataset {{.Dataset}} not found in any registered source.`,
		Fixups: []string{
			`Verify the source path or cohort id.`,
			`Add the dataset to "datasets" or to the prism serve config.`,
		},
	},
	"PRISM_RESOLVE_002": {
		Code:    "PRISM_RESOLVE_002",
		Message: `Local .pulse file {{.Path}} not found on the configured filesystem.`,
		Fixups: []string{
			`Check the path spelling and that the file exists (` + "`ls -lh {{.Path}}`" + `).`,
			`Confirm the working directory matches what the spec assumes — relative paths are resolved against the process cwd unless an afero.Fs jail is in effect.`,
			`If the data lives in an archive, use the anchor form: ` + "`archive.pulse#shard.pulse`" + `.`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_003", "PRISM_RESOLVE_005"},
	},
	"PRISM_RESOLVE_003": {
		Code:    "PRISM_RESOLVE_003",
		Message: `Shard {{.Shard}} not present in archive {{.Archive}}.`,
		Fixups: []string{
			`Run ` + "`prism inspect {{.Archive}}`" + ` to list shard names (basenames only; no path).`,
			`Anchors are case-sensitive; copy the basename verbatim from the archive listing.`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_002"},
	},
	"PRISM_RESOLVE_004": {
		Code:    "PRISM_RESOLVE_004",
		Message: `Cohort id {{.Id}} is not registered in the active resolver registry.`,
		Fixups: []string{
			`Register the id with the resolver's Registry before resolving (` + "`registry.Lookup(\"{{.Id}}\")`" + `).`,
			`If you intended to load a file directly, drop the ` + "`cohort:`" + ` prefix and use the path form.`,
		},
	},
	"PRISM_RESOLVE_005": {
		Code:    "PRISM_RESOLVE_005",
		Message: `Reference {{.Ref}} does not match any known form (path, archive#shard, gs://, or cohort:id).`,
		Fixups: []string{
			`Use one of: ` + "`cohort.pulse`" + `, ` + "`archive.pulse#shard.pulse`" + `, ` + "`gs://bucket/path.pulse`" + `, ` + "`cohort:<id>`" + `.`,
			`Drop trailing whitespace and double-check for leading slashes that imply absolute paths.`,
		},
	},
	"PRISM_RESOLVE_006": {
		Code:    "PRISM_RESOLVE_006",
		Message: `Pulse failed to open {{.Ref}}: {{.Reason}}.`,
		Fixups: []string{
			`Run ` + "`prism inspect {{.Ref}}`" + ` for header diagnostics.`,
			`Verify the file is a real .pulse (the first 8 bytes spell ` + "`PULSE\\x00\\x00\\x00`" + `).`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_002", "PRISM_RESOLVE_003"},
	},
	"PRISM_RESOLVE_007": {
		Code:    "PRISM_RESOLVE_007",
		Message: `Materialisation refused: {{.Actual}} rows would exceed PRISM_TABLE_MAX_ROWS={{.Limit}}.`,
		Fixups: []string{
			`Raise the ceiling by setting ` + "`PRISM_TABLE_MAX_ROWS`" + ` in the environment before running prism.`,
			`Pre-aggregate, sample, or filter at the Pulse layer to bring the result under the cap.`,
			`Switch to a streaming consumer once P03 lands streaming; for v1 every node materialises a Table.`,
		},
	},
	"PRISM_RESOLVE_GCS_UNAVAILABLE": {
		Code:    "PRISM_RESOLVE_GCS_UNAVAILABLE",
		Message: `gs:// references are not implemented in v1 (ref: {{.Ref}}).`,
		Fixups: []string{
			`Stage the .pulse locally (` + "`gsutil cp gs://bucket/path.pulse ./`" + `) and reference the local path.`,
			`Track the upstream phase: gs:// support lands once Pulse ships a generic GCS afero.Fs (planned P-NN-gcs-fs).`,
		},
	},
	"PRISM_RESOLVE_INLINE_TYPE_MISMATCH": {
		Code:    "PRISM_RESOLVE_INLINE_TYPE_MISMATCH",
		Message: `Inline row {{.Row}} field {{.Field}} has type {{.GotType}} but the schema (inferred from row 0) declared {{.WantType}}.`,
		Fixups: []string{
			`Make every row use the same JSON kind per field — strings, numbers, and bools cannot mix in a column.`,
			`Declare types explicitly via ` + "`data.fields`" + ` so the inference path is skipped.`,
		},
		SeeAlso: []string{"PRISM_SPEC_001"},
	},
	"PRISM_SPEC_PATCH_001": {
		Code:    "PRISM_SPEC_PATCH_001",
		Message: `Spec patch operation failed: {{.Op}} on {{.Path}}.`,
		Fixups: []string{
			`Inspect the failing operation index ({{.OpIndex}}) in the returned envelope — operations are applied left-to-right and the first failure stops the patch.`,
			`Confirm the JSON Pointer in {{.Path}} resolves against the current spec (use ` + "`prism plan`" + ` or ` + "`prism scene`" + ` to dump the live shape).`,
			`Atomic semantics: a failing op leaves the original spec unchanged. Re-apply with corrected ops or rebuild from a known baseline.`,
		},
		SeeAlso: []string{"PRISM_SPEC_009"},
	},
	"PRISM_RESOLVE_REF_UNRESOLVED": {
		Code:    "PRISM_RESOLVE_REF_UNRESOLVED",
		Message: `Data ref "{{.Ref}}" was not resolved by the active DataResolver.`,
		Fixups: []string{
			`Pass a DataResolver via ` + "`build.Options.DataResolver`" + ` that handles this ref.`,
			`In the browser, register a callback with ` + "`prism.setDataResolver((ref) => …)`" + ` before calling ` + "`prism.execute`" + `/` + "`prism.compile`" + `.`,
			`Pre-resolve and inject the dataset under the same name via ` + "`datasets`" + ` if a static binding suffices.`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_001", "PRISM_RESOLVE_004"},
	},
	"PRISM_SPEC_010": {
		Code:    "PRISM_SPEC_010",
		Message: `Log scale on channel {{.Channel}} requires a strictly positive domain (got {{.Value}}).`,
		Fixups: []string{
			`Filter out zero and negative values upstream of the encoded field.`,
			`Switch to scale type "linear" or "sqrt" if the domain naturally includes zero.`,
			`If the value comes from a calculate transform, guard with a clamp expression (e.g. ` + "`max(field, 1e-9)`" + `).`,
		},
		SeeAlso: []string{"PRISM_SPEC_007"},
	},
	"PRISM_SPEC_011": {
		Code:    "PRISM_SPEC_011",
		Message: `Format string {{.Spec}} on {{.Where}} is not a recognised d3-format specifier ({{.Reason}}).`,
		Fixups: []string{
			`Supported specifiers: ,.Nf | .N% | % | ,d | .Ne | .Ns | %Y | %m | %d | %H | %M | %S.`,
			`See encode/format/README.md for the full list with examples.`,
			`Drop the format property to fall back to the default rendering.`,
		},
	},
	"PRISM_RENDER_001": {
		Code:    "PRISM_RENDER_001",
		Message: `Mark geometry is malformed for {{.Mark}}.`,
		Fixups: []string{
			`Inspect the encoding values driving this mark — non-finite or null values often cause this.`,
		},
	},
	"PRISM_RENDER_FORMAT_UNAVAILABLE": {
		Code:    "PRISM_RENDER_FORMAT_UNAVAILABLE",
		Message: `Render format {{.Format}} is not available in the current Prism build (lands in {{.Phase}}).`,
		Fixups: []string{
			`SVG (default) and PDF (P15) are the available renderers; use --format svg or --format pdf.`,
			`PNG support is deferred to V2; consume the JS port (prism.mjs) via prism scene + canvas for browser-native screenshots.`,
			`canvas-json consumes the Scene IR directly via 'prism scene <spec>' → render/svg's prism.mjs in the browser.`,
		},
		SeeAlso: []string{"PRISM_RENDER_001"},
	},
	"PRISM_RENDER_PDF_UNSUPPORTED_PATH": {
		Code:    "PRISM_RENDER_PDF_UNSUPPORTED_PATH",
		Message: `PDF renderer cannot translate SVG path command {{.Got}} (only M/L/H/V/Q/C/A/Z + relative forms are supported per D092).`,
		Fixups: []string{
			`Rewrite the path using only the supported subset: M / L / H / V / Q / C / A / Z (and the relative forms m / l / h / v / q / c / a / z).`,
			`Smooth cubic (S / s) and smooth quadratic (T / t) are rejected because they depend on the previous command's reflected control point; expand them to explicit C / Q commands.`,
			`If you need an arbitrary SVG shape, consider using a primitive Prism mark (rect / line / area / arc) instead of a raw <path>.`,
		},
		SeeAlso: []string{"PRISM_SPEC_017", "PRISM_RENDER_001"},
	},
	"PRISM_WARN_PDF_GRADIENT_FLATTENED": {
		Code:    "PRISM_WARN_PDF_GRADIENT_FLATTENED",
		Message: `PDF renderer flattened a gradient fill to its first color stop (gradient {{.Gradient}}).`,
		Fixups: []string{
			`Use a solid color in your spec for byte-identical PDF rendering across SVG and PDF.`,
			`The SVG renderer preserves the gradient; the PDF backend currently only renders solid fills (gopdf lacks a public shading API).`,
		},
		SeeAlso: []string{"PRISM_WARN_PDF_GRADIENT_TEXT_FLATTENED", "PRISM_WARN_PDF_GRADIENT_ANGULAR_FLATTENED", "PRISM_RENDER_001"},
	},
	"PRISM_WARN_PDF_GRADIENT_TEXT_FLATTENED": {
		Code:    "PRISM_WARN_PDF_GRADIENT_TEXT_FLATTENED",
		Message: `PDF renderer flattened a gradient fill on a text mark (gradient {{.Gradient}}).`,
		Fixups: []string{
			`Text marks use a solid fill in PDF output regardless of backend gradient support; this warning is informational.`,
			`The SVG renderer preserves the gradient on text via inline <linearGradient> defs.`,
		},
		SeeAlso: []string{"PRISM_WARN_PDF_GRADIENT_FLATTENED"},
	},
	"PRISM_WARN_PDF_GRADIENT_ANGULAR_FLATTENED": {
		Code:    "PRISM_WARN_PDF_GRADIENT_ANGULAR_FLATTENED",
		Message: `PDF renderer flattened an angular / conic gradient (gradient {{.Gradient}}): the PDF backend supports only linear and radial.`,
		Fixups: []string{
			`Use linear / radial gradients in the spec, or accept the flattened output in PDF.`,
			`The SVG renderer preserves angular gradients via inline <linearGradient> + transform.`,
		},
		SeeAlso: []string{"PRISM_WARN_PDF_GRADIENT_FLATTENED"},
	},
	"PRISM_WARN_PDF_CONDITION_FLATTENED": {
		Code:    "PRISM_WARN_PDF_CONDITION_FLATTENED",
		Message: `PDF renderer painted the fallback branch of a selection-driven condition on mark {{.Mark}}: PDFs are static.`,
		Fixups: []string{
			`Selection-driven conditions need an interactive renderer (SVG via prism-element). Static ` + "`{test: ...}`" + ` conditions render fine in PDF.`,
			`See ` + "`docs/src/concepts/encoding.md#conditions`" + ` for details.`,
		},
		SeeAlso: []string{"PRISM_SPEC_025"},
	},
	"PRISM_RENDER_SCENE_EMPTY": {
		Code:    "PRISM_RENDER_SCENE_EMPTY",
		Message: `Encoded scene is empty — no marks were produced ({{.Reason}}).`,
		Fixups: []string{
			`Check the upstream transform pipeline — a filter may have removed every row.`,
			`Run ` + "`prism execute <spec>`" + ` to inspect the table the encoder consumed.`,
			`If the spec intentionally produces no marks, verify axes still render in the SVG output.`,
		},
	},
	"PRISM_RENDER_THEME_UNKNOWN": {
		Code:    "PRISM_RENDER_THEME_UNKNOWN",
		Message: `Unknown theme {{.Theme}} (registered themes: {{.Available}}).`,
		Fixups: []string{
			`Use one of the built-in theme names: light | dark | print.`,
			`To use a custom theme, load it via theme.LoadFile(path) before rendering.`,
			`Drop --theme to fall back to the default (light).`,
		},
	},
	"PRISM_ENCODE_001": {
		Code:    "PRISM_ENCODE_001",
		Message: `Encode-time mismatch: field {{.Field}} not present in upstream table from source {{.Source}}.`,
		Fixups: []string{
			`Available fields in the upstream table: {{.Available}}.`,
			`Run ` + "`prism validate <spec>`" + ` — most field-existence errors surface as PRISM_SPEC_001 earlier.`,
			`Check that the transform pipeline does not project away the field before the mark consumes it.`,
		},
		SeeAlso: []string{"PRISM_SPEC_001"},
	},

	"PRISM_RESOLVE_DUPLICATE_DATASET": {
		Code: "PRISM_RESOLVE_DUPLICATE_DATASET",
		Message: `Dataset alias {{.Alias}} is declared more than once ` +
			`(first at {{.First}}, again at {{.Second}}).`,
		Fixups: []string{
			`Rename one of the colliding aliases so each dataset has a unique name in the spec.`,
			`If the second occurrence is a transform "as" name, pick a name that does not collide with a registered dataset.`,
			`Run ` + "`prism plan <spec> --format json`" + ` to inspect the alias registry the builder produced.`,
		},
		SeeAlso: []string{"PRISM_PLAN_003", "PRISM_RESOLVE_001"},
	},
	"PRISM_JOIN_001": {
		Code:    "PRISM_JOIN_001",
		Message: `Join key {{.Key}} has incompatible kinds on the two sides (left={{.LeftKind}}, right={{.RightKind}}).`,
		Fixups: []string{
			`Cast the column on one side via a calculate transform so both sides share a Pulse Kind.`,
			`If one side is categorical and the other numeric, decide which storage shape the join semantically requires.`,
			`Inspect the schemas with ` + "`prism execute <spec>`" + ` to see each side's columns + kinds.`,
		},
		SeeAlso: []string{"PRISM_JOIN_002", "PRISM_JOIN_003"},
	},
	"PRISM_JOIN_002": {
		Code:    "PRISM_JOIN_002",
		Message: `Join key {{.Key}} is missing on the {{.Side}} side (available: {{.Available}}).`,
		Fixups: []string{
			`Check the spelling of the join key against the table the {{.Side}} input produces.`,
			`If the column is produced by a transform, ensure that transform runs before the join.`,
			`Use ` + "`prism plan <spec> --format dot`" + ` to confirm the DAG wiring matches the spec.`,
		},
		SeeAlso: []string{"PRISM_JOIN_001"},
	},
	"PRISM_JOIN_003": {
		Code:    "PRISM_JOIN_003",
		Message: `Join would produce {{.Actual}} rows (left × right) and exceeds PRISM_JOIN_MAX_ROWS={{.Limit}}.`,
		Fixups: []string{
			`Pre-aggregate one or both sides upstream of the join so the cartesian product fits under the cap.`,
			`Raise the ceiling by setting ` + "`PRISM_JOIN_MAX_ROWS`" + ` in the environment (warning: 5M ≈ 500MB at 20 columns).`,
			`Push the join down to Pulse once Pulse exposes a relational join (deferred to a future Prism phase).`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_007"},
	},
	"PRISM_PLAN_004": {
		Code:    "PRISM_PLAN_004",
		Message: `Union input schemas disagree: {{.Diff}}.`,
		Fixups: []string{
			`Make every union input expose the same column names and Pulse types in the same order.`,
			`If you need a relational union of differing shapes, project each side first to the shared columns.`,
			`Inspect each input's schema via ` + "`prism plan <spec> --format json`" + ` and reconcile differences.`,
		},
		SeeAlso: []string{"PRISM_PLAN_003"},
	},
	"PRISM_PLAN_005": {
		Code:    "PRISM_PLAN_005",
		Message: `Channel {{.Channel}} cannot be resolved as shared: layers disagree on type ({{.Types}}).`,
		Fixups: []string{
			`Convert one layer's channel to the matching type via a "calculate" cast upstream of the encoder.`,
			`Switch the channel to a Pulse-compatible measure type so every layer publishes the same scale family.`,
			"Set `resolve.scale.{{.Channel}}` to `independent` to keep per-layer scales + per-layer axes.",
		},
		SeeAlso: []string{"PRISM_PLAN_002", "PRISM_SPEC_007", "PRISM_RESOLVE_DUPLICATE_DATASET"},
	},
	"PRISM_PLAN_CHAIN_NOT_MERGEABLE": {
		Code:    "PRISM_PLAN_CHAIN_NOT_MERGEABLE",
		Message: `Pulse rejected a fused chain stage as non-mergeable for {{.Ref}}: {{.Reason}}.`,
		Fixups: []string{
			`Disable the Pulse-chain fusion pass and re-run; the chain falls back to per-node execution against the inmem backend.`,
			`Check that every aggregate in the failing stage emits a scalar (mode/frequency are excluded by the v1 chain gate).`,
			`Inspect the offending request via ` + "`prism plan <spec> --format json`" + ` to confirm which stage Pulse rejected.`,
		},
		SeeAlso: []string{"PRISM_PLAN_003"},
	},
	"PRISM_WARN_DOWNSAMPLE": {
		Code:    "PRISM_WARN_DOWNSAMPLE",
		Message: `Source {{.Source}} exceeds PRISM_RENDER_MAX_MARKS={{.Limit}} ({{.Actual}} rows); injected SampleNode({{.SampleN}}).`,
		Fixups: []string{
			`If you need every row plotted, raise the ceiling via PRISM_RENDER_MAX_MARKS or pass --no-downsample (when --no-downsample is wired).`,
			`If the chart is exploratory, the sample is deterministic for the spec's seed.`,
			`Pre-aggregate upstream of the encoder to avoid the auto-sample entirely.`,
		},
	},
	"PRISM_WARN_LAYER_SKIPPED": {
		Code:    "PRISM_WARN_LAYER_SKIPPED",
		Message: `Layer {{.Layer}} skipped: upstream Source {{.Source}} failed ({{.Code}}).`,
		Fixups: []string{
			"Rerun with `--abort-on-error` to fail fast instead of dropping the layer.",
			`Inspect the upstream error code via ` + "`prism errors lookup {{.Code}}`" + ` and unblock the failing Source.`,
			`Remove the offending dataset from "datasets" if it is no longer published.`,
		},
		SeeAlso: []string{"PRISM_COMPILE_001"},
	},

	"PRISM_SPEC_012": {
		Code:    "PRISM_SPEC_012",
		Message: `Repeat substitution {{.Ref}} references axis {{.Axis}} but the parent repeat block declares only {{.Declared}}.`,
		Fixups: []string{
			`Declare the missing axis on the parent repeat block (e.g. "repeat": {"{{.Axis}}": ["field_a", "field_b"]}).`,
			`If the child spec needs a literal field name, replace the {"repeat": ...} substitution with a bare {"field": "name"}.`,
			`If you intended a different axis, update the substitution to match: {{.Declared}}.`,
		},
		SeeAlso: []string{"PRISM_SPEC_005", "PRISM_PLAN_002"},
	},

	"PRISM_SPEC_013": {
		Code:    "PRISM_SPEC_013",
		Message: `Composite mark {{.Mark}} cannot expand: {{.Reason}}.`,
		Fixups: []string{
			`Check the mark's required channels: pie/donut → theta + color; histogram → x (quantitative); heatmap → x + y + color; boxplot/violin → one category axis + one quantitative axis.`,
			`Replace the mark with a primitive (bar/rect/arc/rule/point) when the encoding does not fit the composite's required shape.`,
			`If you need a different aggregation, write the expansion by hand using primitive marks.`,
		},
		SeeAlso: []string{"PRISM_SPEC_003", "PRISM_SPEC_008"},
	},

	"PRISM_SPEC_016": {
		Code:    "PRISM_SPEC_016",
		Message: `Image URL {{.URL}} is not allowed (offline-first; only data: and relative paths are accepted).`,
		Fixups: []string{
			`Embed the image as a base64 data: URL ("data:image/png;base64,...").`,
			`Reference a relative path under the spec's working directory; the renderer passes the string through to <image href>.`,
			`Remote fetch is intentionally disabled — Prism plots must render without network access. See PROJECT.md.`,
		},
		SeeAlso: []string{"PRISM_RENDER_001"},
	},
	"PRISM_SPEC_017": {
		Code:    "PRISM_SPEC_017",
		Message: `Mark "path" requires a non-empty d field (got {{.Got}}).`,
		Fixups: []string{
			`Set mark_def.path or encoding.path.value to a valid SVG path string (e.g. "M 0 0 L 10 10 Z").`,
			`Path mark is the escape hatch for SVG primitives without first-class Prism support — its sole input is the d string passed through to <path d=...>.`,
			`If you intended a polyline, use mark "line" with x/y encodings instead.`,
		},
		SeeAlso: []string{"PRISM_SPEC_003"},
	},
	"PRISM_SPEC_018": {
		Code:    "PRISM_SPEC_018",
		Message: `Sankey mark requires source, target, and value channels (missing: {{.Missing}}).`,
		Fixups: []string{
			`Bind each channel: { "source": {"field": "src", "type": "nominal"}, "target": {"field": "tgt", "type": "nominal"}, "value": {"field": "v", "type": "quantitative"} }.`,
			`Sankey reads a flat-table form: one row per link with src node, tgt node, and flow magnitude.`,
			`If you have a {nodes, links} two-array form, flatten it to a single table with the three required columns before passing to Prism.`,
		},
		SeeAlso: []string{"PRISM_SPEC_013"},
	},

	"PRISM_SPEC_019": {
		Code:    "PRISM_SPEC_019",
		Message: `Selection {{.Selection}} encoding {{.Channel}} is not bound in the spec encoding block (available: {{.Available}}).`,
		Fixups: []string{
			`Bind the {{.Channel}} channel in the spec's "encoding" block — selections can only respond to channels that have a backing field.`,
			`Remove "{{.Channel}}" from the selection's "encodings" list if the channel is intentionally unbound.`,
			`Channel names are lowercase (x | y | x2 | y2 | theta | color | size | shape | opacity | fill | stroke); match the casing exactly.`,
		},
		SeeAlso: []string{"PRISM_SPEC_004", "PRISM_SPEC_020"},
	},
	"PRISM_SPEC_020": {
		Code:    "PRISM_SPEC_020",
		Message: `Interval selection {{.Selection}} uses non-position channel {{.Channel}} (intervals brush over position axes only).`,
		Fixups: []string{
			`Change "{{.Channel}}" to a position channel (x | y | x2 | y2 | theta); intervals brush over continuous axes only.`,
			`For filtering by color / size / shape values, use a point selection on the underlying field instead of an interval brush.`,
			`Theta intervals brush over polar position; valid for arc / pie / donut marks.`,
		},
		SeeAlso: []string{"PRISM_SPEC_019"},
	},

	"PRISM_WASM_001": {
		Code:    "PRISM_WASM_001",
		Message: `Fetch-backed filesystem failed to load {{.URL}} (HTTP {{.Status}}: {{.Reason}}).`,
		Fixups: []string{
			`Confirm the URL is reachable from the page origin and the server allows CORS for cross-origin requests.`,
			`If the dataset lives behind an authentication wall, expose it through a proxy that adds the credentials before the browser hits it.`,
			`For local development serve the .pulse files via a static file server (e.g. ` + "`python -m http.server`" + `) rather than file:// URLs — fetch refuses file:// in most browsers.`,
		},
		SeeAlso: []string{"PRISM_RESOLVE_002", "PRISM_WASM_002"},
	},
	"PRISM_WASM_002": {
		Code:    "PRISM_WASM_002",
		Message: `Origin server for {{.URL}} does not honour Range: requests (status {{.Status}}); archive-shard random access is unavailable.`,
		Fixups: []string{
			`Serve archive shards from a static host that returns 206 Partial Content for Range requests (GitHub Pages, S3, Cloudflare R2, nginx with default config all do).`,
			`If random access is impossible, materialise individual shards as standalone .pulse files at build time and reference them directly.`,
			`Disable archive forms in the spec — load each shard via its own ` + "`<prism-dataset>`" + ` registration.`,
		},
		SeeAlso: []string{"PRISM_WASM_001", "PRISM_RESOLVE_003"},
	},
	"PRISM_WASM_BUDGET_EXCEEDED": {
		Code:    "PRISM_WASM_BUDGET_EXCEEDED",
		Message: `Compiled prism.wasm exceeds PRISM_WASM_MAX_BYTES={{.Limit}} (gzipped size: {{.Actual}}).`,
		Fixups: []string{
			`Raise the ceiling by setting ` + "`PRISM_WASM_MAX_BYTES`" + ` in the environment before running ` + "`make build-wasm`" + `.`,
			`Drop newly-imported dependencies from the WASM entry — confirm cmd/prismwasm/main.go imports only library packages buildable under js,wasm.`,
			`Check ` + "`go list -deps ./cmd/prismwasm | sort | uniq`" + ` for transitive imports that bloat the binary (apache/arrow-go and gonum dominate).`,
		},
	},
	"PRISM_WARN_WASM_COLD_START": {
		Code:    "PRISM_WARN_WASM_COLD_START",
		Message: `WASM cold-start exceeded the soft timing budget ({{.Actual}}ms vs {{.Budget}}ms p95).`,
		Fixups: []string{
			`Cold-start variance is acceptable on first load; warm renders should fall well under the budget.`,
			`Preload the wasm asset with ` + "`<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" crossorigin>`" + ` so the download starts in parallel with the loader parse.`,
			`Confirm the host serves prism.wasm with ` + "`Content-Type: application/wasm`" + ` so the browser uses ` + "`WebAssembly.instantiateStreaming`" + `.`,
		},
	},
	"PRISM_SPEC_021": {
		Code:    "PRISM_SPEC_021",
		Message: `Geo projection or geo-mark binding is invalid: {{.Field}}.`,
		Fixups: []string{
			`Set ` + "`projection.type`" + ` to one of: mercator | equirectangular | naturalearth | albers_usa | orthographic.`,
			`Geoshape marks require ` + "`encoding.feature.field`" + `; geopoint marks require both ` + "`encoding.longitude.field`" + ` and ` + "`encoding.latitude.field`" + `.`,
			`Tier values must be one of: world-110m | world-50m | admin1-50m.`,
		},
		SeeAlso: []string{"PRISM_GEO_001"},
	},
	"PRISM_GEO_001": {
		Code:    "PRISM_GEO_001",
		Message: `Feature {{.Field}} not found in geodata tier {{.Source}} (got id {{.Available}}).`,
		Fixups: []string{
			`Check that the feature id matches the manifest: admin-0 uses ISO 3166-1 alpha-3 (USA, CAN, GBR, ...); admin-1 uses ISO 3166-2 (US-CA, CA-ON, ...).`,
			`Set ` + "`projection.tier`" + ` to ` + "`admin1-50m`" + ` when looking up state/province features; the default tier (world-110m) only carries countries.`,
			`Run ` + "`prism inspect --geo`" + ` to list the feature ids present in the embedded manifest.`,
		},
		SeeAlso: []string{"PRISM_ENCODE_001"},
	},
	"PRISM_GEO_002": {
		Code:    "PRISM_GEO_002",
		Message: `Geo bundle could not be loaded for tier {{.Tier}}: {{.Reason}}.`,
		Fixups: []string{
			`Host build: this should never fail — regenerate the embedded artifact via ` + "`make geodata`" + `.`,
			`WASM build: confirm ` + "`prism static-bundle`" + ` was run and the geodata/ directory is served at the URL passed to ` + "`prism.geo.setBundleURL`" + ` (default: /static/prism/geodata/).`,
			`Check the browser console for a 404 on the missing tier file.`,
		},
		SeeAlso: []string{"PRISM_GEO_001"},
	},
	"PRISM_SPEC_022": {
		Code:    "PRISM_SPEC_022",
		Message: `animation.easing {{.Easing}} is not a known easing name.`,
		Fixups: []string{
			`Use one of the supported easings: linear, cubic_in, cubic_out, cubic_in_out, quad_in, quad_out, quad_in_out, sine_in, sine_out, sine_in_out, expo_in, expo_out, expo_in_out.`,
			`Omit ` + "`animation.easing`" + ` to use the default (cubic_in_out).`,
		},
		SeeAlso: []string{"PRISM_SPEC_023"},
	},
	"PRISM_SPEC_023": {
		Code:    "PRISM_SPEC_023",
		Message: `animation block declared but no encoding channel carries ` + "`key: true`" + `.`,
		Fixups: []string{
			`Add ` + "`\"key\": true`" + ` to one position or mark channel so tweens can match marks across scene swaps (e.g. ` + "`encoding.x`" + ` for object-constancy on the x-axis category).`,
			`Without a key, the animator falls back to positional matching, which is ambiguous when row counts change.`,
		},
		SeeAlso: []string{"PRISM_SPEC_024", "PRISM_WARN_ANIM_FALLBACK"},
	},
	"PRISM_SPEC_024": {
		Code:    "PRISM_SPEC_024",
		Message: `multiple encoding channels carry ` + "`key: true`" + ` (channels: {{.Channels}}); at most one is allowed.`,
		Fixups: []string{
			`Pick the single channel whose field provides stable per-row identity across scene swaps and remove ` + "`key: true`" + ` from the rest.`,
			`Composite keys are not supported in v1.`,
		},
		SeeAlso: []string{"PRISM_SPEC_023"},
	},
	"PRISM_SPEC_025": {
		Code:    "PRISM_SPEC_025",
		Message: `Condition on channel {{.Channel}} references selection {{.Selection}} which is not declared.`,
		Fixups: []string{
			`Declare the selection in the spec's "selection" block before referencing it in a condition.`,
			`Available selections: {{.Available}}.`,
			`Use ` + "`{test: \"...\"}`" + ` for a Pulse-expression condition instead of a named selection.`,
		},
		SeeAlso: []string{"PRISM_SPEC_004", "PRISM_SPEC_026"},
	},
	"PRISM_SPEC_026": {
		Code:    "PRISM_SPEC_026",
		Message: `Condition on channel {{.Channel}}: test expression failed to parse: {{.Reason}}.`,
		Fixups: []string{
			`Check Pulse expression syntax. Expression: {{.Expression}}`,
			`Quote string literals with single quotes ('value'), not double quotes.`,
			`Use Pulse operators (and, or, not, ==, !=, <, <=, >, >=, +, -, *, /, %).`,
		},
		SeeAlso: []string{"PRISM_SPEC_006"},
	},
	"PRISM_SPEC_028": {
		Code:    "PRISM_SPEC_028",
		Message: `Mark {{.Mark}} requires source + target channels (missing: {{.Missing}}).`,
		Fixups: []string{
			`Bind ` + "`encoding.source`" + ` to the parent-id field and ` + "`encoding.target`" + ` to the child-id field.`,
			`The optional ` + "`encoding.text`" + ` channel supplies per-node labels.`,
		},
		SeeAlso: []string{"PRISM_SPEC_018"},
	},
	"PRISM_SPEC_029": {
		Code:    "PRISM_SPEC_029",
		Message: `tree mark expects exactly one root (parent field empty / null); got {{.Count}}.`,
		Fixups: []string{
			`Exactly one input row must have an empty / null parent field. Synthesise a single root if your data has multiple top-level entries.`,
			`Multi-root forests render via ` + "`layer`" + ` (one tree per layer).`,
		},
		SeeAlso: []string{"PRISM_SPEC_028"},
	},
	"PRISM_WARN_NETWORK_CYCLE": {
		Code:    "PRISM_WARN_NETWORK_CYCLE",
		Message: `network input graph contains a cycle; force layout may produce a visually messy result.`,
		Fixups: []string{
			`Cycles are valid for the network mark — the layout converges but visually-clean output benefits from acyclic / DAG inputs.`,
			`If the data is genuinely hierarchical, switch to the ` + "`tree`" + ` mark which enforces acyclicity (` + "`PRISM_ENCODE_TREE_CYCLE`" + `).`,
		},
		SeeAlso: []string{"PRISM_ENCODE_TREE_CYCLE"},
	},
	"PRISM_ENCODE_TREE_CYCLE": {
		Code:    "PRISM_ENCODE_TREE_CYCLE",
		Message: `tree mark cannot be laid out: input graph has a cycle.`,
		Fixups: []string{
			`Tree-style marks require a directed acyclic graph rooted at one parentless node. Break the cycle in the upstream data or switch to the ` + "`network`" + ` mark.`,
		},
	},
	"PRISM_ENCODE_NETWORK_NONFINITE": {
		Code:    "PRISM_ENCODE_NETWORK_NONFINITE",
		Message: `network force layout failed to converge: a node position became non-finite (NaN / Inf).`,
		Fixups: []string{
			`Reduce ` + "`mark.charge`" + ` magnitude or shrink ` + "`mark.link_distance`" + `; very large repulsion forces can blow up the gradient.`,
			`Disconnected components without any edges can also slip into Inf — keep at least one edge per component.`,
		},
	},
	"PRISM_SPEC_027": {
		Code:    "PRISM_SPEC_027",
		Message: `Condition entry on channel {{.Channel}} must carry exactly one of value or field (got: {{.Got}}).`,
		Fixups: []string{
			`Set ` + "`value`" + ` for a literal applied when the condition matches (e.g. ` + "`{\"selection\":\"brush\",\"value\":\"#22c55e\"}`" + `).`,
			`Set ` + "`field`" + ` (+ ` + "`type`" + `) to bind the matching rows to a field-driven encoding.`,
			`A selection-form entry without ` + "`value`" + ` is allowed only when no ` + "`field`" + ` is also set — it inherits the channel's own field binding.`,
		},
		SeeAlso: []string{"PRISM_SPEC_025", "PRISM_SPEC_026"},
	},
	"PRISM_WARN_NULL_DROPPED": {
		Code:    "PRISM_WARN_NULL_DROPPED",
		Message: `{{.Count}} rows skipped: encoding channels {{.Channels}} carried null values.`,
		Fixups: []string{
			`Source data had {{.Count}} rows where one or more channel-bound fields were null (often from a left / outer join with no match on the right). Filter or impute those rows upstream to suppress the warning.`,
			`See ` + "`docs/src/concepts/multi-source.md`" + ` for join null semantics.`,
		},
		SeeAlso: []string{"PRISM_JOIN_001"},
	},
	"PRISM_WARN_NULL_AGG_ALL": {
		Code:    "PRISM_WARN_NULL_AGG_ALL",
		Message: `Aggregate {{.Op}} over field {{.Field}} produced a null result: every input row was null.`,
		Fixups: []string{
			`The group has no non-null values for ` + "`{{.Field}}`" + `. Filter the empty group upstream or supply a default via a calculate transform.`,
		},
		SeeAlso: []string{"PRISM_WARN_NULL_DROPPED"},
	},
	"PRISM_WARN_ANIM_FALLBACK": {
		Code:    "PRISM_WARN_ANIM_FALLBACK",
		Message: `animation skipped: {{.Reason}}.`,
		Fixups: []string{
			`Animation only runs when successive scenes share the same composition shape (layer count, mark families, axis types). Structural changes snap to the new scene instantly.`,
			`Set ` + "`animation.enter`" + ` and ` + "`animation.exit`" + ` to ` + "`none`" + ` to suppress the fade on first render.`,
		},
		SeeAlso: []string{"PRISM_SPEC_023"},
	},
}

Codes is the canonical Prism error code catalog. Codes share the PRISM_<DOMAIN>_NNN form. New codes append at the bottom of their domain block; existing codes are not renumbered.

Functions

func CodesSorted

func CodesSorted() []string

CodesSorted returns the catalog keys in ascending order.

func RenderMessage

func RenderMessage(code string, ctx map[string]any) string

RenderMessage expands a code's Message template against ctx. Exposed for callers that want to surface the canonical message without constructing a full AppError.

Types

type AppError

type AppError struct {
	// Code is a PRISM_* identifier (e.g. "PRISM_SPEC_001").
	Code string

	// Message is the human-readable, already-formatted message.
	Message string

	// Fixups are the ordered, already-formatted fixup suggestions.
	Fixups []string

	// SeeAlso lists related codes or documentation references.
	SeeAlso []string

	// Context carries the variables that were substituted into the
	// message template (e.g. {"Field": "xfield", "Dataset": "cohort"}).
	Context map[string]any

	// Inner is a wrapped underlying error, if any.
	Inner error
}

AppError is the canonical Prism error type.

func New

func New(code, message string, ctx map[string]any) *AppError

New constructs an AppError with the given code, message, and context. Fixups and SeeAlso are looked up from the code catalog when present.

func Wrap

func Wrap(code, message string, ctx map[string]any, inner error) *AppError

Wrap is New plus an inner error.

func (*AppError) ContextKeys

func (e *AppError) ContextKeys() []string

ContextKeys returns context keys in deterministic order; useful for stable text rendering and tests.

func (*AppError) Error

func (e *AppError) Error() string

Error renders a single-line description suitable for stderr.

func (*AppError) MarshalJSON

func (e *AppError) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler emitting the envelope shape.

func (*AppError) Unwrap

func (e *AppError) Unwrap() error

Unwrap returns the inner error so errors.Is / errors.As work.

type CodeMetadata

type CodeMetadata struct {
	// Code is the PRISM_* identifier.
	Code string
	// Message is the user-facing template (Go text/template syntax).
	Message string
	// Fixups is the ordered list of fixup templates (Go text/template).
	Fixups []string
	// FixupNotApplicable marks codes that legitimately have no fixups.
	FixupNotApplicable bool
	// SeeAlso lists related codes or doc references.
	SeeAlso []string
}

CodeMetadata describes one Prism error code: its message template, fixup templates, and any cross-references.

Jump to

Keyboard shortcuts

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