dataentry

package
v0.0.0-...-169de98 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: AGPL-3.0 Imports: 98 Imported by: 0

Documentation

Overview

Package dataentry provides a config-driven data entry web application built on top of rela's metamodel system. It reads a data-entry.yaml config file alongside a rela project and serves an interactive UI for CRUD operations on entities stored as markdown files.

Configuration types and validation logic live in the dataentryconfig package so that the CLI can validate configs without importing the full web layer. This file re-exports those types for backward compatibility.

Index

Constants

Widget and direction constants — re-exported from dataentryconfig.

ConfigFile is the conventional filename for data-entry configuration within a rela project.

View Source
const DefaultMaxAttachmentBytes = 64 << 20 // 64 MiB

DefaultMaxAttachmentBytes is the product-wide default cap on a single attachment, applied at the HTTP upload ingress. It is generously sized for the expected use (screenshots, PDFs, office docs), not media. A deployment can override it via dataentryconfig; the store backends also enforce their own backstop guard so no path is ever unbounded.

View Source
const DefaultWebhookMaxBodyBytes int64 = 1 << 20

DefaultWebhookMaxBodyBytes caps an inbound declarative-webhook body. The parsed body becomes an in-memory map that every template interpolation reads, so this bounds real memory per concurrent delivery — the action endpoint has no such cap, which is precisely the gap this closes.

1 MiB is generous for the payload shapes this serves (a monitoring alert, a form post, an upstream event) while staying small enough that a burst of concurrent deliveries cannot exhaust the heap.

View Source
const EnvDataEntryUserVar = "RELA_DATAENTRY_USER"

EnvDataEntryUserVar is the local-dev escape hatch: if this env var is set, EnvPrincipalResolver returns its value as the principal user. Documented in docs/server-security.md alongside the --principal-header flag.

Exported so cmd/rela-server can reject it alongside --jwt-* without duplicating the literal: under verified-JWT identity an env var that overrides a cryptographically proven subject is the same downgrade the header fall-through was.

View Source
const MCPPath = "/api/v1/_mcp"

MCPPath is the mount point for the remote MCP endpoint.

It lives under `/api/` on purpose: [isAPIPath] matches it, so the endpoint inherits the full request chain — `stampAuditPrincipal` → `requireVerifiedJWT` → `attachACLRequest` — with no middleware change. A mount outside `/api/` would silently bypass both the identity gate and the ACL, which is exactly the failure RR-P2M7 guards against for the bare `/api`.

View Source
const (

	// MaxUserLogoBytes caps user-uploaded logos. Sidebar logos render at
	// ~28px tall; 256 KiB is generous for that display size.
	MaxUserLogoBytes = 256 << 10
)

User-uploaded theme assets are stored under .rela/theme/. The bytes file ("theme/logo") is opaque; the sidecar ("theme/logo.ext") records the inferred extension so the GET handler can set Content-Type without re-sniffing on every request.

View Source
const ThemePackageMaxBytes = 5 << 20

ThemePackageMaxBytes caps the on-the-wire size of a `.relatheme` upload. The manifest is at most a couple of KB; a logo is ≤256 KiB. 5 MiB leaves generous headroom while bounding the worst-case memory pressure a single import can impose.

View Source
const WorldParam = "world"

WorldParam is the query parameter that selects a world on the read API: `?world=published`. Absent or empty means the DEFAULT world, bound explicitly at this boundary — the interior never sees "unspecified" (design doc §4.4).

Variables

View Source
var CollectConfigWarnings = dataentryconfig.CollectConfigWarnings

CollectConfigWarnings re-exports the non-fatal config-warning collector.

ResolvePalette is re-exported from dataentryconfig.

ValidateConfig re-exports the validation function from dataentryconfig.

Functions

func CheckEmbeddedSPA

func CheckEmbeddedSPA() error

CheckEmbeddedSPA verifies that the embedded Vue SPA bundle is present and usable. Production entry points (cmd/rela-server, cmd/rela-desktop) should call this at startup so a missing or empty build fails loudly with a clear message instead of silently serving a directory listing (the BUG-W144 regression class). Tests that construct routers via NewRouter do not need to call this.

func RewriteDocumentLinks(htmlContent, returnPath string, log *slog.Logger) string

RewriteDocumentLinks walks all href="..." attributes in rendered HTML and rewrites internal links so the SPA can offer a back affordance.

The rewriter runs AFTER the document-render cache (see documentService.GetCached / Render in this package, and the call sites in api_v1.go). It never writes to the cache. This is load-bearing: the cache file is keyed on the entry entity's content hash and must NOT contain any `return_to=` tokens, so that two viewers requesting the same entry under different return_to values each get their own value rewritten in. Do not move this step into doRender.

Behavior, by path class × returnPath presence:

| Path class                 | returnPath == ""              | returnPath != ""                      |
|----------------------------|-------------------------------|---------------------------------------|
| Form (/form/<id>[/...])    | strip return_to; emit id      | strip return_to; emit id; inject ours |
| Non-form internal (/...)   | strip return_to; pass through | strip return_to; inject ours          |
| External / mailto / anchor | passthrough unchanged         | passthrough unchanged                 |
| Legacy edit:// / create:// | log warning; passthrough      | log warning; passthrough              |

Author-supplied `return_to` values on internal links are always stripped, whether or not we have a replacement: the rewriter is the single source of truth for the key on emitted HTML.

Form routes additionally get a stable id="edit-<entityID>-<n>" or id="create-<form>-<n>" attribute so the SPA's document click handler can record a scroll-back anchor that survives title/content edits. The per-base counter (<n>) disambiguates multiple links to the same target within a single rendered document and is stable across re-renders that produce the same link sequence.

The rewriter is idempotent: applying it twice with the same returnPath produces the same bytes as one pass. Applying it twice with different returnPaths yields the last one injected (the first is stripped, then the second is injected).

func SetWorldNeighbors

func SetWorldNeighbors(a *App, s store.Store, classes worldreader.ScopeClassifier) error

SetWorldNeighbors enables world-scoped LINK resolution (`?world=` on a response's relations and `?include=`).

A package-level FUNCTION rather than a method on App, for the reason the world code has taken this shape throughout: App carries a `//plimsoll:max-methods=104` directive pinning it at its current count, and the project rule is to split the type rather than raise the number. The world feature has added ONE method to App so far (App.SetWorlds) and four package functions (resolveWorld, attachWorld, worldCapablePath, and this), which is the discipline recorded on the App type doc.

Not calling this is a valid state: relations then behave as they did before TKT-WRLDAPI item 4 — present under the default world, absent under any other. That is safe but incomplete, which is why the composition root wires it whenever it wires App.SetWorlds.

classes classifies a relation type as content- or identity-scoped; it is supplied by the wiring site because the dispatch it feeds (worldreader.RelationReader) must not be reimplemented here.

Nil: rejected — a nil app, store or classifier returns an error rather than silently leaving link resolution off, which would present as "this world has no links" on every page.

Types

type APIAnalysisResult

type APIAnalysisResult struct {
	Errors   int            `json:"errors"`
	Warnings int            `json:"warnings"`
	Issues   []APIIssue     `json:"issues"`
	ByCheck  map[string]int `json:"byCheck"`

	// TruncatedChecks names the checks that found more issues than they
	// returned, so the UI can mark those lists as incomplete (TKT-1ESTYJ).
	//
	// Per-CHECK rather than one global flag: "duplicates is truncated" is
	// actionable where "something was truncated" is not, and the response
	// is a flat issue list, so the section-level flag would otherwise be
	// lost. Empty (omitted) when every check reported in full.
	//
	// Counts in ByCheck are counts of RETURNED issues; for a truncated
	// check that is the cap, not the true total, which is deliberately
	// not computed.
	TruncatedChecks []string `json:"truncatedChecks,omitempty"`
}

APIAnalysisResult is the JSON representation of analysis results.

type APIDefaultOverride

type APIDefaultOverride struct {
	Types            []string          `json:"types"`
	Defaults         map[string]string `json:"defaults"`
	RelationDefaults map[string]string `json:"relationDefaults"`
}

APIDefaultOverride is the JSON representation of a default override.

type APIIssue

type APIIssue struct {
	EntityID   string `json:"entityId"`
	EntityType string `json:"entityType"`
	Title      string `json:"title,omitempty"`
	Message    string `json:"message"`
	Severity   string `json:"severity"` // "error" or "warning"
	CheckType  string `json:"checkType"`

	// Detail carries optional structured specifics about why the issue
	// fired, beyond the flat Message. For content required-headers
	// violations it holds the missing exact headers. Absent (omitempty)
	// on rows with no structured detail; the frontend reveals it in an
	// expandable detail row under the message.
	Detail []string `json:"detail,omitempty"`

	// ScriptError carries the structured Lua-failure envelope for
	// validation script-error rows. Absent (omitempty) on every
	// other row. The frontend uses presence as the discriminator:
	// rows with scriptError open the ScriptErrorDialog instead of
	// navigating to an entity. Same loopback gating as the
	// action-surface envelope (security.AllowFullScriptDetail).
	ScriptError *ScriptErrorEnvelope `json:"scriptError,omitempty"`
}

APIIssue is the JSON representation of a single analysis issue.

type APIPropertyDef

type APIPropertyDef struct {
	Name   string   `json:"name"`
	Type   string   `json:"type"`
	Values []string `json:"values"`
}

APIPropertyDef describes a property for the settings page.

type APIRelationDef

type APIRelationDef struct {
	Name       string              `json:"name"`
	Label      string              `json:"label"`
	TargetType string              `json:"targetType"`
	Targets    []APIRelationTarget `json:"targets"`
}

APIRelationDef describes a relation for the settings page.

type APIRelationTarget

type APIRelationTarget struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}

APIRelationTarget is a possible target for a relation.

type APISettingsData

type APISettingsData struct {
	UserDefaults  APIUserDefaults                `json:"userDefaults"`
	UserPalette   *dataentryconfig.PaletteConfig `json:"userPalette,omitempty"`
	AllProperties []APIPropertyDef               `json:"allProperties"`
	AllRelations  []APIRelationDef               `json:"allRelations"`
	EntityTypes   []string                       `json:"entityTypes"`
	// LogoURL is the cache-busted URL of the user-uploaded sidebar logo,
	// or nil when no logo is set. The SPA reads this on boot to render
	// the sidebar branding.
	LogoURL *string `json:"logoUrl,omitempty"`
}

APISettingsData contains all data needed for the settings page.

type APIThemeImportResponse

type APIThemeImportResponse struct {
	Palette dataentryconfig.PaletteConfig `json:"palette"`
	LogoURL string                        `json:"logoUrl,omitempty"`
}

APIThemeImportResponse is the typed shape of POST /_theme/import. Field names mirror the analogous fields in APISettingsData so the frontend stays aligned with the rest of the JSON surface.

type APIUserDefaults

type APIUserDefaults struct {
	Defaults         map[string]string    `json:"defaults"`
	RelationDefaults map[string]string    `json:"relationDefaults"`
	Overrides        []APIDefaultOverride `json:"overrides"`
}

APIUserDefaults is the JSON representation of user defaults.

type AffordanceDenialError

type AffordanceDenialError struct {
	Rule   AffordanceDenialRule
	Path   string // property name, relation type, or "<relation-type>.<meta-field>"
	Reason string
	// Attribution names the role/grant that produced the deny, for the
	// audit Summary channel (DR-C5). Empty for resolvers that don't
	// track it. Never serialized to the wire 403 body.
	Attribution string
}

AffordanceDenialError reports why a write was rejected by the affordance validator. The rule and path together form the wire rule_id (e.g. "field-affordance:hidden:priority"). Reason is a short human-readable explanation; UIs surface it as-is.

func (AffordanceDenialError) Error

func (d AffordanceDenialError) Error() string

Error makes AffordanceDenialError satisfy the error interface so it can flow back through caller chains. The format mirrors RuleID() plus the reason.

func (AffordanceDenialError) RuleID

func (d AffordanceDenialError) RuleID() string

RuleID returns the wire-stable identifier for this denial.

type AffordanceDenialRule

type AffordanceDenialRule string

AffordanceDenialRule is the stable identifier surfaced in 403 responses when an affordance validator rejects a write. The full rule_id on the wire is "<rule>:<path>" so a UI or audit reader can reconstruct what was denied.

Rule names are part of the wire contract — changing them is a wire break.

const (
	RuleFieldHidden          AffordanceDenialRule = "field-affordance:hidden"
	RuleFieldReadOnly        AffordanceDenialRule = "field-affordance:read-only"
	RuleFieldEnumFiltered    AffordanceDenialRule = "field-affordance:enum-filtered"
	RuleRelationNotCreatable AffordanceDenialRule = "relation-affordance:not-creatable"
	RuleRelationNotRemovable AffordanceDenialRule = "relation-affordance:not-removable"
	RuleRelationMetaReadOnly AffordanceDenialRule = "relation-affordance:meta-read-only"
)

type AffordanceProfile

type AffordanceProfile string

AffordanceProfile names a verdict-source preset. "none" is the permissive default; "demo" is a fixture against the ticket type exercising every affordance code path. Absent an explicit profile, a policy carrying affordance grants selects the policy-backed resolver.

The env var RELA_AFFORDANCE_PROFILE selects the override at startup. cmd/rela-server and cmd/rela-desktop parse the var and pass the resolver into NewApp; tests pass the resolver directly.

const (
	AffordanceProfileNone AffordanceProfile = "none"
	AffordanceProfileDemo AffordanceProfile = "demo"
)

type AnalysisIssue

type AnalysisIssue struct {
	EntityID   string // Empty for non-entity issues (e.g., ID gaps)
	EntityType string
	Title      string
	Message    string
	Severity   string // "error" or "warning"

	// Detail carries optional structured specifics about why the issue
	// fired, beyond the flat Message. For content required-headers
	// violations it holds the missing exact headers. Nil for issues
	// with no structured detail.
	Detail []string

	// ScriptError carries the raw *lua.ScriptError for validation
	// rules whose Lua script failed. Non-nil only on script-error
	// rows; the HTTP handler converts it to a wire envelope using
	// the per-request loopback gate, so the structured detail
	// (path, source slice, stack) reaches the frontend's existing
	// ScriptErrorDialog rather than a flat string.
	// LoadErrors do NOT get a ScriptError — they're not Lua failures.
	ScriptError *lua.ScriptError
}

AnalysisIssue represents a single validation issue, optionally linked to an entity.

type AnalysisResult

type AnalysisResult struct {
	Sections     []AnalysisSection
	ErrorCount   int
	WarningCount int
}

AnalysisResult is the complete output of running all analyses.

type AnalysisSection

type AnalysisSection struct {
	Name        string
	Description string
	Issues      []AnalysisIssue

	// Truncated reports that the analyzer found MORE issues than it
	// returned, so the UI can say so.
	//
	// Surfaced rather than silent on purpose. An operator who fixes all
	// 100 reported issues, re-runs, and sees 100 again would otherwise
	// conclude analyze is broken. The exact total is deliberately NOT
	// reported: counting every issue means doing all the work the cap
	// exists to avoid, and "100+" is as actionable as "12,431".
	Truncated bool
}

AnalysisSection groups issues by analysis category.

func (AnalysisSection) ErrorCount

func (s AnalysisSection) ErrorCount() int

ErrorCount returns the number of error-severity issues in this section.

func (AnalysisSection) WarningCount

func (s AnalysisSection) WarningCount() int

WarningCount returns the number of warning-severity issues in this section.

type App

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

App is the central application struct for the data-entry server.

Concurrency model

The co-derived reload core (config, metamodel, style map, OpenAPI generator) lives in an immutable Schema published via [schemaProvider]'s atomic.Pointer. Handlers call a.State() once at entry and work against a coherent snapshot for the duration of the request — no lock acquisition, no risk of observing a half-reloaded world. Independently-owned reloadable state (logo, palette, user defaults) lives in its own self-synchronized service, not the snapshot.

Reloads (triggered by the file watcher or by Reload) derive a new Schema and publish it atomically via a.schema.Reload. The previous snapshot is garbage-collected once no reader holds it.

Mutations (CreateEntity, UpdateEntity, DeleteEntity, CreateRelation, UpdateRelation, DeleteRelation, SetProperty, action scripts) serialize via writeMu. writeMu excludes concurrent mutations but does NOT block readers — readers go through a.State(). The workspace's internal reloadMu coordinates the reload itself with the mutation path.

TODO(TKT-R68TV8): App is a god-object. Decompose toward the 40-method load line — extract the API/serialization/relation services into their own types. Ratchet this number DOWN as methods move out; never up EXCEPT for a new required route handler (App owns one method per registered HTTP route by the router's design). The sync route cluster (16 methods) moved to syncHandler (170 → 154); the command cluster (11 methods) moved to commandHandler (154 → 143); the attachment cluster (12 methods) moved to attachmentHandler / package functions (143 → 131); the write nucleus — entity/relation CRUD, clone, conflict-resolve, and the modern relations reconciler (18 methods) — moved to writeHandler (131 → 114); the Lua action handler joined it (115 → 114, from a base that had absorbed the DEC-O59WM4 script-read helpers), completing the write surface: every writeMu write path now lives on or routes through writeHandler. The views cluster — view traversal, section building, the /_views, /_sidepanel, /_sidebar handlers, and the form-resolution helpers (16 methods) — moved to viewsHandler, three receiver-free helpers became package functions, and the dead ungated server-rendered nav path (5 methods, #1043) was deleted (114 → 90).

The directive lags the real count (TKT-N0IKN9 tracks decomposing App); it is a ratchet target, not a budget to spend. SetUserState took it from 98 to 99 — it follows the existing SetSecurityConfig / SetJWTGate setter idiom rather than becoming a 12th positional NewApp parameter — and the CalDAV alias setter that landed alongside it took the count to 100, a later one to 101, and redactedForSuggestion to 102 — the field-redaction seam the next-action candidate path needs, which has to reach affordanceService.

TKT-BDG8U9 adds App.SetRemoteMCP on the same terms — the public opt-in setter for the remote MCP endpoint, matching that setter idiom. The rest of that feature deliberately stays OFF App: `registerMCPRoute` takes its handler as a parameter and `toolForPath` is a package function, so the mount cost one method rather than three.

TKT-N8XQ2R adds App.SetNextActionMatchers on the same terms (103 -> 104): the predicate compiler backing a source's `condition:` lives above this package, so it arrives through the same setter idiom rather than a 13th positional NewApp parameter. The compiling and matching themselves stay OFF App entirely — conditionlint owns them and appbuild bridges — so the feature cost one method, not a subsystem. TKT-DN37J2 adds App.SetWorlds on the same terms, and the same discipline held for the rest: `resolveWorld`, `attachWorld` and `worldCapablePath` are all package functions taking what they need, so request-level world selection cost ONE method rather than four.

The theme/settings/palette cluster (12 methods — /_theme/logo CRUD, the /_theme export/import pair, and the /_settings + /_palette CRUD) moved to appearanceHandler (TKT-8AJ1PM, 104 → 92).

The search-query pipeline (5 methods — executeQuery and its free-text branch, the list `?q=` id-set helper, and the sort / property-filter passes they share) moved to queryService, and the dead isRelationLinked was deleted (TKT-SJ0LRS, 92 → 86).

TKT-WRLDAPI item 4 (world-scoped relations) cost ZERO methods, and that was not free discipline — it was written as five methods first and plimsoll failed the build. They became package functions taking their seams explicitly (`worldOutgoingForEntity`, `worldNeighborsForPage`, `includeCandidates`, `defaultWorldCandidates`, `worldCandidates`, plus `SetWorldNeighbors`), which reads better anyway: the seams a world-scoped link read depends on are named in the signature rather than reached through this struct. The load line doing its job is worth recording, because the habit it interrupts is the one that produced the pre-extraction 104.

The merge of FEAT-9CD2MX with develop adds SetWorlds on top of develop's extractions (86 → 87). Neither side grew it carelessly — the worlds side cost exactly one method and said why above — but the integration is where the count actually moves, so it is recorded here rather than in either branch.

func NewApp

func NewApp(
	fs storage.FS,
	paths *project.Context,
	meta *metamodel.Metamodel,
	st store.Store,
	versions store.VersionService,
	em *entitymanager.Manager,
	searcher search.Searcher,
	visibleSearcher search.VisibleSearcher,
	aclImpl acl.ACL,
	fieldResolver FieldVerdictResolver,
	auditSink audit.Audit,
	stateKV state.KV,
) (*App, error)

NewApp creates and initializes an App. Callers pass in the primitives (fs, paths, meta, store) plus the services that depend on workspace assembly: entityManager (the production write path) and searcher (the live Bleve index). Everything else — state.KV, config.Loader, tracer, templater, validator — is constructed locally.

The store-level file watcher (live-reload of external entity / relation edits) is feature-detected on `st` inside App.StartWatching via the [storeWatcher] interface; callers do not wire it.

func (*App) Cfg

func (a *App) Cfg() *Config

Cfg returns the current data-entry config (convenience accessor). Equivalent to a.State().Cfg.

func (*App) Meta

func (a *App) Meta() *metamodel.Metamodel

Meta returns the current metamodel (convenience accessor).

func (*App) NewRouter

func (a *App) NewRouter() http.Handler

NewRouter returns an http.Handler with all data entry routes registered. The Vue SPA serves as the primary UI at the root path.

When adding a route, add a probe to the route table in router_walk_test.go so registration stays covered.

func (*App) ProjectName

func (a *App) ProjectName() string

ProjectName returns the display name of the loaded project.

func (*App) ProjectRoot

func (a *App) ProjectRoot() string

ProjectRoot returns the root directory of the loaded project.

func (*App) Services

func (a *App) Services() Services

Services returns the services bundle.

func (*App) SetCalDAVAliases

func (a *App) SetCalDAVAliases(s *caldavalias.Service)

SetPrincipalResolver installs a custom PrincipalResolver used by the router's audit-stamp middleware. Must be called before App.NewRouter; subsequent changes have no effect on already-built routers.

The typical wiring (in cmd/rela-server) chains EnvPrincipalResolver and HeaderPrincipalResolver so a `$RELA_DATAENTRY_USER` env var overrides any incoming header and the header itself overrides the default. Passing nil restores [defaultPrincipalResolver] behavior. SetCalDAVAliases installs the CalDAV alias service. Without it the CalDAV routes are not registered.

func (*App) SetJWTGate

func (a *App) SetJWTGate(cfg JWTGateConfig) error

SetJWTGate enables fail-closed verified-JWT identity. Must be called before App.NewRouter; subsequent changes have no effect on already-built routers.

When set, every [isAPIPath] request must carry an assertion that verifies, or it is denied 401 — see [requireVerifiedJWT]. This REPLACES the principal resolver for API requests rather than layering over it: JWT identity is exclusive, so there is no header or env source to fall back to. Callers must not also install a header/env chain via App.SetPrincipalResolver; cmd/rela-server enforces that at startup.

Returns an error when a required field is missing rather than accepting a config that cannot work. An empty HeaderName is fatal in a quiet way — every assertion would read as absent, so the server would boot clean and then 401 every API request.

Note the interface-nil caveat: a TYPED nil (e.g. a (*jwtauth.Verifier)(nil) stored in the interface) is not == nil and cannot be caught here without reflection. Callers must not construct one; cmd/rela-server checks the concrete pointer before it ever reaches this interface.

func (*App) SetNextActionMatchers

func (a *App) SetNextActionMatchers(fn NextActionMatcherFunc) error

SetNextActionMatchers injects the predicate compiler backing a source's `condition:`.

Separate from NewApp for the same reason as App.SetUserState: the compiler lives above this package, so the composition root supplies it rather than this package importing it. Rejects nil for the same reason too — a silently absent compiler would leave every condition unevaluated, showing suggestions for entities the operator explicitly excluded.

func (*App) SetPrincipalHeader

func (a *App) SetPrincipalHeader(name string)

SetPrincipalHeader records the name of the HTTP header that carries the principal identity so API responses can declare `Vary` on it. Call alongside App.SetPrincipalResolver (before App.NewRouter) when wiring a HeaderPrincipalResolver; leave unset otherwise.

func (*App) SetPrincipalResolver

func (a *App) SetPrincipalResolver(r PrincipalResolver)

func (*App) SetRemoteMCP

func (a *App) SetRemoteMCP(factory MCPHandlerFactory) error

SetRemoteMCP enables the remote MCP endpoint, which is OFF by default.

It refuses a configuration that cannot be served safely, at startup, rather than at first request:

  • a nil factory has nothing to serve;
  • a factory that errors means the MCP wiring is broken;
  • **no JWT gate is refused outright.** The endpoint needs a CSRF exemption (a non-browser MCP client sends no Origin), and that exemption is only sound while rela itself verifies a bearer token and requires it. In header-identity mode `requireVerifiedJWT` is never wrapped and the terminal resolver yields `User: "unknown"` — combining that with the exemption would publish an unauthenticated remote write surface. A declarative-ACL deployment would still fail closed (`acl.ErrUnstampedPrincipal` rejects `unknown`), but a NopACL deployment would not, and this must not depend on a second, unrelated setting.

The same reasoning as `validateIdentityFlags` in cmd/rela-server: an auth downgrade happens per request, long after anyone reads a startup warning, so it is refused rather than warned about.

Must be called before App.NewRouter.

func (*App) SetSecurityConfig

func (a *App) SetSecurityConfig(cfg SecurityConfig) error

SetSecurityConfig configures the HTTP security middlewares applied by NewRouter. It must be called before NewRouter.

func (*App) SetUserState

func (a *App) SetUserState(s userstate.Store) error

SetUserState replaces the next-action per-user state backend.

Defaults to an in-memory store (see NewApp), which is correct for a single-process deployment: this state is disposable, so losing it on restart costs a user one repeated suggestion. A deployment that wants snoozes to survive a restart, or to be shared across processes, injects a persistent backend here.

Rejects nil rather than quietly disabling the feature: a silently absent store would make the app stop honoring snoozes and mutes, which users experience as the system ignoring them — the deferred-failure symptom the project's constructor rule exists to prevent.

func (*App) SetWebhookReceiver

func (a *App) SetWebhookReceiver(v webhookVerifier, actionID string)

SetWebhookReceiver enables the POST /webhooks/idp endpoint: a verified IdP callback that dispatches to the named action (which fetches authoritative user data and upserts a person entity). Must be called before NewRouter. A nil verifier or empty actionID leaves the receiver disabled and the route unmounted — matching the inert-when-unconfigured shape of the other optional wiring (SetPrincipalResolver, SetSecurityConfig).

func (*App) SetWorlds

func (a *App) SetWorlds(w WorldLookup)

SetWorlds injects the compiled world map, enabling `?world=` selection.

Until this is called the App serves the default world only and REFUSES any `?world=` naming something else — which is the correct posture for a surface whose wiring never opted in, and means a deployment that does not use worlds cannot accidentally acquire the parameter.

func (*App) StartGitFetch

func (a *App) StartGitFetch() (stop func())

StartGitFetch begins periodic git fetch in the background. Returns a stop function to shut down the fetcher.

coverage-ignore-func: background goroutine with timer

func (*App) StartWatching

func (a *App) StartWatching() error

StartWatching begins file watching for live-reload. It does two independent things:

  1. Subscribes to `data-entry.yaml` via the config loader so SPA reloads pick up dashboard / palette / form config changes.
  2. Asks the store to start watching its own entity/relation files (feature-detected via [storeWatcher] — fsstore implements it; in-memory backends don't). The store's observer chain handles reindex automatically; no callback wiring is needed at this layer.

The returned error covers only the config-subscriber failure (step 1). Store-watcher errors (step 2) are logged via slog.Warn because store watching is a live-reload nice-to-have, not a startup requirement.

Stop via App.StopWatching. Note: that only releases the config subscription — the store-watcher lifecycle is owned by the store (closed when the store is closed).

coverage-ignore-func: requires real filesystem events via fsnotify

func (*App) State

func (a *App) State() *Schema

State returns the current reloadable Schema snapshot. Handlers should call State() once at entry and use the returned snapshot consistently throughout, instead of making multiple calls that could see different snapshots after a concurrent reload.

func (*App) StopWatching

func (a *App) StopWatching()

StopWatching releases the data-entry.yaml subscription started by App.StartWatching. The store-level watcher (when present) has its own lifecycle managed by the store and is stopped during store close, not here — asymmetric on purpose: dataentry doesn't own the store, only its config subscription. StopWatching is lifecycle-only and must be called from a single goroutine (it is the StartWatching counterpart). The stop fields are not synchronized; concurrent Start/Stop is not supported.

type AppConfig

type AppConfig = dataentryconfig.AppConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type AssertedIdentity

type AssertedIdentity struct {
	Subject string
	OrgID   string
	OrgSlug string
	Roles   []string

	// PrincipalType and Scopes drive client attenuation (TKT-IAC8TX): the
	// former selects a ceiling in acl.yaml, the latter re-opens pieces of it.
	// Both are absent for a proxy that doesn't model them, which means "no
	// ceiling applies" — the principal keeps its acting user's grants.
	PrincipalType string
	Scopes        []string

	// Email is the verified `email` claim, when the proxy supplies one. Absent
	// for a proxy that doesn't model it, which is not an error. Threaded onto the
	// Principal so lazy provisioning (TKT-ANUJDS) can stamp it on a stub user
	// entity; nothing in the ACL evaluates it.
	Email string
}

AssertedIdentity is the verified-assertion payload this package consumes. It is dataentry's OWN type, not the verifier's: the wiring site adapts whatever the concrete verifier returns into this shape (see the adapter in cmd/rela-server), so dataentry never imports the verifier package and the verifier stays an arch-lint leaf.

Subject is the only field callers may treat as required. The rest are absent for any proxy that doesn't model orgs or roles, which is not an error.

type Calendar

type Calendar = dataentryconfig.Calendar

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type CalendarSource

type CalendarSource = dataentryconfig.CalendarSource

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type CommandConfig

type CommandConfig = dataentryconfig.CommandConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type CommandMessage

type CommandMessage struct {
	Type       string `json:"type"`
	Text       string `json:"text,omitempty"`
	Level      string `json:"level,omitempty"`
	Path       string `json:"path,omitempty"`
	Label      string `json:"label,omitempty"`
	Action     string `json:"action,omitempty"`
	ID         string `json:"id,omitempty"`
	EntityType string `json:"entity_type,omitempty"`
	URL        string `json:"url,omitempty"`
}

CommandMessage is a structured message parsed from a command's stdout.

type CommandScope

type CommandScope = dataentryconfig.CommandScope

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type Config

type Config = dataentryconfig.Config

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ConfigValidationError

type ConfigValidationError = dataentryconfig.ConfigValidationError

ConfigValidationError is re-exported from dataentryconfig.

type DashboardCard

type DashboardCard = dataentryconfig.DashboardCard

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type DashboardConfig

type DashboardConfig = dataentryconfig.DashboardConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type DefaultOverride

type DefaultOverride = dataentryconfig.DefaultOverride

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type DemoFieldVerdictResolver

type DemoFieldVerdictResolver struct{}

DemoFieldVerdictResolver applies a fixed fixture against the "ticket" entity type. The fixture is hand-picked to exercise every affordance code path:

  • kind: read-only (writable=false)
  • priority: hidden (visible=false)
  • effort: option-filtered ({l: false, xl: false})
  • status: option-filtered ({done: false})
  • affects relation: not creatable
  • implements relation: not removable
  • has-planning relation: meta-field "note" not writable (the metamodel doesn't currently declare relation-meta on this type, so the verdict is still emitted and the contract tests rely on a test-fixture metamodel that adds the meta field)

Other entity types receive empty verdicts. Intended for dev / manual-testing use only — the predicate ticket replaces this with a policy-driven resolver.

func (DemoFieldVerdictResolver) FieldVerdicts

FieldVerdicts returns the demo fixture for ticket entities and the zero value for every other type.

func (DemoFieldVerdictResolver) RelationVerdicts

RelationVerdicts returns the demo relation fixture for ticket entities and the zero value for every other type.

type Direction

type Direction = dataentryconfig.Direction

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type DocumentConfig

type DocumentConfig = dataentryconfig.DocumentConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type DocumentResult

type DocumentResult struct {
	// HTML is the rendered HTML content.
	HTML string
	// ContentHash is the hash of source entities used for cache validation.
	ContentHash string
	// Entities contains all entities involved in the document (for dependency tracking).
	Entities []*entity.Entity
}

DocumentResult holds the result of rendering a document.

type EnumHelp

type EnumHelp struct {
	Property    string
	TypeName    string
	Initial     string // entry state for the diagram; "" when unknown
	Values      []ValueHelp
	Transitions []TransitionHelp
}

EnumHelp documents an enum/state-machine property's allowed values and (when it is a state machine) its lifecycle. Property is the property name, TypeName the custom-type name (empty for an inline enum). Values / Transitions are only populated when present; both may be empty for a plain field, in which case the property contributes no help sections (TKT-DUQBD0).

type FieldVerdictResolver

type FieldVerdictResolver interface {
	FieldVerdicts(ctx context.Context, e *entityPkg.Entity) FieldVerdicts
	RelationVerdicts(ctx context.Context, e *entityPkg.Entity) RelationVerdicts
}

FieldVerdictResolver decides per-entity affordances for fields, enum options, and relation-meta fields. The wire shape it feeds into is documented in docs/data-entry/api-reference.md.

v1 ships two implementations:

  • NopFieldVerdictResolver — returns zero verdicts; every field, option, and relation is permitted. Default unless RELA_AFFORDANCE_PROFILE selects another.
  • DemoFieldVerdictResolver — a hardcoded fixture against the ticket type, exercising every affordance code path so the SPA work in TKT-G7N5 has an observable end-to-end behavior to test against.

The eventual predicate-engine ticket replaces both with a policy-driven implementation that reads acl.yaml. The interface shape is intentionally narrow so the swap is mechanical.

func ResolverFromProfile

func ResolverFromProfile(
	profile string, meta *metamodel.Metamodel, st store.Store,
	declarative *acl.Declarative,
) (FieldVerdictResolver, error)

ResolverFromProfile returns the FieldVerdictResolver for the given profile, policy, and metamodel. Selection order (DR-M3):

  1. profile == "demo" → DemoFieldVerdictResolver (hard override, for dev / e2e fixtures even when a policy is present).
  2. profile == "none" → NopFieldVerdictResolver (hard opt-out).
  3. profile == "" and the policy declares any affordance grants → the policy-backed resolver.
  4. otherwise → NopFieldVerdictResolver.

An unknown profile logs a warning and falls back to step 3/4. A policy-backed resolver whose predicates fail to compile returns an error — the caller fails startup loudly (DR-M4), matching the acl.yaml hard-fail posture for genuinely broken config.

func ResolverFromServices

func ResolverFromServices(svc ResolverServices) (FieldVerdictResolver, error)

ResolverFromServices builds the affordance resolver for an entry point, reading RELA_AFFORDANCE_PROFILE from the environment and the metamodel / store / declarative from svc. Both cmd entry points call this; they differ only in how they handle the returned error (rela-server exits, rela-desktop surfaces it to the UI), so error handling stays at the call site.

type FieldVerdicts

type FieldVerdicts struct {
	// Writable maps fieldName → writable. Absence = writable.
	Writable map[string]bool

	// Visible maps fieldName → visible. Absence = visible. False means
	// the property is omitted from the wire `properties` map AND from
	// `_fields`; the SPA's filter never sees the key.
	Visible map[string]bool

	// Options maps fieldName → optionValue → allowed. Absence of the
	// field OR absence of an option means allowed. Used for enum-typed
	// properties.
	Options map[string]map[string]bool

	// Attribution maps a denied path (field name, or "field=option")
	// to the role/grant that produced the deny. Audit-only — never
	// serialized to the wire. Sparse: only denials appear. Empty for
	// resolvers (Nop / Demo) that don't track attribution.
	Attribution map[string]string
}

FieldVerdicts carries per-entity field-level affordance decisions. All maps use sparse semantics: absence of a key means "default" (the permissive default — writable, visible, all options allowed). Only deviations need to be populated.

func (FieldVerdicts) IsOptionAllowed

func (v FieldVerdicts) IsOptionAllowed(name, opt string) bool

IsOptionAllowed reports whether option `opt` is allowed for the enum-typed field `name`. The default is allowed — absent or true-valued entries both yield true; only explicit false values filter the option out.

func (FieldVerdicts) IsVisible

func (v FieldVerdicts) IsVisible(name string) bool

IsVisible reports whether name is visible. The default is true — absent or true-valued entries both yield true; only explicit false values hide the field.

func (FieldVerdicts) IsWritable

func (v FieldVerdicts) IsWritable(name string) bool

IsWritable reports whether name is writable. The default is true — absent or true-valued entries both yield true; only explicit false values are denials.

type FilterConfig

type FilterConfig = dataentryconfig.FilterConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type FilterControl

type FilterControl = dataentryconfig.FilterControl

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type Form

type Form = dataentryconfig.Form

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type FormField

type FormField = dataentryconfig.FormField

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type FormRelation

type FormRelation = dataentryconfig.FormRelation

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type Gantt

type Gantt = dataentryconfig.Gantt

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type GitStatusResponse

type GitStatusResponse struct {
	Available     bool     `json:"available"`
	Branch        string   `json:"branch,omitempty"`
	LocalChanges  int      `json:"local_changes"`
	RemoteAhead   int      `json:"remote_ahead"`
	Syncing       bool     `json:"syncing"`
	Conflict      bool     `json:"conflict"`
	ConflictFiles []string `json:"conflict_files,omitempty"`
}

GitStatusResponse is the JSON response for /api/git/status.

type GitSyncResponse

type GitSyncResponse struct {
	Success       bool     `json:"success"`
	Error         string   `json:"error,omitempty"`
	ConflictFiles []string `json:"conflict_files,omitempty"`
}

GitSyncResponse is the JSON response for /api/git/sync.

type GroupData

type GroupData struct {
	GroupName string
	Rows      []SectionRowData
	Entities  []SectionEntityData
}

GroupData holds a group of rows/entities for grouped table/card display.

type ImportedThemeAsset

type ImportedThemeAsset struct {
	Bytes []byte
	Ext   string
}

ImportedThemeAsset carries the bytes + sniffed extension of a logo extracted from a theme package. ImportedThemeAsset is nil when the package contained no logo (or didn't reference one).

type JWTGateConfig

type JWTGateConfig struct {
	// Verifier checks the assertion and projects its claims. Required.
	//
	// It returns the full [AssertedIdentity] (subject + org + roles), not just
	// the subject, so the gate can stamp asserted roles onto the Principal it
	// installs. A subject-only verifier here would authenticate correctly but
	// silently strip every asserted role before the ACL sees it (TKT-OJL2GN).
	Verifier assertionVerifier
	// HeaderName is the request header carrying the assertion. Required.
	HeaderName string
	// KeysUnavailable reports whether a verification error means the JWKS was
	// unreachable (an operator-actionable outage) rather than the assertion
	// being bad (a client fault). Both deny; they differ only in how they are
	// logged.
	//
	// It is injected as a predicate rather than imported so this package stays
	// independent of internal/jwtauth — the wiring layer supplies
	// errors.Is(err, jwtauth.ErrKeysUnavailable). Optional: a nil predicate
	// classifies every failure as a client fault.
	KeysUnavailable func(error) bool
}

JWTGateConfig configures fail-closed verified-JWT identity. Install it with App.SetJWTGate; when set, App.NewRouter wraps the API surface in [requireVerifiedJWT] and the principal-resolver chain is bypassed entirely for those requests.

type Kanban

type Kanban = dataentryconfig.Kanban

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type KanbanCard

type KanbanCard = dataentryconfig.KanbanCard

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type KanbanColumn

type KanbanColumn = dataentryconfig.KanbanColumn

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type KanbanSwimlane

type KanbanSwimlane = dataentryconfig.KanbanSwimlane

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type List

type List = dataentryconfig.List

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ListColumn

type ListColumn = dataentryconfig.ListColumn

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type MCPHandlerFactory

type MCPHandlerFactory func() (http.Handler, error)

MCPHandlerFactory builds the MCP HTTP handler.

It returns a plain http.Handler, NOT an SDK server type: `internal/mcp` is the only component permitted to import the MCP go-sdk (arch-lint's `mcpgo` vendor grant), so the SDK type must not appear in this package's API. The wiring site owns the SDK entirely — protocol version, stateless mode, transport — and hands back something this package can serve.

It is called ONCE at router construction, not per request. Per-request state (the verified principal, the ACL Request) travels on the request ctx, which the middleware chain has already populated by the time the returned handler runs; the handler resolves it per call. A per-request factory would rebuild the whole tool registry on every message for no benefit.

Returning an error refuses to build the router at all, so a broken MCP wiring is a startup failure rather than a per-request 500 discovered later.

type NavigationEntry = dataentryconfig.NavigationEntry

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type NextActionMatcherFunc

type NextActionMatcherFunc func(
	cfg *dataentryconfig.Config, meta *metamodel.Metamodel,
) (func(sourceID string) (nextaction.Matcher, bool), []string)

NextActionMatcherFunc compiles the `condition:` of every configured source against the current metamodel, returning a per-source lookup plus one message per problem.

The consumer-side seam for the predicate compiler: this package must not import it (arch-lint keeps the condition/policy engine above the data-entry app), so the composition root supplies an implementation.

type NopFieldVerdictResolver

type NopFieldVerdictResolver struct{}

NopFieldVerdictResolver returns empty verdicts for every entity. computeFields / computeRelations interpret empty verdicts as "no deviations from default" and emit sparse `_fields: {}` and `_relations: {}` on the wire. The SPA renders unchanged.

func (NopFieldVerdictResolver) FieldVerdicts

FieldVerdicts always returns the zero value.

func (NopFieldVerdictResolver) RelationVerdicts

RelationVerdicts always returns the zero value.

type PaletteColors

type PaletteColors = dataentryconfig.PaletteColors

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type PaletteConfig

type PaletteConfig = dataentryconfig.PaletteConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ParsedThemePackage

type ParsedThemePackage struct {
	Manifest *dataentryconfig.ThemeManifest
}

ParsedThemePackage is the typed result of parsing a `.relatheme` upload. Manifest is always populated on success; Logo is non-nil only when the manifest referenced a logo and the bytes passed validation.

type PrincipalResolver

type PrincipalResolver func(*http.Request) principal.Principal

PrincipalResolver maps an incoming HTTP request to the audit Principal that should be stamped on its context. Compose multiple resolvers via ChainResolvers to layer (e.g.) an env-var override over a header reader over the default.

func ChainResolvers

func ChainResolvers(resolvers ...PrincipalResolver) PrincipalResolver

ChainResolvers returns a resolver that tries each supplied resolver in order and returns the first one whose User is non-empty. If no resolver yields a user, falls back to [defaultPrincipalResolver] (Tool=data-entry, User=unknown). Used by cmd/rela-server to layer env → header → default.

**Chain contract for resolver authors.** Return a zero principal.Principal (User=="") to signal fall-through. The chain advances on `p.User == ""` and *ignores* Tool — every data-entry resolver hard-codes Tool=ToolDataEntry today, so distinguishing on Tool would be cosmetic. If a future resolver needs to return a different Tool, give it a non-empty User too and the chain will honor both.

func EnvPrincipalResolver

func EnvPrincipalResolver() PrincipalResolver

EnvPrincipalResolver reads Principal.User from $RELA_DATAENTRY_USER. Returns a zero principal when the env is unset or whitespace-only — chain it (typically first) so it acts as a local-dev escape hatch that overrides any incoming header.

The env var is read on *every* request rather than cached at construction so test fixtures using `t.Setenv` work without rebuilding the resolver. The cost is one map lookup per request (Go runtime takes a RLock); negligible relative to the per-request work of the audit middleware that follows.

Sanitization mirrors HeaderPrincipalResolver.

func HeaderPrincipalResolver

func HeaderPrincipalResolver(headerName string) PrincipalResolver

HeaderPrincipalResolver reads Principal.User from headerName on each request, stamping Tool=data-entry.

The returned resolver is never nil. An empty headerName yields a resolver that always returns a zero Principal — the ChainResolvers composition relies on this shape, so callers don't need to special- case the disabled state. Production wiring in cmd/rela-server passes the raw flag value; the empty-default flag stays inert.

**Trust boundary.** The header value is only as trustworthy as the reverse proxy that sets it. Operators serving data-entry without a trusted proxy must not enable this resolver — anyone can spoof identity by setting the header on the wire. See docs/server-security.md for the deployment guidance.

Sanitization: control characters (C0 + DEL) in the header value are replaced with regular spaces, the value is truncated to 256 runes (UTF-8 safe), and surrounding whitespace is trimmed. Control-only values therefore sanitize to "" and fall through.

func JWTPrincipalResolver deprecated

func JWTPrincipalResolver(v assertionVerifier, headerName string) PrincipalResolver

Deprecated: production wiring uses [requireVerifiedJWT] via App.SetJWTGate, which fails CLOSED. This resolver returns a zero Principal on a verification failure so a chain falls through to the next source — under a header chain that is an auth downgrade, which is why cmd/rela-server no longer wires it. Retained for callers embedding dataentry with their own chain semantics.

JWTPrincipalResolver reads a signed identity assertion from headerName, verifies it (ES256 against the proxy's JWKS, via v), and stamps the verified STABLE subject as Principal.User. This is provider-agnostic — any OIDC proxy that injects a signed JWT works by configuring the issuer/audience/JWKS/header.

Unlike HeaderPrincipalResolver (which trusts the proxy set the header), this resolver CRYPTOGRAPHICALLY verifies the assertion, so it is safe even when the header could reach the server from the network — a spoofed header without a valid signature simply fails verification and falls through.

Tool is principal.ToolDataEntry: the assertion changes WHO authenticated, not the entry point (a verified user still arrives via the data-entry HTTP surface).

It also carries the assertion's org and role claims onto the Principal via principal.Verified. This is the ONLY resolver that may do so — the header and env resolvers have no verified source for them, and a role reaching the ACL from a spoofable header would be a complete authorization bypass. The unexported fields on Principal make that structural rather than a convention.

A missing header, an "Authorization: Bearer <jwt>" wrapper (the scheme is stripped case-insensitively per RFC 6750), or any verification failure yields a zero Principal so the chain falls through — a nil v also yields an inert resolver. Empty headerName ⇒ inert (matches the disabled-flag shape of the other resolvers).

type PropertyHelp

type PropertyHelp struct {
	Name        string
	Type        string
	Required    bool
	Description htmltemplate.HTML
}

PropertyHelp holds documentation for a single property.

type RelationHelp

type RelationHelp struct {
	Name        string
	Label       string
	TargetType  string // target type for outgoing, source type for incoming
	Cardinality string
	Required    bool // true if min cardinality >= 1
	Description htmltemplate.HTML
}

RelationHelp holds documentation for a single relation.

type RelationOp

type RelationOp int

RelationOp identifies which relation-write operation a caller is gating. Pass via App.validateRelationOp.

const (
	// RelationOpCreate gates adding an edge of the given type.
	RelationOpCreate RelationOp = iota
	// RelationOpRemove gates removing any edge of the given type.
	RelationOpRemove
)

type RelationProperty

type RelationProperty = dataentryconfig.RelationProperty

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type RelationVerdict

type RelationVerdict struct {
	Creatable bool
	Removable bool
	// Fields maps metaField → writable. Absence = writable. Applies
	// uniformly to every link of this relation type (per-link
	// affordances are predicate territory, deferred).
	Fields map[string]bool

	// Attribution maps a denied dimension ("create", "remove",
	// "fields.<name>") to the role/grant that denied it. Audit-only,
	// never serialized. Sparse.
	Attribution map[string]string
}

RelationVerdict carries the affordance decision for a single relation type. Zero-value (Creatable=false, Removable=false, Fields=nil) would deny everything; callers always populate explicitly.

type RelationVerdicts

type RelationVerdicts struct {
	Types map[string]RelationVerdict
}

RelationVerdicts carries per-entity relation-level affordance decisions. The map is sparse: relation types not listed default to fully-permitted ({creatable: true, removable: true} with no meta-field restrictions).

type RelationVisibilityResolver

type RelationVisibilityResolver interface {
	RelationFieldVerdicts(
		ctx context.Context, from *entityPkg.Entity, relType string, metaKeys []string,
	) map[string]bool
}

RelationVisibilityResolver is the OPTIONAL sibling of FieldVerdictResolver that answers per-meta-field READ visibility for one relation edge (TKT-B1F5Q1). Kept separate and type-asserted (not embedded) for the same reason as TransitionResolver: only the policy-backed resolver can express a relation `visible:` grant, so the Nop and Demo resolvers don't implement it and relation meta is emitted un-redacted under them (matching how relations behaved before B1F5Q1). This mirrors the store's optional capabilities.

from is the source entity that owns the relation grant block; relType is the edge's relation type; metaKeys are the property names actually present on the edge about to be serialized (the closed-world deny universe). The result is sparse: an absent key means visible, a `false` value means hide that meta key.

type ResolvedCommand

type ResolvedCommand struct {
	ID       string
	Label    string
	Confirm  string
	Context  string
	AutoOpen *bool
}

ResolvedCommand is a command that has been matched to a specific page context.

type ResolvedPalette

type ResolvedPalette = dataentryconfig.ResolvedPalette

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ResolverServices

type ResolverServices interface {
	ACLDeclarative() *acl.Declarative
	Meta() *metamodel.Metamodel
	Store() store.Store
}

ResolverServices is the slice of [appbuild.Services] that ResolverFromServices needs. Declared here at the call site so the entry points pass `svc` directly without each re-spelling the env-read + accessors.

ACLDeclarative may return nil when no policy is wired (NopACL); the resolver constructor handles that case by selecting NopFieldVerdictResolver.

type Schema

type Schema struct {
	Cfg         *Config
	Meta        *metamodel.Metamodel
	StyleMap    map[string]map[string]string
	StyledTypes map[string]bool
	OpenAPIGen  *openapi.Generator
}

Schema is the co-derived reload core of the data-entry app: the config and metamodel, plus everything that is a pure function of the two (the style map and the OpenAPI generator). These MUST move together — the style map is derived from (Cfg, Meta), so publishing them independently could let a reader observe a new metamodel with a stale style map.

It is the residue of the former AppState after the independently-owned state (logo, palette, user defaults) moved into their own self-synchronized services. Readers Load a coherent Schema once per request via [schemaProvider.Current] (exposed on App as State()).

type ScopeDescriptor

type ScopeDescriptor struct {
	Source  string            `json:"source"`            // "list" | "search"
	Type    string            `json:"type"`              // entity type name (singular)
	Filters map[string]string `json:"filters,omitempty"` // filter[...] bracket keys → value
	Sort    string            `json:"sort,omitempty"`    // "-created,title" form
	Q       string            `json:"q,omitempty"`       // free-text query
}

ScopeDescriptor encodes the query that defines an ordered result set the user is navigating — a typed list, a search result, and (later) other sources. It rides on the wire as a single URL-encoded JSON `scope` param, unsigned: every field it carries is already freely issuable by the client against the list endpoint, so there is nothing to protect against tamper. Correctness comes from strict decoding in scopeFromParam, not from a signature. See issue #844 and docs/data-entry/api-reference.md.

Filters carries the same flat bracket-format keys the SPA already emits via filterStateToApiParams ("filter[status]", "filter[due][gte]", …). Reusing that wire format keeps a single source of truth for filter serialization and lets the descriptor rebuild a url.Values that the shared list pipeline consumes verbatim.

type ScriptErrorEnvelope

type ScriptErrorEnvelope struct {
	Error          string           `json:"error"`
	CorrelationID  string           `json:"correlation_id,omitempty"`
	Script         ScriptIdentity   `json:"script"`
	Lua            ScriptErrorLua   `json:"lua"`
	Source         []lua.SourceLine `json:"source,omitempty"`
	Stack          []lua.StackFrame `json:"stack,omitempty"`
	CapturedOutput string           `json:"captured_output,omitempty"`
}

ScriptErrorEnvelope is the on-the-wire representation of a Lua script failure. The HTTP layer always returns this with status 422 so the frontend can branch on `error == "script_error"`. Every field except `correlation_id`, `script.path`, and `lua.message` is loopback-gated: non-loopback callers receive a degraded shape (see security.AllowFullScriptDetail).

JSON tags on lua.ScriptError are advisory: each consumer owns its wire shape. Data-entry uses this envelope; MCP marshals lua.ScriptError directly. Adding a field to lua.ScriptError requires deciding whether to mirror it here or let it surface only over MCP.

type ScriptErrorLua

type ScriptErrorLua struct {
	Message string `json:"message"`
	Line    int    `json:"line,omitempty"`
}

ScriptErrorLua is the message + line that always survives gating, so even a non-loopback caller knows roughly what broke without leaking the full source slice.

type ScriptIdentity

type ScriptIdentity struct {
	Surface  string         `json:"surface"`
	Path     string         `json:"path"`
	EntityID string         `json:"entity_id,omitempty"`
	Args     map[string]any `json:"args,omitempty"`
}

ScriptIdentity carries who-was-running info: the surface (action, document, automation, lua_run, lua_eval), the script path, and any triggering entity / args context the surface decided to capture.

type SectionAddInfo

type SectionAddInfo struct {
	Relation string
	LinkAs   string // "from" or "to" — role of the new entity in the relation
	PeerID   string // entry entity ID
	Targets  []SectionAddTarget
}

SectionAddInfo describes an "Add" button on a view section.

type SectionAddTarget

type SectionAddTarget struct {
	EntityType string
	FormID     string
	Label      string
}

SectionAddTarget holds a possible entity type target for an "Add" button.

type SectionColumnData

type SectionColumnData struct {
	Values     []string
	PropType   string
	Widget     string
	Link       string // resolved link URL or empty
	EntityID   string
	EntityType string
}

SectionColumnData holds a resolved table cell for template rendering.

type SectionData

type SectionData struct {
	Heading      string
	SectionID    string
	Display      string
	Fields       []SectionFieldData
	Entities     []SectionEntityData
	Columns      []ListColumn
	Rows         []SectionRowData
	Groups       []GroupData
	IsGrouped    bool
	EmptyMessage string
	IsEmpty      bool
	Link         string // section-level link configuration (currently unused in templates)
	Content      string
	HasContent   bool
	AddInfo      *SectionAddInfo
	LinkInfo     *SectionLinkInfo
}

SectionData holds all resolved data for a single view section.

type SectionEntityData

type SectionEntityData struct {
	ID            string
	Title         string
	Type          string
	EditFormID    string
	Fields        []SectionFieldData
	Content       string
	HasContent    bool
	Props         map[string]any
	FieldVerdicts map[string]v1.FieldAffordance
	// World is the face-provenance of this entity under the view's world
	// (TKT-WRLDAPI item 4b). Nil under the default world.
	World *v1.EntityWorld
}

SectionEntityData holds a resolved entity for template rendering.

`Props` and `FieldVerdicts` (TKT-IHC7D) carry the typed property values and per-cell writability verdicts for inline-edit hosts on cards/list view sections. Both are hidden-property-stripped. The wire converter dumb-copies them into v1.ViewEntity._props and v1.ViewEntity._fields respectively. They are nil for code paths that don't compute them (notably the entry-source branch and table rows); the wire converter's nil-checks gate emission.

type SectionFieldData

type SectionFieldData struct {
	Property     string
	Label        string
	Values       []string
	PropType     string
	Inaccessible bool
	Span         int
	Render       string
	// Widget is the config's widget override for this field, empty when the
	// author did not set one. Passed through verbatim: resolving it is the
	// SPA's job (its registry owns the type→widget default), and the server
	// has already rejected a name/type mismatch at config load (TKT-3R7RF3).
	Widget string
}

SectionFieldData holds a single resolved field for template rendering. Values is always a list so that list-typed properties (list: true in the metamodel) retain per-item structure; scalar properties become a 1-element slice. Empty properties emit an empty slice.

Property is the raw property name (e.g. "title"); Label is its human-readable form. Inaccessible is true when the underlying entity is git-crypt encrypted and the value cannot be read with the current key — frontends render a lock indicator instead of the (absent) value. Span is the field's width on the 12-column layout grid, carried through from the view config. 0 means full width — the default for every auto-generated view, so a section with no spans authored renders as one scannable column.

Render is the resolved render mode ("display" | "input", TKT-HOIX1) — see dataentryconfig.ResolveFieldRender. Resolved server-side so the SPA never reimplements the section→field inheritance rule.

type SectionLinkInfo

type SectionLinkInfo struct {
	Relation    string   // relation type name
	LinkAs      string   // "from" or "to" — role of the linked entity
	PeerID      string   // entry entity ID
	EntityTypes []string // valid target entity types
}

SectionLinkInfo describes a "Link existing" button on a view section.

type SectionRowData

type SectionRowData struct {
	EntityID   string
	EntityType string
	EditFormID string
	Cells      []SectionColumnData
	Content    string
}

SectionRowData holds a resolved table row for template rendering.

type SecurityConfig

type SecurityConfig struct {
	// BindAddress is the host:port (or :port) the server is bound to.
	// Used to derive the default Host and Origin allowlists.
	BindAddress string
	// AllowedOrigins are extra origins permitted in addition to the
	// loopback defaults derived from BindAddress. Used to allow dev servers
	// such as Vite running on a different port.
	AllowedOrigins []string
}

SecurityConfig configures the HTTP security middlewares.

rela-server is intended to run on a local port, but a browser visiting any other site is already inside the loopback trust boundary. These middlewares reject:

  • Requests whose Host header is not in the loopback allowlist (DNS rebinding defense).
  • Requests to sensitive endpoints whose Origin (or Referer fallback) is not in the allowlist (CSRF / cross-origin read defense).

All sensitive endpoints are protected on every method, not just non-safe ones, because some handlers (e.g. /api/command/) historically accept GET for state-changing operations and a method-based filter would miss `<img src=...>` style attacks.

type Services

type Services struct {
	// Store provides entity/relation CRUD. Handlers use it for read
	// operations; writes go through the workspace's EntityManager.
	Store store.Store

	// Tracer walks relations for trace/path/orphan queries.
	Tracer tracer.Tracer

	// Searcher runs free-text queries against the search index.
	Searcher search.Searcher

	// Meta is the current metamodel snapshot.
	Meta *metamodel.Metamodel
}

Services bundles the backend services the data-entry handlers read from. Each consuming package keeps its own Services type — the coupling reduction is worth the field duplication with lua.WriteDeps and friends.

The bundle carries only what HTTP handlers actually need: read-side access to the store and tracer, free-text search, and the metamodel. Writes continue to flow through the entity manager so automations and validations fire.

type SidePanelConfig

type SidePanelConfig = dataentryconfig.SidePanelConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type SortSpec

type SortSpec = dataentryconfig.SortSpec

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type TransitionHelp

type TransitionHelp struct {
	Move string // the move label, falling back to the To value
	From string
	To   string
	Help htmltemplate.HTML
}

TransitionHelp documents one lifecycle move: the target label (the verb), the From→To states, and the optional Help prose (why/when to make the move).

type TransitionResolver

type TransitionResolver interface {
	TransitionVerdicts(ctx context.Context, e *entityPkg.Entity) map[string][]statemachine.TransitionVerdict
	// EntryValues returns, per state-machine-typed property of entityType, the
	// value a create must enter at (the machine's Initial/Default — BUG-X1C7S).
	// The create form uses it to lock the field to its initial value. Empty when
	// entityType has no machine-typed property.
	EntryValues(entityType string) map[string]string
}

TransitionResolver is the OPTIONAL sibling of FieldVerdictResolver that answers state-machine transition verdicts for an entity (TKT-3G93B8). It is kept separate — and type-asserted, not embedded — because only the policy-backed resolver can answer it; the Nop and Demo resolvers don't implement it, so `_transitions` is simply absent under them (the SPA falls back to the ordinary enum control). This mirrors the store's optional capabilities (e.g. HistoryReader), which are also type-asserted rather than forced into the base interface.

The returned map is keyed by property name; each value is the resolved outgoing transitions for the ctx principal on e. An empty map means "no machine-typed property on this entity" (or no machines wired).

type UserDefaults

type UserDefaults = dataentryconfig.UserDefaults

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ValueHelp

type ValueHelp struct {
	Value       string
	Label       string
	Description htmltemplate.HTML
}

ValueHelp documents one allowed value of an enum: the raw value, its optional display Label, and its optional prose Description (CustomType.Descriptions).

type ViewConfig

type ViewConfig = dataentryconfig.ViewConfig

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ViewEntry

type ViewEntry = dataentryconfig.ViewEntry

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ViewSection

type ViewSection = dataentryconfig.ViewSection

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ViewSectionField

type ViewSectionField = dataentryconfig.ViewSectionField

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type ViewTraverse

type ViewTraverse = dataentryconfig.ViewTraverse

Config type aliases — re-exported from dataentryconfig for backward compatibility.

type Warning

type Warning = entity.Warning

Warning is a type alias for entity.Warning so that handlers in this package can write `dataentry.Warning` without importing the entitymanager package at every call site. Behavior is identical.

type WebhookClaims

type WebhookClaims struct {
	Event  string // the event name, e.g. "membership.created"
	UserID string // the subject the event concerns
	OrgID  string // the tenant the event concerns
	ID     string // the webhook id (jti), for replay dedup
}

WebhookClaims is the verified subset of an inbound webhook the receiver acts on. It mirrors jwtauth.WebhookClaims but is declared HERE so the dataentry package needn't import jwtauth (the inward-pointing layering rule — the same reason the JWT gate takes a local assertionVerifier interface). The wiring layer, which may import both, adapts the concrete verifier to this shape.

type WorldLookup

type WorldLookup interface {
	Lookup(name string) (store.WorldScope, bool)
}

WorldLookup resolves a declared world NAME to its compiled scope.

Consumer-side interface: internal/dataentry may not import internal/worlds (arch-lint), and a store.WorldScope is metamodel-free by construction, so the compiled map is injected from the wiring site via App.SetWorlds.

It must FAIL CLOSED on an unknown name — returning ok=false rather than substituting the default world, which would silently widen a request that asked for a narrower view.

Jump to

Keyboard shortcuts

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