@happyvertical/smrt-core 0.51.27 → 0.51.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +20 -0
- package/README.md +47 -0
- package/agents/build-knowledge.md +16 -0
- package/agents/change-feed.md +95 -4
- package/agents/change-signals.md +6 -1
- package/agents/run-once.md +268 -0
- package/agents/schema-paths.md +17 -5
- package/dist/browser.js +4 -3
- package/dist/change-feed-authz.d.ts +141 -0
- package/dist/change-feed-authz.d.ts.map +1 -0
- package/dist/change-feed-authz.js +260 -0
- package/dist/change-feed-authz.js.map +1 -0
- package/dist/change-feed.d.ts +15 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +4 -0
- package/dist/change-feed.js.map +1 -1
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +36 -1
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +13 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/events-route.d.ts +35 -4
- package/dist/generators/events-route.d.ts.map +1 -1
- package/dist/generators/events-route.js +78 -14
- package/dist/generators/events-route.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +1 -1
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +6 -3
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +11 -5
- package/dist/migrations/differ.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +21 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/types.d.ts +3 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/run-once.d.ts +80 -0
- package/dist/run-once.d.ts.map +1 -0
- package/dist/run-once.js +311 -0
- package/dist/run-once.js.map +1 -0
- package/dist/smrt-knowledge.json +20 -14
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +53 -4
- package/dist/system/retention.d.ts.map +1 -1
- package/dist/system/retention.js +67 -14
- package/dist/system/retention.js.map +1 -1
- package/dist/system/schema.d.ts +33 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +60 -3
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +21 -5
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts.map +1 -1
- package/dist/vite-plugin/events-route.js +9 -0
- package/dist/vite-plugin/events-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +14 -0
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/worker-registration.d.ts +34 -0
- package/dist/vite-plugin/worker-registration.d.ts.map +1 -0
- package/dist/vite-plugin/worker-registration.js +108 -0
- package/dist/vite-plugin/worker-registration.js.map +1 -0
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { changeSignalSubscriberCount, subscribeToChangeSignals, tryReserveChangeSignalSubscriberSlot } from "../change-signals.js";
|
|
2
2
|
import { resolveDispatchTenantScope } from "../dispatch/tenant-resolver.js";
|
|
3
3
|
import { ensureChangeFeedTable, getChangesSince } from "../change-feed.js";
|
|
4
|
+
import { filterVisibleChangeFeedEntries, hasChangeFeedEntryVisibilityHook, hasChangeFeedTableAuthorizerHook, isChangeFeedDenyAll, isChangeFeedEntryVisible, resolveAuthorizedChangeFeedTables } from "../change-feed-authz.js";
|
|
4
5
|
import { resolveChangesDb } from "./changes-route.js";
|
|
5
6
|
import { createLogger } from "@happyvertical/logger";
|
|
6
7
|
//#region src/generators/events-route.ts
|
|
@@ -131,14 +132,31 @@ function encodeSseComment(text) {
|
|
|
131
132
|
* Build the SSE body stream for an `_events` connection.
|
|
132
133
|
*
|
|
133
134
|
* `start(controller)`:
|
|
135
|
+
* 0. Resolve the change-feed table authorization (#3020), if a hook is
|
|
136
|
+
* registered, and capture it for the life of the connection — exactly
|
|
137
|
+
* like `tenantScope`, and for the same reason (delivery runs outside this
|
|
138
|
+
* call's context). Skipped entirely (no `await`) when no table
|
|
139
|
+
* authorizer is registered, so the default connection setup stays
|
|
140
|
+
* synchronous up to the subscribe call below. When a hook IS registered
|
|
141
|
+
* and the connection has no explicit catch-up `cursor` (live-forward-only),
|
|
142
|
+
* the feed's current head is captured BEFORE this await as a gap-fill
|
|
143
|
+
* cursor: `unsubscribe` below only sees signals published after it
|
|
144
|
+
* attaches, so a write that commits (and signals) while the authorizer is
|
|
145
|
+
* pending would otherwise be lost forever, not merely delayed — a
|
|
146
|
+
* supplied `cursor` doesn't need this because its own catch-up phase (c)
|
|
147
|
+
* already starts from a value captured before this same await.
|
|
134
148
|
* a. **Subscribe FIRST**, before catch-up. Subscribing before the catch-up
|
|
135
149
|
* read closes the gap window: a write landing between subscribe and the
|
|
136
150
|
* catch-up read is delivered twice (once live, once in the replay) — which
|
|
137
|
-
* is safe, since the client dedupes by the SSE `id:`/seq.
|
|
151
|
+
* is safe, since the client dedupes by the SSE `id:`/seq. Each live signal
|
|
152
|
+
* is additionally checked against the captured table authorization and,
|
|
153
|
+
* if registered, the row-level `isChangeFeedEntryVisible` hook — denied
|
|
154
|
+
* signals are silently dropped, never enqueued.
|
|
138
155
|
* b. Write the `retry:` reconnection hint.
|
|
139
|
-
* c. If a cursor was supplied,
|
|
140
|
-
*
|
|
141
|
-
*
|
|
156
|
+
* c. If a cursor was supplied, OR step 0 captured a gap-fill cursor, replay
|
|
157
|
+
* changes after it (paging until exhausted, applying the same table/row
|
|
158
|
+
* authorization as live delivery); on `resyncRequired`, emit
|
|
159
|
+
* `event: resync` at the server's fresh horizon.
|
|
142
160
|
* d. Start the heartbeat interval.
|
|
143
161
|
*
|
|
144
162
|
* `cancel()` tears down on disconnect: clears the heartbeat and unsubscribes,
|
|
@@ -146,7 +164,11 @@ function encodeSseComment(text) {
|
|
|
146
164
|
* controller and keep the cross-replica listener refcount above 0).
|
|
147
165
|
*/
|
|
148
166
|
function buildChangeEventStream(db, options) {
|
|
149
|
-
const { cursor, tenantScope, manifestHash } = options;
|
|
167
|
+
const { cursor, tenantScope, manifestHash, locals, request } = options;
|
|
168
|
+
const requestContext = {
|
|
169
|
+
locals,
|
|
170
|
+
request
|
|
171
|
+
};
|
|
150
172
|
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
151
173
|
let unsubscribe = null;
|
|
152
174
|
let releaseSubscriberSlot = options.releaseSubscriberSlot ?? null;
|
|
@@ -170,14 +192,50 @@ function buildChangeEventStream(db, options) {
|
|
|
170
192
|
};
|
|
171
193
|
return new ReadableStream({
|
|
172
194
|
async start(controller) {
|
|
195
|
+
let allowedTables;
|
|
196
|
+
let gapFillCursor = null;
|
|
197
|
+
if (hasChangeFeedTableAuthorizerHook()) {
|
|
198
|
+
if (cursor == null) {
|
|
199
|
+
let headPage;
|
|
200
|
+
try {
|
|
201
|
+
headPage = await getChangesSince(db, {
|
|
202
|
+
since: 0,
|
|
203
|
+
denyAllTables: true
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
teardown();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
gapFillCursor = headPage.resyncRequired ? headPage.resyncCursor ?? 0 : headPage.cursor;
|
|
210
|
+
}
|
|
211
|
+
allowedTables = await resolveAuthorizedChangeFeedTables(requestContext, void 0);
|
|
212
|
+
if (closed) return;
|
|
213
|
+
}
|
|
214
|
+
const allowedTableSet = allowedTables ? new Set(allowedTables) : null;
|
|
215
|
+
const catchupDenyAllTables = isChangeFeedDenyAll(allowedTables);
|
|
216
|
+
const rowVisibilityActive = hasChangeFeedEntryVisibilityHook();
|
|
217
|
+
let deliveryQueue = Promise.resolve();
|
|
173
218
|
unsubscribe = subscribeToChangeSignals(db, (sig) => {
|
|
174
219
|
if (closed) return;
|
|
175
220
|
if (!signalVisibleToTenant(sig, tenantScope)) return;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
221
|
+
if (allowedTableSet && !allowedTableSet.has(sig.table)) return;
|
|
222
|
+
if (!rowVisibilityActive) {
|
|
223
|
+
try {
|
|
224
|
+
controller.enqueue(encodeSseEvent(sig));
|
|
225
|
+
} catch {
|
|
226
|
+
teardown();
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
180
229
|
}
|
|
230
|
+
deliveryQueue = deliveryQueue.then(async () => {
|
|
231
|
+
if (closed) return;
|
|
232
|
+
if (!await isChangeFeedEntryVisible(requestContext, sig)) return;
|
|
233
|
+
try {
|
|
234
|
+
controller.enqueue(encodeSseEvent(sig));
|
|
235
|
+
} catch {
|
|
236
|
+
teardown();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
181
239
|
});
|
|
182
240
|
if (releaseSubscriberSlot) {
|
|
183
241
|
releaseSubscriberSlot();
|
|
@@ -185,20 +243,24 @@ function buildChangeEventStream(db, options) {
|
|
|
185
243
|
}
|
|
186
244
|
controller.enqueue(encoder.encode("retry: 3000\n\n"));
|
|
187
245
|
if (manifestHash !== void 0) controller.enqueue(encodeSseManifestEvent(manifestHash));
|
|
188
|
-
|
|
246
|
+
const catchupCursor = cursor ?? gapFillCursor;
|
|
247
|
+
if (catchupCursor != null) try {
|
|
189
248
|
const catchupTenantId = tenantScope.enforced ? tenantScope.tenantId : void 0;
|
|
190
|
-
let since =
|
|
249
|
+
let since = catchupCursor;
|
|
191
250
|
for (;;) {
|
|
192
251
|
const page = await getChangesSince(db, {
|
|
193
252
|
since,
|
|
194
|
-
tenantId: catchupTenantId
|
|
253
|
+
tenantId: catchupTenantId,
|
|
254
|
+
tables: catchupDenyAllTables ? void 0 : allowedTables,
|
|
255
|
+
denyAllTables: catchupDenyAllTables
|
|
195
256
|
});
|
|
196
257
|
if (page.resyncRequired) {
|
|
197
258
|
const resyncCursor = typeof page.resyncCursor === "number" && Number.isFinite(page.resyncCursor) && page.resyncCursor >= 0 ? page.resyncCursor : since;
|
|
198
259
|
controller.enqueue(encodeSseResyncEvent(resyncCursor));
|
|
199
260
|
break;
|
|
200
261
|
}
|
|
201
|
-
|
|
262
|
+
const visibleChanges = await filterVisibleChangeFeedEntries(requestContext, page.changes);
|
|
263
|
+
for (const change of visibleChanges) controller.enqueue(encodeSseEvent({
|
|
202
264
|
table: change.table,
|
|
203
265
|
operation: change.operation,
|
|
204
266
|
rowId: change.rowId,
|
|
@@ -261,7 +323,9 @@ async function handleEventsRoute(req, options) {
|
|
|
261
323
|
cursor,
|
|
262
324
|
tenantScope,
|
|
263
325
|
manifestHash: options.manifestHash,
|
|
264
|
-
releaseSubscriberSlot
|
|
326
|
+
releaseSubscriberSlot,
|
|
327
|
+
locals: void 0,
|
|
328
|
+
request: authResult
|
|
265
329
|
}), {
|
|
266
330
|
status: 200,
|
|
267
331
|
headers: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events-route.js","names":[],"sources":["../../src/generators/events-route.ts"],"sourcesContent":["/**\n * Generated `_events` SSE route — live change signals (issue #1763, SERVER\n * half; parent PRD #1755).\n *\n * Handles `GET {basePath}/_events` in the REST generator: an auth-guarded,\n * tenant-scoped Server-Sent-Events stream of coarse change signals ({table,\n * operation, rowId, tenantId} + a `seq` cursor in the SSE `id:` field). It is\n * the push companion to the pull-based `_changes` route (#1758): a subscriber\n * reacts to a signal by re-reading through the authorized collection routes,\n * so **no row payload ever crosses this channel** — authorization stays\n * entirely on the read path.\n *\n * The stream lifecycle (subscribe, catch-up replay, heartbeat, teardown) lives\n * in {@link buildChangeEventStream} so it is written and tested once; both the\n * REST generator here and the generated SvelteKit route import it. `rest.ts`\n * only registers the path.\n *\n * Contract:\n * - **Fail-closed auth** (identical to `_changes`, #1540 posture): no\n * `authMiddleware` configured → 401; the middleware may return a Response to\n * short-circuit (e.g. 403). The feed spans every table, so per-model\n * `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scope is captured ONCE at connection open** — signal delivery\n * happens from a different async context (the writer's afterSave, possibly\n * another request or replica) with no tenant ALS active, so the filter must\n * be the value resolved at subscribe time, not re-resolved per signal.\n * - **Cross-origin is opt-in** (#1861): `rest.ts` now wraps `_events` in its\n * CORS layer. With the fail-closed default it stays same-origin only, but\n * when the generator is configured with `enableCors`, an `allowedOrigins`\n * allowlist, and `allowCredentials: true`, an allow-listed cross-origin\n * browser client can subscribe with a credentialed `EventSource`\n * (`withCredentials: true`) — the response echoes the specific origin (never\n * `*`) plus `Access-Control-Allow-Credentials: true`, so its cookies reach\n * the same fail-closed auth guard. The CORS layer only lets the cookie\n * through; it never authorizes — the auth middleware + captured tenant scope\n * are unchanged, so the read posture holds identically across origins.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ensureChangeFeedTable, getChangesSince } from '../change-feed.js';\nimport {\n type ChangeSignal,\n changeSignalSubscriberCount,\n subscribeToChangeSignals,\n tryReserveChangeSignalSubscriberSlot,\n} from '../change-signals.js';\nimport {\n type DispatchTenantScope,\n resolveDispatchTenantScope,\n} from '../dispatch/tenant-resolver.js';\nimport {\n type ChangesAuthMiddleware,\n resolveChangesDb,\n} from './changes-route.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Options for the `_events` route handler. */\nexport interface EventsRouteOptions {\n /** The generator's configured auth middleware, if any. */\n authMiddleware?: ChangesAuthMiddleware;\n /** The generator's `APIContext.db` (instance, config object, or URL string). */\n db?: unknown;\n /**\n * The build's web-collection shape digest (#1764). When supplied, the stream\n * emits it in a connection-open `manifest` event so long-lived tabs can latch\n * `updateAvailable.contract` on reconnect (#1859).\n */\n manifestHash?: string;\n /**\n * Per-process cap on active `_events` subscribers (#1860). Defaults to\n * {@link DEFAULT_EVENTS_MAX_SUBSCRIBERS}; new over-cap connections receive a\n * retryable 503 and existing subscribers are left untouched. Set to 0 for no\n * cap.\n */\n maxSubscribers?: number;\n}\n\n/**\n * Pseudo object name passed to the auth middleware for the events route, so\n * middlewares can recognize and specially authorize it (mirrors `_changes`).\n */\nexport const EVENTS_ROUTE_OBJECT_NAME = '_events';\n\n/** Default heartbeat interval (ms). Overridable via stream options. */\nexport const DEFAULT_EVENTS_HEARTBEAT_MS = 15000;\n\n/** Default per-process `_events` subscriber cap (#1860). */\nexport const DEFAULT_EVENTS_MAX_SUBSCRIBERS = 1000;\n\n/** Retry hint for over-cap `_events` connections (#1860). */\nexport const DEFAULT_EVENTS_RETRY_AFTER_SECONDS = 5;\n\nconst encoder = new TextEncoder();\n\n/** Options for {@link buildChangeEventStream}. */\nexport interface ChangeEventStreamOptions {\n /**\n * Catch-up cursor. When a non-negative number, changes after it are replayed\n * before going live; `null` means live-forward only (no catch-up).\n */\n cursor: number | null;\n /**\n * Tenant scope captured at connection open. Delivery filters against this\n * fixed value — it must NOT be re-resolved per signal (delivery runs outside\n * any tenant ALS context).\n */\n tenantScope: DispatchTenantScope;\n /** Heartbeat interval (ms). Defaults to {@link DEFAULT_EVENTS_HEARTBEAT_MS}. */\n heartbeatMs?: number;\n /**\n * Optional server manifest hash emitted once at connection open as\n * `event: manifest`. The hash carries no tenant/user data.\n */\n manifestHash?: string;\n /**\n * Reservation claimed at the route boundary before the streaming response was\n * returned. Released when the stream subscribes, or during teardown if the\n * stream never reaches `start()`.\n */\n releaseSubscriberSlot?: () => void;\n}\n\n/**\n * Normalize an `_events` subscriber cap.\n *\n * `0` means unlimited rather than reject-all, matching common limit semantics.\n * Invalid values fall back to the default operational cap.\n */\nexport function normalizeEventsMaxSubscribers(\n value: number | undefined,\n): number | null {\n if (value === undefined) return DEFAULT_EVENTS_MAX_SUBSCRIBERS;\n if (value === 0) return null;\n if (!Number.isFinite(value) || value < 0) {\n return DEFAULT_EVENTS_MAX_SUBSCRIBERS;\n }\n return Math.floor(value);\n}\n\n/** True when opening a new `_events` stream would exceed the configured cap. */\nexport function changeEventSubscribersAtCapacity(\n db: DatabaseInterface,\n maxSubscribers?: number,\n): boolean {\n const normalizedMaxSubscribers =\n normalizeEventsMaxSubscribers(maxSubscribers);\n return (\n normalizedMaxSubscribers !== null &&\n changeSignalSubscriberCount(db) >= normalizedMaxSubscribers\n );\n}\n\n/**\n * Atomically claim one `_events` subscriber slot at the route boundary.\n * Returns null when the configured cap is already reached.\n */\nexport function tryReserveChangeEventSubscriberSlot(\n db: DatabaseInterface,\n maxSubscribers?: number,\n): (() => void) | null {\n return tryReserveChangeSignalSubscriberSlot(\n db,\n normalizeEventsMaxSubscribers(maxSubscribers),\n );\n}\n\n/** Retryable over-cap response shared by REST and generated SvelteKit routes. */\nexport function eventStreamCapacityExceededResponse(): Response {\n return new Response(\n JSON.stringify({\n error: 'Live events unavailable: subscriber capacity reached',\n }),\n {\n status: 503,\n headers: {\n 'Content-Type': 'application/json',\n 'Retry-After': String(DEFAULT_EVENTS_RETRY_AFTER_SECONDS),\n },\n },\n );\n}\n\n/**\n * Whether a signal is visible to a captured tenant scope. Exact same rule as\n * `getChangesSince`'s tenantId filter, run **synchronously server-side** inside\n * the enqueue callback before any byte hits the wire:\n * - not enforced → visible.\n * - enforced, no active tenant (`tenantId === null`) → only global signals.\n * - enforced, tenant `T` → `T`'s signals plus global signals.\n */\nexport function signalVisibleToTenant(\n sig: ChangeSignal,\n scope: DispatchTenantScope,\n): boolean {\n if (!scope.enforced) return true;\n if (scope.tenantId === null) return sig.tenantId === null;\n return sig.tenantId === scope.tenantId || sig.tenantId === null;\n}\n\n/**\n * SSE frame for a change signal. The `data` JSON is EXACTLY\n * `{table, operation, rowId, tenantId}` — the `seq` lives only in the `id:`\n * field (the EventSource `Last-Event-ID` a client echoes to resume).\n */\nfunction encodeSseEvent(sig: ChangeSignal): Uint8Array {\n const data = JSON.stringify({\n table: sig.table,\n operation: sig.operation,\n rowId: sig.rowId,\n tenantId: sig.tenantId,\n });\n return encoder.encode(`id: ${sig.seq}\\nevent: change\\ndata: ${data}\\n\\n`);\n}\n\n/** SSE manifest frame emitted at connection open for live contract detection. */\nfunction encodeSseManifestEvent(manifestHash: string): Uint8Array {\n return encoder.encode(\n `event: manifest\\ndata: ${JSON.stringify({ manifestHash })}\\n\\n`,\n );\n}\n\n/** SSE resync frame. The id advances EventSource past an unservable cursor. */\nfunction encodeSseResyncEvent(cursor: number): Uint8Array {\n return encoder.encode(`id: ${cursor}\\nevent: resync\\ndata: {}\\n\\n`);\n}\n\n/** SSE comment line (used for heartbeats — ignored by EventSource). */\nfunction encodeSseComment(text: string): Uint8Array {\n return encoder.encode(`: ${text}\\n\\n`);\n}\n\n/**\n * Build the SSE body stream for an `_events` connection.\n *\n * `start(controller)`:\n * a. **Subscribe FIRST**, before catch-up. Subscribing before the catch-up\n * read closes the gap window: a write landing between subscribe and the\n * catch-up read is delivered twice (once live, once in the replay) — which\n * is safe, since the client dedupes by the SSE `id:`/seq.\n * b. Write the `retry:` reconnection hint.\n * c. If a cursor was supplied, replay changes after it (paging until\n * exhausted); on `resyncRequired`, emit `event: resync` at the server's\n * fresh horizon.\n * d. Start the heartbeat interval.\n *\n * `cancel()` tears down on disconnect: clears the heartbeat and unsubscribes,\n * so a dropped client never leaks its subscription (which would pin the dead\n * controller and keep the cross-replica listener refcount above 0).\n */\nexport function buildChangeEventStream(\n db: DatabaseInterface,\n options: ChangeEventStreamOptions,\n): ReadableStream<Uint8Array> {\n const { cursor, tenantScope, manifestHash } = options;\n const heartbeatMs = options.heartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;\n\n let unsubscribe: (() => void) | null = null;\n let releaseSubscriberSlot = options.releaseSubscriberSlot ?? null;\n let heartbeat: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n\n const teardown = () => {\n if (closed) return;\n closed = true;\n if (heartbeat) {\n clearInterval(heartbeat);\n heartbeat = null;\n }\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n if (releaseSubscriberSlot) {\n releaseSubscriberSlot();\n releaseSubscriberSlot = null;\n }\n };\n\n return new ReadableStream<Uint8Array>({\n async start(controller) {\n // (a) Subscribe FIRST, before catch-up — closes the subscribe/catch-up\n // gap window (a write in between is delivered twice; the client dedupes\n // by seq). The tenant filter uses the scope captured at open, never a\n // per-signal re-resolution.\n unsubscribe = subscribeToChangeSignals(db, (sig) => {\n if (closed) return;\n if (!signalVisibleToTenant(sig, tenantScope)) return;\n try {\n controller.enqueue(encodeSseEvent(sig));\n } catch {\n // Controller already closed (client gone before cancel fired) —\n // tear down so we stop trying to write to a dead controller.\n teardown();\n }\n });\n if (releaseSubscriberSlot) {\n releaseSubscriberSlot();\n releaseSubscriberSlot = null;\n }\n\n // (b) Reconnection hint.\n controller.enqueue(encoder.encode('retry: 3000\\n\\n'));\n // Advertise the server contract at connection open (#1859). A reconnect\n // naturally replays this frame, letting a long-lived tab learn about a\n // shape-only API deploy without a full page load.\n if (manifestHash !== undefined) {\n controller.enqueue(encodeSseManifestEvent(manifestHash));\n }\n\n // (c) Catch-up replay from the cursor, if one was supplied.\n if (cursor != null) {\n try {\n // Catch-up MUST filter by the scope captured at connection open, not\n // re-resolve the tenant via ALS at call time. start() happens to run\n // in-request today, but relying on that is fragile — and it must match\n // the live-signal filter exactly (signalVisibleToTenant): when\n // enforced, `scope.tenantId` (a tenant id → that tenant + global; null\n // → global only); when not enforced, undefined → no tenant filter.\n const catchupTenantId = tenantScope.enforced\n ? tenantScope.tenantId\n : undefined;\n let since = cursor;\n // Page until exhausted (cursor stops advancing / resync).\n for (;;) {\n const page = await getChangesSince(db, {\n since,\n tenantId: catchupTenantId,\n });\n if (page.resyncRequired) {\n const resyncCursor =\n typeof page.resyncCursor === 'number' &&\n Number.isFinite(page.resyncCursor) &&\n page.resyncCursor >= 0\n ? page.resyncCursor\n : since;\n controller.enqueue(encodeSseResyncEvent(resyncCursor));\n break;\n }\n for (const change of page.changes) {\n controller.enqueue(\n encodeSseEvent({\n table: change.table,\n operation: change.operation,\n rowId: change.rowId,\n tenantId: change.tenantId,\n seq: change.seq,\n }),\n );\n }\n if (closed) break;\n if (page.cursor === since || page.changes.length === 0) {\n break;\n }\n since = page.cursor;\n // NOTE: catch-up enqueues per-page without a hard cap. It is\n // bounded — an over-old cursor hits `resyncRequired` and stops — but\n // a large retention window replayed to a slow client could spike\n // memory. Honor the controller's backpressure signal cheaply: when\n // the internal queue is full (`desiredSize <= 0`), yield between\n // pages so the consumer drains first. Bounded by `closed` (set on\n // cancel/disconnect), so it can't spin on a client that never reads.\n while (\n !closed &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n await new Promise((resolve) => setTimeout(resolve, 5));\n }\n }\n } catch (error) {\n logger.warn('_events: cursor catch-up failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // (d) Heartbeat keeps intermediaries from idling the connection out.\n heartbeat = setInterval(() => {\n if (closed) return;\n try {\n controller.enqueue(encodeSseComment('heartbeat'));\n } catch {\n teardown();\n }\n }, heartbeatMs);\n // Do not keep the event loop alive solely for heartbeats.\n (heartbeat as { unref?: () => void }).unref?.();\n },\n cancel() {\n // Client disconnected (abort) — release the subscription + heartbeat.\n teardown();\n },\n });\n}\n\n/**\n * Handle a request against the generated `_events` route.\n *\n * Returns 405 for non-GET; 401 when no auth middleware is configured\n * (fail-closed) or the middleware rejects; 503 when the generator has no\n * database; otherwise a 200 `text/event-stream` response whose body is the\n * live signal stream (built by {@link buildChangeEventStream}).\n */\nexport async function handleEventsRoute(\n req: Request,\n options: EventsRouteOptions,\n): Promise<Response> {\n if (req.method !== 'GET') {\n return new Response(JSON.stringify({ error: 'Method not allowed' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n // Fail-closed (#1540): the signal stream spans every table, so it is never\n // public — an auth middleware must be configured and must pass.\n if (!options.authMiddleware) {\n return new Response(JSON.stringify({ error: 'Authentication required' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n const authCheck = options.authMiddleware(\n EVENTS_ROUTE_OBJECT_NAME,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult;\n }\n\n if (options.db == null) {\n return new Response(\n JSON.stringify({\n error:\n 'Live events unavailable: no database configured for the API generator',\n }),\n { status: 503, headers: { 'Content-Type': 'application/json' } },\n );\n }\n\n const db = await resolveChangesDb(options.db);\n // A raw handle passed straight to the generator may not have gone through\n // framework init; the feed table backs cursor catch-up.\n await ensureChangeFeedTable(db);\n const releaseSubscriberSlot = tryReserveChangeEventSubscriberSlot(\n db,\n options.maxSubscribers,\n );\n if (!releaseSubscriberSlot) {\n return eventStreamCapacityExceededResponse();\n }\n\n // Cursor: Last-Event-ID (reconnection) takes precedence over ?since=.\n // Default = live-forward only (no catch-up).\n const cursor = parseCursor(authResult);\n\n // Capture the tenant scope ONCE at connection open — delivery runs outside\n // any tenant ALS context and must filter against this fixed value.\n const tenantScope = resolveDispatchTenantScope();\n\n return new Response(\n buildChangeEventStream(db, {\n cursor,\n tenantScope,\n manifestHash: options.manifestHash,\n releaseSubscriberSlot,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n },\n );\n}\n\n/**\n * Resolve the catch-up cursor for a request: `Last-Event-ID` header first\n * (what an auto-reconnecting EventSource sends), then `?since=`. Returns a\n * non-negative integer, or `null` for live-forward only.\n */\nfunction parseCursor(req: Request): number | null {\n const lastEventId = req.headers.get('Last-Event-ID');\n if (lastEventId !== null && lastEventId.trim() !== '') {\n const n = Number(lastEventId);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n\n const since = new URL(req.url).searchParams.get('since');\n if (since !== null && since.trim() !== '') {\n const n = Number(since);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"events-route.js","names":[],"sources":["../../src/generators/events-route.ts"],"sourcesContent":["/**\n * Generated `_events` SSE route — live change signals (issue #1763, SERVER\n * half; parent PRD #1755).\n *\n * Handles `GET {basePath}/_events` in the REST generator: an auth-guarded,\n * tenant-scoped Server-Sent-Events stream of coarse change signals ({table,\n * operation, rowId, tenantId} + a `seq` cursor in the SSE `id:` field). It is\n * the push companion to the pull-based `_changes` route (#1758): a subscriber\n * reacts to a signal by re-reading through the authorized collection routes,\n * so **no row payload ever crosses this channel** — authorization stays\n * entirely on the read path.\n *\n * The stream lifecycle (subscribe, catch-up replay, heartbeat, teardown) lives\n * in {@link buildChangeEventStream} so it is written and tested once; both the\n * REST generator here and the generated SvelteKit route import it. `rest.ts`\n * only registers the path.\n *\n * Contract:\n * - **Fail-closed auth** (identical to `_changes`, #1540 posture): no\n * `authMiddleware` configured → 401; the middleware may return a Response to\n * short-circuit (e.g. 403). The feed spans every table, so per-model\n * `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scope is captured ONCE at connection open** — signal delivery\n * happens from a different async context (the writer's afterSave, possibly\n * another request or replica) with no tenant ALS active, so the filter must\n * be the value resolved at subscribe time, not re-resolved per signal.\n * - **Cross-origin is opt-in** (#1861): `rest.ts` now wraps `_events` in its\n * CORS layer. With the fail-closed default it stays same-origin only, but\n * when the generator is configured with `enableCors`, an `allowedOrigins`\n * allowlist, and `allowCredentials: true`, an allow-listed cross-origin\n * browser client can subscribe with a credentialed `EventSource`\n * (`withCredentials: true`) — the response echoes the specific origin (never\n * `*`) plus `Access-Control-Allow-Credentials: true`, so its cookies reach\n * the same fail-closed auth guard. The CORS layer only lets the cookie\n * through; it never authorizes — the auth middleware + captured tenant scope\n * are unchanged, so the read posture holds identically across origins.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ensureChangeFeedTable, getChangesSince } from '../change-feed.js';\nimport {\n type ChangeFeedRequestContext,\n filterVisibleChangeFeedEntries,\n hasChangeFeedEntryVisibilityHook,\n hasChangeFeedTableAuthorizerHook,\n isChangeFeedDenyAll,\n isChangeFeedEntryVisible,\n resolveAuthorizedChangeFeedTables,\n} from '../change-feed-authz.js';\nimport {\n type ChangeSignal,\n changeSignalSubscriberCount,\n subscribeToChangeSignals,\n tryReserveChangeSignalSubscriberSlot,\n} from '../change-signals.js';\nimport {\n type DispatchTenantScope,\n resolveDispatchTenantScope,\n} from '../dispatch/tenant-resolver.js';\nimport {\n type ChangesAuthMiddleware,\n resolveChangesDb,\n} from './changes-route.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Options for the `_events` route handler. */\nexport interface EventsRouteOptions {\n /** The generator's configured auth middleware, if any. */\n authMiddleware?: ChangesAuthMiddleware;\n /** The generator's `APIContext.db` (instance, config object, or URL string). */\n db?: unknown;\n /**\n * The build's web-collection shape digest (#1764). When supplied, the stream\n * emits it in a connection-open `manifest` event so long-lived tabs can latch\n * `updateAvailable.contract` on reconnect (#1859).\n */\n manifestHash?: string;\n /**\n * Per-process cap on active `_events` subscribers (#1860). Defaults to\n * {@link DEFAULT_EVENTS_MAX_SUBSCRIBERS}; new over-cap connections receive a\n * retryable 503 and existing subscribers are left untouched. Set to 0 for no\n * cap.\n */\n maxSubscribers?: number;\n}\n\n/**\n * Pseudo object name passed to the auth middleware for the events route, so\n * middlewares can recognize and specially authorize it (mirrors `_changes`).\n */\nexport const EVENTS_ROUTE_OBJECT_NAME = '_events';\n\n/** Default heartbeat interval (ms). Overridable via stream options. */\nexport const DEFAULT_EVENTS_HEARTBEAT_MS = 15000;\n\n/** Default per-process `_events` subscriber cap (#1860). */\nexport const DEFAULT_EVENTS_MAX_SUBSCRIBERS = 1000;\n\n/** Retry hint for over-cap `_events` connections (#1860). */\nexport const DEFAULT_EVENTS_RETRY_AFTER_SECONDS = 5;\n\nconst encoder = new TextEncoder();\n\n/** Options for {@link buildChangeEventStream}. */\nexport interface ChangeEventStreamOptions {\n /**\n * Catch-up cursor. When a non-negative number, changes after it are replayed\n * before going live; `null` means live-forward only (no catch-up).\n */\n cursor: number | null;\n /**\n * Tenant scope captured at connection open. Delivery filters against this\n * fixed value — it must NOT be re-resolved per signal (delivery runs outside\n * any tenant ALS context).\n */\n tenantScope: DispatchTenantScope;\n /** Heartbeat interval (ms). Defaults to {@link DEFAULT_EVENTS_HEARTBEAT_MS}. */\n heartbeatMs?: number;\n /**\n * Optional server manifest hash emitted once at connection open as\n * `event: manifest`. The hash carries no tenant/user data.\n */\n manifestHash?: string;\n /**\n * Reservation claimed at the route boundary before the streaming response was\n * returned. Released when the stream subscribes, or during teardown if the\n * stream never reaches `start()`.\n */\n releaseSubscriberSlot?: () => void;\n /**\n * Request `locals` (SvelteKit) and the authenticated `request`, forwarded\n * to the consumer-supplied change-feed authorization hooks (#3020) —\n * `authorizeChangeFeed` and `isChangeFeedEntryVisible`, see\n * `change-feed-authz.ts`. Resolved and captured ONCE at connection open,\n * exactly like `tenantScope`: delivery runs from a different async context\n * (the writer's `afterSave`, possibly another request or replica) with no\n * per-signal opportunity to re-derive it. The REST generator has no\n * `locals`, so it passes `locals: undefined` and the\n * `authMiddleware`-processed `Request` as `request` — a REST-hosting\n * consumer identifies the principal from `request`.\n */\n locals?: unknown;\n request?: Request;\n}\n\n/**\n * Normalize an `_events` subscriber cap.\n *\n * `0` means unlimited rather than reject-all, matching common limit semantics.\n * Invalid values fall back to the default operational cap.\n */\nexport function normalizeEventsMaxSubscribers(\n value: number | undefined,\n): number | null {\n if (value === undefined) return DEFAULT_EVENTS_MAX_SUBSCRIBERS;\n if (value === 0) return null;\n if (!Number.isFinite(value) || value < 0) {\n return DEFAULT_EVENTS_MAX_SUBSCRIBERS;\n }\n return Math.floor(value);\n}\n\n/** True when opening a new `_events` stream would exceed the configured cap. */\nexport function changeEventSubscribersAtCapacity(\n db: DatabaseInterface,\n maxSubscribers?: number,\n): boolean {\n const normalizedMaxSubscribers =\n normalizeEventsMaxSubscribers(maxSubscribers);\n return (\n normalizedMaxSubscribers !== null &&\n changeSignalSubscriberCount(db) >= normalizedMaxSubscribers\n );\n}\n\n/**\n * Atomically claim one `_events` subscriber slot at the route boundary.\n * Returns null when the configured cap is already reached.\n */\nexport function tryReserveChangeEventSubscriberSlot(\n db: DatabaseInterface,\n maxSubscribers?: number,\n): (() => void) | null {\n return tryReserveChangeSignalSubscriberSlot(\n db,\n normalizeEventsMaxSubscribers(maxSubscribers),\n );\n}\n\n/** Retryable over-cap response shared by REST and generated SvelteKit routes. */\nexport function eventStreamCapacityExceededResponse(): Response {\n return new Response(\n JSON.stringify({\n error: 'Live events unavailable: subscriber capacity reached',\n }),\n {\n status: 503,\n headers: {\n 'Content-Type': 'application/json',\n 'Retry-After': String(DEFAULT_EVENTS_RETRY_AFTER_SECONDS),\n },\n },\n );\n}\n\n/**\n * Whether a signal is visible to a captured tenant scope. Exact same rule as\n * `getChangesSince`'s tenantId filter, run **synchronously server-side** inside\n * the enqueue callback before any byte hits the wire:\n * - not enforced → visible.\n * - enforced, no active tenant (`tenantId === null`) → only global signals.\n * - enforced, tenant `T` → `T`'s signals plus global signals.\n */\nexport function signalVisibleToTenant(\n sig: ChangeSignal,\n scope: DispatchTenantScope,\n): boolean {\n if (!scope.enforced) return true;\n if (scope.tenantId === null) return sig.tenantId === null;\n return sig.tenantId === scope.tenantId || sig.tenantId === null;\n}\n\n/**\n * SSE frame for a change signal. The `data` JSON is EXACTLY\n * `{table, operation, rowId, tenantId}` — the `seq` lives only in the `id:`\n * field (the EventSource `Last-Event-ID` a client echoes to resume).\n */\nfunction encodeSseEvent(sig: ChangeSignal): Uint8Array {\n const data = JSON.stringify({\n table: sig.table,\n operation: sig.operation,\n rowId: sig.rowId,\n tenantId: sig.tenantId,\n });\n return encoder.encode(`id: ${sig.seq}\\nevent: change\\ndata: ${data}\\n\\n`);\n}\n\n/** SSE manifest frame emitted at connection open for live contract detection. */\nfunction encodeSseManifestEvent(manifestHash: string): Uint8Array {\n return encoder.encode(\n `event: manifest\\ndata: ${JSON.stringify({ manifestHash })}\\n\\n`,\n );\n}\n\n/** SSE resync frame. The id advances EventSource past an unservable cursor. */\nfunction encodeSseResyncEvent(cursor: number): Uint8Array {\n return encoder.encode(`id: ${cursor}\\nevent: resync\\ndata: {}\\n\\n`);\n}\n\n/** SSE comment line (used for heartbeats — ignored by EventSource). */\nfunction encodeSseComment(text: string): Uint8Array {\n return encoder.encode(`: ${text}\\n\\n`);\n}\n\n/**\n * Build the SSE body stream for an `_events` connection.\n *\n * `start(controller)`:\n * 0. Resolve the change-feed table authorization (#3020), if a hook is\n * registered, and capture it for the life of the connection — exactly\n * like `tenantScope`, and for the same reason (delivery runs outside this\n * call's context). Skipped entirely (no `await`) when no table\n * authorizer is registered, so the default connection setup stays\n * synchronous up to the subscribe call below. When a hook IS registered\n * and the connection has no explicit catch-up `cursor` (live-forward-only),\n * the feed's current head is captured BEFORE this await as a gap-fill\n * cursor: `unsubscribe` below only sees signals published after it\n * attaches, so a write that commits (and signals) while the authorizer is\n * pending would otherwise be lost forever, not merely delayed — a\n * supplied `cursor` doesn't need this because its own catch-up phase (c)\n * already starts from a value captured before this same await.\n * a. **Subscribe FIRST**, before catch-up. Subscribing before the catch-up\n * read closes the gap window: a write landing between subscribe and the\n * catch-up read is delivered twice (once live, once in the replay) — which\n * is safe, since the client dedupes by the SSE `id:`/seq. Each live signal\n * is additionally checked against the captured table authorization and,\n * if registered, the row-level `isChangeFeedEntryVisible` hook — denied\n * signals are silently dropped, never enqueued.\n * b. Write the `retry:` reconnection hint.\n * c. If a cursor was supplied, OR step 0 captured a gap-fill cursor, replay\n * changes after it (paging until exhausted, applying the same table/row\n * authorization as live delivery); on `resyncRequired`, emit\n * `event: resync` at the server's fresh horizon.\n * d. Start the heartbeat interval.\n *\n * `cancel()` tears down on disconnect: clears the heartbeat and unsubscribes,\n * so a dropped client never leaks its subscription (which would pin the dead\n * controller and keep the cross-replica listener refcount above 0).\n */\nexport function buildChangeEventStream(\n db: DatabaseInterface,\n options: ChangeEventStreamOptions,\n): ReadableStream<Uint8Array> {\n const { cursor, tenantScope, manifestHash, locals, request } = options;\n const requestContext: ChangeFeedRequestContext = { locals, request };\n const heartbeatMs = options.heartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;\n\n let unsubscribe: (() => void) | null = null;\n let releaseSubscriberSlot = options.releaseSubscriberSlot ?? null;\n let heartbeat: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n\n const teardown = () => {\n if (closed) return;\n closed = true;\n if (heartbeat) {\n clearInterval(heartbeat);\n heartbeat = null;\n }\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n if (releaseSubscriberSlot) {\n releaseSubscriberSlot();\n releaseSubscriberSlot = null;\n }\n };\n\n return new ReadableStream<Uint8Array>({\n async start(controller) {\n // (0) Table authorization (#3020), captured once for the connection's\n // lifetime — mirrors `tenantScope`. `undefined` (no hook registered)\n // keeps every downstream check a no-op; the `await` only runs when a\n // hook is actually registered, so the unconfigured default reaches the\n // subscribe call below with no yield in between.\n let allowedTables: string[] | undefined;\n // Gap-fill catch-up cursor (#3020 follow-up): in live-forward-only mode\n // (`cursor === null`) there is no explicit catch-up phase below to\n // recover a write that commits — and signals — while the table\n // authorizer is awaited. `unsubscribe` below only sees signals\n // published AFTER it attaches, so anything published during this await\n // is otherwise lost forever, not merely delayed. Capture the feed's\n // current head BEFORE the await (a single indexed MIN/MAX bounds\n // query, via `denyAllTables` so it never touches the row-selecting\n // path) so that once the allow-list resolves we can replay exactly the\n // entries appended in that window — same mechanism the explicit-cursor\n // path already gets for free, since its catch-up starts from a cursor\n // captured before this same await. A non-null `cursor` needs no\n // separate capture: its own catch-up phase already starts from that\n // pre-await value.\n let gapFillCursor: number | null = null;\n if (hasChangeFeedTableAuthorizerHook()) {\n if (cursor == null) {\n // A rejected head query errors the stream, and cancelling an\n // errored stream never reaches `cancel()` — release the reserved\n // subscriber slot here or it stays allocated forever.\n let headPage: Awaited<ReturnType<typeof getChangesSince>>;\n try {\n headPage = await getChangesSince(db, {\n since: 0,\n denyAllTables: true,\n });\n } catch (error) {\n teardown();\n throw error;\n }\n gapFillCursor = headPage.resyncRequired\n ? (headPage.resyncCursor ?? 0)\n : headPage.cursor;\n }\n allowedTables = await resolveAuthorizedChangeFeedTables(\n requestContext,\n undefined,\n );\n // The client may have disconnected while that awaited (#3020 P2):\n // `cancel()` runs `teardown()` before any subscription exists,\n // releasing the reserved slot but leaving `unsubscribe` null since\n // there is nothing to unsubscribe yet. Subscribing below anyway\n // would register a listener that no later `teardown()` call could\n // ever remove — `closed` is already `true`, so `teardown()`\n // short-circuits — permanently leaking a `change-signals` local\n // listener (inflating `changeSignalSubscriberCount` forever) and\n // then throwing on the very next `controller.enqueue` against an\n // already-closed controller. Bail out here instead: the slot was\n // already released by that earlier `teardown()`, so nothing further\n // needs releasing.\n if (closed) return;\n }\n const allowedTableSet = allowedTables ? new Set(allowedTables) : null;\n // Same deny-all distinction `readAuthorized` makes (#3020 P1): an\n // explicit empty allow-list must deny the catch-up read via\n // `getChangesSince`'s `denyAllTables` option, never an empty `tables`\n // array (which means \"no filter\") or a synthesized sentinel name.\n const catchupDenyAllTables = isChangeFeedDenyAll(allowedTables);\n const rowVisibilityActive = hasChangeFeedEntryVisibilityHook();\n // Serializes the (possibly async) row-visibility check so signals are\n // still delivered in arrival order; unused — and never allocated a\n // microtask — when no row hook is registered (see the enqueue fast path\n // below), preserving the documented synchronous per-listener delivery.\n let deliveryQueue: Promise<void> = Promise.resolve();\n\n // (a) Subscribe FIRST, before catch-up — closes the subscribe/catch-up\n // gap window (a write in between is delivered twice; the client dedupes\n // by seq). The tenant filter uses the scope captured at open, never a\n // per-signal re-resolution; table authorization is the same captured\n // value.\n unsubscribe = subscribeToChangeSignals(db, (sig) => {\n if (closed) return;\n if (!signalVisibleToTenant(sig, tenantScope)) return;\n if (allowedTableSet && !allowedTableSet.has(sig.table)) return;\n if (!rowVisibilityActive) {\n try {\n controller.enqueue(encodeSseEvent(sig));\n } catch {\n // Controller already closed (client gone before cancel fired) —\n // tear down so we stop trying to write to a dead controller.\n teardown();\n }\n return;\n }\n deliveryQueue = deliveryQueue.then(async () => {\n if (closed) return;\n if (!(await isChangeFeedEntryVisible(requestContext, sig))) return;\n try {\n controller.enqueue(encodeSseEvent(sig));\n } catch {\n teardown();\n }\n });\n });\n if (releaseSubscriberSlot) {\n releaseSubscriberSlot();\n releaseSubscriberSlot = null;\n }\n\n // (b) Reconnection hint.\n controller.enqueue(encoder.encode('retry: 3000\\n\\n'));\n // Advertise the server contract at connection open (#1859). A reconnect\n // naturally replays this frame, letting a long-lived tab learn about a\n // shape-only API deploy without a full page load.\n if (manifestHash !== undefined) {\n controller.enqueue(encodeSseManifestEvent(manifestHash));\n }\n\n // (c) Catch-up replay from the cursor, if one was supplied — or from\n // the gap-fill cursor captured before the authorizer await (above),\n // which replays only what was appended during that await so a\n // live-forward-only connection never silently drops it.\n const catchupCursor = cursor ?? gapFillCursor;\n if (catchupCursor != null) {\n try {\n // Catch-up MUST filter by the scope captured at connection open, not\n // re-resolve the tenant via ALS at call time. start() happens to run\n // in-request today, but relying on that is fragile — and it must match\n // the live-signal filter exactly (signalVisibleToTenant): when\n // enforced, `scope.tenantId` (a tenant id → that tenant + global; null\n // → global only); when not enforced, undefined → no tenant filter.\n const catchupTenantId = tenantScope.enforced\n ? tenantScope.tenantId\n : undefined;\n let since = catchupCursor;\n // Page until exhausted (cursor stops advancing / resync).\n for (;;) {\n const page = await getChangesSince(db, {\n since,\n tenantId: catchupTenantId,\n // Same captured table authorization as live delivery (#3020).\n tables: catchupDenyAllTables ? undefined : allowedTables,\n denyAllTables: catchupDenyAllTables,\n });\n if (page.resyncRequired) {\n const resyncCursor =\n typeof page.resyncCursor === 'number' &&\n Number.isFinite(page.resyncCursor) &&\n page.resyncCursor >= 0\n ? page.resyncCursor\n : since;\n controller.enqueue(encodeSseResyncEvent(resyncCursor));\n break;\n }\n // Row-level visibility (#3020) — filters the replayed page the\n // same way the read `_changes` path does, never touching\n // `page.cursor` below so the client still advances past a denied\n // entry instead of re-requesting it forever.\n const visibleChanges = await filterVisibleChangeFeedEntries(\n requestContext,\n page.changes,\n );\n for (const change of visibleChanges) {\n controller.enqueue(\n encodeSseEvent({\n table: change.table,\n operation: change.operation,\n rowId: change.rowId,\n tenantId: change.tenantId,\n seq: change.seq,\n }),\n );\n }\n if (closed) break;\n if (page.cursor === since || page.changes.length === 0) {\n break;\n }\n since = page.cursor;\n // NOTE: catch-up enqueues per-page without a hard cap. It is\n // bounded — an over-old cursor hits `resyncRequired` and stops — but\n // a large retention window replayed to a slow client could spike\n // memory. Honor the controller's backpressure signal cheaply: when\n // the internal queue is full (`desiredSize <= 0`), yield between\n // pages so the consumer drains first. Bounded by `closed` (set on\n // cancel/disconnect), so it can't spin on a client that never reads.\n while (\n !closed &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n await new Promise((resolve) => setTimeout(resolve, 5));\n }\n }\n } catch (error) {\n logger.warn('_events: cursor catch-up failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // (d) Heartbeat keeps intermediaries from idling the connection out.\n heartbeat = setInterval(() => {\n if (closed) return;\n try {\n controller.enqueue(encodeSseComment('heartbeat'));\n } catch {\n teardown();\n }\n }, heartbeatMs);\n // Do not keep the event loop alive solely for heartbeats.\n (heartbeat as { unref?: () => void }).unref?.();\n },\n cancel() {\n // Client disconnected (abort) — release the subscription + heartbeat.\n teardown();\n },\n });\n}\n\n/**\n * Handle a request against the generated `_events` route.\n *\n * Returns 405 for non-GET; 401 when no auth middleware is configured\n * (fail-closed) or the middleware rejects; 503 when the generator has no\n * database; otherwise a 200 `text/event-stream` response whose body is the\n * live signal stream (built by {@link buildChangeEventStream}).\n */\nexport async function handleEventsRoute(\n req: Request,\n options: EventsRouteOptions,\n): Promise<Response> {\n if (req.method !== 'GET') {\n return new Response(JSON.stringify({ error: 'Method not allowed' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n // Fail-closed (#1540): the signal stream spans every table, so it is never\n // public — an auth middleware must be configured and must pass.\n if (!options.authMiddleware) {\n return new Response(JSON.stringify({ error: 'Authentication required' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n const authCheck = options.authMiddleware(\n EVENTS_ROUTE_OBJECT_NAME,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult;\n }\n\n if (options.db == null) {\n return new Response(\n JSON.stringify({\n error:\n 'Live events unavailable: no database configured for the API generator',\n }),\n { status: 503, headers: { 'Content-Type': 'application/json' } },\n );\n }\n\n const db = await resolveChangesDb(options.db);\n // A raw handle passed straight to the generator may not have gone through\n // framework init; the feed table backs cursor catch-up.\n await ensureChangeFeedTable(db);\n const releaseSubscriberSlot = tryReserveChangeEventSubscriberSlot(\n db,\n options.maxSubscribers,\n );\n if (!releaseSubscriberSlot) {\n return eventStreamCapacityExceededResponse();\n }\n\n // Cursor: Last-Event-ID (reconnection) takes precedence over ?since=.\n // Default = live-forward only (no catch-up).\n const cursor = parseCursor(authResult);\n\n // Capture the tenant scope ONCE at connection open — delivery runs outside\n // any tenant ALS context and must filter against this fixed value.\n const tenantScope = resolveDispatchTenantScope();\n\n return new Response(\n buildChangeEventStream(db, {\n cursor,\n tenantScope,\n manifestHash: options.manifestHash,\n releaseSubscriberSlot,\n // REST has no `locals`; the authorization hooks (#3020) identify the\n // principal from the authMiddleware-processed request instead.\n locals: undefined,\n request: authResult,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n },\n );\n}\n\n/**\n * Resolve the catch-up cursor for a request: `Last-Event-ID` header first\n * (what an auto-reconnecting EventSource sends), then `?since=`. Returns a\n * non-negative integer, or `null` for live-forward only.\n */\nfunction parseCursor(req: Request): number | null {\n const lastEventId = req.headers.get('Last-Event-ID');\n if (lastEventId !== null && lastEventId.trim() !== '') {\n const n = Number(lastEventId);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n\n const since = new URL(req.url).searchParams.get('since');\n if (since !== null && since.trim() !== '') {\n const n = Number(since);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n\n return null;\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
export * from './adapters/index';
|
|
20
20
|
export { buildCascadePlan, type CascadePlan, type CascadePolymorphicReference, type CascadeReference, type CascadeRegistryView, type CascadeResult, cascadeReferencesTo, normalizeOnDelete, type OnDeleteAction, runCascadeDelete, } from './cascade';
|
|
21
21
|
export { type AppendChangeBatch, type AppendChangeInput, appendChange, appendChanges, bumpChangeFeed, CHANGE_FEED_INTERCEPTOR_NAME, CHANGE_FEED_TABLE, type ChangeFeedEntry, type ChangeFeedPage, type ChangeFeedRetention, type ChangeOperation, DEFAULT_CHANGES_LIMIT, drainChangeFeed, ensureChangeFeedTable, type GetChangesOptions, getChangesSince, getTableVersion, getTenantScopedChangesSince, MAX_CHANGES_LIMIT, pruneChangeFeed, registerChangeFeedWriter, resetChangeFeedWarnings, unregisterChangeFeedWriter, } from './change-feed';
|
|
22
|
+
export { type ChangeFeedEntryVisibility, type ChangeFeedEntryVisibilityRequest, type ChangeFeedTableAuthorizationRequest, type ChangeFeedTableAuthorizer, type ChangeFeedVisibilityEntry, filterVisibleChangeFeedEntries, getAuthorizedChangesSince, getAuthorizedTenantScopedChangesSince, hasChangeFeedEntryVisibilityHook, hasChangeFeedTableAuthorizerHook, isChangeFeedDenyAll, isChangeFeedEntryVisible, resolveAuthorizedChangeFeedTables, setChangeFeedAuthorizer, setChangeFeedEntryVisibility, } from './change-feed-authz';
|
|
22
23
|
export { CHANGE_FEED_CREDENTIAL_TABLES, declareChangeFeedSensitiveTable, getChangeFeedSensitiveTables, isChangeFeedSensitiveTable, } from './change-feed-sensitivity';
|
|
23
24
|
export { CHANGE_SIGNAL_CHANNEL, type ChangeSignal, type ChangeSignalListener, resetChangeSignals, stopChangeSignalListeners, subscribeToChangeSignals, } from './change-signals';
|
|
24
25
|
export { applyOneToManyChildAccessors, childAccessorName, } from './child-accessors';
|
|
@@ -58,6 +59,7 @@ export { buildDefaultListOrderBy, DEFAULT_LIST_LIMIT, DEFAULT_LIST_ORDER_BY, typ
|
|
|
58
59
|
export * from './registry';
|
|
59
60
|
export { smrt as smrtRegistry } from './registry';
|
|
60
61
|
export { POSTGRES_REVISION_GUARD_EXPRESSION, postgresRevisionCandidates, postgresRevisionCondition, } from './revision-guard';
|
|
62
|
+
export * from './run-once';
|
|
61
63
|
export * from './runtime/index';
|
|
62
64
|
export { detectEngine, generateDDLForEngine } from './schema/ddl';
|
|
63
65
|
export { type CollectForeignKeyOrphanCountsOptions, collectForeignKeyOrphanCounts, type ForeignKeyOrphanCount, type ForeignKeyOrphanCountReport, type ForeignKeyOrphanSkipped, } from './schema/foreign-key-orphan-report';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EACL,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,2BAA2B,EAChC,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,cAAc,EACnB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,YAAY,EACZ,aAAa,EACb,cAAc,EACd,4BAA4B,EAC5B,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,KAAK,iBAAiB,EACtB,eAAe,EACf,eAAe,EACf,2BAA2B,EAC3B,iBAAiB,EACjB,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { MetricsAdapter } from "./adapters/metrics.js";
|
|
|
4
4
|
import { PubSubAdapter } from "./adapters/pubsub.js";
|
|
5
5
|
import "./adapters/index.js";
|
|
6
6
|
import { classifyDatabaseError, classifyDialectMessage, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isTransientDatabaseError, isUniqueViolationError } from "./db-errors.js";
|
|
7
|
-
import { AIError, ConfigurationError, DatabaseError, ErrorUtils, FilesystemError, NetworkError, RuntimeError, SmrtError, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils } from "./errors.js";
|
|
7
|
+
import { AIError, ConfigurationError, DatabaseError, ErrorUtils, FilesystemError, NetworkError, RunOnceClaimError, RuntimeError, SmrtError, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils } from "./errors.js";
|
|
8
8
|
import { IN_LIST_CHUNK_SIZE, chunkArray } from "./utils/chunk.js";
|
|
9
9
|
import { buildCascadePlan, cascadeReferencesTo, normalizeOnDelete, runCascadeDelete } from "./cascade.js";
|
|
10
10
|
import { CHANGE_FEED_CREDENTIAL_TABLES, declareChangeFeedSensitiveTable, getChangeFeedSensitiveTables, isChangeFeedSensitiveTable } from "./change-feed-sensitivity.js";
|
|
@@ -14,8 +14,10 @@ import { isTenantScopedClassResolved, resolveDispatchTenantId, resolveDispatchTe
|
|
|
14
14
|
import { isEmbeddedDatabase, isPostgresDatabase, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue, withEmbeddedWriteTransaction } from "./embedded-write-queue.js";
|
|
15
15
|
import { GlobalInterceptors, createInterceptorContext, resolveGetStringFilter } from "./interceptors.js";
|
|
16
16
|
import { detectEngine, generateDDLForEngine } from "./schema/ddl/index.js";
|
|
17
|
+
import { RUN_ONCE_CLAIMS_TABLE, SMRT_SCHEMA_VERSION } from "./system/schema.js";
|
|
17
18
|
import { SYSTEM_TABLE_NAMES, getSystemTableShapes } from "./schema/system-table-shapes.js";
|
|
18
19
|
import { CHANGE_FEED_INTERCEPTOR_NAME, CHANGE_FEED_TABLE, DEFAULT_CHANGES_LIMIT, MAX_CHANGES_LIMIT, appendChange, appendChanges, bumpChangeFeed, drainChangeFeed, ensureChangeFeedTable, getChangesSince, getTableVersion, getTenantScopedChangesSince, pruneChangeFeed, registerChangeFeedWriter, resetChangeFeedWarnings, unregisterChangeFeedWriter } from "./change-feed.js";
|
|
20
|
+
import { filterVisibleChangeFeedEntries, getAuthorizedChangesSince, getAuthorizedTenantScopedChangesSince, hasChangeFeedEntryVisibilityHook, hasChangeFeedTableAuthorizerHook, isChangeFeedDenyAll, isChangeFeedEntryVisible, resolveAuthorizedChangeFeedTables, setChangeFeedAuthorizer, setChangeFeedEntryVisibility } from "./change-feed-authz.js";
|
|
19
21
|
import { applyOneToManyChildAccessors, childAccessorName } from "./child-accessors.js";
|
|
20
22
|
import { config } from "./config.js";
|
|
21
23
|
import { importOptionalDependency, registerOptionalDependency } from "./lazy-external.js";
|
|
@@ -81,6 +83,7 @@ import { planForeignKeyCreation } from "./schema/foreign-key-planner.js";
|
|
|
81
83
|
import { SchemaComparer, generateSchemaDiff, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isManualOrAdvisoryChange } from "./migrations/differ.js";
|
|
82
84
|
import { SmrtPolymorphicAssociation } from "./polymorphic-association.js";
|
|
83
85
|
import { applyPostgresPermissions, planPostgresPermissions, validatePostgresPermissionContract } from "./postgres-permissions.js";
|
|
86
|
+
import { deriveRunOnceClaimKey, digestRunOnceContent, resolveExistingRunOnceClaim, runOnce } from "./run-once.js";
|
|
84
87
|
import { createSmrtClient } from "./runtime/client.js";
|
|
85
88
|
import { SmrtMCPServer, createMCPServer } from "./runtime/mcp.js";
|
|
86
89
|
import { createSmrtServer } from "./runtime/server.js";
|
|
@@ -90,9 +93,9 @@ import { LiveSchemaParityError, checkLiveSchemaParity } from "./schema/live-pari
|
|
|
90
93
|
import "./signals/index.js";
|
|
91
94
|
import { DIAGNOSTICS_DEFAULT_LIMIT, SYSTEM_DIAGNOSTICS_TABLES, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics } from "./system/diagnostics.js";
|
|
92
95
|
import { BOOTED_PROVENANCE, assertPlainJson, sanitizeMessagePaths, snapshotRegistry } from "./system/registry-snapshot.js";
|
|
93
|
-
import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./system/retention.js";
|
|
96
|
+
import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, pruneRunOnceClaims, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./system/retention.js";
|
|
94
97
|
import "./system/index.js";
|
|
95
98
|
import { getTestDatabase } from "./testing/database.js";
|
|
96
99
|
import "./tools/index.js";
|
|
97
100
|
import { smrtPlugin } from "./vite-plugin/index.js";
|
|
98
|
-
export { AGENT_SURFACE_HASH_PREFIX, AIError, APIGenerator, AiUsageCollector, AiUsagePersistenceHandler, BOOTED_PROVENANCE, CACHE_INVALIDATION_CHANNEL, CHANGE_FEED_CREDENTIAL_TABLES, CHANGE_FEED_INTERCEPTOR_NAME, CHANGE_FEED_TABLE, CHANGE_SIGNAL_CHANNEL, CRUD_OPERATIONS, ConfigurationError, ContentHasher, CosineSimilarity, DEFAULT_AI_COST_RATES, DEFAULT_CHANGES_LIMIT, DEFAULT_DATA_QUERY_PAGE_LIMIT, DEFAULT_DATA_QUERY_RESULT_BYTES, DEFAULT_EMBEDDING_CONFIG, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, DEFAULT_FACET_LIMIT, DEFAULT_LEARNING_CONFIG, DEFAULT_LIST_LIMIT, DEFAULT_LIST_ORDER_BY, DEFAULT_POSTGRES_TIMEOUTS, DEFAULT_RETENTION_POLICY, DIAGNOSTICS_DEFAULT_LIMIT, DataQueryValidationError, DatabaseError, Dispatch, DispatchBus, DispatchCollection, DispatchSubscription, DispatchSubscriptionCollection, EmbeddingProvider, EmbeddingStorage, EmbeddingUnavailableError, ErrorUtils, FilesystemError, GlobalInterceptors, IN_LIST_CHUNK_SIZE, LearningMemory, LiveSchemaParityError, MAX_CHANGES_LIMIT, MAX_DATA_QUERY_CURSOR_LENGTH, MAX_DATA_QUERY_FACETS, MAX_DATA_QUERY_FILTERS, MAX_DATA_QUERY_FILTER_DEPTH, MAX_DATA_QUERY_IN_VALUES, MAX_DATA_QUERY_OFFSET, MAX_DATA_QUERY_PAGE_LIMIT, MAX_DATA_QUERY_REQUEST_BYTES, MAX_DATA_QUERY_RESULT_BYTES, MAX_DATA_QUERY_WARNINGS, MAX_FACET_FIELDS, MAX_FACET_LIMIT, MAX_LIST_LIMIT, MAX_STI_READ_SCOPE_TYPES, MAX_SYNC_APPLY_BATCH_SIZE, MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, MODULE_DOC_HASH_PREFIX, ManifestBuilder, ManifestGenerator, ManifestManager, MetricsAdapter, NetworkError, ObjectRegistry, PLAYBOOK_PREFLIGHT_CAPABILITY, PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT, POSTGRES_REVISION_GUARD_EXPRESSION, POSTGRES_TIMEOUT_ENV_VARS, PRIVATE_READ_CACHE_CONTROL, PubSubAdapter, QueryBoundsError, QueryOrderByError, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, SYNC_APPLY_ROUTE_SEGMENTS, SYNC_APPLY_UUID_PATTERN, SYSTEM_DIAGNOSTICS_TABLES, SYSTEM_TABLE_NAMES, SchemaComparer, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtMCPServer, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, appendChange, appendChanges, applyOneToManyChildAccessors, applyPendingDecoratorRegistrations, applyPostgresPermissions, applyPostgresRuntimeTimeouts, applySyncWritablePolicy, assertPlainJson, assertPostgresSystemTimestampsCurrent, broadcastCacheInvalidation, buildCascadePlan, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, buildDefaultListOrderBy, buildDomainKnowledgeManifest, buildKnowledgeGraph, bumpChangeFeed, canonicalReadRepresentation, canonicalizeDataQuery, cascadeReferencesTo, changeEventSubscribersAtCapacity, checkKnowledgeGraphFreshness, checkLiveSchemaParity, childAccessorName, chunkArray, classifyDatabaseError, classifyDialectMessage, classifyMethodWireability, classifyParameterWireability, clearRetentionTasks, clone, coerceCustomActionArgument, collectForeignKeyOrphanCounts, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, config, convertTypeToJsonSchema, createClassNamePredicate, createDataQueryFingerprint, createDispatchBus, createFilesystemAdapter, createInterceptorContext, createMCPServer, createManifestClassNamePredicate, createQualifiedName, createRestServer, createSmrtClient, createSmrtServer, crossPackageRef, customActionParameterInputName, declareChangeFeedSensitiveTable, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, detectEngine, discoverKnowledgeArtifactPaths, discoverManifestEntry, discoverScopedPackageDirectories, drainChangeFeed, ensureBootstrapSystemTableCompatibility, ensureCacheInvalidationListener, ensureChangeFeedTable, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, ensureSystemTables, estimateAiUsageCost, eventStreamCapacityExceededResponse, executeCollectionReadPlan, executeToolCall, executeToolCalls, field, findManifestEntryByQualifiedName, foreignKey, formatToolResults, generateDDLForEngine, generateOpenAPISpec, generateSchemaDiff, generateToolFromMethod, generateToolManifest, getAdapterInfo, getCacheGeneration, getChangeFeedSensitiveTables, getChangesSince, getClassConfigResolvers, getClassName, getConfigResolver, getDatabaseEngine, getManifest, getPackageFromQualifiedName, getRetentionTasks, getSQLFromDiff, getSystemTableShapes, getTableVersion, getTenantScopedChangesSince, getTestDatabase, handlePlaybookPreflightRoute, hasActionableChanges, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, importOptionalDependency, invalidateCollectionCache, isAbortedTransactionError, isAdvisoryOnlyChange, isApiActionEnabledForObject, isChangeFeedSensitiveTable, isCrudOperation, isCrudToolAction, isDatabaseInterface, isDeterministicDatabaseError, isEmbeddedDatabase, isFrameworkBaseClass, isFromPackage, isLazyConfigSentinel, isManualOrAdvisoryChange, isNotNullViolationError, isPostgresDatabase, isQualifiedName, isRestActionRoutable, isRestRoutePublic, isSmrtCollectionExtendsName, isStaleWrite, isTenantScopedClassResolved, isTransientDatabaseError, isType, isUniqueViolationError, isValid, listConfigResolvers, loadExternalManifest, loadExternalManifestSync, loadLocalTestManifestSync, loadManifestFromPathSync, manifest, manyToMany, meta, method, migratePostgresSystemTimestamps, normalizeCustomActionFailure, normalizeDataQueryRequest, normalizeDataQueryResult, normalizeDataQuerySchema, normalizeEventsMaxSubscribers, normalizeOnDelete, normalizeTypedHttpError, oneToMany, parse, parseQualifiedName, parseSyncApplyBatch, payloadMatchesRow, planForeignKeyCreation, planPostgresPermissions, planPostgresSystemTimestampMigrations, postgresRevisionCandidates, postgresRevisionCondition, processSyncApplyBatch, pruneAiUsage, pruneChangeFeed, pruneExpiredContexts, qualifiedNamesEqual, queryStringDecoderFor, readAgentModuleDocs, readDispatchHealth, readJobHealth, readMethodDecoratorConfig, readMigrationStatus, readPackageAgentDoc, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics, registerChangeFeedWriter, registerCompatibleFieldDecorator, registerCompatibleMethodDecorator, registerConfigResolver, registerFilesystemAdapterFactory, registerOptionalDependency, registerRetentionTask, resetChangeFeedWarnings, resetChangeSignals, resetCollectionCache, resetConfigResolvers, resetVerifiedTables, resolveAgentModuleDocPaths, resolveApiMethodExposure, resolveCustomActionMetadata, resolveDatabase, resolveDbCacheKey, resolveDeclaredScopeMismatch, resolveDispatchTenantId, resolveDispatchTenantScope, resolveEffectiveActionMetadata, resolveGetStringFilter, resolveLazyConfig, resolveListLimit, resolveListOffset, resolveMCPToolListCacheHint, resolvePostgresTimeouts, resolveReadCacheControl, resolveRegisteredObjectName, resolveTenantEtagDiscriminator, restFieldReadPermissions, restMethodForApiAction, runCascadeDelete, runRetentionSweep, runWithTenantGate, safeParse, safeStringify, sanitizeMessagePaths, setDispatchTenantResolver, setTenantEntryPointRunner, setTenantScopedClassResolver, setupSwaggerUI, shouldIncludeMethod, signalVisibleToTenant, smrt, smrtPlugin, smrt as smrtRegistry, snapshotRegistry, sortMCPTools, stableStringify, startRestServer, staticManifest, stopCacheInvalidationListeners, stopChangeSignalListeners, stringify, subscribeToChangeSignals, tableExists, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber, tryReserveChangeEventSubscriberSlot, unregisterChangeFeedWriter, unregisterConfigResolver, unregisterRetentionTask, usesEmbeddedRevisionFallback, validatePostgresPermissionContract, validateSyncApplyItem, validateToolCall, versionConditionalResponse, warnIfSharedCacheNeutralized, withEmbeddedWriteQueue, withEmbeddedWriteTransaction };
|
|
101
|
+
export { AGENT_SURFACE_HASH_PREFIX, AIError, APIGenerator, AiUsageCollector, AiUsagePersistenceHandler, BOOTED_PROVENANCE, CACHE_INVALIDATION_CHANNEL, CHANGE_FEED_CREDENTIAL_TABLES, CHANGE_FEED_INTERCEPTOR_NAME, CHANGE_FEED_TABLE, CHANGE_SIGNAL_CHANNEL, CRUD_OPERATIONS, ConfigurationError, ContentHasher, CosineSimilarity, DEFAULT_AI_COST_RATES, DEFAULT_CHANGES_LIMIT, DEFAULT_DATA_QUERY_PAGE_LIMIT, DEFAULT_DATA_QUERY_RESULT_BYTES, DEFAULT_EMBEDDING_CONFIG, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, DEFAULT_FACET_LIMIT, DEFAULT_LEARNING_CONFIG, DEFAULT_LIST_LIMIT, DEFAULT_LIST_ORDER_BY, DEFAULT_POSTGRES_TIMEOUTS, DEFAULT_RETENTION_POLICY, DIAGNOSTICS_DEFAULT_LIMIT, DataQueryValidationError, DatabaseError, Dispatch, DispatchBus, DispatchCollection, DispatchSubscription, DispatchSubscriptionCollection, EmbeddingProvider, EmbeddingStorage, EmbeddingUnavailableError, ErrorUtils, FilesystemError, GlobalInterceptors, IN_LIST_CHUNK_SIZE, LearningMemory, LiveSchemaParityError, MAX_CHANGES_LIMIT, MAX_DATA_QUERY_CURSOR_LENGTH, MAX_DATA_QUERY_FACETS, MAX_DATA_QUERY_FILTERS, MAX_DATA_QUERY_FILTER_DEPTH, MAX_DATA_QUERY_IN_VALUES, MAX_DATA_QUERY_OFFSET, MAX_DATA_QUERY_PAGE_LIMIT, MAX_DATA_QUERY_REQUEST_BYTES, MAX_DATA_QUERY_RESULT_BYTES, MAX_DATA_QUERY_WARNINGS, MAX_FACET_FIELDS, MAX_FACET_LIMIT, MAX_LIST_LIMIT, MAX_STI_READ_SCOPE_TYPES, MAX_SYNC_APPLY_BATCH_SIZE, MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, MODULE_DOC_HASH_PREFIX, ManifestBuilder, ManifestGenerator, ManifestManager, MetricsAdapter, NetworkError, ObjectRegistry, PLAYBOOK_PREFLIGHT_CAPABILITY, PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT, POSTGRES_REVISION_GUARD_EXPRESSION, POSTGRES_TIMEOUT_ENV_VARS, PRIVATE_READ_CACHE_CONTROL, PubSubAdapter, QueryBoundsError, QueryOrderByError, RUN_ONCE_CLAIMS_TABLE, RunOnceClaimError, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, SMRT_SCHEMA_VERSION, SYNC_APPLY_ROUTE_SEGMENTS, SYNC_APPLY_UUID_PATTERN, SYSTEM_DIAGNOSTICS_TABLES, SYSTEM_TABLE_NAMES, SchemaComparer, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtMCPServer, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, appendChange, appendChanges, applyOneToManyChildAccessors, applyPendingDecoratorRegistrations, applyPostgresPermissions, applyPostgresRuntimeTimeouts, applySyncWritablePolicy, assertPlainJson, assertPostgresSystemTimestampsCurrent, broadcastCacheInvalidation, buildCascadePlan, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, buildDefaultListOrderBy, buildDomainKnowledgeManifest, buildKnowledgeGraph, bumpChangeFeed, canonicalReadRepresentation, canonicalizeDataQuery, cascadeReferencesTo, changeEventSubscribersAtCapacity, checkKnowledgeGraphFreshness, checkLiveSchemaParity, childAccessorName, chunkArray, classifyDatabaseError, classifyDialectMessage, classifyMethodWireability, classifyParameterWireability, clearRetentionTasks, clone, coerceCustomActionArgument, collectForeignKeyOrphanCounts, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, config, convertTypeToJsonSchema, createClassNamePredicate, createDataQueryFingerprint, createDispatchBus, createFilesystemAdapter, createInterceptorContext, createMCPServer, createManifestClassNamePredicate, createQualifiedName, createRestServer, createSmrtClient, createSmrtServer, crossPackageRef, customActionParameterInputName, declareChangeFeedSensitiveTable, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, deriveRunOnceClaimKey, detectEngine, digestRunOnceContent, discoverKnowledgeArtifactPaths, discoverManifestEntry, discoverScopedPackageDirectories, drainChangeFeed, ensureBootstrapSystemTableCompatibility, ensureCacheInvalidationListener, ensureChangeFeedTable, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, ensureSystemTables, estimateAiUsageCost, eventStreamCapacityExceededResponse, executeCollectionReadPlan, executeToolCall, executeToolCalls, field, filterVisibleChangeFeedEntries, findManifestEntryByQualifiedName, foreignKey, formatToolResults, generateDDLForEngine, generateOpenAPISpec, generateSchemaDiff, generateToolFromMethod, generateToolManifest, getAdapterInfo, getAuthorizedChangesSince, getAuthorizedTenantScopedChangesSince, getCacheGeneration, getChangeFeedSensitiveTables, getChangesSince, getClassConfigResolvers, getClassName, getConfigResolver, getDatabaseEngine, getManifest, getPackageFromQualifiedName, getRetentionTasks, getSQLFromDiff, getSystemTableShapes, getTableVersion, getTenantScopedChangesSince, getTestDatabase, handlePlaybookPreflightRoute, hasActionableChanges, hasChangeFeedEntryVisibilityHook, hasChangeFeedTableAuthorizerHook, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, importOptionalDependency, invalidateCollectionCache, isAbortedTransactionError, isAdvisoryOnlyChange, isApiActionEnabledForObject, isChangeFeedDenyAll, isChangeFeedEntryVisible, isChangeFeedSensitiveTable, isCrudOperation, isCrudToolAction, isDatabaseInterface, isDeterministicDatabaseError, isEmbeddedDatabase, isFrameworkBaseClass, isFromPackage, isLazyConfigSentinel, isManualOrAdvisoryChange, isNotNullViolationError, isPostgresDatabase, isQualifiedName, isRestActionRoutable, isRestRoutePublic, isSmrtCollectionExtendsName, isStaleWrite, isTenantScopedClassResolved, isTransientDatabaseError, isType, isUniqueViolationError, isValid, listConfigResolvers, loadExternalManifest, loadExternalManifestSync, loadLocalTestManifestSync, loadManifestFromPathSync, manifest, manyToMany, meta, method, migratePostgresSystemTimestamps, normalizeCustomActionFailure, normalizeDataQueryRequest, normalizeDataQueryResult, normalizeDataQuerySchema, normalizeEventsMaxSubscribers, normalizeOnDelete, normalizeTypedHttpError, oneToMany, parse, parseQualifiedName, parseSyncApplyBatch, payloadMatchesRow, planForeignKeyCreation, planPostgresPermissions, planPostgresSystemTimestampMigrations, postgresRevisionCandidates, postgresRevisionCondition, processSyncApplyBatch, pruneAiUsage, pruneChangeFeed, pruneExpiredContexts, pruneRunOnceClaims, qualifiedNamesEqual, queryStringDecoderFor, readAgentModuleDocs, readDispatchHealth, readJobHealth, readMethodDecoratorConfig, readMigrationStatus, readPackageAgentDoc, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics, registerChangeFeedWriter, registerCompatibleFieldDecorator, registerCompatibleMethodDecorator, registerConfigResolver, registerFilesystemAdapterFactory, registerOptionalDependency, registerRetentionTask, resetChangeFeedWarnings, resetChangeSignals, resetCollectionCache, resetConfigResolvers, resetVerifiedTables, resolveAgentModuleDocPaths, resolveApiMethodExposure, resolveAuthorizedChangeFeedTables, resolveCustomActionMetadata, resolveDatabase, resolveDbCacheKey, resolveDeclaredScopeMismatch, resolveDispatchTenantId, resolveDispatchTenantScope, resolveEffectiveActionMetadata, resolveExistingRunOnceClaim, resolveGetStringFilter, resolveLazyConfig, resolveListLimit, resolveListOffset, resolveMCPToolListCacheHint, resolvePostgresTimeouts, resolveReadCacheControl, resolveRegisteredObjectName, resolveTenantEtagDiscriminator, restFieldReadPermissions, restMethodForApiAction, runCascadeDelete, runOnce, runRetentionSweep, runWithTenantGate, safeParse, safeStringify, sanitizeMessagePaths, setChangeFeedAuthorizer, setChangeFeedEntryVisibility, setDispatchTenantResolver, setTenantEntryPointRunner, setTenantScopedClassResolver, setupSwaggerUI, shouldIncludeMethod, signalVisibleToTenant, smrt, smrtPlugin, smrt as smrtRegistry, snapshotRegistry, sortMCPTools, stableStringify, startRestServer, staticManifest, stopCacheInvalidationListeners, stopChangeSignalListeners, stringify, subscribeToChangeSignals, tableExists, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber, tryReserveChangeEventSubscriberSlot, unregisterChangeFeedWriter, unregisterConfigResolver, unregisterRetentionTask, usesEmbeddedRevisionFallback, validatePostgresPermissionContract, validateSyncApplyItem, validateToolCall, versionConditionalResponse, warnIfSharedCacheNeutralized, withEmbeddedWriteQueue, withEmbeddedWriteTransaction };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../src/knowledge-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EACV,uBAAuB,EAExB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAexE;AAED,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,uBAAuB,CAAC;CACnC;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,KAAK,GACL,UAAU,GACV,cAAc,GACd,aAAa,GACb,aAAa,CAAC;AAElB,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,8FAA8F;IAC9F,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,QAAQ,EAAE,yBAAyB,EAAE,CAAC;IACtC,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAUD,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,mBAAmB,EAAE,EAC7B,OAAO,GAAE,0BAA+B,GACvC,kBAAkB,CAoKpB;AAOD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD;
|
|
1
|
+
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../src/knowledge-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EACV,uBAAuB,EAExB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAexE;AAED,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,uBAAuB,CAAC;CACnC;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,KAAK,GACL,UAAU,GACV,cAAc,GACd,aAAa,GACb,aAAa,CAAC;AAElB,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,8FAA8F;IAC9F,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,QAAQ,EAAE,yBAAyB,EAAE,CAAC;IACtC,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAUD,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,mBAAmB,EAAE,EAC7B,OAAO,GAAE,0BAA+B,GACvC,kBAAkB,CAoKpB;AAOD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD;AA+BD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,EACA,yBAAyB,GACzB,uBAAuB,GACvB,gCAAgC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1C,4BAA4B,EAAE,CA0FhC"}
|
package/dist/knowledge-graph.js
CHANGED
|
@@ -176,7 +176,7 @@ function stableStringify(value) {
|
|
|
176
176
|
function sortValue(value) {
|
|
177
177
|
if (Array.isArray(value)) return value.map(sortValue);
|
|
178
178
|
if (value && typeof value === "object") {
|
|
179
|
-
const sorted =
|
|
179
|
+
const sorted = Object.create(null);
|
|
180
180
|
for (const key of Object.keys(value).sort()) sorted[key] = sortValue(value[key]);
|
|
181
181
|
return sorted;
|
|
182
182
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knowledge-graph.js","names":[],"sources":["../src/knowledge-graph.ts"],"sourcesContent":["/**\n * Cross-package knowledge graph (#2863).\n *\n * Each package's own `smrt-knowledge.json` (see `knowledge.ts`) is a\n * per-package projection. Nothing merges them, so cross-package invariants\n * that the scanner manifest already carries — `@crossPackageRef` targets, STI\n * parent/child pairs, junction/hierarchical/polymorphic base usage, and\n * `_smrt_` system-table ownership — live only as prose in the root\n * `AGENTS.md`. This module builds the merged, deterministic root artifact\n * (`.smrt/smrt-knowledge-graph.json`) from whichever per-package artifacts\n * are present, and checks it for staleness the same way `knowledge.ts` checks\n * a package artifact: by comparing recorded `sourceHashes` against the\n * current file content.\n *\n * This module never invents an edge: every edge is a direct read of a field\n * already emitted into a package's `DomainKnowledgeManifest` (`related`,\n * `tableStrategy`, `extends`, `relationshipFeatures`, `tableName`).\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type {\n DomainKnowledgeManifest,\n DomainKnowledgeObject,\n} from '@happyvertical/smrt-types';\n\n/**\n * Discovers every currently-buildable per-package artifact path under\n * `<rootDir>/packages/*`, preferring the published `dist/smrt-knowledge.json`\n * over the local dev `.smrt/smrt-knowledge.json` when both exist. Shared by\n * the generator (which needs the manifest content) and the freshness check\n * (which only needs to know whether the current SET of artifacts matches\n * what the graph was built from — a newly built package, or a package that\n * newly started exporting `./smrt-knowledge.json`, must mark the graph stale\n * even though every artifact the graph already knows about is unchanged).\n */\nexport function discoverKnowledgeArtifactPaths(rootDir: string): string[] {\n const packagesDir = join(rootDir, 'packages');\n if (!existsSync(packagesDir)) return [];\n const paths: string[] = [];\n for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const packageDir = join(packagesDir, entry.name);\n const candidates = [\n join(packageDir, 'dist', 'smrt-knowledge.json'),\n join(packageDir, '.smrt', 'smrt-knowledge.json'),\n ];\n const found = candidates.find((path) => existsSync(path));\n if (found) paths.push(relative(rootDir, found).split('\\\\').join('/'));\n }\n return paths.sort();\n}\n\n/** One package's `smrt-knowledge.json`, located relative to the graph root. */\nexport interface KnowledgeGraphInput {\n /** Path to the artifact, relative to the repo root, POSIX-separated. */\n artifactPath: string;\n manifest: DomainKnowledgeManifest;\n}\n\nexport interface KnowledgeGraphPackageNode {\n name: string;\n version?: string;\n artifactPath: string;\n tags: string[];\n risks: string[];\n summary?: string;\n objectCount: number;\n}\n\nexport interface KnowledgeGraphObjectNode {\n /** `<packageName>#<qualifiedName-or-name>`, this graph's stable object id. */\n id: string;\n packageName: string;\n name: string;\n qualifiedName?: string;\n collection: string;\n tableName?: string;\n tableStrategy?: 'cti' | 'sti';\n extends?: string;\n}\n\nexport type KnowledgeGraphEdgeType =\n | 'crossPackageRef'\n | 'sti'\n | 'junction'\n | 'hierarchical'\n | 'polymorphic'\n | 'systemTable';\n\nexport interface KnowledgeGraphEdge {\n type: KnowledgeGraphEdgeType;\n /** Object id (or `<packageName>#<extends>`-style unresolved target) this edge starts from. */\n from: string;\n /**\n * Object id the edge points to when resolvable against a scanned object;\n * otherwise the raw declared target (e.g. a qualified name this run did\n * not scan), so the edge is still readable rather than dropped.\n */\n to?: string;\n /** Declaring field name, for `crossPackageRef` edges. */\n field?: string;\n}\n\nexport interface SmrtKnowledgeGraph {\n schemaVersion: 1;\n generatedAt: string;\n /** `artifactPath -> sha256(content)`, sorted by key; the freshness input. */\n sourceHashes: Record<string, string>;\n packages: KnowledgeGraphPackageNode[];\n objects: KnowledgeGraphObjectNode[];\n edges: KnowledgeGraphEdge[];\n}\n\nfunction hashContent(content: string): string {\n return createHash('sha256').update(content).digest('hex');\n}\n\nfunction objectId(packageName: string, object: DomainKnowledgeObject): string {\n return `${packageName}#${object.qualifiedName ?? object.name}`;\n}\n\nexport interface BuildKnowledgeGraphOptions {\n /**\n * The graph currently on disk, when regenerating. `generatedAt` is\n * preserved from it rather than reset to the clock whenever the rebuilt\n * graph is otherwise semantically identical — the same pattern\n * `preserveKnowledgeGeneratedAt` uses for per-package artifacts\n * (`packages/core/src/vite-plugin/index.ts`) — so re-running the generator\n * with nothing merged actually changed does not churn the file (#2872\n * review).\n */\n previousGraph?: SmrtKnowledgeGraph;\n}\n\n/** Builds the merged graph. Deterministic: every array is sorted. */\nexport function buildKnowledgeGraph(\n inputs: KnowledgeGraphInput[],\n options: BuildKnowledgeGraphOptions = {},\n): SmrtKnowledgeGraph {\n const sortedInputs = [...inputs].sort((a, b) =>\n a.artifactPath.localeCompare(b.artifactPath),\n );\n\n const sourceHashes: Record<string, string> = {};\n const packages: KnowledgeGraphPackageNode[] = [];\n const objects: KnowledgeGraphObjectNode[] = [];\n const edges: KnowledgeGraphEdge[] = [];\n\n // Qualified-name -> object id, so a crossPackageRef/STI target declared in\n // one package can be resolved to the node emitted by whichever package\n // scanned it, regardless of scan order.\n const idByQualifiedName = new Map<string, string>();\n // Distinct ids per simple name, not a plain array: the same external\n // object can appear in more than one input (e.g. a package artifact and a\n // smrtConsumer aggregate that also scanned it), and counting the\n // duplicate would make resolveTarget's `.length === 1` uniqueness check\n // see a false ambiguity for an otherwise-unique name (#2872 review).\n const idBySimpleName = new Map<string, Set<string>>();\n // A `tableStrategy: 'sti'` object's `extends` is an unqualified simple name\n // (its own package's base class, never a cross-package reference — STI\n // shares one table within a package), so a same-package simple-name match\n // must win over an ambiguous or wrong-package GLOBAL simple-name match:\n // two packages both declaring `Account` must not point one package's\n // subclass at the other package's `Account` (#2863 review).\n const idBySimpleNamePerPackage = new Map<string, Map<string, string>>();\n\n for (const input of sortedInputs) {\n const manifestPackageName =\n input.manifest.packageName ?? input.artifactPath;\n for (const object of input.manifest.objects) {\n // A merged consumer manifest (`smrtConsumer`) aggregates objects\n // scanned from multiple external packages under one local artifact;\n // each object then carries its own `packageName` and that must own\n // the id, or every external object misattributes to the local\n // project and edges resolve to the wrong node (#2872 review).\n const packageName = object.packageName ?? manifestPackageName;\n const perPackage =\n idBySimpleNamePerPackage.get(packageName) ?? new Map<string, string>();\n const id = objectId(packageName, object);\n idByQualifiedName.set(object.qualifiedName ?? id, id);\n const bySimple = idBySimpleName.get(object.name) ?? new Set<string>();\n bySimple.add(id);\n idBySimpleName.set(object.name, bySimple);\n perPackage.set(object.name, id);\n idBySimpleNamePerPackage.set(packageName, perPackage);\n }\n }\n\n const resolveTarget = (\n raw: string | undefined,\n declaringPackageName?: string,\n ): string | undefined => {\n if (!raw) return undefined;\n if (idByQualifiedName.has(raw)) return idByQualifiedName.get(raw);\n if (declaringPackageName) {\n const sameId = idBySimpleNamePerPackage\n .get(declaringPackageName)\n ?.get(raw);\n if (sameId) return sameId;\n }\n const bySimple = idBySimpleName.get(raw);\n if (bySimple && bySimple.size === 1) return [...bySimple][0];\n return undefined;\n };\n\n const seenObjectIds = new Set<string>();\n for (const input of sortedInputs) {\n sourceHashes[input.artifactPath] = hashContent(\n stableStringify(input.manifest),\n );\n const packageName = input.manifest.packageName ?? input.artifactPath;\n\n packages.push({\n name: packageName,\n version: input.manifest.packageVersion,\n artifactPath: input.artifactPath,\n tags: [...input.manifest.tags].sort(),\n risks: [...input.manifest.risks].sort(),\n summary: input.manifest.summary,\n objectCount: input.manifest.objects.length,\n });\n\n for (const object of input.manifest.objects) {\n // See the id-map pass above: an aggregated consumer manifest attributes\n // each object to its own scanned package, not the local project.\n const objectPackageName = object.packageName ?? packageName;\n const id = objectId(objectPackageName, object);\n if (!seenObjectIds.has(id)) {\n seenObjectIds.add(id);\n objects.push({\n id,\n packageName: objectPackageName,\n name: object.name,\n qualifiedName: object.qualifiedName,\n collection: object.collection,\n tableName: object.tableName,\n tableStrategy: object.tableStrategy,\n extends: object.extends,\n });\n }\n\n for (const field of object.relationships) {\n if (field.type !== 'crossPackageRef') continue;\n edges.push({\n type: 'crossPackageRef',\n from: id,\n to: resolveTarget(field.related) ?? field.related,\n field: field.name,\n });\n }\n\n if (object.tableStrategy === 'sti' && object.extends) {\n edges.push({\n type: 'sti',\n from: id,\n to:\n resolveTarget(object.extends, objectPackageName) ??\n `${objectPackageName}#${object.extends}`,\n });\n }\n\n for (const feature of object.relationshipFeatures) {\n if (feature === 'SmrtJunction') {\n edges.push({ type: 'junction', from: id });\n } else if (feature === 'SmrtHierarchical') {\n edges.push({ type: 'hierarchical', from: id });\n } else if (feature === 'SmrtPolymorphicAssociation') {\n edges.push({ type: 'polymorphic', from: id });\n }\n }\n\n if (object.tableName?.startsWith('_smrt_')) {\n edges.push({ type: 'systemTable', from: id, to: object.tableName });\n }\n }\n }\n\n packages.sort((a, b) => a.name.localeCompare(b.name));\n objects.sort((a, b) => a.id.localeCompare(b.id));\n edges.sort((a, b) => {\n const key = (edge: KnowledgeGraphEdge) =>\n `${edge.type}|${edge.from}|${edge.to ?? ''}|${edge.field ?? ''}`;\n return key(a).localeCompare(key(b));\n });\n\n const graph: SmrtKnowledgeGraph = {\n schemaVersion: 1,\n generatedAt: new Date().toISOString(),\n sourceHashes: sortRecord(sourceHashes),\n packages,\n objects,\n edges,\n };\n\n const { previousGraph } = options;\n if (\n previousGraph &&\n semanticGraphJson(previousGraph) === semanticGraphJson(graph)\n ) {\n return { ...graph, generatedAt: previousGraph.generatedAt };\n }\n return graph;\n}\n\nfunction semanticGraphJson(graph: SmrtKnowledgeGraph): string {\n const { generatedAt: _generatedAt, ...rest } = graph;\n return stableStringify(rest);\n}\n\n/** Deterministic `JSON.stringify` — object keys are sorted at every level. */\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(sortValue(value));\n}\n\nfunction sortValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortValue);\n if (value && typeof value === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = sortValue((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n return value;\n}\n\nfunction sortRecord(record: Record<string, string>): Record<string, string> {\n const sorted: Record<string, string> = {};\n for (const key of Object.keys(record).sort()) sorted[key] = record[key];\n return sorted;\n}\n\nexport interface KnowledgeGraphFreshnessIssue {\n severity: 'error' | 'warning';\n code:\n | 'missing-knowledge-graph'\n | 'stale-knowledge-graph'\n | 'knowledge-graph-source-missing';\n message: string;\n file?: string;\n}\n\n/**\n * Checks the merged graph's recorded `sourceHashes` against the current\n * content of each per-package artifact it was built from — the same pattern\n * `knowledge.ts`/`smrt-dev-mcp` use for a single package's `sourceHashes`.\n *\n * `graphPath` and each `artifactPath` in the graph are resolved relative to\n * `rootDir` (the repo root).\n */\nexport function checkKnowledgeGraphFreshness(\n rootDir: string,\n graphPath: string,\n options: { requireArtifact?: boolean } = {},\n): KnowledgeGraphFreshnessIssue[] {\n const issues: KnowledgeGraphFreshnessIssue[] = [];\n const absoluteGraphPath = `${rootDir}/${graphPath}`;\n if (!existsSync(absoluteGraphPath)) {\n if (!options.requireArtifact) return issues;\n issues.push({\n severity: 'error',\n code: 'missing-knowledge-graph',\n message: `${graphPath} does not exist; run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n return issues;\n }\n\n let graph: SmrtKnowledgeGraph;\n try {\n graph = JSON.parse(readFileSync(absoluteGraphPath, 'utf8'));\n } catch (error) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${graphPath} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n file: graphPath,\n });\n return issues;\n }\n\n const recordedPaths = Object.keys(graph.sourceHashes ?? {});\n for (const [artifactPath, expectedHash] of Object.entries(\n graph.sourceHashes ?? {},\n )) {\n const absoluteArtifactPath = `${rootDir}/${artifactPath}`;\n if (!existsSync(absoluteArtifactPath)) {\n issues.push({\n severity: 'error',\n code: 'knowledge-graph-source-missing',\n message: `${artifactPath} used to build ${graphPath} no longer exists`,\n file: graphPath,\n });\n continue;\n }\n let manifest: unknown;\n try {\n manifest = JSON.parse(readFileSync(absoluteArtifactPath, 'utf8'));\n } catch (error) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${artifactPath} is not valid JSON (${\n error instanceof Error ? error.message : String(error)\n }); run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n continue;\n }\n const actualHash = hashContent(stableStringify(manifest));\n if (actualHash !== expectedHash) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${artifactPath} changed since ${graphPath} was generated; run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n }\n }\n\n // A per-key hash comparison alone cannot see an ADDED artifact: a package\n // that just finished its first build, or just started exporting\n // `./smrt-knowledge.json`, contributes a path the graph never recorded, so\n // every recorded hash still matches. Compare the current discoverable SET\n // against the recorded set to catch that (#2863 review).\n const currentPaths = discoverKnowledgeArtifactPaths(rootDir);\n const recordedSet = new Set(recordedPaths);\n const newPaths = currentPaths.filter((path) => !recordedSet.has(path));\n if (newPaths.length > 0) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${newPaths.join(', ')} ${\n newPaths.length === 1\n ? 'is a new package artifact'\n : 'are new package artifacts'\n } not yet merged into ${graphPath}; run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n }\n\n return issues;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"knowledge-graph.js","names":[],"sources":["../src/knowledge-graph.ts"],"sourcesContent":["/**\n * Cross-package knowledge graph (#2863).\n *\n * Each package's own `smrt-knowledge.json` (see `knowledge.ts`) is a\n * per-package projection. Nothing merges them, so cross-package invariants\n * that the scanner manifest already carries — `@crossPackageRef` targets, STI\n * parent/child pairs, junction/hierarchical/polymorphic base usage, and\n * `_smrt_` system-table ownership — live only as prose in the root\n * `AGENTS.md`. This module builds the merged, deterministic root artifact\n * (`.smrt/smrt-knowledge-graph.json`) from whichever per-package artifacts\n * are present, and checks it for staleness the same way `knowledge.ts` checks\n * a package artifact: by comparing recorded `sourceHashes` against the\n * current file content.\n *\n * This module never invents an edge: every edge is a direct read of a field\n * already emitted into a package's `DomainKnowledgeManifest` (`related`,\n * `tableStrategy`, `extends`, `relationshipFeatures`, `tableName`).\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type {\n DomainKnowledgeManifest,\n DomainKnowledgeObject,\n} from '@happyvertical/smrt-types';\n\n/**\n * Discovers every currently-buildable per-package artifact path under\n * `<rootDir>/packages/*`, preferring the published `dist/smrt-knowledge.json`\n * over the local dev `.smrt/smrt-knowledge.json` when both exist. Shared by\n * the generator (which needs the manifest content) and the freshness check\n * (which only needs to know whether the current SET of artifacts matches\n * what the graph was built from — a newly built package, or a package that\n * newly started exporting `./smrt-knowledge.json`, must mark the graph stale\n * even though every artifact the graph already knows about is unchanged).\n */\nexport function discoverKnowledgeArtifactPaths(rootDir: string): string[] {\n const packagesDir = join(rootDir, 'packages');\n if (!existsSync(packagesDir)) return [];\n const paths: string[] = [];\n for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const packageDir = join(packagesDir, entry.name);\n const candidates = [\n join(packageDir, 'dist', 'smrt-knowledge.json'),\n join(packageDir, '.smrt', 'smrt-knowledge.json'),\n ];\n const found = candidates.find((path) => existsSync(path));\n if (found) paths.push(relative(rootDir, found).split('\\\\').join('/'));\n }\n return paths.sort();\n}\n\n/** One package's `smrt-knowledge.json`, located relative to the graph root. */\nexport interface KnowledgeGraphInput {\n /** Path to the artifact, relative to the repo root, POSIX-separated. */\n artifactPath: string;\n manifest: DomainKnowledgeManifest;\n}\n\nexport interface KnowledgeGraphPackageNode {\n name: string;\n version?: string;\n artifactPath: string;\n tags: string[];\n risks: string[];\n summary?: string;\n objectCount: number;\n}\n\nexport interface KnowledgeGraphObjectNode {\n /** `<packageName>#<qualifiedName-or-name>`, this graph's stable object id. */\n id: string;\n packageName: string;\n name: string;\n qualifiedName?: string;\n collection: string;\n tableName?: string;\n tableStrategy?: 'cti' | 'sti';\n extends?: string;\n}\n\nexport type KnowledgeGraphEdgeType =\n | 'crossPackageRef'\n | 'sti'\n | 'junction'\n | 'hierarchical'\n | 'polymorphic'\n | 'systemTable';\n\nexport interface KnowledgeGraphEdge {\n type: KnowledgeGraphEdgeType;\n /** Object id (or `<packageName>#<extends>`-style unresolved target) this edge starts from. */\n from: string;\n /**\n * Object id the edge points to when resolvable against a scanned object;\n * otherwise the raw declared target (e.g. a qualified name this run did\n * not scan), so the edge is still readable rather than dropped.\n */\n to?: string;\n /** Declaring field name, for `crossPackageRef` edges. */\n field?: string;\n}\n\nexport interface SmrtKnowledgeGraph {\n schemaVersion: 1;\n generatedAt: string;\n /** `artifactPath -> sha256(content)`, sorted by key; the freshness input. */\n sourceHashes: Record<string, string>;\n packages: KnowledgeGraphPackageNode[];\n objects: KnowledgeGraphObjectNode[];\n edges: KnowledgeGraphEdge[];\n}\n\nfunction hashContent(content: string): string {\n return createHash('sha256').update(content).digest('hex');\n}\n\nfunction objectId(packageName: string, object: DomainKnowledgeObject): string {\n return `${packageName}#${object.qualifiedName ?? object.name}`;\n}\n\nexport interface BuildKnowledgeGraphOptions {\n /**\n * The graph currently on disk, when regenerating. `generatedAt` is\n * preserved from it rather than reset to the clock whenever the rebuilt\n * graph is otherwise semantically identical — the same pattern\n * `preserveKnowledgeGeneratedAt` uses for per-package artifacts\n * (`packages/core/src/vite-plugin/index.ts`) — so re-running the generator\n * with nothing merged actually changed does not churn the file (#2872\n * review).\n */\n previousGraph?: SmrtKnowledgeGraph;\n}\n\n/** Builds the merged graph. Deterministic: every array is sorted. */\nexport function buildKnowledgeGraph(\n inputs: KnowledgeGraphInput[],\n options: BuildKnowledgeGraphOptions = {},\n): SmrtKnowledgeGraph {\n const sortedInputs = [...inputs].sort((a, b) =>\n a.artifactPath.localeCompare(b.artifactPath),\n );\n\n const sourceHashes: Record<string, string> = {};\n const packages: KnowledgeGraphPackageNode[] = [];\n const objects: KnowledgeGraphObjectNode[] = [];\n const edges: KnowledgeGraphEdge[] = [];\n\n // Qualified-name -> object id, so a crossPackageRef/STI target declared in\n // one package can be resolved to the node emitted by whichever package\n // scanned it, regardless of scan order.\n const idByQualifiedName = new Map<string, string>();\n // Distinct ids per simple name, not a plain array: the same external\n // object can appear in more than one input (e.g. a package artifact and a\n // smrtConsumer aggregate that also scanned it), and counting the\n // duplicate would make resolveTarget's `.length === 1` uniqueness check\n // see a false ambiguity for an otherwise-unique name (#2872 review).\n const idBySimpleName = new Map<string, Set<string>>();\n // A `tableStrategy: 'sti'` object's `extends` is an unqualified simple name\n // (its own package's base class, never a cross-package reference — STI\n // shares one table within a package), so a same-package simple-name match\n // must win over an ambiguous or wrong-package GLOBAL simple-name match:\n // two packages both declaring `Account` must not point one package's\n // subclass at the other package's `Account` (#2863 review).\n const idBySimpleNamePerPackage = new Map<string, Map<string, string>>();\n\n for (const input of sortedInputs) {\n const manifestPackageName =\n input.manifest.packageName ?? input.artifactPath;\n for (const object of input.manifest.objects) {\n // A merged consumer manifest (`smrtConsumer`) aggregates objects\n // scanned from multiple external packages under one local artifact;\n // each object then carries its own `packageName` and that must own\n // the id, or every external object misattributes to the local\n // project and edges resolve to the wrong node (#2872 review).\n const packageName = object.packageName ?? manifestPackageName;\n const perPackage =\n idBySimpleNamePerPackage.get(packageName) ?? new Map<string, string>();\n const id = objectId(packageName, object);\n idByQualifiedName.set(object.qualifiedName ?? id, id);\n const bySimple = idBySimpleName.get(object.name) ?? new Set<string>();\n bySimple.add(id);\n idBySimpleName.set(object.name, bySimple);\n perPackage.set(object.name, id);\n idBySimpleNamePerPackage.set(packageName, perPackage);\n }\n }\n\n const resolveTarget = (\n raw: string | undefined,\n declaringPackageName?: string,\n ): string | undefined => {\n if (!raw) return undefined;\n if (idByQualifiedName.has(raw)) return idByQualifiedName.get(raw);\n if (declaringPackageName) {\n const sameId = idBySimpleNamePerPackage\n .get(declaringPackageName)\n ?.get(raw);\n if (sameId) return sameId;\n }\n const bySimple = idBySimpleName.get(raw);\n if (bySimple && bySimple.size === 1) return [...bySimple][0];\n return undefined;\n };\n\n const seenObjectIds = new Set<string>();\n for (const input of sortedInputs) {\n sourceHashes[input.artifactPath] = hashContent(\n stableStringify(input.manifest),\n );\n const packageName = input.manifest.packageName ?? input.artifactPath;\n\n packages.push({\n name: packageName,\n version: input.manifest.packageVersion,\n artifactPath: input.artifactPath,\n tags: [...input.manifest.tags].sort(),\n risks: [...input.manifest.risks].sort(),\n summary: input.manifest.summary,\n objectCount: input.manifest.objects.length,\n });\n\n for (const object of input.manifest.objects) {\n // See the id-map pass above: an aggregated consumer manifest attributes\n // each object to its own scanned package, not the local project.\n const objectPackageName = object.packageName ?? packageName;\n const id = objectId(objectPackageName, object);\n if (!seenObjectIds.has(id)) {\n seenObjectIds.add(id);\n objects.push({\n id,\n packageName: objectPackageName,\n name: object.name,\n qualifiedName: object.qualifiedName,\n collection: object.collection,\n tableName: object.tableName,\n tableStrategy: object.tableStrategy,\n extends: object.extends,\n });\n }\n\n for (const field of object.relationships) {\n if (field.type !== 'crossPackageRef') continue;\n edges.push({\n type: 'crossPackageRef',\n from: id,\n to: resolveTarget(field.related) ?? field.related,\n field: field.name,\n });\n }\n\n if (object.tableStrategy === 'sti' && object.extends) {\n edges.push({\n type: 'sti',\n from: id,\n to:\n resolveTarget(object.extends, objectPackageName) ??\n `${objectPackageName}#${object.extends}`,\n });\n }\n\n for (const feature of object.relationshipFeatures) {\n if (feature === 'SmrtJunction') {\n edges.push({ type: 'junction', from: id });\n } else if (feature === 'SmrtHierarchical') {\n edges.push({ type: 'hierarchical', from: id });\n } else if (feature === 'SmrtPolymorphicAssociation') {\n edges.push({ type: 'polymorphic', from: id });\n }\n }\n\n if (object.tableName?.startsWith('_smrt_')) {\n edges.push({ type: 'systemTable', from: id, to: object.tableName });\n }\n }\n }\n\n packages.sort((a, b) => a.name.localeCompare(b.name));\n objects.sort((a, b) => a.id.localeCompare(b.id));\n edges.sort((a, b) => {\n const key = (edge: KnowledgeGraphEdge) =>\n `${edge.type}|${edge.from}|${edge.to ?? ''}|${edge.field ?? ''}`;\n return key(a).localeCompare(key(b));\n });\n\n const graph: SmrtKnowledgeGraph = {\n schemaVersion: 1,\n generatedAt: new Date().toISOString(),\n sourceHashes: sortRecord(sourceHashes),\n packages,\n objects,\n edges,\n };\n\n const { previousGraph } = options;\n if (\n previousGraph &&\n semanticGraphJson(previousGraph) === semanticGraphJson(graph)\n ) {\n return { ...graph, generatedAt: previousGraph.generatedAt };\n }\n return graph;\n}\n\nfunction semanticGraphJson(graph: SmrtKnowledgeGraph): string {\n const { generatedAt: _generatedAt, ...rest } = graph;\n return stableStringify(rest);\n}\n\n/** Deterministic `JSON.stringify` — object keys are sorted at every level. */\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(sortValue(value));\n}\n\nfunction sortValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortValue);\n if (value && typeof value === 'object') {\n // Null-prototype target: on an ordinary `{}`, assigning the key\n // `\"__proto__\"` through bracket notation invokes `Object.prototype`'s\n // `__proto__` accessor instead of creating an own property, so that key\n // silently vanishes from the result and `JSON.stringify()` never emits\n // it — two inputs that differ only by an own `\"__proto__\"` key (which\n // `JSON.parse()` does create as a real own property) then serialize\n // identically. An object with no prototype has no such accessor to\n // shadow the assignment, so every own key — including `\"__proto__\"` —\n // becomes a real own data property. `JSON.stringify()` does not care\n // about an object's prototype, only its own enumerable properties, so\n // this changes no other output.\n const sorted: Record<string, unknown> = Object.create(null);\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = sortValue((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n return value;\n}\n\nfunction sortRecord(record: Record<string, string>): Record<string, string> {\n const sorted: Record<string, string> = {};\n for (const key of Object.keys(record).sort()) sorted[key] = record[key];\n return sorted;\n}\n\nexport interface KnowledgeGraphFreshnessIssue {\n severity: 'error' | 'warning';\n code:\n | 'missing-knowledge-graph'\n | 'stale-knowledge-graph'\n | 'knowledge-graph-source-missing';\n message: string;\n file?: string;\n}\n\n/**\n * Checks the merged graph's recorded `sourceHashes` against the current\n * content of each per-package artifact it was built from — the same pattern\n * `knowledge.ts`/`smrt-dev-mcp` use for a single package's `sourceHashes`.\n *\n * `graphPath` and each `artifactPath` in the graph are resolved relative to\n * `rootDir` (the repo root).\n */\nexport function checkKnowledgeGraphFreshness(\n rootDir: string,\n graphPath: string,\n options: { requireArtifact?: boolean } = {},\n): KnowledgeGraphFreshnessIssue[] {\n const issues: KnowledgeGraphFreshnessIssue[] = [];\n const absoluteGraphPath = `${rootDir}/${graphPath}`;\n if (!existsSync(absoluteGraphPath)) {\n if (!options.requireArtifact) return issues;\n issues.push({\n severity: 'error',\n code: 'missing-knowledge-graph',\n message: `${graphPath} does not exist; run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n return issues;\n }\n\n let graph: SmrtKnowledgeGraph;\n try {\n graph = JSON.parse(readFileSync(absoluteGraphPath, 'utf8'));\n } catch (error) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${graphPath} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n file: graphPath,\n });\n return issues;\n }\n\n const recordedPaths = Object.keys(graph.sourceHashes ?? {});\n for (const [artifactPath, expectedHash] of Object.entries(\n graph.sourceHashes ?? {},\n )) {\n const absoluteArtifactPath = `${rootDir}/${artifactPath}`;\n if (!existsSync(absoluteArtifactPath)) {\n issues.push({\n severity: 'error',\n code: 'knowledge-graph-source-missing',\n message: `${artifactPath} used to build ${graphPath} no longer exists`,\n file: graphPath,\n });\n continue;\n }\n let manifest: unknown;\n try {\n manifest = JSON.parse(readFileSync(absoluteArtifactPath, 'utf8'));\n } catch (error) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${artifactPath} is not valid JSON (${\n error instanceof Error ? error.message : String(error)\n }); run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n continue;\n }\n const actualHash = hashContent(stableStringify(manifest));\n if (actualHash !== expectedHash) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${artifactPath} changed since ${graphPath} was generated; run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n }\n }\n\n // A per-key hash comparison alone cannot see an ADDED artifact: a package\n // that just finished its first build, or just started exporting\n // `./smrt-knowledge.json`, contributes a path the graph never recorded, so\n // every recorded hash still matches. Compare the current discoverable SET\n // against the recorded set to catch that (#2863 review).\n const currentPaths = discoverKnowledgeArtifactPaths(rootDir);\n const recordedSet = new Set(recordedPaths);\n const newPaths = currentPaths.filter((path) => !recordedSet.has(path));\n if (newPaths.length > 0) {\n issues.push({\n severity: 'error',\n code: 'stale-knowledge-graph',\n message: `${newPaths.join(', ')} ${\n newPaths.length === 1\n ? 'is a new package artifact'\n : 'are new package artifacts'\n } not yet merged into ${graphPath}; run \\`pnpm knowledge:graph\\``,\n file: graphPath,\n });\n }\n\n return issues;\n}\n"],"mappings":"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"}
|