import { SmrtCollection } from '@happyvertical/smrt-core'; import { type RegisteredFieldInfo } from '../field-definitions.js'; import { FieldUsageCounter } from '../models/FieldUsageCounter.js'; import type { FieldUsageReportEntry, FieldUsageReportResult } from '../types.js'; /** * Deviation comparison: strict equality, then a JSON round-trip so structured * and Date-shaped values compare by serialization. Mirrors the browser-side * `fieldUsageValuesEqual` (the packaging boundary bars value imports across the * subpath split — the permission-slug precedent); a node test pins the two * behaviours equal. */ export declare function usageValuesEqual(a: unknown, b: unknown): boolean; /** * Per-call bound on reported entries — the rate rail for the fire-and-forget * ingestion action (the resolveBatch precedent). The browser deliberately * does not impose a duplicate limit: every report is server-bounded here. */ export declare const MAX_USAGE_REPORT_ENTRIES = 100; /** * Collection surface for {@link FieldUsageCounter} plus the batched usage * ingestion action (#2051). * * `reportUsage` is a custom collection-scoped action (the resolveBatch * mechanism — single-segment path, so the generated SvelteKit transport AND * core's runtime `APIGenerator` both dispatch it). Everything else is CLOSED: * no generated CRUD, no CLI, no MCP — counters are written only through this * action and read only by trusted server-side code (the learning jobs). */ export declare class FieldUsageCounterCollection extends SmrtCollection { static readonly _itemClass: typeof FieldUsageCounter; /** * Record a batch of field submissions into the current UTC-day counters. * * Identity is AMBIENT-ONLY (the resolveBatch posture): the tenant AND the * user come exclusively from the tenant context established by the app's * auth hook — the request body cannot attribute usage to another tenant or * user. Both are REQUIRED and the call fails closed without them (#2047 * round-3 posture): * * - No ambient tenant ⇒ the report is unattributable. * - No ambient USER ⇒ the caller is an unauthenticated principal (a * deployment that resolves the tenant from host/header alone). Such * callers could otherwise inflate counts and histograms without bound * (the batch cap is per call), and `distinctUsers` — the promote signal — * is meaningless without a principal. Consequence, accepted deliberately: * ANONYMOUS/PUBLIC FORMS DO NOT CONTRIBUTE USAGE. The learning loop is * for authenticated org users. * * Any authenticated in-tenant principal may report (no manage slug needed). * A durable receipt admits at most one `(tenant, user, object, field, UTC * day)` contribution, and the batch stays bounded per call * ({@link MAX_USAGE_REPORT_ENTRIES}). * * Server-side derivation (the client is never the authority): * - Fields are validated against the live `ObjectRegistry`; unknown or * non-usage-addressable entries (system, relationship pseudo-fields, STI * meta storage, transient) are DROPPED and counted, never failing the * batch (stale clients after a redeploy are expected). * - Sensitivity comes from BOTH `field.sensitive` and `field._meta.sensitive` * (and `readPermission` in both places): such fields are recorded * COUNT-ONLY — their values are never persisted anywhere in usage data, * default-matching or not. * - DEVIATION (`setCount`, distinct users) is decided by comparing the * submitted value against the default RESOLVED HERE for the calling * identity — never against a client claim. Every accepted entry also * increments `submissionCount`, the dominance denominator. * - Values are histogrammed only for low-cardinality field types * (`boolean`, `foreignKey`, `crossPackageRef`) with type-checked samples; * free text is never histogrammed. * * Bounded trust in `matchedDefault`: an entry may omit `value` entirely * (`collectFieldUsageEntries({ includeValues: false })` — apps that prefer * no value transit). Only then is the entry's `matchedDefault` flag read, to * decide the deviation bit alone. It can never create a histogram entry, * never bypass the count-only rail, and never manufacture distinct users * beyond the caller's own id. The once-per-field/day receipt means a lying * client gets only one such contribution, so repeated POSTs cannot inflate * thresholds or steer a `default` suggestion. */ reportUsage(options?: { entries?: FieldUsageReportEntry[]; }): Promise; /** * The defaults the CALLER's forms would have prefilled — the deviation * authority. Resolved for the ambient `(tenant, user)` identity (the * resolver's own TTL cache keeps repeat batches cheap). * * Ingestion is fire-and-forget, so a stored-layer read failure degrades to * the CODE-SEED-only resolution (no db) rather than failing the request; * `null` (both attempts failed) makes the caller drop the group. */ private resolveDefaultsForCaller; /** * Counter rows within a period window (inclusive bounds, lexicographic ISO * day comparison), optionally restricted to one tenant — the read the * suggestion-generation job consumes. */ listWindow(options: { fromPeriod: string; toPeriod?: string; tenantId?: string | null; }): Promise; /** * Idempotent read-modify-write merge into the deterministic day bucket. * Counters are approximate by design: concurrent cross-process merges may * lose an increment (documented on the model), while the deterministic id * plus the natural-key unique index keep concurrent creates converging on * one row. */ private mergeIntoBucket; } /** * Deterministic bucket row id (the TenantUsageMetric `recordUsage` precedent): * SHA-256 over the natural key, formatted as a v5-style UUID so the id column * stays native UUID on PostgreSQL/DuckDB. */ export declare function fieldUsageCounterId(tenantId: string, objectRef: string, fieldName: string, period: string): Promise; /** Deterministic receipt id for the durable once-per-day contribution rule. */ export declare function fieldUsageReportReceiptId(tenantId: string, userId: string, objectRef: string, fieldName: string, period: string): Promise; /** * Histogram eligibility (#2051 pin): ONLY low-cardinality field types — * `boolean` and reference ids (`foreignKey` / `crossPackageRef`). Free text * is NEVER histogrammed, even when non-sensitive (PII risk); numeric, * datetime, and json fields are count-only too. */ export declare function isHistogramEligibleField(field: RegisteredFieldInfo): boolean; /** * Serialize one sample into a histogram key, type-checked against the field: * booleans must be real booleans (`'true'` / `'false'` keys); reference ids * must be STORABLE ids for the field — `isStorableReferenceId` applies the same * native-UUID / `idType: 'text'` rule stored defaults are held to, within the * bounded key length. Anything else is skipped (the submission still counts — * count-only for that sample). * * The reference check is load-bearing, not cosmetic: recording an unstorable * id would let an authenticated caller poison a histogram, win dominance with * it, and produce a `default` suggestion whose write is then rejected by the * same rule — turning a bad sample into a stuck generation candidate. */ export declare function serializeHistogramSample(field: RegisteredFieldInfo, value: unknown): string | null; /** Decode a histogram key back into the typed value it was recorded from. */ export declare function decodeHistogramKey(field: RegisteredFieldInfo, key: string): unknown; //# sourceMappingURL=FieldUsageCounterCollection.d.ts.map