import { SmrtObject, type SmrtObjectOptions } from '@happyvertical/smrt-core'; import { type FieldPolicySuggestionData, type FieldPolicySuggestionKind, type FieldPolicySuggestionStatus } from '../types.js'; /** * `activeKey` sentinel for the single ACTIVE (pending) suggestion per * `(objectRef, fieldName, tenantId, kind)`. Settled rows key themselves by id, * so history never competes for the slot. */ export declare const ACTIVE_SUGGESTION_KEY = "active"; export interface FieldPolicySuggestionOptions extends SmrtObjectOptions { objectRef?: string; fieldName?: string; tenantId?: string; kind?: FieldPolicySuggestionKind; proposedValue?: string | null; evidence?: string; status?: FieldPolicySuggestionStatus; cooldownUntil?: Date | null; decidedBy?: string | null; decidedAt?: Date | null; } /** * A pending, human-reviewable field-policy improvement proposed from real * usage (epic #2045, issue #2051): promote a field to the `basic` tier, or * seed an org default with the dominant observed value. * * Suggestion-first by design — a row DOES NOTHING until a * `fields.policy.manage` holder accepts it, and acceptance writes the * org-scope {@link ../models/FieldPolicy.FieldPolicy} row through NORMAL * validation (registry check, type check, security rail, required-field * invariant, ownership + permission split). Dismissing sets a cool-down that * suppresses regeneration of the same suggestion. * * **One ACTIVE suggestion per identity, structurally** (not merely by a * check-then-insert): {@link activeKey} is a computed column holding the * sentinel {@link ACTIVE_SUGGESTION_KEY} while the row is `pending` and the * row's own id once it settles, and it participates in `conflictColumns`. The * unique index therefore admits at most ONE pending row per * `(objectRef, fieldName, tenantId, kind)` while every settled row keys * itself — so two overlapping generation runs (e.g. a global and a * tenant-specific schedule) UPSERT onto the same row instead of duplicating, * with no transaction spanning their reads. It is the `FieldPolicy.scopeKey` * trick applied to a lifecycle slot. On settle the column flips to the row's * id, which frees the slot for a post-cool-down regeneration while keeping the * dismissed/accepted history (and its id) intact — core conflicts a persisted * row on its primary key (#1472), so the flip is a plain UPDATE. * * #1885 seam: this substrate is fully independent of personas' * `DirectiveProposal` review queue (no shared producer discriminator — * deliberately out of scope). Tenant learning agents MAY create * FieldPolicySuggestion rows through this model's normal validation; the * reviewed `fields.policy.manage` acceptance gate is unchanged by who * proposed. */ export declare class FieldPolicySuggestion extends SmrtObject { /** Qualified class name of the target object (`@package/name:ClassName`). */ objectRef: string; /** Field name on the target object (validated against the registry). */ fieldName: string; /** Owning tenant (required — suggestions always target one org). */ tenantId?: string; /** What the suggestion proposes ('promote' | 'default'). */ kind: FieldPolicySuggestionKind; /** * JSON-encoded proposed default (`kind: 'default'` only) — the exact * encoding `FieldPolicy.defaultValue` stores, so acceptance passes it * through unchanged. NULL for `promote`. */ proposedValue: string | null; /** * Human-readable evidence as a JSON string: a `summary` sentence plus the * structured window/threshold numbers behind it (see * `buildFieldUsageEvidence`). */ evidence: string; /** Lifecycle status ('pending' | 'accepted' | 'dismissed'). */ status: FieldPolicySuggestionStatus; /** * Computed lifecycle-slot key, set in `save()`: {@link ACTIVE_SUGGESTION_KEY} * while `pending`, else the row's own id. It exists ONLY to make the * `conflictColumns` unique index express "at most one ACTIVE suggestion per * identity, unlimited settled history" (the `FieldPolicy.scopeKey` * precedent) — never read it for logic; `status` owns that. */ activeKey: string; /** * Until this instant, a dismissed suggestion suppresses regeneration of the * same `(objectRef, fieldName, tenantId, kind)` suggestion. NULL until * dismissed. */ cooldownUntil: Date | null; /** Who accepted/dismissed (audit attribution, #2050); not validated. */ decidedBy: string | null; /** When the suggestion was accepted/dismissed. */ decidedAt: Date | null; constructor(options?: FieldPolicySuggestionOptions); /** Parse the stored evidence object (guarded; junk parses as empty). */ getEvidence(): Record; /** Serialize an evidence object into the stored JSON string. */ setEvidence(evidence: Record): void; /** Parse the proposed value; `undefined` when none is stored. */ getProposedValue(): unknown; /** Serialized row shape for the collection actions. */ toSuggestionData(): FieldPolicySuggestionData; save(): Promise; /** * Recompute the lifecycle-slot key: the shared sentinel while pending (so * the unique index admits exactly one), the row's own id once settled (so * history never competes for the slot and the freed slot allows a * post-cool-down regeneration). A settled row that has not been persisted * yet is assigned its id here — the key must be unique from the first write. */ private applyActiveKey; delete(): Promise; /** * The "normal validation" the #1885 seam promises producers: registry-known * field, policy-addressable, never sensitive/read-permission-gated/transient * (those fields are count-only in usage data and get no suggestions), valid * kind/status, and for `default` suggestions a JSON proposed value that * type-checks against the manifest field type. */ private validateFieldPolicySuggestion; /** * Tenant write boundary (the FieldPolicy posture): inside a non-bypass * tenant context a caller may only touch its own tenant's suggestions — * checked against BOTH the in-memory scope and, for persisted rows, the * PERSISTED tenant (a foreign row cannot be re-scoped into the caller's * tenant). Trusted execution (no context / bypass) is exempt — that is what * lets the scheduled generation job and platform flows operate. */ private assertRowOwnedByAmbientContext; } //# sourceMappingURL=FieldPolicySuggestion.d.ts.map