@happyvertical/smrt-core 0.46.0 → 0.47.1
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 +39 -0
- package/agents/generators.md +19 -5
- package/agents/system-diagnostics.md +43 -0
- package/dist/browser.js +2 -1
- package/dist/change-feed.d.ts +8 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +1 -1
- package/dist/change-feed.js.map +1 -1
- package/dist/decorators/compatibility.d.ts +46 -0
- package/dist/decorators/compatibility.d.ts.map +1 -1
- package/dist/decorators/compatibility.js +48 -5
- package/dist/decorators/compatibility.js.map +1 -1
- package/dist/decorators/index.d.ts +119 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +52 -8
- package/dist/decorators/index.js.map +1 -1
- package/dist/generators/custom-action.d.ts +379 -2
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +694 -17
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -2
- package/dist/generators/preflight-route.d.ts +12 -10
- package/dist/generators/preflight-route.d.ts.map +1 -1
- package/dist/generators/preflight-route.js +46 -14
- package/dist/generators/preflight-route.js.map +1 -1
- package/dist/generators/rest.d.ts +44 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +84 -9
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +59 -15
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +125 -53
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +184 -53
- package/dist/postgres-permissions.d.ts +9 -1
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +127 -13
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/shared-state.d.ts +19 -0
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +16 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry.d.ts +136 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +231 -1
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +18 -16
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +59 -6
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/smrt-knowledge.json +42 -35
- package/dist/system/diagnostics.d.ts +299 -0
- package/dist/system/diagnostics.d.ts.map +1 -0
- package/dist/system/diagnostics.js +530 -0
- package/dist/system/diagnostics.js.map +1 -0
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +2 -1
- package/dist/utils/scanner-module.d.ts +16 -0
- package/dist/utils/scanner-module.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +10 -8
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +34 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +82 -3
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +28 -2
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +121 -51
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +1 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/dist/vite-plugin.js +2 -2
- package/package.json +16 -11
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { getChangesSince } from "../change-feed.js";
|
|
2
|
+
import { getDatabaseEngine, tableExists } from "./compatibility.js";
|
|
3
|
+
//#region src/system/diagnostics.ts
|
|
4
|
+
/** Physical `_smrt_*` tables the diagnostics reader knows about. */
|
|
5
|
+
var SYSTEM_DIAGNOSTICS_TABLES = {
|
|
6
|
+
migrations: "_smrt_schema_migrations",
|
|
7
|
+
dispatch: "_smrt_dispatch",
|
|
8
|
+
dispatchSubscriptions: "_smrt_dispatch_subscriptions",
|
|
9
|
+
changes: "_smrt_changes",
|
|
10
|
+
jobs: "_smrt_jobs",
|
|
11
|
+
jobEvents: "_smrt_job_events",
|
|
12
|
+
workers: "_smrt_workers",
|
|
13
|
+
schedules: "_smrt_agent_schedules",
|
|
14
|
+
registry: "_smrt_registry"
|
|
15
|
+
};
|
|
16
|
+
/** Default page size for row-listing categories. */
|
|
17
|
+
var DIAGNOSTICS_DEFAULT_LIMIT = 50;
|
|
18
|
+
/**
|
|
19
|
+
* Bounded, SELECT-only migration history read.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately does not use `MigrationTracker` — its history helpers run
|
|
22
|
+
* DDL-initializing bootstrap — and reads `_smrt_schema_migrations` directly.
|
|
23
|
+
*/
|
|
24
|
+
async function readMigrationStatus(db, options = {}) {
|
|
25
|
+
const table = SYSTEM_DIAGNOSTICS_TABLES.migrations;
|
|
26
|
+
if (!await tableExists(db, table)) return unavailable(table, "table-missing");
|
|
27
|
+
const limit = boundedLimit(options.limit);
|
|
28
|
+
try {
|
|
29
|
+
const p = placeholders(db);
|
|
30
|
+
const countRows = queryRows(await db.query(`SELECT status, COUNT(*) AS total FROM ${table} GROUP BY status`));
|
|
31
|
+
const byStatus = {};
|
|
32
|
+
let total = 0;
|
|
33
|
+
for (const row of countRows) {
|
|
34
|
+
const status = String(row.status ?? "unknown");
|
|
35
|
+
const count = toCount(row.total);
|
|
36
|
+
byStatus[status] = count;
|
|
37
|
+
total += count;
|
|
38
|
+
}
|
|
39
|
+
const completed = byStatus.completed ?? 0;
|
|
40
|
+
const running = byStatus.running ?? 0;
|
|
41
|
+
const failed = byStatus.failed ?? 0;
|
|
42
|
+
const rolledBack = byStatus.rolled_back ?? 0;
|
|
43
|
+
const latest = queryRows(await db.query(`SELECT ${MIGRATION_COLUMNS} FROM ${table}
|
|
44
|
+
WHERE status = 'completed'
|
|
45
|
+
ORDER BY applied_at DESC, id ASC
|
|
46
|
+
LIMIT ${p(1)}`, limit)).map(toMigrationRow);
|
|
47
|
+
const failedRows = queryRows(await db.query(`SELECT ${MIGRATION_COLUMNS} FROM ${table}
|
|
48
|
+
WHERE status = 'failed'
|
|
49
|
+
ORDER BY applied_at DESC, id ASC
|
|
50
|
+
LIMIT ${p(1)}`, limit)).map(toMigrationRow);
|
|
51
|
+
return {
|
|
52
|
+
available: true,
|
|
53
|
+
summary: {
|
|
54
|
+
total,
|
|
55
|
+
byStatus,
|
|
56
|
+
completed,
|
|
57
|
+
running,
|
|
58
|
+
failed,
|
|
59
|
+
rolledBack
|
|
60
|
+
},
|
|
61
|
+
latest,
|
|
62
|
+
failed: failedRows
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return unavailable(table, "read-error");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var MIGRATION_COLUMNS = [
|
|
69
|
+
"id",
|
|
70
|
+
"name",
|
|
71
|
+
"version",
|
|
72
|
+
"status",
|
|
73
|
+
"applied_at",
|
|
74
|
+
"execution_time_ms",
|
|
75
|
+
"attempts",
|
|
76
|
+
"error_message",
|
|
77
|
+
"package_name",
|
|
78
|
+
"source_file",
|
|
79
|
+
"is_reversible",
|
|
80
|
+
"rolled_back_at",
|
|
81
|
+
"applied_by",
|
|
82
|
+
"batch"
|
|
83
|
+
].join(", ");
|
|
84
|
+
function toMigrationRow(row) {
|
|
85
|
+
return {
|
|
86
|
+
id: String(row.id ?? ""),
|
|
87
|
+
name: String(row.name ?? ""),
|
|
88
|
+
version: String(row.version ?? ""),
|
|
89
|
+
status: String(row.status ?? ""),
|
|
90
|
+
appliedAt: toIso(row.applied_at),
|
|
91
|
+
executionTimeMs: toNumber(row.execution_time_ms),
|
|
92
|
+
attempts: toNumber(row.attempts),
|
|
93
|
+
errorMessage: toNullableString(row.error_message),
|
|
94
|
+
packageName: toNullableString(row.package_name),
|
|
95
|
+
sourceFile: toNullableString(row.source_file),
|
|
96
|
+
isReversible: toBoolean(row.is_reversible),
|
|
97
|
+
rolledBackAt: toIso(row.rolled_back_at),
|
|
98
|
+
appliedBy: toNullableString(row.applied_by),
|
|
99
|
+
batch: toNumber(row.batch)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Read `_smrt_jobs`, `_smrt_workers`, and a `_smrt_job_events` count. */
|
|
103
|
+
async function readJobHealth(db, options = {}) {
|
|
104
|
+
const table = SYSTEM_DIAGNOSTICS_TABLES.jobs;
|
|
105
|
+
if (!await tableExists(db, table)) return unavailable(table, "table-missing");
|
|
106
|
+
const limit = boundedLimit(options.limit);
|
|
107
|
+
const staleAfterMs = options.staleAfterMs ?? 5 * 6e4;
|
|
108
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
109
|
+
const staleCutoff = new Date(now.getTime() - staleAfterMs);
|
|
110
|
+
try {
|
|
111
|
+
const p = placeholders(db);
|
|
112
|
+
const statusRows = queryRows(await db.query(`SELECT status, COUNT(*) AS total FROM ${table} GROUP BY status`));
|
|
113
|
+
const byStatus = {};
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const row of statusRows) {
|
|
116
|
+
const status = String(row.status ?? "unknown");
|
|
117
|
+
const count = toCount(row.total);
|
|
118
|
+
byStatus[status] = count;
|
|
119
|
+
total += count;
|
|
120
|
+
}
|
|
121
|
+
const failed = byStatus.failed ?? 0;
|
|
122
|
+
const workersTable = SYSTEM_DIAGNOSTICS_TABLES.workers;
|
|
123
|
+
const workersAvailable = await tableExists(db, workersTable);
|
|
124
|
+
const stuck = toCount(queryRows(workersAvailable ? await db.query(`SELECT COUNT(*) AS total FROM ${table} AS j
|
|
125
|
+
WHERE j.status = 'running'
|
|
126
|
+
AND (j.worker_heartbeat IS NULL OR j.worker_heartbeat < ${p(1)})
|
|
127
|
+
AND NOT EXISTS (
|
|
128
|
+
SELECT 1 FROM ${workersTable} AS w
|
|
129
|
+
WHERE w.worker_id = j.worker_id
|
|
130
|
+
AND w.lease_expires_at > ${p(2)}
|
|
131
|
+
)`, staleCutoff.toISOString(), now.toISOString()) : await db.query(`SELECT COUNT(*) AS total FROM ${table}
|
|
132
|
+
WHERE status = 'running'
|
|
133
|
+
AND (worker_heartbeat IS NULL OR worker_heartbeat < ${p(1)})`, staleCutoff.toISOString()))[0]?.total);
|
|
134
|
+
const jobs = queryRows(await db.query(`SELECT ${JOB_COLUMNS} FROM ${table}
|
|
135
|
+
ORDER BY run_at DESC, id ASC
|
|
136
|
+
LIMIT ${p(1)}`, limit)).map(toJobHealthRow);
|
|
137
|
+
let workers = [];
|
|
138
|
+
if (workersAvailable) workers = queryRows(await db.query(`SELECT ${WORKER_COLUMNS} FROM ${workersTable}
|
|
139
|
+
ORDER BY heartbeat_at DESC, id ASC
|
|
140
|
+
LIMIT ${p(1)}`, limit)).map(toWorkerHealthRow);
|
|
141
|
+
let eventCount = null;
|
|
142
|
+
const eventsTable = SYSTEM_DIAGNOSTICS_TABLES.jobEvents;
|
|
143
|
+
if (await tableExists(db, eventsTable)) eventCount = toNumber(queryRows(await db.query(`SELECT COUNT(*) AS total FROM ${eventsTable}`))[0]?.total);
|
|
144
|
+
return {
|
|
145
|
+
available: true,
|
|
146
|
+
summary: {
|
|
147
|
+
total,
|
|
148
|
+
byStatus,
|
|
149
|
+
failed,
|
|
150
|
+
stuck,
|
|
151
|
+
eventCount
|
|
152
|
+
},
|
|
153
|
+
jobs,
|
|
154
|
+
workers
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return unavailable(table, "read-error");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
var JOB_COLUMNS = [
|
|
161
|
+
"id",
|
|
162
|
+
"tenant_id",
|
|
163
|
+
"queue",
|
|
164
|
+
"object_type",
|
|
165
|
+
"object_id",
|
|
166
|
+
"method",
|
|
167
|
+
"status",
|
|
168
|
+
"attempts",
|
|
169
|
+
"max_attempts",
|
|
170
|
+
"run_at",
|
|
171
|
+
"started_at",
|
|
172
|
+
"completed_at",
|
|
173
|
+
"last_error",
|
|
174
|
+
"task_id",
|
|
175
|
+
"task_owner_id",
|
|
176
|
+
"worker_id",
|
|
177
|
+
"worker_heartbeat"
|
|
178
|
+
].join(", ");
|
|
179
|
+
var WORKER_COLUMNS = [
|
|
180
|
+
"id",
|
|
181
|
+
"worker_id",
|
|
182
|
+
"pid",
|
|
183
|
+
"hostname",
|
|
184
|
+
"started_at",
|
|
185
|
+
"heartbeat_at",
|
|
186
|
+
"lease_expires_at",
|
|
187
|
+
"status"
|
|
188
|
+
].join(", ");
|
|
189
|
+
function toJobHealthRow(row) {
|
|
190
|
+
return {
|
|
191
|
+
id: String(row.id ?? ""),
|
|
192
|
+
tenantId: toNullableString(row.tenant_id),
|
|
193
|
+
queue: String(row.queue ?? ""),
|
|
194
|
+
objectType: String(row.object_type ?? ""),
|
|
195
|
+
objectId: toNullableString(row.object_id),
|
|
196
|
+
method: String(row.method ?? ""),
|
|
197
|
+
status: String(row.status ?? ""),
|
|
198
|
+
attempts: toNumber(row.attempts),
|
|
199
|
+
maxAttempts: toNumber(row.max_attempts),
|
|
200
|
+
runAt: toIso(row.run_at),
|
|
201
|
+
startedAt: toIso(row.started_at),
|
|
202
|
+
completedAt: toIso(row.completed_at),
|
|
203
|
+
lastError: toNullableString(row.last_error),
|
|
204
|
+
taskId: toNullableString(row.task_id),
|
|
205
|
+
taskOwnerId: toNullableString(row.task_owner_id),
|
|
206
|
+
workerId: toNullableString(row.worker_id),
|
|
207
|
+
workerHeartbeat: toIso(row.worker_heartbeat)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function toWorkerHealthRow(row) {
|
|
211
|
+
return {
|
|
212
|
+
id: String(row.id ?? ""),
|
|
213
|
+
workerId: String(row.worker_id ?? ""),
|
|
214
|
+
pid: toNumber(row.pid),
|
|
215
|
+
hostname: toNullableString(row.hostname),
|
|
216
|
+
startedAt: toIso(row.started_at),
|
|
217
|
+
heartbeatAt: toIso(row.heartbeat_at),
|
|
218
|
+
leaseExpiresAt: toIso(row.lease_expires_at),
|
|
219
|
+
status: String(row.status ?? "")
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** Read `_smrt_agent_schedules` (owned by `@happyvertical/smrt-agents`). */
|
|
223
|
+
async function readScheduleHealth(db, options = {}) {
|
|
224
|
+
const table = SYSTEM_DIAGNOSTICS_TABLES.schedules;
|
|
225
|
+
if (!await tableExists(db, table)) return unavailable(table, "table-missing");
|
|
226
|
+
const limit = boundedLimit(options.limit);
|
|
227
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
228
|
+
try {
|
|
229
|
+
const p = placeholders(db);
|
|
230
|
+
const enabledRows = queryRows(await db.query(`SELECT enabled, status, COUNT(*) AS total FROM ${table} GROUP BY enabled, status`));
|
|
231
|
+
let total = 0;
|
|
232
|
+
let enabled = 0;
|
|
233
|
+
let active = 0;
|
|
234
|
+
let errored = 0;
|
|
235
|
+
for (const row of enabledRows) {
|
|
236
|
+
const count = toCount(row.total);
|
|
237
|
+
total += count;
|
|
238
|
+
if (toBoolean(row.enabled)) enabled += count;
|
|
239
|
+
const status = String(row.status ?? "");
|
|
240
|
+
if (status === "active") active += count;
|
|
241
|
+
if (status === "error") errored += count;
|
|
242
|
+
}
|
|
243
|
+
const failed = toCount(queryRows(await db.query(`SELECT COUNT(*) AS total FROM ${table} WHERE last_status = 'failed'`))[0]?.total);
|
|
244
|
+
const overdue = toCount(queryRows(await db.query(`SELECT COUNT(*) AS total FROM ${table}
|
|
245
|
+
WHERE enabled = true AND status = 'active' AND next_run IS NOT NULL AND next_run <= ${p(1)}`, now.toISOString()))[0]?.total);
|
|
246
|
+
const schedules = queryRows(await db.query(`SELECT ${SCHEDULE_COLUMNS} FROM ${table}
|
|
247
|
+
ORDER BY next_run ASC, id ASC
|
|
248
|
+
LIMIT ${p(1)}`, limit)).map(toScheduleHealthRow);
|
|
249
|
+
return {
|
|
250
|
+
available: true,
|
|
251
|
+
summary: {
|
|
252
|
+
total,
|
|
253
|
+
enabled,
|
|
254
|
+
active,
|
|
255
|
+
errored,
|
|
256
|
+
failed,
|
|
257
|
+
overdue
|
|
258
|
+
},
|
|
259
|
+
schedules
|
|
260
|
+
};
|
|
261
|
+
} catch {
|
|
262
|
+
return unavailable(table, "read-error");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
var SCHEDULE_COLUMNS = [
|
|
266
|
+
"id",
|
|
267
|
+
"tenant_id",
|
|
268
|
+
"agent_type",
|
|
269
|
+
"agent_id",
|
|
270
|
+
"cron",
|
|
271
|
+
"timezone",
|
|
272
|
+
"enabled",
|
|
273
|
+
"status",
|
|
274
|
+
"last_run",
|
|
275
|
+
"next_run",
|
|
276
|
+
"last_status",
|
|
277
|
+
"last_error",
|
|
278
|
+
"run_count",
|
|
279
|
+
"success_count",
|
|
280
|
+
"failure_count",
|
|
281
|
+
"max_concurrent",
|
|
282
|
+
"running_count",
|
|
283
|
+
"timeout",
|
|
284
|
+
"method"
|
|
285
|
+
].join(", ");
|
|
286
|
+
function toScheduleHealthRow(row) {
|
|
287
|
+
return {
|
|
288
|
+
id: String(row.id ?? ""),
|
|
289
|
+
tenantId: toNullableString(row.tenant_id),
|
|
290
|
+
agentType: String(row.agent_type ?? ""),
|
|
291
|
+
agentId: toNullableString(row.agent_id),
|
|
292
|
+
cron: String(row.cron ?? ""),
|
|
293
|
+
timezone: String(row.timezone ?? ""),
|
|
294
|
+
enabled: toBoolean(row.enabled),
|
|
295
|
+
status: String(row.status ?? ""),
|
|
296
|
+
lastRun: toIso(row.last_run),
|
|
297
|
+
nextRun: toIso(row.next_run),
|
|
298
|
+
lastStatus: toNullableString(row.last_status),
|
|
299
|
+
lastError: toNullableString(row.last_error),
|
|
300
|
+
runCount: toNumber(row.run_count),
|
|
301
|
+
successCount: toNumber(row.success_count),
|
|
302
|
+
failureCount: toNumber(row.failure_count),
|
|
303
|
+
maxConcurrent: toNumber(row.max_concurrent),
|
|
304
|
+
runningCount: toNumber(row.running_count),
|
|
305
|
+
timeout: toNumber(row.timeout),
|
|
306
|
+
method: String(row.method ?? "")
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** Read `_smrt_dispatch` and `_smrt_dispatch_subscriptions`. */
|
|
310
|
+
async function readDispatchHealth(db, options = {}) {
|
|
311
|
+
const table = SYSTEM_DIAGNOSTICS_TABLES.dispatch;
|
|
312
|
+
if (!await tableExists(db, table)) return unavailable(table, "table-missing");
|
|
313
|
+
const limit = boundedLimit(options.limit);
|
|
314
|
+
try {
|
|
315
|
+
const p = placeholders(db);
|
|
316
|
+
const statusRows = queryRows(await db.query(`SELECT status, COUNT(*) AS total FROM ${table} GROUP BY status`));
|
|
317
|
+
const byStatus = {};
|
|
318
|
+
let total = 0;
|
|
319
|
+
for (const row of statusRows) {
|
|
320
|
+
const status = String(row.status ?? "unknown");
|
|
321
|
+
const count = toCount(row.total);
|
|
322
|
+
byStatus[status] = count;
|
|
323
|
+
total += count;
|
|
324
|
+
}
|
|
325
|
+
const pending = byStatus.pending ?? 0;
|
|
326
|
+
const processing = byStatus.processing ?? 0;
|
|
327
|
+
const failed = byStatus.failed ?? 0;
|
|
328
|
+
const completed = byStatus.completed ?? 0;
|
|
329
|
+
const dispatches = queryRows(await db.query(`SELECT ${DISPATCH_COLUMNS} FROM ${table}
|
|
330
|
+
ORDER BY created_at DESC, id ASC
|
|
331
|
+
LIMIT ${p(1)}`, limit)).map(toDispatchHealthRow);
|
|
332
|
+
let subscriptions = [];
|
|
333
|
+
let subscriptionCount = 0;
|
|
334
|
+
const subsTable = SYSTEM_DIAGNOSTICS_TABLES.dispatchSubscriptions;
|
|
335
|
+
if (await tableExists(db, subsTable)) {
|
|
336
|
+
subscriptionCount = toCount(queryRows(await db.query(`SELECT COUNT(*) AS total FROM ${subsTable}`))[0]?.total);
|
|
337
|
+
subscriptions = queryRows(await db.query(`SELECT ${SUBSCRIPTION_COLUMNS} FROM ${subsTable}
|
|
338
|
+
ORDER BY created_at DESC, id ASC
|
|
339
|
+
LIMIT ${p(1)}`, limit)).map(toSubscriptionHealthRow);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
available: true,
|
|
343
|
+
summary: {
|
|
344
|
+
total,
|
|
345
|
+
byStatus,
|
|
346
|
+
pending,
|
|
347
|
+
processing,
|
|
348
|
+
failed,
|
|
349
|
+
completed,
|
|
350
|
+
subscriptions: subscriptionCount
|
|
351
|
+
},
|
|
352
|
+
dispatches,
|
|
353
|
+
subscriptions
|
|
354
|
+
};
|
|
355
|
+
} catch {
|
|
356
|
+
return unavailable(table, "read-error");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
var DISPATCH_COLUMNS = [
|
|
360
|
+
"id",
|
|
361
|
+
"type",
|
|
362
|
+
"source",
|
|
363
|
+
"source_id",
|
|
364
|
+
"status",
|
|
365
|
+
"attempts",
|
|
366
|
+
"last_error",
|
|
367
|
+
"processed_at",
|
|
368
|
+
"processed_by",
|
|
369
|
+
"target_subscriber",
|
|
370
|
+
"correlation_id",
|
|
371
|
+
"tenant_id",
|
|
372
|
+
"created_at",
|
|
373
|
+
"updated_at"
|
|
374
|
+
].join(", ");
|
|
375
|
+
var SUBSCRIPTION_COLUMNS = [
|
|
376
|
+
"id",
|
|
377
|
+
"signal_type",
|
|
378
|
+
"subscriber",
|
|
379
|
+
"handler",
|
|
380
|
+
"delivery",
|
|
381
|
+
"enabled",
|
|
382
|
+
"tenant_id",
|
|
383
|
+
"created_at",
|
|
384
|
+
"updated_at"
|
|
385
|
+
].join(", ");
|
|
386
|
+
function toDispatchHealthRow(row) {
|
|
387
|
+
return {
|
|
388
|
+
id: String(row.id ?? ""),
|
|
389
|
+
type: String(row.type ?? ""),
|
|
390
|
+
source: String(row.source ?? ""),
|
|
391
|
+
sourceId: toNullableString(row.source_id),
|
|
392
|
+
status: String(row.status ?? ""),
|
|
393
|
+
attempts: toNumber(row.attempts),
|
|
394
|
+
lastError: toNullableString(row.last_error),
|
|
395
|
+
processedAt: toIso(row.processed_at),
|
|
396
|
+
processedBy: toNullableString(row.processed_by),
|
|
397
|
+
targetSubscriber: toNullableString(row.target_subscriber),
|
|
398
|
+
correlationId: toNullableString(row.correlation_id),
|
|
399
|
+
tenantId: toNullableString(row.tenant_id),
|
|
400
|
+
createdAt: toIso(row.created_at),
|
|
401
|
+
updatedAt: toIso(row.updated_at)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function toSubscriptionHealthRow(row) {
|
|
405
|
+
return {
|
|
406
|
+
id: String(row.id ?? ""),
|
|
407
|
+
signalType: String(row.signal_type ?? ""),
|
|
408
|
+
subscriber: String(row.subscriber ?? ""),
|
|
409
|
+
handler: String(row.handler ?? ""),
|
|
410
|
+
delivery: String(row.delivery ?? ""),
|
|
411
|
+
enabled: toBoolean(row.enabled),
|
|
412
|
+
tenantId: toNullableString(row.tenant_id),
|
|
413
|
+
createdAt: toIso(row.created_at),
|
|
414
|
+
updatedAt: toIso(row.updated_at)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Read the recent change feed via the canonical {@link getChangesSince}
|
|
419
|
+
* reader with `drain: false`, so the PostgreSQL staged-entry drain (a write
|
|
420
|
+
* through `_smrt_drain_changes()`) never runs from a diagnostic. Entries still
|
|
421
|
+
* staged by an open caller transaction stay invisible until a writer drains
|
|
422
|
+
* them. A missing `_smrt_changes` table degrades to category-unavailable.
|
|
423
|
+
*/
|
|
424
|
+
async function readRecentChanges(db, options = {}) {
|
|
425
|
+
const table = SYSTEM_DIAGNOSTICS_TABLES.changes;
|
|
426
|
+
if (!await tableExists(db, table)) return unavailable(table, "table-missing");
|
|
427
|
+
try {
|
|
428
|
+
const page = await getChangesSince(db, {
|
|
429
|
+
since: options.since ?? 0,
|
|
430
|
+
drain: false,
|
|
431
|
+
tables: options.tables,
|
|
432
|
+
tenantId: options.tenantId,
|
|
433
|
+
limit: boundedLimit(options.limit, 200)
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
available: true,
|
|
437
|
+
changes: page.changes,
|
|
438
|
+
cursor: page.cursor,
|
|
439
|
+
...page.resyncRequired !== void 0 ? {
|
|
440
|
+
resyncRequired: page.resyncRequired,
|
|
441
|
+
resyncCursor: page.resyncCursor
|
|
442
|
+
} : {},
|
|
443
|
+
count: page.changes.length
|
|
444
|
+
};
|
|
445
|
+
} catch {
|
|
446
|
+
return unavailable(table, "read-error");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function readRegistryDrift(db) {
|
|
450
|
+
const table = SYSTEM_DIAGNOSTICS_TABLES.registry;
|
|
451
|
+
const stillPresent = await tableExists(db, table);
|
|
452
|
+
return {
|
|
453
|
+
available: false,
|
|
454
|
+
reason: "retired",
|
|
455
|
+
message: stillPresent ? `The ${table} table is retired (SMRT system schema 1.10.1) and is no longer written or read; a legacy empty table remains on this database and can be dropped. No registry drift is reported.` : `The ${table} table is retired (SMRT system schema 1.10.1) and is no longer written or read; no such table was found on this database. No registry drift is reported.`,
|
|
456
|
+
tableName: table,
|
|
457
|
+
stillPresent
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/** Read every diagnostics category in one pass (SELECT-only). */
|
|
461
|
+
async function readSystemDiagnostics(db, options = {}) {
|
|
462
|
+
const [migrations, jobs, schedules, dispatch, changes, registry] = await Promise.all([
|
|
463
|
+
readMigrationStatus(db, options.migrations),
|
|
464
|
+
readJobHealth(db, options.jobs),
|
|
465
|
+
readScheduleHealth(db, options.schedules),
|
|
466
|
+
readDispatchHealth(db, options.dispatch),
|
|
467
|
+
readRecentChanges(db, options.changes),
|
|
468
|
+
readRegistryDrift(db)
|
|
469
|
+
]);
|
|
470
|
+
return {
|
|
471
|
+
migrations,
|
|
472
|
+
jobs,
|
|
473
|
+
schedules,
|
|
474
|
+
dispatch,
|
|
475
|
+
changes,
|
|
476
|
+
registry
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function unavailable(tableName, reason, message) {
|
|
480
|
+
return {
|
|
481
|
+
available: false,
|
|
482
|
+
reason,
|
|
483
|
+
message: message ?? (reason === "retired" ? `${tableName} is retired and no longer read.` : reason === "table-missing" ? `The ${tableName} system table is not present on this database.` : `Reading ${tableName} failed; the live diagnostic is unavailable.`),
|
|
484
|
+
tableName
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function boundedLimit(value, fallback = 50) {
|
|
488
|
+
if (!Number.isFinite(value) || value === void 0) return fallback;
|
|
489
|
+
return Math.min(Math.max(Math.floor(value), 1), 500);
|
|
490
|
+
}
|
|
491
|
+
function placeholders(db) {
|
|
492
|
+
return getDatabaseEngine(db) === "postgres" ? (index) => `$${index}` : () => "?";
|
|
493
|
+
}
|
|
494
|
+
function queryRows(result) {
|
|
495
|
+
if (Array.isArray(result)) return result;
|
|
496
|
+
if (result && typeof result === "object" && "rows" in result) {
|
|
497
|
+
const rows = result.rows;
|
|
498
|
+
if (Array.isArray(rows)) return rows;
|
|
499
|
+
}
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
function toNullableString(value) {
|
|
503
|
+
if (value === null || value === void 0) return null;
|
|
504
|
+
return String(value);
|
|
505
|
+
}
|
|
506
|
+
function toNumber(value) {
|
|
507
|
+
if (value === null || value === void 0) return null;
|
|
508
|
+
const numeric = Number(value);
|
|
509
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
510
|
+
}
|
|
511
|
+
/** Count aggregation: `COUNT(*)` always yields a number (0 when absent). */
|
|
512
|
+
function toCount(value) {
|
|
513
|
+
return toNumber(value) ?? 0;
|
|
514
|
+
}
|
|
515
|
+
function toBoolean(value) {
|
|
516
|
+
if (value === null || value === void 0) return null;
|
|
517
|
+
if (value === true || value === "true" || value === "t" || value === "1" || value === 1) return true;
|
|
518
|
+
if (value === false || value === "false" || value === "f" || value === "0" || value === 0) return false;
|
|
519
|
+
return Boolean(value);
|
|
520
|
+
}
|
|
521
|
+
function toIso(value) {
|
|
522
|
+
if (value === null || value === void 0) return null;
|
|
523
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
524
|
+
const text = String(value);
|
|
525
|
+
return text.length > 0 ? text : null;
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
export { DIAGNOSTICS_DEFAULT_LIMIT, SYSTEM_DIAGNOSTICS_TABLES, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics };
|
|
529
|
+
|
|
530
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","names":[],"sources":["../../src/system/diagnostics.ts"],"sourcesContent":["/**\n * SMRT system-diagnostics reader.\n *\n * A typed, SELECT-only view over the framework-owned `_smrt_*` system tables\n * for development tooling (smrt-dev-mcp runtime diagnostics). Every read is a\n * bounded SELECT against explicitly projected safe columns; sensitive columns\n * are never selected and no statement here writes.\n *\n * The reader takes a `DatabaseInterface` (connection resolution lives with the\n * caller) and degrades per category to a `CategoryUnavailable` result when its\n * table is missing or a read fails — it never throws for missing tables and\n * never fabricates data. `_smrt_registry` is a retired system table\n * ({@link RETIRED_SYSTEM_TABLES}) and is reported as retired rather than\n * queried.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { type ChangeFeedEntry, getChangesSince } from '../change-feed.js';\nimport { getDatabaseEngine, tableExists } from './compatibility.js';\n\n/** Physical `_smrt_*` tables the diagnostics reader knows about. */\nexport const SYSTEM_DIAGNOSTICS_TABLES = {\n migrations: '_smrt_schema_migrations',\n dispatch: '_smrt_dispatch',\n dispatchSubscriptions: '_smrt_dispatch_subscriptions',\n changes: '_smrt_changes',\n jobs: '_smrt_jobs',\n jobEvents: '_smrt_job_events',\n workers: '_smrt_workers',\n schedules: '_smrt_agent_schedules',\n registry: '_smrt_registry',\n} as const;\n\n/** Default page size for row-listing categories. */\nexport const DIAGNOSTICS_DEFAULT_LIMIT = 50;\n\n/**\n * A category whose table is missing (or whose read failed) on the live\n * database. Callers must surface this as \"category unavailable\" — never as an\n * empty-but-healthy category and never as a fabricated value.\n */\nexport interface CategoryUnavailable {\n available: false;\n reason: 'table-missing' | 'retired' | 'read-error';\n message: string;\n tableName?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Migration status\n// ---------------------------------------------------------------------------\n\n/** Safe projection of one `_smrt_schema_migrations` row. */\nexport interface MigrationStatusRow {\n id: string;\n name: string;\n version: string;\n status: string;\n appliedAt: string | null;\n executionTimeMs: number | null;\n attempts: number | null;\n errorMessage: string | null;\n packageName: string | null;\n sourceFile: string | null;\n isReversible: boolean | null;\n rolledBackAt: string | null;\n appliedBy: string | null;\n batch: number | null;\n}\n\n/**\n * Counts use `MigrationTracker`'s real status vocabulary: `completed`\n * (successfully applied), `running` (in flight), `failed`, and `rolled_back`.\n */\nexport interface MigrationStatusSummary {\n total: number;\n byStatus: Record<string, number>;\n completed: number;\n running: number;\n failed: number;\n rolledBack: number;\n}\n\nexport type MigrationStatusResult =\n | {\n available: true;\n summary: MigrationStatusSummary;\n /** Most recently applied migrations, newest first. */\n latest: MigrationStatusRow[];\n /** Failed migrations, newest first. */\n failed: MigrationStatusRow[];\n }\n | CategoryUnavailable;\n\nexport interface MigrationStatusOptions {\n /** Row budget for `latest`/`failed` lists (default {@link DIAGNOSTICS_DEFAULT_LIMIT}). */\n limit?: number;\n}\n\n/**\n * Bounded, SELECT-only migration history read.\n *\n * Deliberately does not use `MigrationTracker` — its history helpers run\n * DDL-initializing bootstrap — and reads `_smrt_schema_migrations` directly.\n */\nexport async function readMigrationStatus(\n db: DatabaseInterface,\n options: MigrationStatusOptions = {},\n): Promise<MigrationStatusResult> {\n const table = SYSTEM_DIAGNOSTICS_TABLES.migrations;\n if (!(await tableExists(db, table))) {\n return unavailable(table, 'table-missing');\n }\n const limit = boundedLimit(options.limit);\n\n try {\n const p = placeholders(db);\n const countRows = queryRows(\n await db.query(\n `SELECT status, COUNT(*) AS total FROM ${table} GROUP BY status`,\n ),\n );\n const byStatus: Record<string, number> = {};\n let total = 0;\n for (const row of countRows) {\n const status = String(row.status ?? 'unknown');\n const count = toCount(row.total);\n byStatus[status] = count;\n total += count;\n }\n const completed = byStatus.completed ?? 0;\n const running = byStatus.running ?? 0;\n const failed = byStatus.failed ?? 0;\n const rolledBack = byStatus.rolled_back ?? 0;\n\n const latest = queryRows(\n await db.query(\n `SELECT ${MIGRATION_COLUMNS} FROM ${table}\n WHERE status = 'completed'\n ORDER BY applied_at DESC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n ).map(toMigrationRow);\n\n const failedRows = queryRows(\n await db.query(\n `SELECT ${MIGRATION_COLUMNS} FROM ${table}\n WHERE status = 'failed'\n ORDER BY applied_at DESC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n ).map(toMigrationRow);\n\n return {\n available: true,\n summary: { total, byStatus, completed, running, failed, rolledBack },\n latest,\n failed: failedRows,\n };\n } catch {\n return unavailable(table, 'read-error');\n }\n}\n\nconst MIGRATION_COLUMNS = [\n 'id',\n 'name',\n 'version',\n 'status',\n 'applied_at',\n 'execution_time_ms',\n 'attempts',\n 'error_message',\n 'package_name',\n 'source_file',\n 'is_reversible',\n 'rolled_back_at',\n 'applied_by',\n 'batch',\n].join(', ');\n\nfunction toMigrationRow(row: Record<string, unknown>): MigrationStatusRow {\n return {\n id: String(row.id ?? ''),\n name: String(row.name ?? ''),\n version: String(row.version ?? ''),\n status: String(row.status ?? ''),\n appliedAt: toIso(row.applied_at),\n executionTimeMs: toNumber(row.execution_time_ms),\n attempts: toNumber(row.attempts),\n errorMessage: toNullableString(row.error_message),\n packageName: toNullableString(row.package_name),\n sourceFile: toNullableString(row.source_file),\n isReversible: toBoolean(row.is_reversible),\n rolledBackAt: toIso(row.rolled_back_at),\n appliedBy: toNullableString(row.applied_by),\n batch: toNumber(row.batch),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Job health\n// ---------------------------------------------------------------------------\n\n/**\n * Safe projection of one `_smrt_jobs` row. Job payload/result columns\n * (`args`, `task_result`, `task_input_requests`, `task_input_responses`,\n * `result_pointer`) are deliberately never selected.\n */\nexport interface JobHealthRow {\n id: string;\n tenantId: string | null;\n queue: string;\n objectType: string;\n objectId: string | null;\n method: string;\n status: string;\n attempts: number | null;\n maxAttempts: number | null;\n runAt: string | null;\n startedAt: string | null;\n completedAt: string | null;\n lastError: string | null;\n taskId: string | null;\n taskOwnerId: string | null;\n workerId: string | null;\n workerHeartbeat: string | null;\n}\n\n/** Safe projection of one `_smrt_workers` row (no sensitive columns). */\nexport interface WorkerHealthRow {\n id: string;\n workerId: string;\n pid: number | null;\n hostname: string | null;\n startedAt: string | null;\n heartbeatAt: string | null;\n leaseExpiresAt: string | null;\n status: string;\n}\n\nexport interface JobHealthSummary {\n total: number;\n byStatus: Record<string, number>;\n failed: number;\n /** `running` jobs whose heartbeat is older than the stale threshold. */\n stuck: number;\n /** Total rows in `_smrt_job_events`, or null when that table is missing. */\n eventCount: number | null;\n}\n\nexport type JobHealthResult =\n | {\n available: true;\n summary: JobHealthSummary;\n jobs: JobHealthRow[];\n workers: WorkerHealthRow[];\n }\n | CategoryUnavailable;\n\nexport interface JobHealthOptions {\n /** Row budget for `jobs` (default {@link DIAGNOSTICS_DEFAULT_LIMIT}). */\n limit?: number;\n /**\n * Heartbeat older than this (ms) marks a `running` job as stuck — unless\n * its owning worker still holds a fresh `_smrt_workers` lease.\n */\n staleAfterMs?: number;\n /** Clock for staleness computations (defaults to `new Date()`). */\n now?: Date;\n}\n\n/** Read `_smrt_jobs`, `_smrt_workers`, and a `_smrt_job_events` count. */\nexport async function readJobHealth(\n db: DatabaseInterface,\n options: JobHealthOptions = {},\n): Promise<JobHealthResult> {\n const table = SYSTEM_DIAGNOSTICS_TABLES.jobs;\n if (!(await tableExists(db, table))) {\n return unavailable(table, 'table-missing');\n }\n const limit = boundedLimit(options.limit);\n const staleAfterMs = options.staleAfterMs ?? 5 * 60_000;\n const now = options.now ?? new Date();\n const staleCutoff = new Date(now.getTime() - staleAfterMs);\n\n try {\n const p = placeholders(db);\n const statusRows = queryRows(\n await db.query(\n `SELECT status, COUNT(*) AS total FROM ${table} GROUP BY status`,\n ),\n );\n const byStatus: Record<string, number> = {};\n let total = 0;\n for (const row of statusRows) {\n const status = String(row.status ?? 'unknown');\n const count = toCount(row.total);\n byStatus[status] = count;\n total += count;\n }\n const failed = byStatus.failed ?? 0;\n\n // Mirror the runner's recovery rule (#1474): a stale per-job heartbeat is\n // only telemetry. A running job whose worker still holds a fresh liveness\n // lease in `_smrt_workers` (renewed off the handler event loop) is alive\n // even while its handler blocks, so it is not stuck. Without the workers\n // table liveness is unknowable and the heartbeat is the only signal.\n const workersTable = SYSTEM_DIAGNOSTICS_TABLES.workers;\n const workersAvailable = await tableExists(db, workersTable);\n const stuckRows = queryRows(\n workersAvailable\n ? await db.query(\n `SELECT COUNT(*) AS total FROM ${table} AS j\n WHERE j.status = 'running'\n AND (j.worker_heartbeat IS NULL OR j.worker_heartbeat < ${p(1)})\n AND NOT EXISTS (\n SELECT 1 FROM ${workersTable} AS w\n WHERE w.worker_id = j.worker_id\n AND w.lease_expires_at > ${p(2)}\n )`,\n staleCutoff.toISOString(),\n now.toISOString(),\n )\n : await db.query(\n `SELECT COUNT(*) AS total FROM ${table}\n WHERE status = 'running'\n AND (worker_heartbeat IS NULL OR worker_heartbeat < ${p(1)})`,\n staleCutoff.toISOString(),\n ),\n );\n const stuck = toCount(stuckRows[0]?.total);\n\n const jobs = queryRows(\n await db.query(\n `SELECT ${JOB_COLUMNS} FROM ${table}\n ORDER BY run_at DESC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n ).map(toJobHealthRow);\n\n let workers: WorkerHealthRow[] = [];\n if (workersAvailable) {\n workers = queryRows(\n await db.query(\n `SELECT ${WORKER_COLUMNS} FROM ${workersTable}\n ORDER BY heartbeat_at DESC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n ).map(toWorkerHealthRow);\n }\n\n let eventCount: number | null = null;\n const eventsTable = SYSTEM_DIAGNOSTICS_TABLES.jobEvents;\n if (await tableExists(db, eventsTable)) {\n const eventRows = queryRows(\n await db.query(`SELECT COUNT(*) AS total FROM ${eventsTable}`),\n );\n eventCount = toNumber(eventRows[0]?.total);\n }\n\n return {\n available: true,\n summary: { total, byStatus, failed, stuck, eventCount },\n jobs,\n workers,\n };\n } catch {\n return unavailable(table, 'read-error');\n }\n}\n\nconst JOB_COLUMNS = [\n 'id',\n 'tenant_id',\n 'queue',\n 'object_type',\n 'object_id',\n 'method',\n 'status',\n 'attempts',\n 'max_attempts',\n 'run_at',\n 'started_at',\n 'completed_at',\n 'last_error',\n 'task_id',\n 'task_owner_id',\n 'worker_id',\n 'worker_heartbeat',\n].join(', ');\n\nconst WORKER_COLUMNS = [\n 'id',\n 'worker_id',\n 'pid',\n 'hostname',\n 'started_at',\n 'heartbeat_at',\n 'lease_expires_at',\n 'status',\n].join(', ');\n\nfunction toJobHealthRow(row: Record<string, unknown>): JobHealthRow {\n return {\n id: String(row.id ?? ''),\n tenantId: toNullableString(row.tenant_id),\n queue: String(row.queue ?? ''),\n objectType: String(row.object_type ?? ''),\n objectId: toNullableString(row.object_id),\n method: String(row.method ?? ''),\n status: String(row.status ?? ''),\n attempts: toNumber(row.attempts),\n maxAttempts: toNumber(row.max_attempts),\n runAt: toIso(row.run_at),\n startedAt: toIso(row.started_at),\n completedAt: toIso(row.completed_at),\n lastError: toNullableString(row.last_error),\n taskId: toNullableString(row.task_id),\n taskOwnerId: toNullableString(row.task_owner_id),\n workerId: toNullableString(row.worker_id),\n workerHeartbeat: toIso(row.worker_heartbeat),\n };\n}\n\nfunction toWorkerHealthRow(row: Record<string, unknown>): WorkerHealthRow {\n return {\n id: String(row.id ?? ''),\n workerId: String(row.worker_id ?? ''),\n pid: toNumber(row.pid),\n hostname: toNullableString(row.hostname),\n startedAt: toIso(row.started_at),\n heartbeatAt: toIso(row.heartbeat_at),\n leaseExpiresAt: toIso(row.lease_expires_at),\n status: String(row.status ?? ''),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Schedule health\n// ---------------------------------------------------------------------------\n\n/**\n * Safe projection of one `_smrt_agent_schedules` row. The sensitive\n * `agent_config` and `method_args` columns are never selected.\n */\nexport interface ScheduleHealthRow {\n id: string;\n tenantId: string | null;\n agentType: string;\n agentId: string | null;\n cron: string;\n timezone: string;\n enabled: boolean | null;\n status: string;\n lastRun: string | null;\n nextRun: string | null;\n lastStatus: string | null;\n lastError: string | null;\n runCount: number | null;\n successCount: number | null;\n failureCount: number | null;\n maxConcurrent: number | null;\n runningCount: number | null;\n timeout: number | null;\n method: string;\n}\n\nexport interface ScheduleHealthSummary {\n total: number;\n enabled: number;\n active: number;\n errored: number;\n /** Schedules whose last run failed (`last_status = 'failed'`). */\n failed: number;\n /** Enabled, active schedules whose `next_run` is due (<= now). */\n overdue: number;\n}\n\nexport type ScheduleHealthResult =\n | {\n available: true;\n summary: ScheduleHealthSummary;\n schedules: ScheduleHealthRow[];\n }\n | CategoryUnavailable;\n\nexport interface ScheduleHealthOptions {\n /** Row budget for `schedules` (default {@link DIAGNOSTICS_DEFAULT_LIMIT}). */\n limit?: number;\n /** Clock for overdue computation (defaults to `new Date()`). */\n now?: Date;\n}\n\n/** Read `_smrt_agent_schedules` (owned by `@happyvertical/smrt-agents`). */\nexport async function readScheduleHealth(\n db: DatabaseInterface,\n options: ScheduleHealthOptions = {},\n): Promise<ScheduleHealthResult> {\n const table = SYSTEM_DIAGNOSTICS_TABLES.schedules;\n if (!(await tableExists(db, table))) {\n return unavailable(table, 'table-missing');\n }\n const limit = boundedLimit(options.limit);\n const now = options.now ?? new Date();\n\n try {\n const p = placeholders(db);\n const enabledRows = queryRows(\n await db.query(\n `SELECT enabled, status, COUNT(*) AS total FROM ${table} GROUP BY enabled, status`,\n ),\n );\n let total = 0;\n let enabled = 0;\n let active = 0;\n let errored = 0;\n for (const row of enabledRows) {\n const count = toCount(row.total);\n total += count;\n if (toBoolean(row.enabled)) enabled += count;\n const status = String(row.status ?? '');\n if (status === 'active') active += count;\n if (status === 'error') errored += count;\n }\n\n const failedRows = queryRows(\n await db.query(\n `SELECT COUNT(*) AS total FROM ${table} WHERE last_status = 'failed'`,\n ),\n );\n const failed = toCount(failedRows[0]?.total);\n\n const overdueRows = queryRows(\n await db.query(\n `SELECT COUNT(*) AS total FROM ${table}\n WHERE enabled = true AND status = 'active' AND next_run IS NOT NULL AND next_run <= ${p(1)}`,\n now.toISOString(),\n ),\n );\n const overdue = toCount(overdueRows[0]?.total);\n\n const schedules = queryRows(\n await db.query(\n `SELECT ${SCHEDULE_COLUMNS} FROM ${table}\n ORDER BY next_run ASC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n ).map(toScheduleHealthRow);\n\n return {\n available: true,\n summary: { total, enabled, active, errored, failed, overdue },\n schedules,\n };\n } catch {\n return unavailable(table, 'read-error');\n }\n}\n\nconst SCHEDULE_COLUMNS = [\n 'id',\n 'tenant_id',\n 'agent_type',\n 'agent_id',\n 'cron',\n 'timezone',\n 'enabled',\n 'status',\n 'last_run',\n 'next_run',\n 'last_status',\n 'last_error',\n 'run_count',\n 'success_count',\n 'failure_count',\n 'max_concurrent',\n 'running_count',\n 'timeout',\n 'method',\n].join(', ');\n\nfunction toScheduleHealthRow(row: Record<string, unknown>): ScheduleHealthRow {\n return {\n id: String(row.id ?? ''),\n tenantId: toNullableString(row.tenant_id),\n agentType: String(row.agent_type ?? ''),\n agentId: toNullableString(row.agent_id),\n cron: String(row.cron ?? ''),\n timezone: String(row.timezone ?? ''),\n enabled: toBoolean(row.enabled),\n status: String(row.status ?? ''),\n lastRun: toIso(row.last_run),\n nextRun: toIso(row.next_run),\n lastStatus: toNullableString(row.last_status),\n lastError: toNullableString(row.last_error),\n runCount: toNumber(row.run_count),\n successCount: toNumber(row.success_count),\n failureCount: toNumber(row.failure_count),\n maxConcurrent: toNumber(row.max_concurrent),\n runningCount: toNumber(row.running_count),\n timeout: toNumber(row.timeout),\n method: String(row.method ?? ''),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Dispatch health\n// ---------------------------------------------------------------------------\n\n/**\n * Safe projection of one `_smrt_dispatch` row. The `payload` and `metadata`\n * columns are never selected.\n */\nexport interface DispatchHealthRow {\n id: string;\n type: string;\n source: string;\n sourceId: string | null;\n status: string;\n attempts: number | null;\n lastError: string | null;\n processedAt: string | null;\n processedBy: string | null;\n targetSubscriber: string | null;\n correlationId: string | null;\n tenantId: string | null;\n createdAt: string | null;\n updatedAt: string | null;\n}\n\n/** Safe projection of one `_smrt_dispatch_subscriptions` row. */\nexport interface DispatchSubscriptionHealthRow {\n id: string;\n signalType: string;\n subscriber: string;\n handler: string;\n delivery: string;\n enabled: boolean | null;\n tenantId: string | null;\n createdAt: string | null;\n updatedAt: string | null;\n}\n\nexport interface DispatchHealthSummary {\n total: number;\n byStatus: Record<string, number>;\n pending: number;\n processing: number;\n failed: number;\n completed: number;\n subscriptions: number;\n}\n\nexport type DispatchHealthResult =\n | {\n available: true;\n summary: DispatchHealthSummary;\n dispatches: DispatchHealthRow[];\n subscriptions: DispatchSubscriptionHealthRow[];\n }\n | CategoryUnavailable;\n\nexport interface DispatchHealthOptions {\n /** Row budget for `dispatches`/`subscriptions` (default {@link DIAGNOSTICS_DEFAULT_LIMIT}). */\n limit?: number;\n}\n\n/** Read `_smrt_dispatch` and `_smrt_dispatch_subscriptions`. */\nexport async function readDispatchHealth(\n db: DatabaseInterface,\n options: DispatchHealthOptions = {},\n): Promise<DispatchHealthResult> {\n const table = SYSTEM_DIAGNOSTICS_TABLES.dispatch;\n if (!(await tableExists(db, table))) {\n return unavailable(table, 'table-missing');\n }\n const limit = boundedLimit(options.limit);\n\n try {\n const p = placeholders(db);\n const statusRows = queryRows(\n await db.query(\n `SELECT status, COUNT(*) AS total FROM ${table} GROUP BY status`,\n ),\n );\n const byStatus: Record<string, number> = {};\n let total = 0;\n for (const row of statusRows) {\n const status = String(row.status ?? 'unknown');\n const count = toCount(row.total);\n byStatus[status] = count;\n total += count;\n }\n const pending = byStatus.pending ?? 0;\n const processing = byStatus.processing ?? 0;\n const failed = byStatus.failed ?? 0;\n const completed = byStatus.completed ?? 0;\n\n const dispatches = queryRows(\n await db.query(\n `SELECT ${DISPATCH_COLUMNS} FROM ${table}\n ORDER BY created_at DESC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n ).map(toDispatchHealthRow);\n\n let subscriptions: DispatchSubscriptionHealthRow[] = [];\n let subscriptionCount = 0;\n const subsTable = SYSTEM_DIAGNOSTICS_TABLES.dispatchSubscriptions;\n if (await tableExists(db, subsTable)) {\n // The summary count is a COUNT(*) aggregate, not the listed-page length:\n // `subscriptions` honors the row budget like every other list, so the\n // list length would silently under-report topology above the limit.\n const countRows = queryRows(\n await db.query(`SELECT COUNT(*) AS total FROM ${subsTable}`),\n );\n subscriptionCount = toCount(countRows[0]?.total);\n const subsRows = queryRows(\n await db.query(\n `SELECT ${SUBSCRIPTION_COLUMNS} FROM ${subsTable}\n ORDER BY created_at DESC, id ASC\n LIMIT ${p(1)}`,\n limit,\n ),\n );\n subscriptions = subsRows.map(toSubscriptionHealthRow);\n }\n\n return {\n available: true,\n summary: {\n total,\n byStatus,\n pending,\n processing,\n failed,\n completed,\n subscriptions: subscriptionCount,\n },\n dispatches,\n subscriptions,\n };\n } catch {\n return unavailable(table, 'read-error');\n }\n}\n\nconst DISPATCH_COLUMNS = [\n 'id',\n 'type',\n 'source',\n 'source_id',\n 'status',\n 'attempts',\n 'last_error',\n 'processed_at',\n 'processed_by',\n 'target_subscriber',\n 'correlation_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n].join(', ');\n\nconst SUBSCRIPTION_COLUMNS = [\n 'id',\n 'signal_type',\n 'subscriber',\n 'handler',\n 'delivery',\n 'enabled',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n].join(', ');\n\nfunction toDispatchHealthRow(row: Record<string, unknown>): DispatchHealthRow {\n return {\n id: String(row.id ?? ''),\n type: String(row.type ?? ''),\n source: String(row.source ?? ''),\n sourceId: toNullableString(row.source_id),\n status: String(row.status ?? ''),\n attempts: toNumber(row.attempts),\n lastError: toNullableString(row.last_error),\n processedAt: toIso(row.processed_at),\n processedBy: toNullableString(row.processed_by),\n targetSubscriber: toNullableString(row.target_subscriber),\n correlationId: toNullableString(row.correlation_id),\n tenantId: toNullableString(row.tenant_id),\n createdAt: toIso(row.created_at),\n updatedAt: toIso(row.updated_at),\n };\n}\n\nfunction toSubscriptionHealthRow(\n row: Record<string, unknown>,\n): DispatchSubscriptionHealthRow {\n return {\n id: String(row.id ?? ''),\n signalType: String(row.signal_type ?? ''),\n subscriber: String(row.subscriber ?? ''),\n handler: String(row.handler ?? ''),\n delivery: String(row.delivery ?? ''),\n enabled: toBoolean(row.enabled),\n tenantId: toNullableString(row.tenant_id),\n createdAt: toIso(row.created_at),\n updatedAt: toIso(row.updated_at),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Recent changes\n// ---------------------------------------------------------------------------\n\nexport type RecentChangesResult =\n | {\n available: true;\n changes: ChangeFeedEntry[];\n cursor: number;\n resyncRequired?: boolean;\n resyncCursor?: number;\n count: number;\n }\n | CategoryUnavailable;\n\nexport interface RecentChangesOptions {\n /** Cursor to read after (default 0). */\n since?: number;\n /** Restrict to these physical table names. */\n tables?: string[];\n /** Tenant narrowing filter (see the change-feed reader's tenant semantics). */\n tenantId?: string | null;\n /** Page size (default 200, capped like the change feed's own limit). */\n limit?: number;\n}\n\n/**\n * Read the recent change feed via the canonical {@link getChangesSince}\n * reader with `drain: false`, so the PostgreSQL staged-entry drain (a write\n * through `_smrt_drain_changes()`) never runs from a diagnostic. Entries still\n * staged by an open caller transaction stay invisible until a writer drains\n * them. A missing `_smrt_changes` table degrades to category-unavailable.\n */\nexport async function readRecentChanges(\n db: DatabaseInterface,\n options: RecentChangesOptions = {},\n): Promise<RecentChangesResult> {\n const table = SYSTEM_DIAGNOSTICS_TABLES.changes;\n if (!(await tableExists(db, table))) {\n return unavailable(table, 'table-missing');\n }\n try {\n const page = await getChangesSince(db, {\n since: options.since ?? 0,\n drain: false,\n tables: options.tables,\n tenantId: options.tenantId,\n limit: boundedLimit(options.limit, 200),\n });\n return {\n available: true,\n changes: page.changes,\n cursor: page.cursor,\n ...(page.resyncRequired !== undefined\n ? {\n resyncRequired: page.resyncRequired,\n resyncCursor: page.resyncCursor,\n }\n : {}),\n count: page.changes.length,\n };\n } catch {\n return unavailable(table, 'read-error');\n }\n}\n\n// ---------------------------------------------------------------------------\n// Registry (retired)\n// ---------------------------------------------------------------------------\n\n/**\n * `_smrt_registry` is retired: never written since system schema 1.10.1, absent\n * on new databases, and deliberately not dropped by the framework. The\n * diagnostics reader reports the retirement — it never reads registry rows and\n * never fabricates drift; `stillPresent` is a table-existence probe only, so a\n * probe failure reads as absence, which the message keeps non-committal about.\n */\nexport interface RegistryDriftResult {\n available: false;\n reason: 'retired';\n message: string;\n tableName: string;\n /** True when a legacy `_smrt_registry` table still exists on this database. */\n stillPresent: boolean;\n}\n\nexport async function readRegistryDrift(\n db: DatabaseInterface,\n): Promise<RegistryDriftResult> {\n const table = SYSTEM_DIAGNOSTICS_TABLES.registry;\n const stillPresent = await tableExists(db, table);\n return {\n available: false,\n reason: 'retired',\n message: stillPresent\n ? `The ${table} table is retired (SMRT system schema ${'1.10.1'}) and is no longer written or read; a legacy empty table remains on this database and can be dropped. No registry drift is reported.`\n : `The ${table} table is retired (SMRT system schema ${'1.10.1'}) and is no longer written or read; no such table was found on this database. No registry drift is reported.`,\n tableName: table,\n stillPresent,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Facade\n// ---------------------------------------------------------------------------\n\nexport interface SystemDiagnosticsOptions {\n migrations?: MigrationStatusOptions;\n jobs?: JobHealthOptions;\n schedules?: ScheduleHealthOptions;\n dispatch?: DispatchHealthOptions;\n changes?: RecentChangesOptions;\n}\n\nexport interface SystemDiagnosticsResult {\n migrations: MigrationStatusResult;\n jobs: JobHealthResult;\n schedules: ScheduleHealthResult;\n dispatch: DispatchHealthResult;\n changes: RecentChangesResult;\n registry: RegistryDriftResult;\n}\n\n/** Read every diagnostics category in one pass (SELECT-only). */\nexport async function readSystemDiagnostics(\n db: DatabaseInterface,\n options: SystemDiagnosticsOptions = {},\n): Promise<SystemDiagnosticsResult> {\n const [migrations, jobs, schedules, dispatch, changes, registry] =\n await Promise.all([\n readMigrationStatus(db, options.migrations),\n readJobHealth(db, options.jobs),\n readScheduleHealth(db, options.schedules),\n readDispatchHealth(db, options.dispatch),\n readRecentChanges(db, options.changes),\n readRegistryDrift(db),\n ]);\n return { migrations, jobs, schedules, dispatch, changes, registry };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction unavailable(\n tableName: string,\n reason: CategoryUnavailable['reason'],\n message?: string,\n): CategoryUnavailable {\n return {\n available: false,\n reason,\n message:\n message ??\n (reason === 'retired'\n ? `${tableName} is retired and no longer read.`\n : reason === 'table-missing'\n ? `The ${tableName} system table is not present on this database.`\n : `Reading ${tableName} failed; the live diagnostic is unavailable.`),\n tableName,\n };\n}\n\nfunction boundedLimit(\n value: number | undefined,\n fallback = DIAGNOSTICS_DEFAULT_LIMIT,\n): number {\n if (!Number.isFinite(value) || value === undefined) return fallback;\n return Math.min(Math.max(Math.floor(value), 1), 500);\n}\n\nfunction placeholders(db: DatabaseInterface): (index: number) => string {\n // Engine detection follows the repo convention (`getDatabaseEngine` prefers\n // an explicit `type`/config hint, then the connection URL's scheme), so a\n // postgres URL opened without a matching `type` field still binds `$n`.\n const engine = getDatabaseEngine(db);\n return engine === 'postgres' ? (index) => `$${index}` : () => '?';\n}\n\nfunction queryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) {\n return result as Record<string, unknown>[];\n }\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) {\n return rows as Record<string, unknown>[];\n }\n }\n return [];\n}\n\nfunction toNullableString(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n return String(value);\n}\n\nfunction toNumber(value: unknown): number | null {\n if (value === null || value === undefined) return null;\n const numeric = Number(value);\n return Number.isFinite(numeric) ? numeric : null;\n}\n\n/** Count aggregation: `COUNT(*)` always yields a number (0 when absent). */\nfunction toCount(value: unknown): number {\n return toNumber(value) ?? 0;\n}\n\nfunction toBoolean(value: unknown): boolean | null {\n if (value === null || value === undefined) return null;\n if (\n value === true ||\n value === 'true' ||\n value === 't' ||\n value === '1' ||\n value === 1\n ) {\n return true;\n }\n if (\n value === false ||\n value === 'false' ||\n value === 'f' ||\n value === '0' ||\n value === 0\n ) {\n return false;\n }\n return Boolean(value);\n}\n\nfunction toIso(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value.toISOString();\n }\n const text = String(value);\n return text.length > 0 ? text : null;\n}\n"],"mappings":"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"}
|
package/dist/system/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/system/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/system/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
package/dist/system/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertPostgresSystemTimestampsCurrent, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists } from "./compatibility.js";
|
|
2
2
|
import { ensureSystemTables } from "./bootstrap.js";
|
|
3
|
+
import { DIAGNOSTICS_DEFAULT_LIMIT, SYSTEM_DIAGNOSTICS_TABLES, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics } from "./diagnostics.js";
|
|
3
4
|
import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./retention.js";
|
|
4
|
-
export { DEFAULT_RETENTION_POLICY, assertPostgresSystemTimestampsCurrent, clearRetentionTasks, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, ensureSystemTables, getDatabaseEngine, getRetentionTasks, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, tableExists, unregisterRetentionTask };
|
|
5
|
+
export { DEFAULT_RETENTION_POLICY, DIAGNOSTICS_DEFAULT_LIMIT, SYSTEM_DIAGNOSTICS_TABLES, assertPostgresSystemTimestampsCurrent, clearRetentionTasks, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, ensureSystemTables, getDatabaseEngine, getRetentionTasks, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, pruneAiUsage, pruneExpiredContexts, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics, registerRetentionTask, runRetentionSweep, tableExists, unregisterRetentionTask };
|
|
@@ -85,5 +85,21 @@ export interface ScannerModule {
|
|
|
85
85
|
exclude: string[];
|
|
86
86
|
followSymbolicLinks?: boolean;
|
|
87
87
|
}): Promise<string[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Report an emitted intent whose derived WebMCP tool name a generated model
|
|
90
|
+
* tool or a fixed UI tool also claims (#2725).
|
|
91
|
+
*
|
|
92
|
+
* The scanner owns the rule because it owns the tool-name derivation; core
|
|
93
|
+
* owns the INPUT because only core resolves the exposure policy. Names are
|
|
94
|
+
* passed rather than re-derived, so the scanner never becomes a second place
|
|
95
|
+
* that decides what a generated tool is called.
|
|
96
|
+
*/
|
|
97
|
+
checkAgentSurfaceToolNames(surface: ScannerAgentSurface, options: {
|
|
98
|
+
generatedToolNames?: ReadonlyArray<{
|
|
99
|
+
name: string;
|
|
100
|
+
declaredBy?: string;
|
|
101
|
+
}>;
|
|
102
|
+
uiToolPrefixes?: readonly string[];
|
|
103
|
+
}): ScannerAgentSurface['diagnostics'];
|
|
88
104
|
}
|
|
89
105
|
//# sourceMappingURL=scanner-module.d.ts.map
|