metamodel

package
v0.0.0-...-8baf8eb Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Index

Constants

View Source
const (
	PropertyTypeString   = "string"
	PropertyTypeDate     = "date"
	PropertyTypeDatetime = "datetime"
	PropertyTypeInteger  = "integer"
	PropertyTypeBoolean  = "boolean"
	PropertyTypeEnum     = "enum"
	PropertyTypeFile     = "file"
	PropertyTypeRrule    = "rrule"
)

Built-in property types

View Source
const (
	IDTypeShort      = "short"      // IDs are random base36 strings (e.g., REQ-a3f8) - default
	IDTypeSequential = "sequential" // IDs are auto-generated with numeric suffix (e.g., REQ-001)
	IDTypeManual     = "manual"     // IDs are manually specified strings (e.g., auth-module)

	// Deprecated alias (still accepted for backwards compatibility)
	IDTypeString = "string" // Deprecated: use "manual" instead
)

ID types for entities

View Source
const (
	IDCapsUpper = "upper" // Random suffix is uppercase (e.g., REQ-A3F8) - default
	IDCapsLower = "lower" // Random suffix is lowercase (e.g., REQ-a3f8)
)

ID capitalization modes for short IDs

View Source
const (
	OrderPropertyOut = "_order_out"
	OrderPropertyIn  = "_order_in"
)

Reserved relation-property names that hold the user-controlled order value. The names are stable across mode changes so that promoting a relation from outgoing-only to both (or vice versa) keeps existing values on disk valid.

View Source
const DefaultDateFormat = "2006-01-02"

DefaultDateFormat is the default format for date properties (ISO 8601)

View Source
const DefaultDatetimeFormat = time.RFC3339

DefaultDatetimeFormat is the default format for datetime properties (RFC3339, a time-bearing ISO 8601 instant). Unlike date, a datetime value carries a time-of-day and (canonically) a UTC offset.

Variables

View Source
var ErrRruleExhausted = errors.New("rrule exhausted")

ErrRruleExhausted marks "this rule has no further occurrence" — a COUNT that has been reached or an UNTIL that has passed. It is distinct from a malformed rule so a caller can tell a finished schedule from an operator typo.

View Source
var ReservedPropertyNames = map[string]bool{
	"id":   true,
	"type": true,
}

ReservedPropertyNames contains property names that cannot be used in metamodel definitions because they conflict with built-in entity fields.

Functions

func DefaultMetamodelYAML

func DefaultMetamodelYAML() string

DefaultMetamodelYAML returns the default metamodel as YAML

func FiniteOrder

func FiniteOrder(v any) (float64, bool)

FiniteOrder converts a JSON- or YAML-decoded relation-property value to a finite float64. Returns (value, true) for any built-in Go numeric type (int*, uint*, float32, float64) that is finite; returns (0, false) for nil, non-numeric types, NaN, or +/-Inf.

All consumers that interpret managed order properties go through this helper: the entity manager's auto-assign and renumber paths, the data-entry sort and wire validators, the analyzer, and the CLI commands. Keep one canonical implementation to avoid drift; do not redefine variants in callers.

func IsBuiltinType

func IsBuiltinType(t string) bool

IsBuiltinType returns true if the type is a built-in property type

func NextRrule

func NextRrule(s string, after time.Time) (time.Time, error)

NextRrule returns the first occurrence of s strictly after `after`, truncated to UTC midnight.

It lives here, beside ValidateRrule, so RRULE mechanics stay in one package: callers get validation and occurrence-stepping with identical prefix handling and identical error text, and no other package needs to depend on the rrule library.

A rule without DTSTART is anchored at `after`, so a bare rule stored in an `rrule` property is interpreted relative to the date the caller asked about. (ValidateRrule already rejects INTERVAL > 1 without DTSTART, since that combination drifts.)

Returns ErrRruleExhausted when the rule has no occurrence left.

func ParseBooleanValue

func ParseBooleanValue(val any) (bool, error)

ParseBooleanValue parses a boolean from various input types

func ParseDateValue

func ParseDateValue(s string, propDef *PropertyDef) (time.Time, error)

ParseDateValue parses a date string using the property's format. It tries the specified format first, then falls back to common formats to handle dates stored with timestamps (e.g., from YAML parsing).

func ParseIntegerValue

func ParseIntegerValue(val any) (int, error)

ParseIntegerValue parses an integer from various input types. A float64 is accepted only when it has no fractional part — truncating 3.5 to 3 would silently corrupt the value (matching the integer property-validation rule).

func RenameEntityType

func RenameEntityType(path, oldType, newType string, fs storage.FS) error

RenameEntityType performs an AST-level rename of an entity type in a metamodel YAML file using the given filesystem. It preserves comments, formatting, and key ordering.

Updates:

  • The entity key under `entities:`
  • All references in `relations:` `from:` and `to:` arrays
  • All references in `validations:` `entity_type:` fields

func ValidateIDPrefix

func ValidateIDPrefix(prefix string) error

ValidateIDPrefix rejects id_prefix values whose generated IDs would fail entity ID validation (BUG-RHFHTH). Generated short/sequential IDs have the shape <base>-<suffix>, where base is the prefix with one trailing dash trimmed — so the base must be non-empty, contain only [A-Za-z0-9_-], and neither contain nor end in a dash run that would re-create the forbidden "--" sequence (reserved as the relation key separator). Enforced at metamodel load; entity.GenerateShortID assumes a load-validated prefix.

func ValidateRrule

func ValidateRrule(s string) error

ValidateRrule validates an RRULE string. It strips the "RRULE:" prefix if present, parses the rule, and rejects INTERVAL > 1 without DTSTART (which would cause interval cadence drift).

This function is the single source of truth for RRULE validation, used by both the metamodel property validator and the Lua rrule_next helper.

func ValidateSchemaName

func ValidateSchemaName(name string) error

ValidateSchemaName reports whether name is safe to interpolate into reconciler DDL (see [unsafeSchemaNameChar]). It is intentionally a blocklist of dangerous characters rather than an allowlist, because entity-type and property names in shipped metamodels legitimately use dashes and internal spaces (e.g. "review-response", "some property"); an allowlist would reject existing valid schemas. It also rejects a leading/trailing space, which is a likely typo and confuses the DDL literal. Exported so the reconciler can re-check as defense-in-depth before emitting DDL rather than trusting that load-time validation ran.

Types

type ACLBypass

type ACLBypass string

ACLBypass declares which ACL-bypassing capabilities a Lua surface may unlock through rela.bypass_acl (TKT-D8T148, TKT-Y3JVFK).

It is a ROUGH GUARD, not a permission model. Its job is to tell whoever deploys a script whether its bypass block needs reading carefully:

  • absent ⇒ the script can only do what the invoking principal can. The ACL already bounds it, so it needs no special scrutiny.
  • present ⇒ a human reads that closure before deployment.

The value says WHICH KIND of scrutiny — is this reading data the principal cannot see, or writing past their permissions? That is all the review decision needs, which is why the capability is not sliced by verb (`create`, `update`, `delete`). Verb granularity would add config surface without changing what the reviewer does. It would also name capabilities the elevated handle does not have: there is no elevated create_entity or update_entity today, and config naming a nonexistent capability is worse than a missing field because it appears to work (DEC-O59WM4).

If elevated entity creation ever lands, a set-valued form (`allow_acl_bypass: [read, write]`) would let verbs be added without a second migration. Do not grow this into a string-matched grammar.

const (
	// ACLBypassNone is the zero value: no elevation. rela.bypass_acl is not
	// registered at all, so the script cannot elevate however it is written.
	ACLBypassNone ACLBypass = ""
	// ACLBypassRead unlocks the elevated READ methods only
	// (admin.get_entity / list_entities / get_relations). The admin table has
	// no write methods, so the surface is structurally unable to mutate. This
	// is what a document render uses to aggregate over rows its caller cannot
	// see.
	ACLBypassRead ACLBypass = "read"
	// ACLBypassWrite unlocks the elevated WRITE methods only
	// (admin.create_relation / delete_relation / delete_entity).
	ACLBypassWrite ACLBypass = "write"
	// ACLBypassReadWrite unlocks both. This is what `allow_acl_bypass: true`
	// meant before the enum existed, and what the migration rewrites it to.
	ACLBypassReadWrite ACLBypass = "read+write"
)

func (ACLBypass) AllowsRead

func (a ACLBypass) AllowsRead() bool

AllowsRead reports whether elevated reads are unlocked.

func (ACLBypass) AllowsWrite

func (a ACLBypass) AllowsWrite() bool

AllowsWrite reports whether elevated writes are unlocked.

func (ACLBypass) Enabled

func (a ACLBypass) Enabled() bool

Enabled reports whether any elevation is unlocked.

func (*ACLBypass) UnmarshalYAML

func (a *ACLBypass) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML accepts only the string forms. The legacy boolean `allow_acl_bypass: true` is REFUSED with a message naming the replacement, rather than silently reinterpreted.

Accepting the bool would be the easy path and is deliberately not taken. This field grants ACL bypass, and a parser that maps a legacy value to the BROADEST setting is the wrong default for a privilege field — if the two spellings ever drift, the shim resolves toward more access. A compatibility shim also has no forcing function that ever removes it, so two representations of one concept would persist indefinitely and every reader (validation, docs, tooling, the next capability added) would handle both. `rela migrate` rewrites `true` ⇒ `read+write`, which is the one-time cost that buys a single representation.

type AttachmentPolicy

type AttachmentPolicy struct {
	// contains filtered or unexported fields
}

AttachmentPolicy is a focused read-view over a metamodel's attachment-scan configuration. It is constructed with NewAttachmentPolicy rather than living as methods on Metamodel, keeping the scan accessors off the metamodel's wide public surface (the plimsoll god-object load line) — consumers depend on the narrow scan policy, not the whole schema.

func NewAttachmentPolicy

func NewAttachmentPolicy(m *Metamodel) AttachmentPolicy

NewAttachmentPolicy returns the scan-policy view over m. m must be non-nil.

func (AttachmentPolicy) HasUnconfiguredScan

func (p AttachmentPolicy) HasUnconfiguredScan() bool

HasUnconfiguredScan reports whether the metamodel declares at least one `file`-type property while no scan command is configured for it (no global command, no property command) and it has not explicitly opted out with `scan: off`. The composition root uses this to emit a single startup warning nudging the operator to wire a scanner or explicitly disable scanning.

func (AttachmentPolicy) ScanCommandFor

func (p AttachmentPolicy) ScanCommandFor(prop PropertyDef) []string

ScanCommandFor resolves the scan command that should run for a file property, or nil when the property is not scanned. Scanning runs when a command is configured (property-level wins over global) and the property has not opted out with `scan: off`.

func (AttachmentPolicy) ScanSockets

func (p AttachmentPolicy) ScanSockets() []string

ScanSockets returns the operator-configured extra socket paths to bind read-only into the scan command's sandbox (`attachments.scan_sockets`), on top of the always-bound well-known clamd locations. Nil when unset.

type AttachmentsConfig

type AttachmentsConfig struct {
	// Allow names the MIME allowlist preset (e.g. "default-safe") or, when it
	// holds more than a preset name, an explicit list of allowed sniffed MIME
	// types. Empty means the built-in default-safe preset.
	Allow []string `yaml:"allow,omitempty"`

	// ScanCmd is the global external scan command (array args). Its presence
	// enables scanning for every `file` property that does not opt out with
	// `scan: off`.
	ScanCmd []string `yaml:"scan_cmd,omitempty"`

	// ScanSockets are extra host paths bound read-only into the scan command's
	// sandbox, on top of the well-known clamd socket locations that are always
	// bound. The motivating case is a `clamd.conf` whose `LocalSocket` lives
	// outside the defaults. A unix socket bound this way is reachable without
	// opening network egress. Empty on the common path (stock ClamAV install).
	ScanSockets []string `yaml:"scan_sockets,omitempty"`
}

AttachmentsConfig is the top-level `attachments:` block: the global safety floor applied to every `file` property unless a property overrides it.

type AutomationAction

type AutomationAction struct {
	Set            string                `yaml:"set,omitempty"`
	Value          string                `yaml:"value,omitempty"`
	CreateRelation *CreateRelationAction `yaml:"create_relation,omitempty"`
	CreateEntity   *CreateEntityAction   `yaml:"create_entity,omitempty"`
	Lua            string                `yaml:"lua,omitempty"`      // Inline Lua code to execute
	LuaFile        string                `yaml:"lua_file,omitempty"` // Path to Lua script in scripts/ directory

	// AllowACLBypass unlocks rela.bypass_acl in this Lua action (TKT-D8T148).
	// Operator-only (lives in the schema file). When set, the script may call
	// rela.bypass_acl(fn) to obtain a closure-scoped elevated handle whose
	// access skips the ACL deny (still audited, real principal preserved).
	// Ignored for non-Lua actions.
	//
	// Since TKT-Y3JVFK this is an enum, not a bool: `read`, `write` or
	// `read+write` select which methods the handle carries. The legacy
	// `true` is refused at parse time with a message naming `read+write`;
	// `rela migrate` rewrites it.
	AllowACLBypass ACLBypass `yaml:"allow_acl_bypass,omitempty"`

	// Capabilities declares which ambient capabilities this Lua action may
	// reach: http, ai, write_file and named secrets (TKT-YH52OM). Omitting it
	// grants NONE of them — automations run on the write path of any HTTP
	// request, so they are not an operator-shell surface and do not get the
	// trusted default. Ignored for non-Lua actions.
	Capabilities Capabilities `yaml:"capabilities,omitempty"`
}

AutomationAction specifies an operation to perform.

type AutomationCheck

type AutomationCheck struct {
	Check    string `yaml:"check"`
	Severity string `yaml:"severity,omitempty"`
	Message  string `yaml:"message"`
}

AutomationCheck specifies a validation condition.

type AutomationDef

type AutomationDef struct {
	Name        string             `yaml:"name"`
	Description string             `yaml:"description,omitempty"`
	On          AutomationTrigger  `yaml:"on"`
	Do          []AutomationAction `yaml:"do,omitempty"`
	Validate    []AutomationCheck  `yaml:"validate,omitempty"`
}

AutomationDef defines a trigger-action automation rule.

type AutomationTrigger

type AutomationTrigger struct {
	Entity          StringOrSlice `yaml:"entity,omitempty"`
	Property        string        `yaml:"property,omitempty"`
	Becomes         string        `yaml:"becomes,omitempty"`
	From            string        `yaml:"from,omitempty"`
	Created         bool          `yaml:"created,omitempty"`
	RelationCreated string        `yaml:"relation_created,omitempty"`
	RelationRemoved string        `yaml:"relation_removed,omitempty"`
	When            []string      `yaml:"when,omitempty"` // Property conditions that must match (AND logic)

	// Condition is a predicate EXPRESSION that must hold for the
	// automation to fire, ANDed with every When clause.
	//
	// When and Condition are separate keys because their syntaxes
	// overlap without erroring: filter.Parse accepts
	// "days_between(entity.due, today()) <= 7" as a filter on a property
	// literally named "days_between(entity.due, today())", which then
	// matches nothing, silently. Sniffing which dialect a string is
	// written in would guess, and guess quietly — so the operator says
	// which one they meant by choosing the key.
	//
	// `when:` is filter syntax (`status=todo`) transpiled to predicate on
	// load; `condition:` is predicate source evaluated as written, so it
	// gets boolean composition and the host-function stdlib — notably the
	// date arithmetic (today/days_between/date_add/rrule_next) that a
	// property filter cannot express.
	Condition string `yaml:"condition,omitempty"`
}

AutomationTrigger specifies conditions that activate an automation.

type Capabilities

type Capabilities struct {
	// HTTP grants the `http` global (outbound requests).
	HTTP bool `yaml:"http,omitempty"`

	// AI grants the `ai` global. Note ai.* calls are billable.
	AI bool `yaml:"ai,omitempty"`

	// WriteFile grants rela.write_file (already confined to output/).
	WriteFile bool `yaml:"write_file,omitempty"`

	// Secrets names the keys from .rela/secrets.yaml this script may read.
	// A key not listed is absent from rela.secrets entirely.
	Secrets []string `yaml:"secrets,omitempty"`
}

Capabilities is the operator-authored declaration of which ambient, non-graph capabilities a Lua script may reach (TKT-YH52OM). It is the YAML face of lua.Capabilities; the wiring site translates one to the other.

It appears under a `capabilities:` key on the config blocks that name a script — automation actions, data-entry actions, and documents:

actions:
  notify_slack:
    script: notify.lua
    capabilities:
      http: true
      secrets: [slack_webhook_url]

Fail-closed

Omitting the block grants nothing. That is deliberate and is the whole point of the ticket: before this existed, every script on every surface — including read-only document renders and validation rules — held `http`, `ai` and the ENTIRE contents of .rela/secrets.yaml, which is a two-call exfiltration path. A default of "closed" means a capability is present only where an operator wrote it down.

Secrets is a list

Capabilities.Secrets names individual keys rather than being a boolean, because a boolean grants the whole file: an action needing one Slack webhook would also receive the database DSN. There is deliberately no "all" spelling here — the broad grant exists only as a Go-side wiring choice (lua.TrustedCapabilities), so it cannot be reached from a config file.

func (Capabilities) Any

func (c Capabilities) Any() bool

Any reports whether the block grants anything at all.

Note this deliberately has no AllSecrets term, unlike lua.Capabilities.Any: there is no "all secrets" spelling in YAML, so a config block can only grant via the named list. See the AllSecrets field on lua.Capabilities.

func (Capabilities) Fields

func (c Capabilities) Fields() (http, ai, writeFile bool, secrets []string)

Fields returns the grant as plain values.

This is the SINGLE translation seam between the YAML type and every runtime consumer (TKT-YH52OM). It exists because the obvious alternative — each consumer copying the struct field-by-field — is what produced the defect this method was added to prevent: the grant was hand-copied at five sites, and a sixth path dropped it silently.

It returns loose values rather than a lua.Capabilities because metamodel must not import lua, and autocascade may import neither (see .go-arch-lint.yml). Each consumer converts at its own boundary, but they all read the fields from here, so adding a capability means changing this signature — a COMPILE error at every consumer rather than a silent per-surface omission.

func (*Capabilities) UnmarshalYAML

func (c *Capabilities) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML decodes the mapping form and REFUSES a bare boolean.

`capabilities: true` is rejected rather than read as "grant everything", following the same reasoning ACLBypass.UnmarshalYAML records for allow_acl_bypass: for a privilege field, a parser that maps a loose value to the BROADEST setting is the wrong default. Here it is sharper still, since "everything" would include the entire secrets file — precisely the grant this type exists to make impossible to write by accident.

type ChecklistRule

type ChecklistRule struct {
	// AllChecked requires all checklist items to be checked
	AllChecked bool `yaml:"all-checked,omitempty"`

	// AllowSkipped treats strikethrough items as complete (e.g., "- [x] ~~task~~ (N/A: reason)")
	AllowSkipped bool `yaml:"allow-skipped,omitempty"`
}

ChecklistRule defines validation rules for markdown checklists.

type CircularIncludeError

type CircularIncludeError struct {
	Chain []string // e.g., ["a.yaml", "b.yaml", "a.yaml"]
}

CircularIncludeError is returned when a circular include chain is detected.

func (*CircularIncludeError) Error

func (e *CircularIncludeError) Error() string

type ConflictingIDPrefixError

type ConflictingIDPrefixError struct {
	EntityType string
}

ConflictingIDPrefixError is returned when both id_prefix and id_prefixes are specified.

func (*ConflictingIDPrefixError) Error

func (e *ConflictingIDPrefixError) Error() string

type ContentRule

type ContentRule struct {
	// RequiredHeaders specifies headers that must appear in the content
	RequiredHeaders []HeaderCheck `yaml:"required-headers,omitempty"`

	// Checklist specifies validation rules for markdown checklists (task lists)
	Checklist *ChecklistRule `yaml:"checklist,omitempty"`
}

ContentRule defines validation rules for markdown body content.

type CreateEntityAction

type CreateEntityAction struct {
	Type       string            `yaml:"type"`                 // Entity type to create
	Template   string            `yaml:"template,omitempty"`   // Optional: template variant, supports interpolation (e.g., "{{new.kind}}")
	Properties map[string]string `yaml:"properties,omitempty"` // Properties (values support interpolation)
	Relation   string            `yaml:"relation,omitempty"`   // Optional: relation FROM trigger TO created entity
	IfExists   string            `yaml:"if_exists,omitempty"`  // Behavior when relation already exists: skip (default), error, replace
}

CreateEntityAction specifies parameters for creating a new entity.

type CreateRelationAction

type CreateRelationAction struct {
	Relation string `yaml:"relation"`
	To       string `yaml:"to"`
}

CreateRelationAction specifies parameters for creating a relation.

type CustomType

type CustomType struct {
	Values []string          `yaml:"values,omitempty"` // Allowed values (makes this an enum type)
	Labels map[string]string `yaml:"labels,omitempty"` // Optional display labels keyed by value (display-only; value stays the identity)
	// Descriptions is optional per-value prose keyed by value: what each value
	// MEANS, as opposed to Labels (the short display text). Display-only,
	// surfaced by the generated documentation (FEAT-G4VO53) so a reader
	// understands e.g. what "blocked" or "in-review" signifies. A value with no
	// entry simply has no description. Distinct from the type-level Description
	// scalar below (which documents the type as a whole).
	Descriptions map[string]string `yaml:"descriptions,omitempty"`
	Default      string            `yaml:"default,omitempty"`     // Default value
	Description  string            `yaml:"description,omitempty"` // Documentation for the type
	Validations  []TypeValidation  `yaml:"validations,omitempty"` // Regex validations with error messages

	// Transitions declares the legal value→value moves for this enum,
	// making it a state machine (TKT-E4LW2). This is declarative source
	// data only — the metamodel does not enforce it. At startup
	// internal/statemachine.Compile reads these into an executable machine
	// that the entitymanager runs on the write path (legality 422, guard
	// 403, precondition 422). Empty means "any value may change to any
	// other" (the historical, unconstrained behavior). Only meaningful on a
	// named type — inline `type: enum` properties carry no transitions.
	Transitions []TransitionDef `yaml:"transitions,omitempty"`

	// Initial names the only legal entry value on entity create when this
	// type is a state machine. Empty falls back to Default. Consumed by
	// internal/statemachine at compile time.
	Initial string `yaml:"initial,omitempty"`
}

CustomType defines a reusable type with optional enum values and/or regex validations.

type DuplicateDefinitionError

type DuplicateDefinitionError struct {
	Kind  string // "type", "entity", "relation", or "validation"
	Name  string
	File1 string
	File2 string
}

DuplicateDefinitionError is returned when the same name is defined in multiple included files.

func (*DuplicateDefinitionError) Error

func (e *DuplicateDefinitionError) Error() string

type EntityDef

type EntityDef struct {
	Label         string                 `yaml:"label"`
	LabelPlural   string                 `yaml:"label_plural,omitempty"`
	Description   string                 `yaml:"description,omitempty"` // Documentation explaining intent/usage
	Plural        string                 `yaml:"plural,omitempty"`      // Used for directory names (e.g., "policies" for "policy")
	Aliases       []string               `yaml:"aliases,omitempty"`
	IDType        string                 `yaml:"id_type,omitempty"`     // "short" (default), "sequential", or "manual"
	IDCaps        string                 `yaml:"id_caps,omitempty"`     // "upper" (default) or "lower" - capitalization for short ID suffix
	IDPrefix      string                 `yaml:"id_prefix,omitempty"`   // Single ID prefix (sugar for single-element id_prefixes)
	IDPrefixes    []string               `yaml:"id_prefixes,omitempty"` // Multiple ID prefixes
	RDFType       string                 `yaml:"rdf_type,omitempty"`
	Properties    map[string]PropertyDef `yaml:"properties"`
	PropertyOrder []string               `yaml:"-"`                      // Order of properties as defined in YAML (computed at load)
	DefaultSort   []SortSpec             `yaml:"default_sort,omitempty"` // Default sort order for this entity type
	Color         string                 `yaml:"color,omitempty"`
	BorderColor   string                 `yaml:"border_color,omitempty"`
	// DisplayProperty names the property whose value renders as the
	// human-readable display name in lists, cards, link text, etc.
	// When empty, GetPrimaryProperty() falls back to the autoderivation
	// (priority list title/name/label, then alphabetical fallback).
	// Validated at metamodel-load time: must reference a defined
	// property and must not have leading/trailing whitespace.
	DisplayProperty string `yaml:"display_property,omitempty"`
}

EntityDef defines an entity type in the metamodel

TODO(TKT-N0IKN9): 24 exported methods, over the 20 exported-method line. Schema value type; ratchet candidate alongside Metamodel. DisplayProperties (TKT-NJTBQX) is the 24th — it reports the property set backing the display title so the ACL locked-title guard can gate on templated display_property (see internal/dataentry mentions); ratchet back down when this type is decomposed.

func (*EntityDef) DisplayProperties

func (e *EntityDef) DisplayProperties() []string

DisplayProperties returns every property whose value backs the display title, so callers can reason about the title's data sources — notably to decide whether the title is unreadable when one of those properties is access-controlled (see internal/dataentry mentions).

  • Templated display_property: all placeholder property names, in order.
  • Bare display_property or an autoderived primary: that single name.
  • No display name source (autoderivation found nothing): empty.

Unlike GetPrimaryProperty (which returns "" for a template because a template names no single *writable* target), this reports the full read set. The template was validated at load, so parsing here cannot fail; a malformed template (only reachable via the no-validate migration path) yields no names rather than an error.

func (*EntityDef) DisplayTitle

func (e *EntityDef) DisplayTitle(id string, properties map[string]any) string

DisplayTitle returns the display title for an entity using its type's primary property. Behavior:

  • String value: returned verbatim (the common case).
  • Non-string value (number, boolean, enum stored as a typed value): stringified via fmt.Sprintf("%v", val) so an explicit display_property: status (an enum) shows the value and not the ID. nil values fall through to the ID — `%v` on nil yields "<nil>" which would be a worse display name than the ID.
  • Missing or empty-after-stringification: falls back to the ID.

The non-string stringification is what makes the explicit display_property override pay off for enum-typed fields. See review-response RR-9CW5N.

When display_property is a template (contains `{`), the placeholders are substituted from properties, whitespace is collapsed, and the result is returned — falling back to the ID when it renders empty.

func (*EntityDef) GetAliases

func (e *EntityDef) GetAliases() []string

GetAliases returns the entity aliases

func (*EntityDef) GetBorderColor

func (e *EntityDef) GetBorderColor() string

GetBorderColor returns the border color

func (*EntityDef) GetColor

func (e *EntityDef) GetColor() string

GetColor returns the color

func (*EntityDef) GetDefaultStatus

func (e *EntityDef) GetDefaultStatus(m *Metamodel) string

GetDefaultStatus returns the default status value for this entity type. It checks the entity's status property definition for a custom type or inline values. If no explicit default exists, returns the first valid value, or "draft" as final fallback.

func (*EntityDef) GetIDCaps

func (e *EntityDef) GetIDCaps() string

GetIDCaps returns the ID capitalization mode for short IDs, defaulting to "upper".

func (*EntityDef) GetIDPatterns deprecated

func (e *EntityDef) GetIDPatterns() []string

GetIDPatterns returns the entity ID prefixes.

Deprecated: Use GetIDPrefixes instead.

func (*EntityDef) GetIDPrefixes

func (e *EntityDef) GetIDPrefixes() []string

GetIDPrefixes returns the effective ID prefixes for this entity type. It normalizes id_prefix (singular) and id_prefixes (plural) into a single list.

func (*EntityDef) GetIDType

func (e *EntityDef) GetIDType() string

GetIDType returns the ID type for this entity, defaulting to "short".

func (*EntityDef) GetLabel

func (e *EntityDef) GetLabel() string

GetLabel returns the entity label

func (*EntityDef) GetLabelPlural

func (e *EntityDef) GetLabelPlural() string

GetLabelPlural returns the human-readable plural label for an entity type (used in UI strings, e.g. "List of Features").

func (*EntityDef) GetPlural

func (e *EntityDef) GetPlural(typeName string) string

GetPlural returns the slug-form plural for an entity type (used as URL segments, fsstore directory names, OpenAPI paths). Falls back to naive pluralization of the type name when not explicitly set.

func (*EntityDef) GetPrimaryProperty

func (e *EntityDef) GetPrimaryProperty() string

GetPrimaryProperty returns the name of the primary property used as the entity's display name.

Resolution order:

  1. Explicit `display_property` set on the entity definition. The name is returned verbatim — load-time validation already guaranteed it references a defined property.
  2. The first match in the priority list `title`/`name`/`label`, when defined as a required string.
  3. Any required string property (alphabetical for determinism).
  4. Empty string when no candidate exists.

A *templated* display_property (containing `{`) has no single primary property — it is a readonly, derived display string. GetPrimaryProperty returns "" for it; DisplayTitle renders the template directly. Callers that treat the result as a writable property key (e.g. a create title shortcut) therefore get "" and skip, which is correct: there is no single field to write into.

func (*EntityDef) GetProperties

func (e *EntityDef) GetProperties() any

GetProperties returns the entity properties for JSON output. Note: This returns interface{} to satisfy the SchemaEntityDef interface. For typed access, use PropertyDefs() which implements PropertySchema.

func (*EntityDef) GetPropertyOrder

func (e *EntityDef) GetPropertyOrder() []string

GetPropertyOrder returns the property names in their definition order. If PropertyOrder was not populated during loading, returns nil. Returns a copy to prevent external modification.

func (*EntityDef) GetRDFType

func (e *EntityDef) GetRDFType() string

GetRDFType returns the RDF type

func (*EntityDef) HasContent

func (e *EntityDef) HasContent() bool

HasContent implements PropertySchema for EntityDef. Entities always support markdown body content.

func (*EntityDef) HasPattern

func (e *EntityDef) HasPattern(pattern string) bool

HasPattern checks if the entity type matches a given ID pattern

func (*EntityDef) IsManualID

func (e *EntityDef) IsManualID() bool

IsManualID returns true if this entity type uses manually-specified IDs

func (*EntityDef) IsSequentialID

func (e *EntityDef) IsSequentialID() bool

IsSequentialID returns true if this entity type uses auto-generated sequential IDs

func (*EntityDef) IsShortID

func (e *EntityDef) IsShortID() bool

IsShortID returns true if this entity type uses short random IDs

func (*EntityDef) MatchesID

func (e *EntityDef) MatchesID(id string) bool

MatchesID checks if an ID matches any of this entity type's prefixes

func (*EntityDef) PropertyDefs

func (e *EntityDef) PropertyDefs() map[string]PropertyDef

PropertyDefs implements PropertySchema for EntityDef.

type EntityProjection

type EntityProjection struct {
	// DisplayProperty is the explicit display-name property (may be empty, in
	// which case rendering falls back to the title/name/label autoderivation).
	DisplayProperty string `json:"display_property,omitempty"`
	// PropertyOrder is the property order as defined in the metamodel YAML,
	// preserved because display and diff rendering present properties in order.
	PropertyOrder []string `json:"property_order,omitempty"`
	// Properties maps each property name to its render-relevant definition.
	Properties map[string]PropertyProjection `json:"properties"`
}

EntityProjection is the render-relevant projection of one entity type.

type EntityShape

type EntityShape struct {
	Properties map[string]PropertyShape `json:"properties"`
}

EntityShape is the data-shape projection of one entity type.

type FSLoader

type FSLoader struct {
	// contains filtered or unexported fields
}

FSLoader loads a metamodel from a file on the given filesystem. It performs a migration-detection pre-check: if the file uses deprecated syntax the current code can't safely interpret, Load returns a *migration.Error telling the user to run `rela migrate`.

func NewFSLoader

func NewFSLoader(fs storage.FS, path string) *FSLoader

NewFSLoader constructs a filesystem-backed metamodel loader.

func (*FSLoader) Load

func (l *FSLoader) Load(_ context.Context) (*Metamodel, []string, error)

Load runs migration detection and then parses the metamodel. Includes are resolved recursively; the returned file list covers the main file plus every included file.

func (*FSLoader) Subscribe

func (l *FSLoader) Subscribe(_ context.Context, onChange func()) (func(), error)

Subscribe watches the metamodel file and its includes. Because includes can change between reloads, the file list is refreshed on each event — new includes are added to the watcher so they fire subsequent events.

type HeaderCheck

type HeaderCheck struct {
	// Header is an exact header string to match (e.g., "## Context")
	Header string `yaml:"header,omitempty"`

	// Pattern is a regex pattern to match headers (e.g., "## (Alternative|Alternatives)")
	Pattern string `yaml:"pattern,omitempty"`
}

HeaderCheck specifies a header to check for in markdown content. Can be unmarshaled from either a simple string (exact match) or an object with pattern field.

func (*HeaderCheck) GetMatchString

func (h *HeaderCheck) GetMatchString() string

GetMatchString returns the pattern or header string to match against

func (*HeaderCheck) IsPattern

func (h *HeaderCheck) IsPattern() bool

IsPattern returns true if this is a regex pattern match

func (*HeaderCheck) UnmarshalYAML

func (h *HeaderCheck) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML allows HeaderCheck to be unmarshaled from either a string or an object. String form: "## Context" (exact header match) Object form: { pattern: "## (Alternative|Alternatives)" }

type ImageStep

type ImageStep struct {
	// Reencode is the canonical output format: "jpeg" (default) or "png". Both
	// are always within the default-safe MIME allowlist, so the re-encoded
	// output is safe by construction.
	Reencode string `yaml:"reencode,omitempty"`
	// Quality is the JPEG quality (1..100) when Reencode is "jpeg". Zero uses
	// the package default. Ignored for PNG.
	Quality int `yaml:"quality,omitempty"`
}

ImageStep is a native, in-process image transform: it decodes the upload with a memory-safe pure-Go decoder, bakes in EXIF orientation, and re-encodes to a canonical format, dropping all metadata. Supported inputs are PNG, JPEG, GIF, and WebP (decode); the output is always PNG or JPEG (there is no pure-Go WebP encoder). Because decoding is memory-safe, this needs no external tool and no OS sandbox. Resize/thumbnail is a later phase and deliberately absent here.

func (ImageStep) ImageReencode

func (s ImageStep) ImageReencode() string

ImageReencode returns the effective re-encode target for the step, applying the "jpeg" default when unset.

type IncludeHasRootFieldError

type IncludeHasRootFieldError struct {
	Path  string
	Field string
}

IncludeHasRootFieldError is returned when an included file contains version or namespace.

func (*IncludeHasRootFieldError) Error

func (e *IncludeHasRootFieldError) Error() string

type IncludeNotFoundError

type IncludeNotFoundError struct {
	Path         string
	IncludedFrom string
}

IncludeNotFoundError is returned when an included file does not exist.

func (*IncludeNotFoundError) Error

func (e *IncludeNotFoundError) Error() string

type InvalidIDCapsError

type InvalidIDCapsError struct {
	EntityType string
	IDCaps     string
}

InvalidIDCapsError is returned when an entity has an invalid id_caps value.

func (*InvalidIDCapsError) Error

func (e *InvalidIDCapsError) Error() string

type InvalidIDPrefixError

type InvalidIDPrefixError struct {
	EntityType string
	Prefix     string
	Reason     string
}

InvalidIDPrefixError is returned when an id_prefix would generate IDs that fail entity ID validation (BUG-RHFHTH).

func (*InvalidIDPrefixError) Error

func (e *InvalidIDPrefixError) Error() string

type InvalidIDTypeError

type InvalidIDTypeError struct {
	EntityType string
	IDType     string
}

InvalidIDTypeError is returned when an entity has an invalid id_type value.

func (*InvalidIDTypeError) Error

func (e *InvalidIDTypeError) Error() string

type InvalidRelationError

type InvalidRelationError struct {
	Relation string
	From     string
	To       string
	Message  string
}

InvalidRelationError is returned when a relation is not valid between two entity types.

func (*InvalidRelationError) Error

func (e *InvalidRelationError) Error() string

type InverseDef

type InverseDef struct {
	// ID is the identifier for the inverse relation (e.g., "addressedBy")
	ID string `yaml:"id,omitempty"`

	// Label is the display label for the inverse relation (e.g., "addressed by").
	// If not specified, the raw ID is displayed — labels are authored, never
	// derived (DEC-6C1NAA).
	Label string `yaml:"label,omitempty"`
}

InverseDef defines the inverse of a relation. Can be unmarshaled from either a simple string (inverse identifier only) or an object with id and label fields.

func (*InverseDef) GetID

func (i *InverseDef) GetID() string

GetID returns the inverse relation identifier

func (*InverseDef) GetLabel

func (i *InverseDef) GetLabel() string

GetLabel returns the display label, falling back to the raw ID.

A label is authored, never derived (DEC-6C1NAA). This used to convert camelCase to space-separated lowercase ("addressedBy" → "addressed by"), which bakes an English orthographic convention into a language-neutral metamodel. Write an explicit `label:` to control the display text.

func (*InverseDef) UnmarshalYAML

func (i *InverseDef) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML allows InverseDef to be unmarshaled from either a string or an object. String form: "addressedBy" (ID only; the ID doubles as the display label) Object form: { id: "addressedBy", label: "addressed by" }

type Loader

type Loader interface {
	Load(ctx context.Context) (*Metamodel, []string, error)
}

Loader is the top-level service for loading a project's metamodel. Implementations cover different sources (filesystem, remote API, database) behind a uniform contract.

The returned []string lists the source files that were read when the loader has a filesystem-like layout. Sources that don't (remote APIs) return nil — the value is informational for consumers like watchers, not part of the load contract.

type Metamodel

type Metamodel struct {
	Version   string `yaml:"version"`
	Namespace string `yaml:"namespace"`
	// Description is optional end-user prose describing what this deployment /
	// application is for. Display-only, surfaced by the generated documentation
	// (the `rela docs` generator — FEAT-G4VO53). Empty by default; the metamodel
	// does not otherwise consult it.
	Description string                 `yaml:"description,omitempty"`
	Includes    []string               `yaml:"includes,omitempty"`
	Types       map[string]CustomType  `yaml:"types"`
	Entities    map[string]EntityDef   `yaml:"entities"`
	Relations   map[string]RelationDef `yaml:"relations"`
	Validations []ValidationRule       `yaml:"validations,omitempty"`
	Automations []AutomationDef        `yaml:"automations,omitempty"`

	// Attachments holds the global attachment safety floor (MIME allowlist,
	// scan policy) applied to every `file` property unless overridden.
	Attachments *AttachmentsConfig `yaml:"attachments,omitempty"`

	// Transforms is the view-export registry: named markdown -> format
	// conversions run via an external command (e.g. pandoc). Registering a
	// transform here makes it available to every markdown-producing surface
	// (entity view, list view, Lua document) as an "Export as ..." format. Lives
	// in the metamodel (not data-entry config) so the CLI can reach it too. See
	// internal/transform.
	Transforms map[string]TransformDef `yaml:"transforms,omitempty"`
	// contains filtered or unexported fields
}

Metamodel represents the full metamodel configuration

TODO(TKT-N0IKN9): 30 exported methods, over the 20 exported-method line. This is the schema accessor — wide read-API by nature — but a ratchet candidate: group the type/relation/property lookups behind focused accessors (the attachment-scan accessors moved behind AttachmentPolicy this way).

32nd exported method is ShapeProjection (TKT-0C57FS), the data-shape sibling of RenderProjection.

func DefaultMetamodel

func DefaultMetamodel() *Metamodel

DefaultMetamodel returns a minimal default metamodel

func Load

func Load(path string, fs storage.FS) (*Metamodel, []string, error)

Load reads and parses a metamodel from a YAML file using the given filesystem. If the metamodel contains an `includes:` key, included files are recursively loaded and merged. Include paths are resolved relative to the directory containing the metamodel file.

The returned []string contains the absolute paths of all files that were read: the main metamodel.yaml path plus all include files.

func LoadWithoutMigrationCheck

func LoadWithoutMigrationCheck(path string, fs storage.FS) (*Metamodel, []string, error)

LoadWithoutMigrationCheck loads a metamodel without checking for migrations. This is used by the migrate command itself to avoid chicken-and-egg issues. Returns nil if loading fails (caller should handle gracefully).

The returned []string contains the absolute paths of all files that were read.

func Parse

func Parse(data []byte) (*Metamodel, error)

Parse parses and validates metamodel YAML content.

func (*Metamodel) DisplayTitle

func (m *Metamodel) DisplayTitle(id, entityType string, properties map[string]any) string

DisplayTitle returns the display title for an entity using its type's primary property. Falls back to entity ID if no entity definition found or no primary property value is set.

func (*Metamodel) EntityTypes

func (m *Metamodel) EntityTypes() []string

EntityTypes returns all entity type names

func (*Metamodel) GetEntities

func (m *Metamodel) GetEntities() any

GetEntities returns the entities map for JSON output

func (*Metamodel) GetEntityDef

func (m *Metamodel) GetEntityDef(entityType string) (*EntityDef, bool)

GetEntityDef returns the entity definition for a type (resolving aliases)

func (*Metamodel) GetNamespace

func (m *Metamodel) GetNamespace() string

GetNamespace returns the metamodel namespace

func (*Metamodel) GetPropertyDefault

func (m *Metamodel) GetPropertyDefault(entityType, property string) string

GetPropertyDefault returns the default value for a property.

func (*Metamodel) GetPropertyType

func (m *Metamodel) GetPropertyType(entityType, property string) string

GetPropertyType returns the type of a property for an entity type (empty if not found).

func (*Metamodel) GetRelationDef

func (m *Metamodel) GetRelationDef(name string) (*RelationDef, bool)

GetRelationDef returns the relation definition

func (*Metamodel) GetRelationFrom

func (m *Metamodel) GetRelationFrom(relation string) []string

GetRelationFrom returns the "from" entity types for a relation.

func (*Metamodel) GetRelationLabel

func (m *Metamodel) GetRelationLabel(relation string) string

GetRelationLabel returns the label for a relation (empty if not found).

func (*Metamodel) GetRelationTo

func (m *Metamodel) GetRelationTo(relation string) []string

GetRelationTo returns the "to" entity types for a relation.

func (*Metamodel) GetRelations

func (m *Metamodel) GetRelations() any

GetRelations returns the relations map for JSON output

func (*Metamodel) GetTypeDefault

func (m *Metamodel) GetTypeDefault(typeName string) string

GetTypeDefault returns the default value for a custom type.

func (*Metamodel) GetTypes

func (m *Metamodel) GetTypes() any

GetTypes returns the custom types map for JSON output

func (*Metamodel) GetVersion

func (m *Metamodel) GetVersion() string

GetVersion returns the metamodel version

func (*Metamodel) HasEntityType

func (m *Metamodel) HasEntityType(entityType string) bool

HasEntityType returns true if the entity type exists in the metamodel.

func (*Metamodel) HasValidationRule

func (m *Metamodel) HasValidationRule(ruleName string) bool

HasValidationRule returns true if a validation rule with the given name exists.

func (*Metamodel) InferEntityType

func (m *Metamodel) InferEntityType(id string) string

InferEntityType tries to determine the entity type from an ID

func (*Metamodel) InitAliases

func (m *Metamodel) InitAliases()

InitAliases initializes the alias map from entity definitions. Call this after programmatically constructing a Metamodel (e.g., via testutil builders) to enable alias resolution. Not needed when using Parse() which calls this automatically.

func (*Metamodel) InverseOwner

func (m *Metamodel) InverseOwner(inverseName string) (string, bool)

InverseOwner returns the canonical relation type that declares the given inverse name, if any. Populated at load time by the metamodel loader after rejecting collisions and canonical-name shadowing. The data-entry unified-PATCH wire format uses this to resolve inverse body keys without scanning the relation map on every request.

For a relation declared `symmetric: true` with `inverse: <self>`, the inverse maps back to the same canonical relation — callers that care about direction should consult `RelationDef.Symmetric` and treat the path entity as source regardless of the body key.

func (*Metamodel) IsEnumType

func (m *Metamodel) IsEnumType(typeName string) bool

IsEnumType returns whether a type is an enum-like type (has values).

func (*Metamodel) IsPropertyRequired

func (m *Metamodel) IsPropertyRequired(entityType, property string) bool

IsPropertyRequired returns whether a property is required.

func (*Metamodel) RelationTypes

func (m *Metamodel) RelationTypes() []string

RelationTypes returns all relation type names

func (*Metamodel) RenderProjection

func (m *Metamodel) RenderProjection() RenderProjection

RenderProjection returns the render-relevant projection of the metamodel. The result is deterministic: map iteration is never observed (all output is keyed maps and the hash sorts keys), so two calls on an equal metamodel produce an equal projection and hash.

func (*Metamodel) ResolveAlias

func (m *Metamodel) ResolveAlias(alias string) string

ResolveAlias returns the canonical entity type name for an alias

func (*Metamodel) ResolveWidgetFromType

func (m *Metamodel) ResolveWidgetFromType(propType string) string

ResolveWidgetFromType returns the canonical widget for a property type. This is the single source of truth for the type→widget mapping used by the data entry app and the migration system.

func (*Metamodel) ShapeProjection

func (m *Metamodel) ShapeProjection() ShapeProjection

ShapeProjection returns the data-shape projection of the metamodel. The result is deterministic: all output is keyed maps plus slices copied in declaration order, and the hash sorts every map key.

func (*Metamodel) ValidateEntity

func (m *Metamodel) ValidateEntity(id, entityType string, properties map[string]any) []*ValidationError

ValidateEntity validates an entity's type, properties, and ID prefix against the metamodel.

func (*Metamodel) ValidateProperties

func (m *Metamodel) ValidateProperties(props map[string]any, schema PropertySchema) []*ValidationError

ValidateProperties validates a properties map against a PropertySchema. This is shared between entity and relation validation.

func (*Metamodel) ValidatePropertyValue

func (m *Metamodel) ValidatePropertyValue(propName string, propDef *PropertyDef, val any) error

ValidatePropertyValue validates a single property value against its definition. Returns a plain error for backward compatibility with existing callers.

Note: The explicit nil check is required because returning a nil *ValidationError directly as error creates a non-nil interface with nil value (Go interface gotcha).

func (*Metamodel) ValidateRelation

func (m *Metamodel) ValidateRelation(relationType, fromType, toType string) error

ValidateRelation checks if a relation is valid between two entity types

func (*Metamodel) ValidateRelationProperties

func (m *Metamodel) ValidateRelationProperties(
	relationType string, properties map[string]any,
) []*ValidationError

ValidateRelationProperties validates a relation's properties against the metamodel.

type OrderableMode

type OrderableMode string

OrderableMode controls which side(s) of a relation type are user-orderable.

const (
	OrderableNone     OrderableMode = ""
	OrderableOutgoing OrderableMode = "outgoing"
	OrderableIncoming OrderableMode = "incoming"
	OrderableBoth     OrderableMode = "both"
)

func (OrderableMode) IsValid

func (m OrderableMode) IsValid() bool

IsValid reports whether the value is a recognized OrderableMode (including the empty "not orderable" value).

type PropertyDef

type PropertyDef struct {
	Type        string            `yaml:"type"`
	Required    bool              `yaml:"required,omitempty"`
	Values      []string          `yaml:"values,omitempty"` // For inline enum types
	Labels      map[string]string `yaml:"labels,omitempty"` // Optional display labels keyed by value (display-only; value stays the identity)
	Default     string            `yaml:"default,omitempty"`
	Description string            `yaml:"description,omitempty"` // Documentation for the property
	Format      string            `yaml:"format,omitempty"`      // Date format (Go layout, e.g., "2006-01-02")
	List        bool              `yaml:"list,omitempty"`        // True for multi-select properties (allows multiple values)
	// Computed is a pure Lua-compatible scalar expression evaluated from the
	// entity's other properties on every write. Computed properties are
	// materialized but never caller-authored. Entity-local dependencies are
	// inferred from the compiled expression.
	Computed string `yaml:"computed,omitempty" json:"Computed,omitempty"`
	// Unique constrains the property to a natural key: no two entities of
	// the same type may carry the same non-empty value. Enforced at write
	// time by the entitymanager (a colliding create/update is rejected as
	// a validation error → 422). Empty values are exempt (a property is
	// unique among the entities that set it). Ignored on `list` properties
	// (a natural key is a scalar).
	//
	// Guarantee level is not uniform across backends. The write-path check
	// is a check-then-write, not an atomic constraint, so under concurrent
	// writers two racing creates with the same value can both commit on
	// ANY backend. For race-free enforcement an operator adds a store-level
	// unique index (a partial unique index on pgstore), which is the only
	// mechanism that makes the constraint atomic. Uniqueness is therefore
	// NOT part of the store conformance contract — a new store.Store
	// implementation is not required to enforce it. See
	// `internal/entitymanager` checkUniqueProperties and the ACL
	// `principal_property` gate, which requires the referenced property to
	// be unique (and non-list).
	Unique bool `yaml:"unique,omitempty"`
	// Max caps how many attachments a `file`-type property may hold.
	// Zero/unset means 1 (the default, single-attachment). When > 1 the
	// property holds a list of attachment paths and the data-entry UI
	// switches from replace-mode to multi-file add-mode. Only meaningful
	// for `type: file`.
	Max int `yaml:"max,omitempty"`

	// Accept narrows the MIME allowlist for this `file` property to these
	// sniffed MIME types (e.g. ["application/pdf"]). Empty means inherit the
	// global allowlist. Only meaningful for `type: file`.
	Accept []string `yaml:"accept,omitempty"`

	// Scan overrides the global virus-scan policy for this `file` property.
	// ScanUnset (the zero value) means inherit the global policy. Only
	// meaningful for `type: file`.
	Scan ScanPolicy `yaml:"scan,omitempty"`

	// ScanCmd is the external scan command (array args) run when the effective
	// scan policy is `required`. Empty inherits the global scan command. Only
	// meaningful for `type: file`. See [AttachmentsConfig.ScanCmd].
	ScanCmd []string `yaml:"scan_cmd,omitempty"`

	// Transform is the ordered list of byte transforms (each an external
	// command) applied to this `file` property's uploads. Only meaningful for
	// `type: file`.
	Transform []TransformStep `yaml:"transform,omitempty"`
}

PropertyDef defines a property on an entity or relation

func (PropertyDef) FileMax

func (p PropertyDef) FileMax() int

FileMax returns the effective attachment cap for a file property: Max when set (>0), otherwise 1. Callers should use this rather than reading Max directly so the unset-means-one default lives in one place.

func (*PropertyDef) GetDateFormat

func (p *PropertyDef) GetDateFormat() string

GetDateFormat returns the date format for a property, defaulting to ISO 8601. For datetime properties the default is RFC3339 (time-bearing); an explicit Format still overrides.

type PropertyProjection

type PropertyProjection struct {
	Type     string `json:"type"`
	Required bool   `json:"required"`
	List     bool   `json:"list"`
	Format   string `json:"format,omitempty"`
	// Values is the inline enum value list (for properties whose type is an
	// inline enum rather than a named custom type).
	Values []string `json:"values,omitempty"`
}

PropertyProjection is the render-relevant projection of one property definition: the fields that affect how a value is titled, typed, stringified, or diffed. Attachment/scan/transform config, description, and default are omitted — they don't change how a stored value renders.

type PropertySchema

type PropertySchema interface {
	// PropertyDefs returns the property definitions map
	PropertyDefs() map[string]PropertyDef
	// HasContent returns true if markdown body content is supported
	HasContent() bool
}

PropertySchema abstracts property definitions for entities and relations. Both EntityDef and RelationDef implement this interface, allowing shared validation and form generation logic.

type PropertyShape

type PropertyShape struct {
	Type     string   `json:"type"`
	Required bool     `json:"required,omitempty"`
	List     bool     `json:"list,omitempty"`
	Format   string   `json:"format,omitempty"`
	Values   []string `json:"values,omitempty"` // inline enum value list
	// Default is included even though it only affects future creates:
	// the generator offers backfill steps from it, and the classifier
	// tiers default-only changes additive (amendment A7).
	Default string `json:"default,omitempty"`
	// Computed changes affect already-materialized values and therefore take
	// part in shape identity even though bulk recomputation is operator-driven.
	Computed string `json:"computed,omitempty"`
}

PropertyShape is the data-shape projection of one property definition: the fields that determine whether a stored value conforms and how it would be coerced. Display labels, descriptions, uniqueness and attachment/scan config are omitted — they constrain writes or presentation, not the shape of values already stored.

type RelationDef

type RelationDef struct {
	Label       string      `yaml:"label"`
	Description string      `yaml:"description,omitempty"`
	From        []string    `yaml:"from"`
	To          []string    `yaml:"to"`
	Inverse     *InverseDef `yaml:"inverse,omitempty"`
	Symmetric   bool        `yaml:"symmetric,omitempty"`
	MinOutgoing *int        `yaml:"min_outgoing,omitempty"`
	MaxOutgoing *int        `yaml:"max_outgoing,omitempty"`
	MinIncoming *int        `yaml:"min_incoming,omitempty"`
	MaxIncoming *int        `yaml:"max_incoming,omitempty"`

	// Properties defines typed properties that can be attached to relations of this type.
	// Uses the same PropertyDef structure as entity properties.
	Properties map[string]PropertyDef `yaml:"properties,omitempty"`

	// Content indicates whether relations of this type support markdown body content.
	// When true, the data-entry UI will show a content editor for the relation.
	Content bool `yaml:"content,omitempty"`

	// Orderable declares which side(s) of this relation type are user-orderable.
	// When set, the data-entry UI offers drag-to-reorder controls on the enabled
	// side(s); the API returns relations sorted by the corresponding managed
	// order property (OrderPropertyOut / OrderPropertyIn).
	Orderable OrderableMode `yaml:"orderable,omitempty"`
}

RelationDef defines a relation type in the metamodel

func (*RelationDef) GetDescription

func (r *RelationDef) GetDescription() string

GetDescription returns the relation description

func (*RelationDef) GetFrom

func (r *RelationDef) GetFrom() []string

GetFrom returns the source entity types

func (*RelationDef) GetInverse

func (r *RelationDef) GetInverse() any

GetInverse returns the inverse definition for JSON output

func (*RelationDef) GetLabel

func (r *RelationDef) GetLabel() string

GetLabel returns the relation label

func (*RelationDef) GetMaxIncoming

func (r *RelationDef) GetMaxIncoming() *int

GetMaxIncoming returns the maximum incoming cardinality (to-side constraint)

func (*RelationDef) GetMaxOutgoing

func (r *RelationDef) GetMaxOutgoing() *int

GetMaxOutgoing returns the maximum outgoing cardinality (from-side constraint)

func (*RelationDef) GetMinIncoming

func (r *RelationDef) GetMinIncoming() *int

GetMinIncoming returns the minimum incoming cardinality (to-side constraint)

func (*RelationDef) GetMinOutgoing

func (r *RelationDef) GetMinOutgoing() *int

GetMinOutgoing returns the minimum outgoing cardinality (from-side constraint)

func (*RelationDef) GetTo

func (r *RelationDef) GetTo() []string

GetTo returns the target entity types

func (*RelationDef) HasAdvancedFeatures

func (r *RelationDef) HasAdvancedFeatures() bool

HasAdvancedFeatures returns true if this relation type has properties or content, indicating that the data-entry UI should use the advanced cards+modal interface.

func (*RelationDef) HasContent

func (r *RelationDef) HasContent() bool

HasContent implements PropertySchema for RelationDef.

func (*RelationDef) IncomingOrderProperty

func (r *RelationDef) IncomingOrderProperty() string

IncomingOrderProperty returns the relation-property name that holds the incoming-side order value, or "" if incoming ordering is not enabled.

func (*RelationDef) IsSymmetric

func (r *RelationDef) IsSymmetric() bool

IsSymmetric returns whether the relation is symmetric

func (*RelationDef) OutgoingOrderProperty

func (r *RelationDef) OutgoingOrderProperty() string

OutgoingOrderProperty returns the relation-property name that holds the outgoing-side order value, or "" if outgoing ordering is not enabled.

func (*RelationDef) PropertyDefs

func (r *RelationDef) PropertyDefs() map[string]PropertyDef

PropertyDefs implements PropertySchema for RelationDef.

type RelationNotFoundError

type RelationNotFoundError struct {
	Name string
}

RelationNotFoundError is returned when a relation type is not defined in the metamodel.

func (*RelationNotFoundError) Error

func (e *RelationNotFoundError) Error() string

type RelationShape

type RelationShape struct {
	From        []string                 `json:"from,omitempty"`
	To          []string                 `json:"to,omitempty"`
	Symmetric   bool                     `json:"symmetric,omitempty"`
	MinOutgoing *int                     `json:"min_outgoing,omitempty"`
	MaxOutgoing *int                     `json:"max_outgoing,omitempty"`
	MinIncoming *int                     `json:"min_incoming,omitempty"`
	MaxIncoming *int                     `json:"max_incoming,omitempty"`
	Content     bool                     `json:"content,omitempty"`
	Properties  map[string]PropertyShape `json:"properties,omitempty"`
}

RelationShape is the data-shape projection of one relation type.

type RenderProjection

type RenderProjection struct {
	// Entities maps each entity type name to its render projection. All types
	// are included (not just the version's own type) so a diff/timeline can
	// render related-entity titles as-of.
	Entities map[string]EntityProjection `json:"entities"`
	// Types maps each custom (enum) type name to its ordered value list — the
	// values a property of that type may take, needed to render/validate an
	// enum value in a historical snapshot.
	Types map[string][]string `json:"types"`
}

RenderProjection is the render-relevant slice of a metamodel: exactly the schema facts needed to render or diff a stored entity version faithfully — property definitions, display configuration, and the enum value lists a property may reference. It deliberately EXCLUDES the churny, non-render parts of the metamodel (automations, validations, cascade rules, colors, id config), so its content hash stays stable across edits that don't change how an entity renders.

This is the unit the pgstore versioning feature (TKT-9INY0Y) content-addresses into schema_versions: an entity_version row references the hash of the projection in force when it was captured, so a historical version renders against the schema it was created under, not today's (possibly drifted) one.

Projecting to render-relevant fields — rather than hashing the whole metamodel — is a dedup-correctness win: hashing the full schema would churn the hash on every automation/validation edit, forcing a new schema_versions row (and a new pointer on every subsequent version) even though nothing render-relevant changed.

func (RenderProjection) Hash

func (p RenderProjection) Hash() string

Hash returns the content-address of the projection: a hex-encoded SHA-256 over a length-prefixed, key-sorted encoding of every field. Length prefixes make the encoding unambiguous (a value cannot smuggle a delimiter to forge a different structure with the same bytes — the same defense internal/canonical uses for entity hashes), and sorting every map key makes the digest independent of Go map iteration order.

func (RenderProjection) JSON

func (p RenderProjection) JSON() ([]byte, error)

JSON returns the projection serialized as deterministic JSON, for storage in schema_versions.projection. The bytes are content-addressed by RenderProjection.Hash (a separate length-prefixed digest), so this serialization is for storage and re-render, not identity — encoding/json with sorted map keys is sufficient.

It returns an error rather than panicking because it is called on the write path (the entitymanager version hook), where the contract is that versioning must never fail a write — the caller logs and swallows. In practice RenderProjection holds only strings, bools, and slices/maps of them, so an error is not reachable short of a runtime bug.

type ReservedPropertyError

type ReservedPropertyError struct {
	EntityType   string
	PropertyName string
}

ReservedPropertyError is returned when a property name conflicts with a reserved name.

func (*ReservedPropertyError) Error

func (e *ReservedPropertyError) Error() string

type ReservedTypeNameError

type ReservedTypeNameError struct {
	TypeName string
}

ReservedTypeNameError is returned when a custom type name conflicts with a built-in type.

func (*ReservedTypeNameError) Error

func (e *ReservedTypeNameError) Error() string

type ScanPolicy

type ScanPolicy int

ScanPolicy is the per-scope virus-scan switch for attachments. Scanning is driven by the *presence of a scan command*: if a `scan_cmd` is configured (globally or on the property) the upload is scanned, fail-closed. There is no separate "required" level — configuring a scanner is the intent to use it.

ScanPolicy exists only as an OPT-OUT: a property may set `scan: off` to skip scanning despite a global `scan_cmd`. The zero value (ScanDefault) means "scan when a command is configured."

const (
	// ScanDefault (the zero value) means scan iff a scan command is configured
	// for this scope. No explicit policy was set.
	ScanDefault ScanPolicy = iota
	// ScanOff disables scanning for this property, even when a global scan
	// command exists.
	ScanOff
)

func (ScanPolicy) MarshalYAML

func (s ScanPolicy) MarshalYAML() (any, error)

MarshalYAML renders only the explicit `off`; the default is omitted so round-tripping a metamodel does not invent a key.

func (ScanPolicy) String

func (s ScanPolicy) String() string

String renders the policy for diagnostics.

func (*ScanPolicy) UnmarshalYAML

func (s *ScanPolicy) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML accepts the string `off` (case-insensitive). `required`/`on` are accepted as no-ops for forgiveness (scanning is already implied by configuring a command), mapping to ScanDefault. An absent key leaves ScanDefault.

type SchemaValidationError

type SchemaValidationError struct {
	Errors []string
}

SchemaValidationError collects multiple validation issues found in a metamodel.

func (*SchemaValidationError) Error

func (e *SchemaValidationError) Error() string

type ShapeDelta

type ShapeDelta struct {
	Tier ShapeTier
	// Kind is a stable machine-readable delta kind (e.g. "property_removed",
	// "property_type_changed", "possible_property_rename"). The generator
	// switches on it.
	Kind string
	// Subject names what changed: "task", "task.status", "rel:implements",
	// "rel:implements.weight", or "type:status" for a named enum.
	Subject string
	// Detail is the human-readable explanation shown in gate notices.
	Detail string
	// Counterpart names the removed half of a possible-rename pair (the old
	// subject), for possible_property_rename / possible_entity_type_rename.
	Counterpart string
	// Removed/Added carry the value diff for enum_values_* kinds, so the
	// migration generator can draft a map_values stub without re-diffing.
	Removed []string
	Added   []string
}

ShapeDelta is one classified difference between two shape projections.

type ShapeProjection

type ShapeProjection struct {
	// Entities maps each entity type name to its property shapes.
	Entities map[string]EntityShape `json:"entities"`
	// Relations maps each relation type name to its shape.
	Relations map[string]RelationShape `json:"relations"`
	// Types maps each named custom (enum) type to its ordered value list.
	Types map[string][]string `json:"types"`
}

ShapeProjection is the data-shape slice of a metamodel: exactly the schema facts that determine whether STORED CONTENT conforms to the schema — entity properties (type, required, list, format, inline values, default), named enum value lists, and relation types (endpoints, cardinality, symmetry, content flag, relation properties).

It is the identity unit of the data-migration system (TKT-0C57FS): the hash of the projection in force is the "schema version" a store's content conforms to, recorded in the state.KV marker and referenced by migration files. Cosmetic schema edits (labels, descriptions, colors, views, automations, validations, display configuration, id prefixes) do not move the hash, so they never demand a migration.

This is a SIBLING of RenderProjection, not a replacement. The two answer different questions — "how does a stored version render?" versus "does the stored data still fit the schema?" — and have independent stability contracts: RenderProjection's hash content-addresses schema_versions rows for the pgstore versioning feature and must not churn, while ShapeProjection deliberately includes relation shape and property defaults that rendering never needs. Do not merge them.

id prefixes are deliberately EXCLUDED (TKT-0C57FS amendment A5): no v1 migration step can rewrite entity IDs, so including prefixes would let a prefix edit create a needs-migration state no migration could resolve.

func ShapeProjectionFromJSON

func ShapeProjectionFromJSON(data []byte) (ShapeProjection, error)

ShapeProjectionFromJSON parses a projection previously serialized by ShapeProjection.JSON (from a migration file or the state marker).

func (ShapeProjection) Hash

func (p ShapeProjection) Hash() string

Hash returns the content-address of the shape projection: a hex-encoded SHA-256 over a length-prefixed, key-sorted encoding of every field — the same writer discipline as RenderProjection.Hash and internal/canonical (length prefixes make the encoding unambiguous; sorted keys make it independent of map iteration order). The leading tag byte differs ('S' versus RenderProjection's 'P') so the two hash spaces can never collide even on structurally similar input.

func (ShapeProjection) JSON

func (p ShapeProjection) JSON() ([]byte, error)

JSON returns the projection serialized as deterministic JSON, for embedding in migration files and the state.KV marker. Identity is ShapeProjection.Hash (a separate length-prefixed digest); this serialization is for storage and re-load.

type ShapeReport

type ShapeReport struct {
	Deltas []ShapeDelta
}

ShapeReport is the classified diff between two shape projections.

func CompareShapes

func CompareShapes(from, to ShapeProjection) ShapeReport

CompareShapes classifies every difference between two shape projections. `from` is the shape the stored data conforms to; `to` is the live schema.

The one inherent blind spot: a rename is indistinguishable from a delete+add. When a removed and an added property in the same entity type share type/list/format, the report carries an explicit possible_property_rename delta (drift tier) so the operator is told — but it cannot be more than a warning. Same for entity types of similar property shape.

func (ShapeReport) ByTier

func (r ShapeReport) ByTier(t ShapeTier) []ShapeDelta

ByTier returns the deltas of one tier, in report order.

func (ShapeReport) Compatible

func (r ShapeReport) Compatible() bool

Compatible reports whether the target shape can be adopted without a migration (no needs-migration deltas).

func (ShapeReport) Tier

func (r ShapeReport) Tier() ShapeTier

Tier reduces the report to a single verdict: the highest tier among the deltas. An empty report (identical shapes) is TierAdditive.

type ShapeTier

type ShapeTier int

ShapeTier classifies one schema-shape delta by its impact on stored data. The ordering is meaningful: a higher tier subsumes a lower one when a report is reduced to a single verdict.

const (
	// TierAdditive deltas cannot invalidate stored content (new types, new
	// optional properties, new enum values, widenings, default-only edits).
	// A store may adopt the new shape silently.
	TierAdditive ShapeTier = iota
	// TierDrift deltas leave stored content stale but never broken:
	// deletions orphan data (GC territory), a new required property yields
	// soft warnings. A store adopts the new shape but the operator is told.
	TierDrift
	// TierMigration deltas mean stored values no longer fit the schema
	// (type/format changes, list flips, enum replacements, narrowings).
	// The store must not adopt the new shape until a migration runs.
	TierMigration
)

func (ShapeTier) String

func (t ShapeTier) String() string

type SortSpec

type SortSpec struct {
	Property  string `yaml:"property"  json:"property"`
	Direction string `yaml:"direction,omitempty" json:"direction,omitempty"` // "asc" (default) or "desc"
}

SortSpec describes a single sort criterion used in metamodel default_sort, queries, and data-entry config. Property can be a real entity property name or a virtual property: "id" (entity ID) or "modified" (file modification time).

func (SortSpec) IsDescending

func (s SortSpec) IsDescending() bool

IsDescending returns true if direction is "desc".

type StringOrSlice

type StringOrSlice []string

StringOrSlice is a YAML type that can be unmarshaled from either a string or []string.

func (*StringOrSlice) UnmarshalYAML

func (s *StringOrSlice) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML allows StringOrSlice to be unmarshaled from either a string or a slice.

type Subscriber

type Subscriber interface {
	// Subscribe starts a watcher that invokes onChange whenever the
	// underlying metamodel data changes. The returned stop function
	// releases all watcher resources.
	Subscribe(ctx context.Context, onChange func()) (stop func(), err error)
}

Subscriber is the optional change-notification interface on a Loader. Implementations that can detect external changes to the metamodel data satisfy Subscriber; consumers type-assert to subscribe. Backends with no change-detection capability (e.g. an embedded byte-slice loader) simply don't implement it.

type TransformDef

type TransformDef struct {
	From     string   `yaml:"from"`
	Command  []string `yaml:"command"`
	Produces string   `yaml:"produces"`
}

TransformDef is one entry in the top-level `transforms:` view-export registry. It converts From-format bytes (v1: always "markdown") to the Produces content-type by running Command as an argv array. Command may reference the {in}/{out} placeholders (temp paths owned by the runner); otherwise input is on stdin and output on stdout. Commands come from project config (this file), never from a request — a request may only name a registered transform.

The mirror of this in internal/transform is transform.Def; the metamodel keeps its own YAML-tagged type so internal/transform need not be imported here.

type TransformStep

type TransformStep struct {
	// Cmd is an external command (array args) that rewrites the bytes; it
	// receives templated {in}/{out} paths owned by the runner.
	Cmd []string `yaml:"cmd,omitempty"`
	// Image is a native in-process image transform (decode, orient, re-encode).
	// Mutually exclusive with Cmd.
	Image *ImageStep `yaml:"image,omitempty"`
}

TransformStep is one entry in a `transform:` pipeline. A step is EITHER an external command (Cmd) OR a native, in-process image operation (Image) — exactly one must be set, enforced at load. The two kinds have very different trust models: a Cmd shells out to an operator-configured binary (sandboxed by internal/cmdexec), while an Image step runs a memory-safe pure-Go decoder in-process with no external tool and no sandbox. See the attachment-security guide.

func (TransformStep) Kind

func (t TransformStep) Kind() string

Kind reports which of the two mutually-exclusive step kinds is set, or an empty string when the step is malformed (both/neither set).

type TransitionDef

type TransitionDef struct {
	From string `yaml:"from"` // Source value; must be one of CustomType.Values
	To   string `yaml:"to"`   // Target value; must be one of CustomType.Values

	// Label is optional display text for the MOVE (the action), not the
	// destination state — e.g. "Start progress" for todo→doing rather than the
	// state noun "Doing". Purely presentational: a machine-aware status control
	// lists transitions as verbs. Empty falls back to the target value's display
	// label (CustomType.Labels[To]) and then the raw To value. Display-only; the
	// executable machine ignores it for enforcement.
	Label string `yaml:"label,omitempty"`

	// Help is optional longer prose explaining WHY or WHEN a user would make
	// this move, beyond the short verb Label — e.g. "Send for review once the
	// implementation is complete and tests pass." Display-only, surfaced by the
	// generated documentation (FEAT-G4VO53); the executable machine ignores it.
	Help string `yaml:"help,omitempty"`

	// Guard names an ACL permission the acting principal must hold for this
	// transition. Enforced only on served paths (a principal exists); inert
	// on direct CLI writes. Empty means the transition is legal for anyone
	// who may otherwise write the entity.
	Guard string `yaml:"guard,omitempty"`

	// When is an internal/predicate expression evaluated as a precondition
	// against the entity + graph at write time. False rejects the transition
	// (422). Empty means no precondition.
	When string `yaml:"when,omitempty"`
}

TransitionDef is one edge in an enum state machine: a legal move from one value to another, optionally gated by an ACL permission (Guard) and/or a data precondition (When). This is declarative source data; the executable machine is built from it by internal/statemachine.Compile.

type TypeValidation

type TypeValidation struct {
	Pattern string `yaml:"pattern"` // Regex pattern that values must match
	Error   string `yaml:"error"`   // User-friendly error message if pattern doesn't match
	// contains filtered or unexported fields
}

TypeValidation defines a regex validation for a custom type.

func (*TypeValidation) Compiled

func (tv *TypeValidation) Compiled() *regexp.Regexp

Compiled returns the pre-compiled regex pattern. Returns nil if the pattern hasn't been compiled yet.

func (*TypeValidation) SetCompiled

func (tv *TypeValidation) SetCompiled(re *regexp.Regexp)

SetCompiled sets the pre-compiled regex pattern.

type ValidationError

type ValidationError struct {
	Type     ValidationErrorType
	Property string // The property name that failed validation (empty for entity-level errors)
	Message  string // Human-readable error message
}

ValidationError represents a structured validation error with field information.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface.

func (*ValidationError) IsSoft

func (e *ValidationError) IsSoft() bool

IsSoft reports whether the error describes a soft condition per DEC-HWZHA — a state a hand-edited markdown file can produce that the API should tolerate at write time and surface as a warning rather than reject with a 422.

Property-level mistakes (required-field-missing, type mismatch, invalid value such as out-of-enum / bad date / bad RRULE) are soft: the file already on disk likely contains them after a hand-edit, so rejecting them on the next API write would create a hostile asymmetry. Entity-level structural problems (unknown entity type, ID prefix that doesn't match the type) are hard: the storage layer can't construct a path to persist the entity at all.

The categorization lives next to the error type so every consumer (workspace, future per-edge endpoints, MCP, etc.) gets a single authoritative answer.

type ValidationErrorType

type ValidationErrorType string

ValidationErrorType indicates the kind of validation error.

const (
	ValidationErrorRequired     ValidationErrorType = "required"
	ValidationErrorInvalidValue ValidationErrorType = "invalid_value"
	ValidationErrorInvalidType  ValidationErrorType = "invalid_type"
	ValidationErrorUnknownType  ValidationErrorType = "unknown_type"
	ValidationErrorIDPrefix     ValidationErrorType = "id_prefix"
	// ValidationErrorUnique reports a duplicate value for a property
	// declared `unique: true`. It is a HARD error (IsSoft returns false):
	// a natural-key collision is a constraint violation the write path
	// must reject with a 422, not a tolerable hand-edit state. Unlike the
	// other types, this one is raised by the entitymanager write path
	// (which can query other entities), not by the pure per-entity
	// [Metamodel.ValidateEntity].
	ValidationErrorUnique ValidationErrorType = "unique"
)

type ValidationRule

type ValidationRule struct {
	// Name is a unique identifier for the validation rule
	Name string `yaml:"name"`

	// Description explains what this validation checks
	Description string `yaml:"description"`

	// EntityType limits the validation to a specific entity type (optional)
	// If empty, the validation applies to all entity types
	EntityType string `yaml:"entity_type,omitempty"`

	// When specifies filter conditions that select which entities this rule applies to
	// Uses the same syntax as --where filters (e.g., "status=approved")
	// Multiple conditions are ANDed together
	// If empty, the rule applies to all entities (of the specified type)
	When []string `yaml:"when,omitempty"`

	// Then specifies filter conditions that matching entities must satisfy
	// Uses the same syntax as --where filters (e.g., "owner!=")
	// Multiple conditions are ANDed together
	Then []string `yaml:"then,omitempty"`

	// WhenCondition is a predicate EXPRESSION selecting which entities the
	// rule applies to, ANDed with every When clause. Expression syntax
	// (unlike When's filter syntax) gets boolean composition and the
	// host-function stdlib, including date arithmetic.
	WhenCondition string `yaml:"when_condition,omitempty"`

	// ThenCondition is a predicate EXPRESSION that matching entities must
	// satisfy, ANDed with every Then clause.
	//
	// A rule may use any mix: `when:` + `then_condition:` is a filter
	// selecting entities that an expression then asserts over.
	ThenCondition string `yaml:"then_condition,omitempty"`

	// Content specifies validation rules for markdown body content
	Content *ContentRule `yaml:"content,omitempty"`

	// Severity is the severity level of violations: "error" or "warning"
	// Defaults to "warning" if not specified
	Severity string `yaml:"severity,omitempty"`

	// Lua specifies inline Lua code for custom validation logic.
	// The code should return true if the entity is valid, or false/nil for a violation.
	// The entity being validated is available as the `entity` global variable.
	// Read-only workspace access is available via rela.get_entity(), rela.list_entities(), etc.
	Lua string `yaml:"lua,omitempty"`

	// LuaFile specifies a path to a Lua script file in the scripts/ directory.
	// The script should return true if valid, or false/nil for a violation.
	// Example: "validate-dates.lua" loads scripts/validate-dates.lua
	LuaFile string `yaml:"lua_file,omitempty"`

	// LuaArgs specifies arguments to pass to Lua validation scripts.
	// Available as rela.args in the Lua runtime.
	LuaArgs []string `yaml:"lua_args,omitempty"`
}

ValidationRule defines a custom validation rule for entities

func (*ValidationRule) GetSeverity

func (v *ValidationRule) GetSeverity() string

GetSeverity returns the severity level, defaulting to "warning"

func (*ValidationRule) IsError

func (v *ValidationRule) IsError() bool

IsError returns true if this validation has error severity

type WhitespacePropertyError

type WhitespacePropertyError struct {
	EntityType   string
	PropertyName string
}

WhitespacePropertyError is returned when a property name has leading or trailing whitespace.

func (*WhitespacePropertyError) Error

func (e *WhitespacePropertyError) Error() string

Jump to

Keyboard shortcuts

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