import { z } from 'zod'; import * as _objectstack_spec_data from '@objectstack/spec/data'; import { AuditProvenanceField } from '@objectstack/spec/data'; export { AUDIT_FIELD_DEFS, InjectedColumnProvenance, OWNER_FIELD_DEF, OWNING_BUSINESS_UNIT_FIELD_DEF, TENANT_SCOPE_FIELD_DEF, injectedSystemColumnDefs, platformProvisionsStorage, resolveInjectedColumnProvenance, unprovisionedInjectedColumns } from '@objectstack/spec/data'; /** * Metadata Repository types — see ADR-0008 §2. * * All shapes are defined as Zod schemas so the same definition serves * runtime validation and static typing (`z.infer`). */ /** * Canonical metadata type names. Aligned with the `MetadataTypeSchema` * enum in `@objectstack/spec/kernel/metadata-plugin.zod.ts`. New types are * added here in lockstep with that file. */ declare const MetadataTypeSchema: z.ZodEnum<{ object: "object"; field: "field"; hook: "hook"; mapping: "mapping"; view: "view"; page: "page"; dashboard: "dashboard"; app: "app"; action: "action"; flow: "flow"; workflow: "workflow"; job: "job"; agent: "agent"; tool: "tool"; skill: "skill"; report: "report"; translation: "translation"; role: "role"; profile: "profile"; permission: "permission"; policy: "policy"; api: "api"; endpoint: "endpoint"; datasource: "datasource"; cube: "cube"; settings: "settings"; email_template: "email_template"; }>; type MetadataType = z.infer; /** * Fully-qualified reference to a metadata item. Identity is `(org, type, name)`. * * Per ADR-0008 v2 (2026-05) the metadata layer no longer carries `project` * or `branch`. Project survives only as an **artifact packaging concept** * (the unit a CLI/CI run compiles into `dist/objectstack.json`); it does * not appear in the runtime customization scope. Branching belongs to Git * (or your VCS of choice) and never propagated cleanly into the runtime * model — so it has been removed entirely. * * Higher layers may default `org='system'` for built-ins. * * `version` is optional: omit to mean "HEAD", supply to pin. */ declare const MetaRefSchema: z.ZodObject<{ org: z.ZodString; type: z.ZodEnum<{ object: "object"; field: "field"; hook: "hook"; mapping: "mapping"; view: "view"; page: "page"; dashboard: "dashboard"; app: "app"; action: "action"; flow: "flow"; workflow: "workflow"; job: "job"; agent: "agent"; tool: "tool"; skill: "skill"; report: "report"; translation: "translation"; role: "role"; profile: "profile"; permission: "permission"; policy: "policy"; api: "api"; endpoint: "endpoint"; datasource: "datasource"; cube: "cube"; settings: "settings"; email_template: "email_template"; }>; name: z.ZodString; version: z.ZodOptional; }, z.core.$strip>; type MetaRef = z.infer; /** * Construct a stable string key from a MetaRef (excluding `version`, * which is mutable). Used as cache keys and log indexes. */ declare function refKey(ref: Pick): string; /** * Full metadata item as stored / returned by the Repository. * * `body` is the **canonical, Zod-normalised** spec (with defaults filled * in). `hash` is `sha256(canonicalize(body))`. Equal hashes imply equal * specs. */ declare const MetadataItemSchema: z.ZodObject<{ ref: z.ZodObject<{ org: z.ZodString; type: z.ZodEnum<{ object: "object"; field: "field"; hook: "hook"; mapping: "mapping"; view: "view"; page: "page"; dashboard: "dashboard"; app: "app"; action: "action"; flow: "flow"; workflow: "workflow"; job: "job"; agent: "agent"; tool: "tool"; skill: "skill"; report: "report"; translation: "translation"; role: "role"; profile: "profile"; permission: "permission"; policy: "policy"; api: "api"; endpoint: "endpoint"; datasource: "datasource"; cube: "cube"; settings: "settings"; email_template: "email_template"; }>; name: z.ZodString; version: z.ZodOptional; }, z.core.$strip>; body: z.ZodRecord; hash: z.ZodString; parentHash: z.ZodNullable; authoredBy: z.ZodNullable; authoredAt: z.ZodString; message: z.ZodOptional; seq: z.ZodNumber; schemaVersion: z.ZodOptional; }, z.core.$strip>; type MetadataItem = z.infer; /** Lightweight header for listing — `body` omitted. */ type MetadataItemHeader = Omit; declare const MetadataOpSchema: z.ZodEnum<{ create: "create"; update: "update"; delete: "delete"; rename: "rename"; publish: "publish"; revert: "revert"; }>; type MetadataOp = z.infer; /** * The single event payload broadcast by the change log. ADR-0008 §2.4. * * For `rename`, `previousName` carries the old machine name. For * `delete`, `hash` is null. The payload is intentionally small — * consumers re-fetch via the cache when they need the full body. */ declare const MetadataEventSchema: z.ZodObject<{ seq: z.ZodNumber; op: z.ZodEnum<{ create: "create"; update: "update"; delete: "delete"; rename: "rename"; publish: "publish"; revert: "revert"; }>; ref: z.ZodObject<{ org: z.ZodString; type: z.ZodEnum<{ object: "object"; field: "field"; hook: "hook"; mapping: "mapping"; view: "view"; page: "page"; dashboard: "dashboard"; app: "app"; action: "action"; flow: "flow"; workflow: "workflow"; job: "job"; agent: "agent"; tool: "tool"; skill: "skill"; report: "report"; translation: "translation"; role: "role"; profile: "profile"; permission: "permission"; policy: "policy"; api: "api"; endpoint: "endpoint"; datasource: "datasource"; cube: "cube"; settings: "settings"; email_template: "email_template"; }>; name: z.ZodString; version: z.ZodOptional; }, z.core.$strip>; hash: z.ZodNullable; parentHash: z.ZodNullable; version: z.ZodOptional; previousName: z.ZodOptional; actor: z.ZodNullable; message: z.ZodOptional; ts: z.ZodString; source: z.ZodString; }, z.core.$strip>; type MetadataEvent = z.infer; /** * Two-tier metadata authorization intent (ADR-0005 extension). * * - `override-artifact`: the write targets an item that ships from a code * package (an artifact). Only permitted when the type opts into * per-org overlay writes via `allowOrgOverride: true`. * - `runtime-only`: the write targets a brand-new item OR an item that * exists only in `sys_metadata` (no artifact backing). Permitted for * types that opt into runtime creation via `allowRuntimeCreate: true`, * even when they explicitly forbid artifact overrides. * * The protocol layer determines the intent by consulting the schema * registry; the repository's `assertAllowed()` enforces it as * defense-in-depth. Defaults to `override-artifact` for backward * compatibility with callers that predate the two-tier model. */ type MetadataWriteIntent = 'override-artifact' | 'runtime-only'; interface PutOptions { /** * Hash this writer believed was at HEAD. `null` means "creating, expect * absence". A mismatch throws ConflictError. */ parentVersion: string | null; /** * Identity of the writer; mirrored to MetadataEvent.actor and stored in * `sys_metadata_history.recorded_by` (a `lookup('sys_user')`). * * **Required but nullable, deliberately** (#4556). Pass `null` — never a * label like `'system'` — when the write has no human actor: a boot * metadata sync, a data migration, a scheduled job. Keeping the property * required rather than optional forces every call site to state which of * the two it is, so a forgotten actor cannot silently become a fake * foreign key in a lookup column. */ actor: string | null; /** Optional human-readable commit message. */ message?: string; /** Optional label for the change log "source" column. */ source?: string; /** Two-tier authorization intent; defaults to `override-artifact`. */ intent?: MetadataWriteIntent; /** * Software-package id to bind this metadata row to (`sys_metadata.package_id`). * Set when authoring inside a Studio package workspace. On create the row is * stamped with this id; on update an existing non-null binding is preserved * (never silently re-bound). Omit/undefined for env-local overlays. */ packageId?: string | null; } interface PutResult { /** New content hash assigned to the spec. */ version: string; /** Sequence number of the emitted MetadataEvent. */ seq: number; /** The committed item (canonicalised). */ item: MetadataItem; } interface DeleteOptions { parentVersion: string; /** Identity of the writer; `null` = system-initiated. See {@link PutOptions.actor}. */ actor: string | null; message?: string; source?: string; /** Two-tier authorization intent; defaults to `override-artifact`. */ intent?: MetadataWriteIntent; } interface DeleteResult { seq: number; } interface ListFilter { org?: string; type?: MetadataType; /** Substring match on `name`; case-sensitive. */ nameContains?: string; /** Pagination cursor; opaque string from a previous response. */ cursor?: string; /** Page size; implementations may clamp. */ limit?: number; } interface WatchFilter { org?: string; type?: MetadataType; /** When omitted, match all names within the scope. */ name?: string; } interface HistoryOptions { /** Lower bound (exclusive) for pagination. */ sinceSeq?: number; limit?: number; } /** * Typed errors thrown by Repository implementations. Implementations must * use these exact classes (or subclasses) so callers can `instanceof` * across package boundaries. */ declare class MetadataError extends Error { readonly code: string; constructor(code: string, message: string); } /** * Thrown when a `put` or `delete` operation's `parentVersion` does not * match the current HEAD. Maps to HTTP 412 Precondition Failed. */ declare class ConflictError extends MetadataError { readonly ref: MetaRef; readonly expectedParent: string | null; readonly actualHead: string | null; constructor(ref: MetaRef, expectedParent: string | null, actualHead: string | null); } /** Thrown when a read targets a missing item. Maps to HTTP 404. */ declare class NotFoundError extends MetadataError { readonly ref: MetaRef; constructor(ref: MetaRef); } /** * Thrown when a `put`'s spec fails Zod validation against the canonical * schema for the metadata type. Maps to HTTP 422. */ declare class SchemaValidationError extends MetadataError { readonly ref: MetaRef; readonly issues: unknown; constructor(ref: MetaRef, issues: unknown); } /** Thrown for parent_branch / fork / merge edge cases. */ declare class BranchError extends MetadataError { constructor(message: string); } /** Stable JSON serialisation. See module-level doc for guarantees. */ declare function canonicalize(value: unknown): string; /** * Compute the canonical sha256 hash of a spec, returned as * `"sha256:<64-hex>"`. Equal hashes imply equal canonical forms. */ declare function hashSpec(value: unknown): string; /** * The `MetadataRepository` interface — single point of pluggability for * the metadata storage backend. See ADR-0008 §2.6. * * Implementations: * * - `InMemoryRepository` (this package, for tests & edge) * - `FileSystemRepository` (`@objectstack/metadata`) * - `LayeredRepository` (`@objectstack/metadata`) * - `PostgresRepository` (`@objectstack/metadata-postgres`, M1) * * Implementation contract — what every backend MUST guarantee: * * 1. **Atomic put.** A successful `put()` either fully applies (item * visible to subsequent `get` AND an event present in the log) or * does not apply at all. No half-states. * 2. **Monotonic seq per org.** `seq` is strictly increasing within * `org`. Different orgs have independent sequences. (Repositories * scoped to a single org may treat the entire repo as one log.) * 3. **Optimistic locking.** `put` and `delete` throw `ConflictError` * when `parentVersion` does not match the current HEAD. * 4. **Canonical hashing.** `item.hash === hashSpec(item.body)` — always. * 5. **Event ordering.** Subscribers to `watch()` receive events in * monotonically-increasing `seq` order with no gaps. * 6. **Resumability, and where it stops.** `watch(_, since)` called with a * NUMBER MUST replay all events with `seq > since` before delivering live * events. Called with NO `since`, `watch()` owes **live events only** — * the events that commit after the subscription is established; an * implementation MAY additionally deliver events that had already * committed, but a caller MUST NOT rely on it, and a caller that needs the * already-committed prefix MUST pass a numeric `since` (or read * `history()`). Neither form may deliver the same `seq` twice. * * The second sentence is written down because it was load-bearing while * unwritten. Invariant 6 spoke only of `seq > since`, and with no `since` * there is no such set — so "no `since` replays everything" existed only * as `InMemoryRepository`'s implementation, and the shared contract suite * silently leaned on it. Two of the three implementations shipped today do * replay the whole matching log on a bare `watch(filter)` * (`InMemoryRepository`, `FileSystemRepository`); `SysMetadataRepository` * delivers live events only. That spread is exactly why this is a MAY and * not a MUST in either direction: forbidding the replay would break two * implementations and the consumers that lean on them, requiring it would * flood every `MetadataManager.setRepository()` / `MetadataCache.start()` * — both of which subscribe with no `since` — with the org's entire * history at attach time. What a consumer may *rely* on is the floor, and * the floor is now stated instead of inherited from whichever * implementation was read first. * 7. **Tombstones, not holes.** `delete` produces a `delete` event; * `get` returns null but `history` still shows the lineage. * 8. **Shutdown terminates; it does not emit.** An implementation that offers * a repository-level shutdown (`close()`) MUST end every live `watch()` * iterator: a `next()` parked at that moment settles with `done: true` and * no value, and every later `next()` does the same. That is the identical * observation the consumer's own `iterator.return()` produces, deliberately * — so no consumer has to tell "the repository shut down under me" apart * from "I broke my own loop". Events still queued or unreplayed at that * moment MAY be dropped, on both paths alike. * * **Shutdown MUST NOT be delivered AS an event.** Written as a MUST NOT * because it was tried, and both of its halves were measured (#11021). A * synthetic "we are closing" event is subject to the very filters `watch()` * applies to real ones, so the subscriptions that most need draining are * exactly the ones that drop it: any non-empty `filter` rejects a ref * invented to belong to no org, and any numeric `since` rejects a seq * invented to precede every real one. Those consumers then wait forever, * because the same shutdown unsubscribes them. Meanwhile a consumer whose * filter happens to admit it is not rescued either — it reads a real * metadata change for a ref that never existed (invalidating caches and * re-emitting downstream), and its iterator hangs on the *next* pull * regardless, because delivering an event has never ended one. * * Stated conditionally because `close()` is not on the interface below; * it is offered by some implementations and not others. Where it is * offered, this is what it owes. Measured across today's three, and there * are **no declared exceptions**: `SysMetadataRepository` conforms (#11021); * `FileSystemRepository` conforms (#11127 — its `close()` used to retire * the filesystem watcher and the resync sweep without ever reaching its * event broker, leaving a parked iterator parked for every subscription * shape, `watch({})` included; it now runs each subscription's terminator); * `InMemoryRepository` offers no repository-level shutdown at all, so its * iterators end only through `return()`. * * A new implementation that offers `close()` joins that list or it does not * conform — this row carries the measurement, so an implementation added * without one is the omission, not an exception. */ interface MetadataRepository { /** Read HEAD or a pinned version. Returns null if absent. */ get(ref: MetaRef): Promise; /** * Resolve a historical version by content hash (ADR-0009). * * Returns the `MetadataItem` whose canonical sha256 equals `hash` * for the given ref, or `null` if no such version is recorded. * * Implementations MUST search history (not just HEAD) so that * `executionPinned` types remain resolvable through definition * upgrades. For non-`executionPinned` types, implementations MAY * return `null` if they have GC'd the corresponding history row. */ getByHash(ref: MetaRef, hash: string): Promise; /** * Write a new version. Atomic. * @throws ConflictError if `parentVersion` does not match HEAD. * @throws SchemaValidationError if `spec` fails Zod normalisation. */ put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise; /** * Soft-delete (tombstone). `parentVersion` is required. * @throws ConflictError on parent mismatch. */ delete(ref: MetaRef, opts: DeleteOptions): Promise; /** Enumerate items matching a filter. Implementations may stream. */ list(filter: ListFilter): AsyncIterable; /** Per-item history; events in monotonic `seq` order. */ history(ref: MetaRef, opts?: HistoryOptions): AsyncIterable; /** * Live event stream. The iterator MUST: * * - When `since` is a number: replay all events with `seq > since` * before yielding any new event. * - When `since` is omitted: deliver live events only — the events that * commit after this subscription is established. Events that had * already committed MAY also be delivered, but callers MUST NOT rely * on it; a caller that needs them passes a numeric `since` or reads * `history()`. See invariant 6. * - Stay open until the consumer breaks the loop — or until the * repository shuts down under it, where an implementation offers a * `close()`. Both end the stream the same way: `done: true`, no value, * never a synthetic event standing in for shutdown. See invariant 8. * - Survive transient backend disconnects (implementation's choice * how to resume — Postgres LISTEN reconnect, JSONL tail, etc.). */ watch(filter: WatchFilter, since?: number): AsyncIterable; } /** * Sentinel symbol used by `LayeredRepository` (M0 PR-5) to label which * underlying layer emitted an event. Defined here so the contract is * shared. */ declare const LAYER_SOURCE: unique symbol; /** * `InMemoryRepository` — reference implementation of `MetadataRepository` * backed by plain JS Maps. Used by: * * - tests (parameterized contract-test suite) * - edge / serverless runtimes (no FS, no DB) * - `LayeredRepository` fallbacks * * State model * ─────────── * items : refKey → MetadataItem (current head) * logs : org → MetadataEvent[] (append-only, monotonic per org) * seqs : org → number * * `watch()` is implemented over a simple subscriber list. Each subscriber * receives a deep-copy of the event so they cannot mutate the log. */ interface InMemoryRepositoryOptions { /** Optional clock injection for deterministic tests. Default: Date.now. */ now?: () => Date; } declare class InMemoryRepository implements MetadataRepository { private readonly items; /** Per-org event log. */ private readonly logs; /** Next seq per org. */ private readonly seqs; private readonly subscribers; private readonly now; constructor(opts?: InMemoryRepositoryOptions); get(ref: MetaRef): Promise; getByHash(ref: MetaRef, hash: string): Promise; put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise; delete(ref: MetaRef, opts: DeleteOptions): Promise; list(filter: ListFilter): AsyncIterable; history(ref: MetaRef, opts?: HistoryOptions): AsyncIterable; watch(filter: WatchFilter, since?: number): AsyncIterable; private bumpSeq; private appendEvent; private orgKeysMatching; } /** * `MetadataCache` — bounded, event-invalidated LRU sitting in front of a * `MetadataRepository`. See ADR-0008 §2.5. * * Design contract * ─────────────── * * 1. **Lazy fill.** Cache entries are only created on first read miss. * No bulk preload — that would defeat the whole point of being * bounded. * 2. **Event-driven invalidation.** The cache subscribes to * `repo.watch({...})` and drops or replaces affected entries * whenever the repository emits an event. Stale reads are bounded * by the event-propagation latency of the underlying repo. * 3. **Bounded.** Both `maxEntries` and `maxBytes` are enforced; LRU * eviction happens on `set()` when either limit is exceeded. * 4. **Coherent under races.** Concurrent `get()`s for the same key * coalesce onto a single backend fetch (the "thundering herd" * fix). If an invalidation event arrives during an in-flight * fetch, the resulting value is discarded — the next read fetches * fresh. * 5. **Negative caching.** A miss (repo returned `null`) is also * cached, with a smaller TTL semantics — it stays until an event * for that ref arrives. This makes "does X exist?" cheap during * tight loops without compromising correctness. */ interface MetadataCacheOptions { /** Maximum number of entries to keep. Default: 1024. */ maxEntries?: number; /** * Maximum approximate body size in bytes. Default: 8 MiB. Each entry's * size is estimated from `JSON.stringify(item.body).length`. */ maxBytes?: number; /** * Watch filter. Only events matching this filter invalidate cache * entries. Default: no filter (all events). */ watchFilter?: WatchFilter; } interface CacheStats { entries: number; bytes: number; hits: number; misses: number; invalidations: number; /** Reads that arrived while a fetch was already in-flight for the same key. */ coalesced: number; } declare class MetadataCache { private readonly repo; private readonly maxEntries; private readonly maxBytes; private readonly watchFilter; /** LRU is implemented via insertion-order Map; touch on get/set. */ private readonly entries; private bytes; /** De-duplicate concurrent fetches for the same key. */ private readonly inflight; /** * Generation counter incremented on every invalidation. Each in-flight * fetch captures the gen at start; if the gen changes before it * resolves, the result is NOT cached. */ private readonly fetchGen; private watchIterator; private watchClosed; private watchLoop; private readonly stats; constructor(repo: MetadataRepository, opts?: MetadataCacheOptions); /** * Start the background watch subscription. Idempotent; safe to call * multiple times. Caller is responsible for calling `close()` when * the cache is no longer needed. */ start(): void; /** Tear down the watch subscription and clear in-flight tracking. */ close(): Promise; /** Read with cache. Coalesces concurrent reads for the same key. */ get(ref: MetaRef): Promise; /** Drop a single entry by ref. */ invalidate(ref: MetaRef): void; /** Drop the entire cache (e.g. on a reset). */ clear(): void; getStats(): Readonly; private applyEvent; private cacheSet; private evictIfNeeded; private bumpGen; } /** * `LayeredRepository` — composes N child `MetadataRepository`s into a * single read-through stack. See ADR-0008 §10 PR-5. * * Read semantics * ────────────── * - `get(ref)` walks the layers top-to-bottom; first non-null wins. * - `list()` deduplicates by `refKey(ref)`, preferring the top layer. * - `history()` and `watch()` merge events from all layers, each * tagged with the source layer label in `evt.source` * (`