@skill-map/cli 0.23.1 → 0.24.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/dist/cli.js +244 -200
- package/dist/cli.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +1969 -1961
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/chunk-2W3RSIZM.js +1 -0
- package/dist/ui/chunk-5ABLL5JC.js +123 -0
- package/dist/ui/chunk-FHS2JKF6.js +500 -0
- package/dist/ui/chunk-JCNVT3F3.js +251 -0
- package/dist/ui/chunk-JVJ4QSO5.js +107 -0
- package/dist/ui/chunk-KHERXP5A.js +61 -0
- package/dist/ui/{chunk-2TPMJJYQ.js → chunk-MMBMFL5D.js} +5 -5
- package/dist/ui/{chunk-STE4Z72W.js → chunk-RIDAAD4I.js} +1 -1
- package/dist/ui/chunk-SVDEA4G5.js +1 -0
- package/dist/ui/chunk-YEWLFFFB.js +135 -0
- package/dist/ui/{chunk-BMAKIDAV.js → chunk-YVTETTWF.js} +30 -80
- package/dist/ui/chunk-ZR2E2QJ4.js +965 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/main-63GTHO24.js +2 -0
- package/dist/ui/{styles-ALBMEXCF.css → styles-YEWJTI4X.css} +1 -1
- package/package.json +2 -2
- package/dist/ui/chunk-3RAME7PF.js +0 -251
- package/dist/ui/chunk-4BVLXZO3.js +0 -61
- package/dist/ui/chunk-C7PRRCVD.js +0 -123
- package/dist/ui/chunk-GJJZ5QH6.js +0 -450
- package/dist/ui/chunk-I7EELB7M.js +0 -1
- package/dist/ui/chunk-KRNW54CI.js +0 -5
- package/dist/ui/chunk-NJ4PSNK3.js +0 -965
- package/dist/ui/chunk-OU26UMVW.js +0 -237
- package/dist/ui/chunk-SCSYN7U2.js +0 -1
- package/dist/ui/main-WQA6J5V5.js +0 -2
package/dist/kernel/index.d.ts
CHANGED
|
@@ -1088,2244 +1088,2252 @@ declare function makePluginStore(opts: {
|
|
|
1088
1088
|
}): IPluginStore | undefined;
|
|
1089
1089
|
|
|
1090
1090
|
/**
|
|
1091
|
-
*
|
|
1092
|
-
* `
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* One row per `(plugin_id, extension_id, node_path, contribution_id)`
|
|
1096
|
-
* tuple. See `spec/architecture.md` § View contribution system →
|
|
1097
|
-
* Persistence and `migrations/001_initial.sql` § View contribution
|
|
1098
|
-
* layer for the normative shape.
|
|
1099
|
-
*
|
|
1100
|
-
* Replace-all semantics mirror the rest of the `scan_*` zone: every
|
|
1101
|
-
* scan is a fresh snapshot, so prior rows are deleted before insert.
|
|
1102
|
-
* Wrapped in the same transaction `persistScanResult` opens.
|
|
1103
|
-
*
|
|
1104
|
-
* The rename heuristic does NOT need to migrate `node_path` here,
|
|
1105
|
-
* because of replace-all, every contribution is re-emitted on the new
|
|
1106
|
-
* path automatically. Keeping the rename path lighter than `state_*`
|
|
1107
|
-
* (which IS rename-migrated because state survives across scans).
|
|
1108
|
-
*/
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* In-memory contribution record buffered during scan and flushed to
|
|
1112
|
-
* `scan_contributions` by `persistScanResult`. One entry per accepted
|
|
1113
|
-
* `ctx.emitContribution(id, payload)` call. Payload validation against
|
|
1114
|
-
* the slot's payload schema happens at emit time (orchestrator);
|
|
1115
|
-
* by the time records reach this adapter they are wire-shape clean.
|
|
1091
|
+
* Row-level filter for `port.scans.findNodes(...)` (driven by
|
|
1092
|
+
* `sm list`'s flags). All fields are optional, an empty filter
|
|
1093
|
+
* returns every node sorted by `path` asc.
|
|
1116
1094
|
*/
|
|
1117
|
-
interface
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
nodePath: string;
|
|
1121
|
-
contributionId: string;
|
|
1095
|
+
interface INodeFilter {
|
|
1096
|
+
/** Restrict to a single node kind. Open string (matches `Node.kind`). */
|
|
1097
|
+
kind?: string;
|
|
1122
1098
|
/**
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1099
|
+
* When `true`, keep only nodes whose path is referenced by at least
|
|
1100
|
+
* one `scan_issues.nodeIds` array.
|
|
1125
1101
|
*/
|
|
1126
|
-
|
|
1127
|
-
/**
|
|
1128
|
-
|
|
1129
|
-
|
|
1102
|
+
hasIssues?: boolean;
|
|
1103
|
+
/**
|
|
1104
|
+
* Sort column. The adapter validates against its own whitelist and
|
|
1105
|
+
* rejects anything else with an Error (the CLI's own usage-error
|
|
1106
|
+
* exit is the right place to surface a bad `--sort-by`; the port
|
|
1107
|
+
* defends in depth).
|
|
1108
|
+
*/
|
|
1109
|
+
sortBy?: string;
|
|
1110
|
+
/** `'asc'` or `'desc'`. Defaults to the adapter's per-column convention. */
|
|
1111
|
+
sortDirection?: 'asc' | 'desc';
|
|
1112
|
+
/** Cap the result. Positive integer; absent → no limit. */
|
|
1113
|
+
limit?: number;
|
|
1130
1114
|
}
|
|
1131
1115
|
/**
|
|
1132
|
-
*
|
|
1133
|
-
* `
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1116
|
+
* Bundled fetch for `port.scans.findNode(path)`, one node and
|
|
1117
|
+
* everything `sm show <path>` displays alongside it. Every field is
|
|
1118
|
+
* computed from `scan_*` zone reads only; per-domain data (history,
|
|
1119
|
+
* jobs, plugin enrichments) ships through other namespaces.
|
|
1136
1120
|
*/
|
|
1137
|
-
interface
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
slot: string;
|
|
1143
|
-
payload: unknown;
|
|
1144
|
-
emittedAt: number;
|
|
1121
|
+
interface INodeBundle {
|
|
1122
|
+
node: Node;
|
|
1123
|
+
linksOut: Link[];
|
|
1124
|
+
linksIn: Link[];
|
|
1125
|
+
issues: Issue[];
|
|
1145
1126
|
}
|
|
1146
|
-
|
|
1147
|
-
/**
|
|
1148
|
-
* `loadScanResult`, driving inverse of `persistScanResult`. Reads the
|
|
1149
|
-
* `scan_*` tables and reconstructs a `ScanResult` shape so the
|
|
1150
|
-
* orchestrator can run an incremental scan (`sm scan --changed`) on
|
|
1151
|
-
* top of a prior snapshot.
|
|
1152
|
-
*
|
|
1153
|
-
* The reconstruction is faithful for everything that was actually
|
|
1154
|
-
* persisted: nodes (with triple-split bytes / tokens, denormalised
|
|
1155
|
-
* counts, JSON frontmatter), internal links (with regrouped
|
|
1156
|
-
* `trigger` / `location`, parsed `sources[]`), and issues
|
|
1157
|
-
* (with parsed `nodeIds` / `linkIndices` / `fix` / `data`).
|
|
1158
|
-
*
|
|
1159
|
-
* **Documented omission**: external pseudo-links (those whose target is
|
|
1160
|
-
* an `http://` / `https://` URL emitted by the external-url-counter
|
|
1161
|
-
* extractor) are NEVER persisted to `scan_links`, only their per-node
|
|
1162
|
-
* count survives in `scan_nodes.external_refs_count`. Therefore the
|
|
1163
|
-
* `result.links` returned by `loadScanResult` contains only internal
|
|
1164
|
-
* graph links, and `node.externalRefsCount` is the authoritative count
|
|
1165
|
-
* carried over from the prior scan. The orchestrator's incremental path
|
|
1166
|
-
* preserves that count for "unchanged" nodes and re-derives it for
|
|
1167
|
-
* new / modified nodes from a fresh extractor pass.
|
|
1168
|
-
*
|
|
1169
|
-
* Meta envelope: the `scan_meta` table persists `scope` / `roots` /
|
|
1170
|
-
* `scannedAt` / `scannedBy` / `providers` / `stats.filesWalked` /
|
|
1171
|
-
* `stats.filesSkipped` / `stats.durationMs`. When the row exists,
|
|
1172
|
-
* those fields come back authoritatively. When it does not (DB
|
|
1173
|
-
* freshly migrated but never scanned, or a legacy DB never
|
|
1174
|
-
* re-persisted), the loader degrades to a synthetic envelope:
|
|
1175
|
-
*
|
|
1176
|
-
* - `scannedAt` ← max(`scan_nodes.scanned_at`); falls back to `Date.now()`
|
|
1177
|
-
* for empty snapshots so the field stays a positive integer.
|
|
1178
|
-
* - `scope` ← `'project'`.
|
|
1179
|
-
* - `roots` ← `['.']` to satisfy spec's `minItems: 1`. NOT
|
|
1180
|
-
* load-bearing: the orchestrator's incremental path only reads
|
|
1181
|
-
* `nodes` / `links` / `issues` from the prior; it never reuses the
|
|
1182
|
-
* prior `roots`.
|
|
1183
|
-
* - `providers` ← `[]`.
|
|
1184
|
-
* - `stats` ← zeros for `filesWalked` / `filesSkipped` /
|
|
1185
|
-
* `durationMs`; the three count fields derive from row counts.
|
|
1186
|
-
*
|
|
1187
|
-
* Both branches keep `nodesCount` / `linksCount` / `issuesCount` derived
|
|
1188
|
-
* from `COUNT(*)` of the loaded rows, never persisted, always recomputed.
|
|
1189
|
-
*/
|
|
1190
|
-
|
|
1191
1127
|
/**
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
* default when the table is empty (fresh DB, never-scanned scope, or
|
|
1196
|
-
* every extractor has been uninstalled since the last scan).
|
|
1197
|
-
*
|
|
1198
|
-
* Returned shape: `Map<nodePath, Map<extractorId, IPriorExtractorRun>>`.
|
|
1199
|
-
* The inner value carries the body hash AND the sidecar-annotations
|
|
1200
|
-
* hash so the orchestrator can apply the widened cache key (both must
|
|
1201
|
-
* match for a cache hit).
|
|
1128
|
+
* Output of `port.scans.countRows()`. Used by `sm scan` to decide
|
|
1129
|
+
* whether the persist would wipe a populated DB (the "refusing to
|
|
1130
|
+
* wipe" guard) and by `sm db status` for the human summary.
|
|
1202
1131
|
*/
|
|
1203
|
-
interface
|
|
1204
|
-
|
|
1205
|
-
|
|
1132
|
+
interface INodeCounts {
|
|
1133
|
+
nodes: number;
|
|
1134
|
+
links: number;
|
|
1135
|
+
issues: number;
|
|
1206
1136
|
}
|
|
1207
|
-
|
|
1208
1137
|
/**
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
* 1. Single-source defaults, `src/config/defaults/skillmapignore` is
|
|
1216
|
-
* the canonical default list, loaded once at module init (or at
|
|
1217
|
-
* explicit build time, depending on bundling). The runtime never
|
|
1218
|
-
* re-reads it per scan.
|
|
1219
|
-
* 2. Stable interface, Providers and the orchestrator depend on a
|
|
1220
|
-
* minimal `IIgnoreFilter` shape, so the underlying library can be
|
|
1221
|
-
* swapped without touching every consumer.
|
|
1222
|
-
* 3. Path normalization, every consumer passes the path RELATIVE to
|
|
1223
|
-
* the scan root (POSIX separators); the wrapper guarantees that
|
|
1224
|
-
* contract before delegating to `ignore`.
|
|
1138
|
+
* Lightweight option bag for `port.scans.persist`. Mirrors the trailing
|
|
1139
|
+
* arguments of the legacy `persistScanResult(db, result, renameOps,
|
|
1140
|
+
* extractorRuns, enrichments)` free function so the adapter
|
|
1141
|
+
* implementation is a one-line delegation today; the named-bag shape
|
|
1142
|
+
* tomorrow lets new optional inputs land without breaking callers.
|
|
1225
1143
|
*/
|
|
1226
|
-
interface
|
|
1144
|
+
interface IPersistOptions {
|
|
1145
|
+
renameOps?: RenameOp[];
|
|
1146
|
+
extractorRuns?: IExtractorRunRecord[];
|
|
1147
|
+
enrichments?: IEnrichmentRecord[];
|
|
1148
|
+
contributions?: IContributionRecord[];
|
|
1227
1149
|
/**
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1150
|
+
* Phase 3 / View contribution system, active runtime catalog of
|
|
1151
|
+
* registered view contributions, keyed by qualified id
|
|
1152
|
+
* `<pluginId>/<extensionId>/<contributionId>`. Passed to the
|
|
1153
|
+
* `scan_contributions` upsert so the catalog sweep can drop rows
|
|
1154
|
+
* belonging to plugins / extensions that are no longer in the
|
|
1155
|
+
* catalog (uninstalled plugins, disabled bundles, removed
|
|
1156
|
+
* contributions). Empty / absent set = no catalog sweep (legacy
|
|
1157
|
+
* behaviour, leaves disabled-plugin rows stale per design F24
|
|
1158
|
+
* pre-fix).
|
|
1232
1159
|
*/
|
|
1233
|
-
|
|
1160
|
+
registeredContributionKeys?: ReadonlySet<string>;
|
|
1161
|
+
/**
|
|
1162
|
+
* Phase 3 / View contribution system, set of `(plugin, extension,
|
|
1163
|
+
* node)` tuples where the extension actually RAN against that node
|
|
1164
|
+
* in this scan. Format: `<pluginId>/<extensionId>/<nodePath>` (no
|
|
1165
|
+
* contribution-id segment, the sweep operates at the (plugin,
|
|
1166
|
+
* extension, node) level and inspects the buffer to decide which
|
|
1167
|
+
* contribution-ids survive).
|
|
1168
|
+
*
|
|
1169
|
+
* Membership rules:
|
|
1170
|
+
* - Extractor + cache miss: tuple INCLUDED (extract() ran).
|
|
1171
|
+
* - Extractor + cache hit: tuple OMITTED (extract() skipped, prior
|
|
1172
|
+
* rows must be preserved).
|
|
1173
|
+
* - Rule, every node in `ctx.nodes`: tuple INCLUDED (rules always
|
|
1174
|
+
* run and see the full graph).
|
|
1175
|
+
*
|
|
1176
|
+
* Drives the per-tuple sweep documented in `spec/architecture.md`
|
|
1177
|
+
* §View contribution system → Persistence (sweep #3): rows whose
|
|
1178
|
+
* `(plugin_id, extension_id, node_path)` is in this set but whose
|
|
1179
|
+
* `(plugin_id, extension_id, node_path, contribution_id)` is NOT in
|
|
1180
|
+
* the buffer get DELETEd before the upsert. Catches the "extractor
|
|
1181
|
+
* used to emit, now does not" case (e.g. body change removes the
|
|
1182
|
+
* trigger). Empty / absent set = no per-tuple sweep (legacy
|
|
1183
|
+
* callers preserve the pre-fix behaviour where stale rows linger).
|
|
1184
|
+
*/
|
|
1185
|
+
freshlyRunTuples?: ReadonlySet<string>;
|
|
1234
1186
|
}
|
|
1235
|
-
|
|
1236
1187
|
/**
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
* the
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
* Pure data: parsers never log or throw; they describe the failure
|
|
1245
|
-
* here and let the orchestrator decide how to surface it.
|
|
1188
|
+
* Issue row as the storage layer sees it, paired with its DB-assigned
|
|
1189
|
+
* id so `port.issues.deleteById(id)` can target it inside a
|
|
1190
|
+
* transaction. The runtime `Issue` shape (per `issue.schema.json`) does
|
|
1191
|
+
* not carry `id` because the spec models issues as ephemeral findings
|
|
1192
|
+
* scoped to a scan; the DB does need the synthetic id to update / delete
|
|
1193
|
+
* a single row.
|
|
1246
1194
|
*/
|
|
1247
|
-
interface
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
* is `frontmatter-yaml` reporting a YAML parse error
|
|
1251
|
-
* (`'frontmatter-parse-error'`); the set may grow as new parsers
|
|
1252
|
-
* land.
|
|
1253
|
-
*/
|
|
1254
|
-
code: string;
|
|
1255
|
-
/**
|
|
1256
|
-
* Human-readable message, sanitised. Never includes the raw input
|
|
1257
|
-
* (a hostile YAML could embed multi-line garbage); only the
|
|
1258
|
-
* parser-error string is interpolated.
|
|
1259
|
-
*/
|
|
1260
|
-
message: string;
|
|
1195
|
+
interface IIssueRow {
|
|
1196
|
+
id: number;
|
|
1197
|
+
issue: Issue;
|
|
1261
1198
|
}
|
|
1262
|
-
|
|
1263
1199
|
/**
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1200
|
+
* Filter + pagination shape for `port.issues.list(...)`, driven by the
|
|
1201
|
+
* BFF's `/api/issues` route. Every field is optional, an empty filter
|
|
1202
|
+
* returns every issue ordered by `id` ASC (insertion order, stable
|
|
1203
|
+
* across pages so `offset` / `limit` paging is deterministic).
|
|
1266
1204
|
*
|
|
1267
|
-
*
|
|
1268
|
-
* `StoragePort.adapter`, etc.). A `Provider` is an extension kind authored
|
|
1269
|
-
* by plugins to declare a platform's universe (the catalog of kinds it
|
|
1270
|
-
* emits, the per-kind frontmatter schema, the filesystem directory it
|
|
1271
|
-
* owns); a hexagonal adapter is an internal implementation of a port.
|
|
1272
|
-
* Both can coexist without confusion because they live in different
|
|
1273
|
-
* namespaces.
|
|
1205
|
+
* The three semantic filters mirror `/api/issues`'s query params:
|
|
1274
1206
|
*
|
|
1275
|
-
* `
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1207
|
+
* - `severities`, narrowed list of `Severity` values. Empty / absent
|
|
1208
|
+
* matches every severity.
|
|
1209
|
+
* - `analyzerIds`, accepts qualified (`<plugin>/<id>`) AND short
|
|
1210
|
+
* (`<id>`) forms; the suffix-match semantics live in
|
|
1211
|
+
* `matchesAnalyzerFilter`. Each entry generates two SQL clauses
|
|
1212
|
+
* (`= ?` and `LIKE '%/' || ?`) ORed together so the filter remains
|
|
1213
|
+
* a single SQL pass with parameterised values, no string
|
|
1214
|
+
* interpolation. Empty / absent matches every analyzer id.
|
|
1215
|
+
* - `nodePath`, keeps issues whose `nodeIds` JSON array contains the
|
|
1216
|
+
* given path (correlated EXISTS over `json_each`). Absent / null
|
|
1217
|
+
* skips the filter.
|
|
1279
1218
|
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
* and its refresh action. Spec keeps only `frontmatter/base.schema.json`
|
|
1285
|
-
* (universal); per-kind schemas live with the Provider.
|
|
1219
|
+
* Pagination is mandatory; the route layer fills the defaults via
|
|
1220
|
+
* `parsePagination`. `total` in `IIssueListResult` reports the total
|
|
1221
|
+
* MATCHING the filters (not just the page slice) so the SPA can
|
|
1222
|
+
* surface a correct page-count without a second round-trip.
|
|
1286
1223
|
*/
|
|
1287
|
-
|
|
1288
|
-
interface IRawNode {
|
|
1289
|
-
/** Path relative to the scan root that produced this node. */
|
|
1290
|
-
path: string;
|
|
1291
|
-
/** Raw markdown body (everything after the frontmatter fence). */
|
|
1292
|
-
body: string;
|
|
1293
|
-
/** Raw frontmatter text (between `---` fences). Empty string when absent. */
|
|
1294
|
-
frontmatterRaw: string;
|
|
1295
|
-
/** Parsed frontmatter, or `{}` when absent / unparseable. */
|
|
1296
|
-
frontmatter: Record<string, unknown>;
|
|
1224
|
+
interface IIssueListFilter {
|
|
1297
1225
|
/**
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1226
|
+
* Severity tokens to match. Typed as open `string` (not the
|
|
1227
|
+
* `Severity` union) so an unknown value from a URL query string
|
|
1228
|
+
* surfaces as a zero-match SQL query, not a kernel validation
|
|
1229
|
+
* error. The adapter parameterises each entry into the `IN(...)`
|
|
1230
|
+
* clause; unrecognised severities simply match no rows.
|
|
1303
1231
|
*/
|
|
1304
|
-
|
|
1232
|
+
severities?: readonly string[];
|
|
1233
|
+
analyzerIds?: readonly string[];
|
|
1234
|
+
nodePath?: string | null;
|
|
1235
|
+
offset: number;
|
|
1236
|
+
limit: number;
|
|
1305
1237
|
}
|
|
1306
1238
|
/**
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
* default refresh action id the UI dispatches for nodes of this kind.
|
|
1311
|
-
*
|
|
1312
|
-
* The split between `schema` (manifest-level path) and `schemaJson`
|
|
1313
|
-
* (runtime-loaded JSON) keeps the manifest shape spec-conformant while
|
|
1314
|
-
* letting the runtime instance carry the parsed schema without a second
|
|
1315
|
-
* filesystem read at scan time. Built-in Providers populate `schemaJson`
|
|
1316
|
-
* via `import schema from './schemas/skill.schema.json' with { type: 'json' }`;
|
|
1317
|
-
* user-plugin Providers loaded by `PluginLoader` will have it filled in
|
|
1318
|
-
* by the loader after manifest validation.
|
|
1239
|
+
* Output of `port.issues.list(...)`. `items` is the page slice (length
|
|
1240
|
+
* ≤ `filter.limit`); `total` is the count of rows matching the filters
|
|
1241
|
+
* before pagination was applied.
|
|
1319
1242
|
*/
|
|
1320
|
-
interface
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
* Provider's package directory. Mirrors the spec field of the same
|
|
1324
|
-
* name in `extensions/provider.schema.json#/properties/kinds/.../schema`.
|
|
1325
|
-
*/
|
|
1326
|
-
schema: string;
|
|
1327
|
-
/**
|
|
1328
|
-
* Loaded JSON Schema document for the kind. The kernel registers this
|
|
1329
|
-
* with AJV at scan boot and validates each node's frontmatter against
|
|
1330
|
-
* it. The schema MUST extend the spec's
|
|
1331
|
-
* `frontmatter/base.schema.json` via `allOf` + `$ref` to base's
|
|
1332
|
-
* `$id`; the loader registers base into the same AJV instance so
|
|
1333
|
-
* cross-package `$ref`-by-`$id` resolves transparently.
|
|
1334
|
-
*
|
|
1335
|
-
* `unknown` rather than a stronger type because AJV consumes any JSON
|
|
1336
|
-
* Schema object; tightening to a concrete shape would require mirroring
|
|
1337
|
-
* the JSON Schema vocabulary in TypeScript.
|
|
1338
|
-
*/
|
|
1339
|
-
schemaJson: unknown;
|
|
1340
|
-
/**
|
|
1341
|
-
* Qualified action id (`<plugin-id>/<action-id>`) the probabilistic-
|
|
1342
|
-
* refresh UI dispatches for nodes of this kind. The kernel resolves
|
|
1343
|
-
* the id against its qualified action registry; a dangling reference
|
|
1344
|
-
* disables the Provider with status `invalid-manifest`.
|
|
1345
|
-
*/
|
|
1346
|
-
defaultRefreshAction: string;
|
|
1347
|
-
/**
|
|
1348
|
-
* Presentation metadata the UI consumes to render nodes of this kind
|
|
1349
|
-
* (palette swatches, list tags, graph nodes, filter chips). Required
|
|
1350
|
-
* so the UI never has to invent visuals for a Provider-declared kind.
|
|
1351
|
-
* Mirrors `extensions/provider.schema.json#/properties/kinds/.../ui`.
|
|
1352
|
-
*/
|
|
1353
|
-
ui: IProviderKindUi;
|
|
1243
|
+
interface IIssueListResult {
|
|
1244
|
+
items: Issue[];
|
|
1245
|
+
total: number;
|
|
1354
1246
|
}
|
|
1355
|
-
/**
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
*/
|
|
1363
|
-
interface
|
|
1364
|
-
/**
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
/**
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
*/
|
|
1377
|
-
color: string;
|
|
1378
|
-
/**
|
|
1379
|
-
* Optional dark-theme variant of `color`. When absent, the UI falls
|
|
1380
|
-
* back to `color`. Declared explicitly because a luminosity flip
|
|
1381
|
-
* rarely matches the brand intent for kinds that should stand out in
|
|
1382
|
-
* dark mode.
|
|
1383
|
-
*/
|
|
1384
|
-
colorDark?: string;
|
|
1385
|
-
/**
|
|
1386
|
-
* Optional decorative emoji used as a fallback when `icon` is absent
|
|
1387
|
-
* or fails to render. Length-bound so the UI can lay it out
|
|
1388
|
-
* predictably alongside text.
|
|
1389
|
-
*/
|
|
1390
|
-
emoji?: string;
|
|
1391
|
-
/**
|
|
1392
|
-
* Optional discriminated icon descriptor. The UI prefers `icon` over
|
|
1393
|
-
* `emoji`; when both are absent, the UI falls back to the first
|
|
1394
|
-
* letter of `label` colored with `color`.
|
|
1395
|
-
*/
|
|
1396
|
-
icon?: TProviderKindIcon;
|
|
1247
|
+
/** Output of `port.jobs.pruneTerminal` / `listTerminalCandidates`. */
|
|
1248
|
+
interface IPruneResult {
|
|
1249
|
+
/** How many `state_jobs` rows were deleted (or would be, in dry-run). */
|
|
1250
|
+
deletedCount: number;
|
|
1251
|
+
/** Job-file paths from the affected rows; the CLI unlinks these from disk. `null` `filePath` rows contribute nothing here. */
|
|
1252
|
+
filePaths: string[];
|
|
1253
|
+
}
|
|
1254
|
+
/** Filter shape for `port.history.list`. All fields optional. */
|
|
1255
|
+
interface IListExecutionsFilter {
|
|
1256
|
+
/** Restrict to executions whose `nodeIds` array contains this path. */
|
|
1257
|
+
nodePath?: string;
|
|
1258
|
+
/** Exact match on `extension_id`. */
|
|
1259
|
+
actionId?: string;
|
|
1260
|
+
/** Subset of {`completed`,`failed`,`cancelled`}. */
|
|
1261
|
+
statuses?: ExecutionStatus[];
|
|
1262
|
+
/** Lower bound (inclusive) on `started_at`. Unix ms. */
|
|
1263
|
+
sinceMs?: number;
|
|
1264
|
+
/** Upper bound (exclusive) on `started_at`. Unix ms. */
|
|
1265
|
+
untilMs?: number;
|
|
1266
|
+
/** Cap result count. No default. */
|
|
1267
|
+
limit?: number;
|
|
1397
1268
|
}
|
|
1269
|
+
/** Window shape for `port.history.aggregateStats`. */
|
|
1270
|
+
interface IHistoryStatsRange {
|
|
1271
|
+
/** Inclusive lower bound. `null` = all-time. */
|
|
1272
|
+
sinceMs: number | null;
|
|
1273
|
+
/** Exclusive upper bound. */
|
|
1274
|
+
untilMs: number;
|
|
1275
|
+
}
|
|
1276
|
+
/** Period bucket granularity for `port.history.aggregateStats`. */
|
|
1277
|
+
type THistoryStatsPeriod = 'day' | 'week' | 'month';
|
|
1398
1278
|
/**
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
* `currentColor`. The discriminator (`kind`) keeps the UI dispatch
|
|
1403
|
-
* exhaustive without string-sniffing the payload.
|
|
1279
|
+
* Output of `port.transaction(tx => tx.history.migrateNodeFks(from, to))`.
|
|
1280
|
+
* Lists how many rows in each `state_*` table were repointed plus any
|
|
1281
|
+
* composite-PK collisions that forced a drop instead of an update.
|
|
1404
1282
|
*/
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
interface IProvider extends IExtensionBase {
|
|
1413
|
-
kind: 'provider';
|
|
1414
|
-
/**
|
|
1415
|
-
* Catalog of node kinds this Provider emits. Keyed by kind name. Every
|
|
1416
|
-
* kind the Provider can `classify()` MUST have an entry; an entry is
|
|
1417
|
-
* the union of the kind's frontmatter schema and its default refresh
|
|
1418
|
-
* action.
|
|
1419
|
-
*
|
|
1420
|
-
* The string keys are typed loosely (`string`) rather than `NodeKind`
|
|
1421
|
-
* because the value space is open by design: a future Cursor Provider
|
|
1422
|
-
* could declare `rule`, an Obsidian Provider could declare `daily`.
|
|
1423
|
-
* The kernel's hard-coded `NodeKind` union represents the kinds the
|
|
1424
|
-
* built-in Claude Provider emits; it is NOT the kernel-wide kind type
|
|
1425
|
-
* (see `kernel/types.ts:NodeKind` docstring). `Node.kind`, the AJV
|
|
1426
|
-
* `node.schema.json` validator, and the SQLite `scan_nodes.kind`
|
|
1427
|
-
* column all accept any non-empty string an enabled Provider returns.
|
|
1428
|
-
*/
|
|
1429
|
-
kinds: Record<string, IProviderKind>;
|
|
1283
|
+
interface IMigrateNodeFksReport {
|
|
1284
|
+
jobs: number;
|
|
1285
|
+
executions: number;
|
|
1286
|
+
summaries: number;
|
|
1287
|
+
enrichments: number;
|
|
1288
|
+
pluginKvs: number;
|
|
1289
|
+
nodeFavorites: number;
|
|
1430
1290
|
/**
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
* `
|
|
1438
|
-
* `skill.schema.json` and `command.schema.json` can `$ref` it without
|
|
1439
|
-
* duplicating fields.
|
|
1440
|
-
*
|
|
1441
|
-
* Runtime-only, does NOT appear in the spec's `provider.schema.json`
|
|
1442
|
-
* manifest. Manifest-validated schemas remain the per-kind ones in
|
|
1443
|
-
* `kinds[<kind>].schema`; auxiliary schemas are an implementation
|
|
1444
|
-
* concern of how the runtime composes those.
|
|
1291
|
+
* Collisions encountered when migrating any of the keyed-by-node
|
|
1292
|
+
* `state_*` tables because a row already existed at the destination
|
|
1293
|
+
* PK. The pre-existing rows are preserved, the migrating rows are
|
|
1294
|
+
* dropped (deleted from `fromPath` without a corresponding INSERT).
|
|
1295
|
+
* One entry per dropped row, with the affected PK fields included
|
|
1296
|
+
* for diagnostic output. `state_node_favorites` has no composite key
|
|
1297
|
+
* so its `keys` is the empty object.
|
|
1445
1298
|
*/
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1299
|
+
collisions: Array<{
|
|
1300
|
+
table: 'state_summaries' | 'state_enrichments' | 'state_plugin_kvs' | 'state_node_favorites';
|
|
1301
|
+
fromPath: string;
|
|
1302
|
+
toPath: string;
|
|
1303
|
+
keys: Record<string, string>;
|
|
1304
|
+
}>;
|
|
1305
|
+
}
|
|
1306
|
+
/** A single `config_plugins` override row as the kernel sees it. */
|
|
1307
|
+
interface IPluginConfigRow {
|
|
1308
|
+
pluginId: string;
|
|
1309
|
+
enabled: boolean;
|
|
1310
|
+
configJson: string | null;
|
|
1311
|
+
updatedAt: number;
|
|
1312
|
+
}
|
|
1313
|
+
/** Discovered kernel migration file (one of `NNN_snake_case.sql`). */
|
|
1314
|
+
interface IMigrationFile {
|
|
1315
|
+
version: number;
|
|
1316
|
+
description: string;
|
|
1317
|
+
filePath: string;
|
|
1318
|
+
}
|
|
1319
|
+
/** A row from the `config_schema_versions` ledger for the kernel scope. */
|
|
1320
|
+
interface IMigrationRecord {
|
|
1321
|
+
scope: string;
|
|
1322
|
+
ownerId: string;
|
|
1323
|
+
version: number;
|
|
1324
|
+
description: string;
|
|
1325
|
+
appliedAt: number;
|
|
1326
|
+
}
|
|
1327
|
+
/** `port.migrations.plan` output: applied vs pending. */
|
|
1328
|
+
interface IMigrationPlan {
|
|
1329
|
+
applied: IMigrationRecord[];
|
|
1330
|
+
pending: IMigrationFile[];
|
|
1331
|
+
}
|
|
1332
|
+
/** Apply-time options for `port.migrations.apply`. */
|
|
1333
|
+
interface IApplyOptions {
|
|
1334
|
+
backup?: boolean;
|
|
1335
|
+
dryRun?: boolean;
|
|
1336
|
+
to?: number;
|
|
1337
|
+
}
|
|
1338
|
+
/** Result of `port.migrations.apply`. */
|
|
1339
|
+
interface IApplyResult {
|
|
1340
|
+
applied: IMigrationFile[];
|
|
1341
|
+
backupPath: string | null;
|
|
1342
|
+
}
|
|
1343
|
+
/** Discovered plugin migration file. Same `NNN_snake_case.sql` convention. */
|
|
1344
|
+
interface IPluginMigrationFile {
|
|
1345
|
+
version: number;
|
|
1346
|
+
description: string;
|
|
1347
|
+
filePath: string;
|
|
1348
|
+
}
|
|
1349
|
+
/** A row from the `config_schema_versions` ledger for a single plugin. */
|
|
1350
|
+
interface IPluginMigrationRecord {
|
|
1351
|
+
version: number;
|
|
1352
|
+
description: string;
|
|
1353
|
+
appliedAt: number;
|
|
1354
|
+
}
|
|
1355
|
+
/** `port.pluginMigrations.plan` output for a single plugin. */
|
|
1356
|
+
interface IPluginMigrationPlan {
|
|
1357
|
+
pluginId: string;
|
|
1358
|
+
applied: IPluginMigrationRecord[];
|
|
1359
|
+
pending: IPluginMigrationFile[];
|
|
1360
|
+
}
|
|
1361
|
+
/** Apply-time options for `port.pluginMigrations.apply`. */
|
|
1362
|
+
interface IPluginApplyOptions {
|
|
1363
|
+
/** No actual writes; surfaces what would run. Default false. */
|
|
1364
|
+
dryRun?: boolean;
|
|
1365
|
+
}
|
|
1366
|
+
/** Result of `port.pluginMigrations.apply`. */
|
|
1367
|
+
interface IPluginApplyResult {
|
|
1368
|
+
pluginId: string;
|
|
1369
|
+
applied: IPluginMigrationFile[];
|
|
1370
|
+
/** Catalog intrusions caught by Layer 3 (post-apply sweep). Empty when clean. */
|
|
1371
|
+
intrusions: string[];
|
|
1507
1372
|
}
|
|
1508
1373
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1374
|
+
* Single contribution row as returned to callers of the
|
|
1375
|
+
* `contributions` namespace on `StoragePort`. The payload is
|
|
1376
|
+
* `unknown` because the slot space is open at the type layer (catalog
|
|
1377
|
+
* evolution is a kernel + spec concern); narrow at the call site by
|
|
1378
|
+
* reading `slot`.
|
|
1379
|
+
*
|
|
1380
|
+
* Lives next to the port (not under `adapters/sqlite/`) so non-SQLite
|
|
1381
|
+
* implementations of `StoragePort` (in-memory test harness, future
|
|
1382
|
+
* Postgres adapter) can satisfy the port contract without importing
|
|
1383
|
+
* from the SQLite adapter. The SQLite adapter re-exports this type
|
|
1384
|
+
* for backwards compatibility with callers that still import from
|
|
1385
|
+
* the adapter path.
|
|
1512
1386
|
*/
|
|
1513
|
-
interface
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
* Parser id from the kernel-internal registry. Built-ins:
|
|
1522
|
-
* `'frontmatter-yaml'`, `'plain'`. Unknown ids surface as
|
|
1523
|
-
* `UnknownParserError` from the walker; the orchestrator translates
|
|
1524
|
-
* the error into a Provider issue with status `invalid-manifest`.
|
|
1525
|
-
*/
|
|
1526
|
-
parser: string;
|
|
1387
|
+
interface IPersistedContribution {
|
|
1388
|
+
pluginId: string;
|
|
1389
|
+
extensionId: string;
|
|
1390
|
+
nodePath: string;
|
|
1391
|
+
contributionId: string;
|
|
1392
|
+
slot: string;
|
|
1393
|
+
payload: unknown;
|
|
1394
|
+
emittedAt: number;
|
|
1527
1395
|
}
|
|
1528
1396
|
|
|
1529
1397
|
/**
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
* nodes, the graph, or the DB. Cross-node reasoning lives in rules.
|
|
1534
|
-
*
|
|
1535
|
-
* Extractors are deterministic-only. They run synchronously inside the
|
|
1536
|
-
* scan loop; LLM-driven enrichment of a node is an Action concern, not
|
|
1537
|
-
* an Extractor concern. The Extractor context therefore exposes no
|
|
1538
|
-
* `RunnerPort`, see spec `architecture.md` §Execution modes.
|
|
1539
|
-
*
|
|
1540
|
-
* Output channels (all on the context):
|
|
1398
|
+
* `scan_contributions` adapter, replace-all writer used by
|
|
1399
|
+
* `persistScanResult`, plus read helpers consumed by the BFF
|
|
1400
|
+
* (`/api/contributions/...`) and rules (`core/contribution-orphan`).
|
|
1541
1401
|
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
* onto the node. Strictly separate from the author-supplied frontmatter
|
|
1547
|
-
* (the latter remains immutable and survives verbatim). Persistence
|
|
1548
|
-
* is spec'd in § A.8.
|
|
1549
|
-
* - `ctx.store`, plugin-scoped persistence. Present only when the
|
|
1550
|
-
* plugin declares `storage.mode` in `plugin.json`; shape depends on the
|
|
1551
|
-
* mode (`KvStore` for mode A, scoped `Database` for mode B). See
|
|
1552
|
-
* `plugin-kv-api.md` for the contract.
|
|
1402
|
+
* One row per `(plugin_id, extension_id, node_path, contribution_id)`
|
|
1403
|
+
* tuple. See `spec/architecture.md` § View contribution system →
|
|
1404
|
+
* Persistence and `migrations/001_initial.sql` § View contribution
|
|
1405
|
+
* layer for the normative shape.
|
|
1553
1406
|
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1407
|
+
* Replace-all semantics mirror the rest of the `scan_*` zone: every
|
|
1408
|
+
* scan is a fresh snapshot, so prior rows are deleted before insert.
|
|
1409
|
+
* Wrapped in the same transaction `persistScanResult` opens.
|
|
1556
1410
|
*
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1411
|
+
* The rename heuristic does NOT need to migrate `node_path` here,
|
|
1412
|
+
* because of replace-all, every contribution is re-emitted on the new
|
|
1413
|
+
* path automatically. Keeping the rename path lighter than `state_*`
|
|
1414
|
+
* (which IS rename-migrated because state survives across scans).
|
|
1560
1415
|
*/
|
|
1561
1416
|
|
|
1562
1417
|
/**
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1418
|
+
* In-memory contribution record buffered during scan and flushed to
|
|
1419
|
+
* `scan_contributions` by `persistScanResult`. One entry per accepted
|
|
1420
|
+
* `ctx.emitContribution(id, payload)` call. Payload validation against
|
|
1421
|
+
* the slot's payload schema happens at emit time (orchestrator);
|
|
1422
|
+
* by the time records reach this adapter they are wire-shape clean.
|
|
1567
1423
|
*/
|
|
1568
|
-
interface
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
*/
|
|
1574
|
-
emitLink(link: Link): void;
|
|
1575
|
-
/**
|
|
1576
|
-
* Merge canonical, kernel-curated properties onto the current node's
|
|
1577
|
-
* enrichment layer. The author-supplied frontmatter stays untouched
|
|
1578
|
-
* (Decision #109 in `ROADMAP.md`). Persistence and stale-tracking
|
|
1579
|
-
* semantics live in spec § A.8; the orchestrator already buffers the
|
|
1580
|
-
* partials and `persistScanResult` upserts them.
|
|
1581
|
-
*/
|
|
1582
|
-
enrichNode(partial: Partial<Node>): void;
|
|
1583
|
-
/**
|
|
1584
|
-
* Emit a per-node view contribution. The first argument is the
|
|
1585
|
-
* extension-local Record key declared under
|
|
1586
|
-
* `extension.viewContributions[<contributionId>]`; the second is a
|
|
1587
|
-
* payload that conforms to the slot's payload schema in
|
|
1588
|
-
* `spec/schemas/view-slots.schema.json#/$defs/payloads/<slot>`,
|
|
1589
|
-
* where `<slot>` is the slot the manifest declared for this
|
|
1590
|
-
* contribution. The orchestrator validates the payload against the
|
|
1591
|
-
* slot's schema before persisting to `scan_contributions`; off-shape
|
|
1592
|
-
* payloads are silently dropped with an `extension.error` event
|
|
1593
|
-
* (mirror of `emitLink` rejecting off-`emitsLinkKinds` links).
|
|
1594
|
-
* Calling `emitContribution` with a `contributionId` that is not
|
|
1595
|
-
* declared in the manifest is also dropped with an `extension.error`.
|
|
1596
|
-
* See `architecture.md` §View contribution system → Emit path.
|
|
1597
|
-
*/
|
|
1598
|
-
emitContribution(contributionId: string, payload: unknown): void;
|
|
1599
|
-
}
|
|
1600
|
-
interface IExtractorContext extends IExtractorCallbacks {
|
|
1601
|
-
node: Node;
|
|
1602
|
-
body: string;
|
|
1603
|
-
frontmatter: Record<string, unknown>;
|
|
1424
|
+
interface IContributionRecord {
|
|
1425
|
+
pluginId: string;
|
|
1426
|
+
extensionId: string;
|
|
1427
|
+
nodePath: string;
|
|
1428
|
+
contributionId: string;
|
|
1604
1429
|
/**
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
* (`set(key, value)`), `DedicatedStoreWrapper` for mode B
|
|
1608
|
-
* (`write(table, row)`). See `spec/plugin-kv-api.md`.
|
|
1609
|
-
*
|
|
1610
|
-
* Typed as `unknown` so this contract module stays free of any
|
|
1611
|
-
* adapter-side imports, the concrete `IPluginStore` lives in
|
|
1612
|
-
* `kernel/adapters/plugin-store.js`. Plugin authors narrow at the
|
|
1613
|
-
* call site based on the storage mode declared in their manifest.
|
|
1614
|
-
* The orchestrator looks up the wrapper per-extractor in
|
|
1615
|
-
* `RunScanOptions.pluginStores` (keyed by `pluginId`) and attaches
|
|
1616
|
-
* it here.
|
|
1430
|
+
* Closed enum value mirroring `view-slots.schema.json#/$defs/SlotName`.
|
|
1431
|
+
* Persisted as TEXT (no SQL CHECK by design, see migration comment).
|
|
1617
1432
|
*/
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
emitsLinkKinds: LinkKind[];
|
|
1623
|
-
defaultConfidence: Confidence;
|
|
1624
|
-
scope: 'frontmatter' | 'body' | 'both';
|
|
1625
|
-
/**
|
|
1626
|
-
* Optional opt-in filter on `node.kind`. When declared, the orchestrator
|
|
1627
|
-
* skips invocation of `extract()` for any node whose `kind` is NOT in
|
|
1628
|
-
* this list, fail-fast, before context construction, so the extractor
|
|
1629
|
-
* wastes zero CPU on inapplicable nodes.
|
|
1630
|
-
*
|
|
1631
|
-
* Absent (`undefined`) is the default: the extractor applies to every
|
|
1632
|
-
* kind. There are no wildcards, the absence of the field already
|
|
1633
|
-
* encodes "every kind". An empty array (`[]`) is rejected at load
|
|
1634
|
-
* time by AJV (`minItems: 1` in the schema).
|
|
1635
|
-
*
|
|
1636
|
-
* Unknown kinds (no installed Provider declares them) do NOT block
|
|
1637
|
-
* the load: the extractor keeps `loaded` status and `sm plugins doctor`
|
|
1638
|
-
* surfaces a warning. The Provider that declares the kind may arrive
|
|
1639
|
-
* later (e.g. a user installs the corresponding plugin).
|
|
1640
|
-
*
|
|
1641
|
-
* Spec: `spec/schemas/extensions/extractor.schema.json#/properties/applicableKinds`.
|
|
1642
|
-
*/
|
|
1643
|
-
applicableKinds?: string[];
|
|
1644
|
-
/**
|
|
1645
|
-
* Extractor entry point. Returns nothing; output flows through
|
|
1646
|
-
* `ctx.emitLink`, `ctx.enrichNode`, and `ctx.store`.
|
|
1647
|
-
*/
|
|
1648
|
-
extract(ctx: IExtractorContext): void | Promise<void>;
|
|
1433
|
+
slot: string;
|
|
1434
|
+
/** Already-validated payload. Serialised via `JSON.stringify` at write. */
|
|
1435
|
+
payload: unknown;
|
|
1436
|
+
emittedAt: number;
|
|
1649
1437
|
}
|
|
1650
1438
|
|
|
1651
1439
|
/**
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1440
|
+
* `loadScanResult`, driving inverse of `persistScanResult`. Reads the
|
|
1441
|
+
* `scan_*` tables and reconstructs a `ScanResult` shape so the
|
|
1442
|
+
* orchestrator can run an incremental scan (`sm scan --changed`) on
|
|
1443
|
+
* top of a prior snapshot.
|
|
1444
|
+
*
|
|
1445
|
+
* The reconstruction is faithful for everything that was actually
|
|
1446
|
+
* persisted: nodes (with triple-split bytes / tokens, denormalised
|
|
1447
|
+
* counts, JSON frontmatter), internal links (with regrouped
|
|
1448
|
+
* `trigger` / `location`, parsed `sources[]`), and issues
|
|
1449
|
+
* (with parsed `nodeIds` / `linkIndices` / `fix` / `data`).
|
|
1450
|
+
*
|
|
1451
|
+
* **Documented omission**: external pseudo-links (those whose target is
|
|
1452
|
+
* an `http://` / `https://` URL emitted by the external-url-counter
|
|
1453
|
+
* extractor) are NEVER persisted to `scan_links`, only their per-node
|
|
1454
|
+
* count survives in `scan_nodes.external_refs_count`. Therefore the
|
|
1455
|
+
* `result.links` returned by `loadScanResult` contains only internal
|
|
1456
|
+
* graph links, and `node.externalRefsCount` is the authoritative count
|
|
1457
|
+
* carried over from the prior scan. The orchestrator's incremental path
|
|
1458
|
+
* preserves that count for "unchanged" nodes and re-derives it for
|
|
1459
|
+
* new / modified nodes from a fresh extractor pass.
|
|
1460
|
+
*
|
|
1461
|
+
* Meta envelope: the `scan_meta` table persists `scope` / `roots` /
|
|
1462
|
+
* `scannedAt` / `scannedBy` / `providers` / `stats.filesWalked` /
|
|
1463
|
+
* `stats.filesSkipped` / `stats.durationMs`. When the row exists,
|
|
1464
|
+
* those fields come back authoritatively. When it does not (DB
|
|
1465
|
+
* freshly migrated but never scanned, or a legacy DB never
|
|
1466
|
+
* re-persisted), the loader degrades to a synthetic envelope:
|
|
1467
|
+
*
|
|
1468
|
+
* - `scannedAt` ← max(`scan_nodes.scanned_at`); falls back to `Date.now()`
|
|
1469
|
+
* for empty snapshots so the field stays a positive integer.
|
|
1470
|
+
* - `scope` ← `'project'`.
|
|
1471
|
+
* - `roots` ← `['.']` to satisfy spec's `minItems: 1`. NOT
|
|
1472
|
+
* load-bearing: the orchestrator's incremental path only reads
|
|
1473
|
+
* `nodes` / `links` / `issues` from the prior; it never reuses the
|
|
1474
|
+
* prior `roots`.
|
|
1475
|
+
* - `providers` ← `[]`.
|
|
1476
|
+
* - `stats` ← zeros for `filesWalked` / `filesSkipped` /
|
|
1477
|
+
* `durationMs`; the three count fields derive from row counts.
|
|
1478
|
+
*
|
|
1479
|
+
* Both branches keep `nodesCount` / `linksCount` / `issuesCount` derived
|
|
1480
|
+
* from `COUNT(*)` of the loaded rows, never persisted, always recomputed.
|
|
1660
1481
|
*/
|
|
1661
1482
|
|
|
1662
1483
|
/**
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1484
|
+
* Spec § A.9, load the fine-grained Extractor cache as a per-node map
|
|
1485
|
+
* from qualified extractor id (`<pluginId>/<id>`) to the run-time
|
|
1486
|
+
* hashes the extractor recorded on its last run. Empty map is the
|
|
1487
|
+
* default when the table is empty (fresh DB, never-scanned scope, or
|
|
1488
|
+
* every extractor has been uninstalled since the last scan).
|
|
1489
|
+
*
|
|
1490
|
+
* Returned shape: `Map<nodePath, Map<extractorId, IPriorExtractorRun>>`.
|
|
1491
|
+
* The inner value carries the body hash AND the sidecar-annotations
|
|
1492
|
+
* hash so the orchestrator can apply the widened cache key (both must
|
|
1493
|
+
* match for a cache hit).
|
|
1667
1494
|
*/
|
|
1668
|
-
interface
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
/** Absolute path of the missing `.md` the sidecar was anchored to. */
|
|
1672
|
-
expectedMdPath: string;
|
|
1495
|
+
interface IPriorExtractorRun {
|
|
1496
|
+
bodyHash: string;
|
|
1497
|
+
sidecarAnnotationsHash: string;
|
|
1673
1498
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* `.skillmapignore` parser + filter facade. Wraps `ignore` (kaelzhang)
|
|
1502
|
+
* with the project-local layering: bundled defaults → `config.ignore`
|
|
1503
|
+
* (from `.skill-map/settings.json`) → `.skillmapignore` file content.
|
|
1504
|
+
*
|
|
1505
|
+
* Why a wrapper instead of exposing `ignore` directly:
|
|
1506
|
+
*
|
|
1507
|
+
* 1. Single-source defaults, `src/config/defaults/skillmapignore` is
|
|
1508
|
+
* the canonical default list, loaded once at module init (or at
|
|
1509
|
+
* explicit build time, depending on bundling). The runtime never
|
|
1510
|
+
* re-reads it per scan.
|
|
1511
|
+
* 2. Stable interface, Providers and the orchestrator depend on a
|
|
1512
|
+
* minimal `IIgnoreFilter` shape, so the underlying library can be
|
|
1513
|
+
* swapped without touching every consumer.
|
|
1514
|
+
* 3. Path normalization, every consumer passes the path RELATIVE to
|
|
1515
|
+
* the scan root (POSIX separators); the wrapper guarantees that
|
|
1516
|
+
* contract before delegating to `ignore`.
|
|
1517
|
+
*/
|
|
1518
|
+
interface IIgnoreFilter {
|
|
1677
1519
|
/**
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1520
|
+
* Returns `true` when `relativePath` should be skipped. The caller
|
|
1521
|
+
* MUST pass paths relative to the scan root, with POSIX separators
|
|
1522
|
+
* (forward slashes), no leading `/`. Directories MAY be passed with
|
|
1523
|
+
* or without trailing `/`; the wrapper does not require it.
|
|
1681
1524
|
*/
|
|
1682
|
-
|
|
1525
|
+
ignores(relativePath: string): boolean;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Diagnostic surfaced by a parser when the raw input was structurally
|
|
1530
|
+
* malformed (e.g. YAML parse error). The parser MUST still return a
|
|
1531
|
+
* usable `{ frontmatter, frontmatterRaw, body }` triple (defaults are
|
|
1532
|
+
* fine) so the scan keeps making progress; this carries the message
|
|
1533
|
+
* the orchestrator translates into a kernel `Issue` with severity
|
|
1534
|
+
* `warn` (and `error` under `--strict`).
|
|
1535
|
+
*
|
|
1536
|
+
* Pure data: parsers never log or throw; they describe the failure
|
|
1537
|
+
* here and let the orchestrator decide how to surface it.
|
|
1538
|
+
*/
|
|
1539
|
+
interface IParseIssue {
|
|
1683
1540
|
/**
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
* re-reading the file from disk. Absent (or `undefined` per node)
|
|
1689
|
-
* when no sidecar accompanies the node, or when the sidecar failed
|
|
1690
|
-
* to parse. Treat as read-only.
|
|
1541
|
+
* Stable tag describing the failure class. The only emitter today
|
|
1542
|
+
* is `frontmatter-yaml` reporting a YAML parse error
|
|
1543
|
+
* (`'frontmatter-parse-error'`); the set may grow as new parsers
|
|
1544
|
+
* land.
|
|
1691
1545
|
*/
|
|
1692
|
-
|
|
1546
|
+
code: string;
|
|
1693
1547
|
/**
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
* split without reaching back into the kernel. Empty array when no
|
|
1698
|
-
* plugin declares contributions; absent for legacy callers (older
|
|
1699
|
-
* runScan sites that never wired the catalog through).
|
|
1548
|
+
* Human-readable message, sanitised. Never includes the raw input
|
|
1549
|
+
* (a hostile YAML could embed multi-line garbage); only the
|
|
1550
|
+
* parser-error string is interpolated.
|
|
1700
1551
|
*/
|
|
1701
|
-
|
|
1552
|
+
message: string;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Provider runtime contract. Walks filesystem roots and emits raw node
|
|
1557
|
+
* records; classification maps path conventions to a node kind.
|
|
1558
|
+
*
|
|
1559
|
+
* Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`,
|
|
1560
|
+
* `StoragePort.adapter`, etc.). A `Provider` is an extension kind authored
|
|
1561
|
+
* by plugins to declare a platform's universe (the catalog of kinds it
|
|
1562
|
+
* emits, the per-kind frontmatter schema, the filesystem directory it
|
|
1563
|
+
* owns); a hexagonal adapter is an internal implementation of a port.
|
|
1564
|
+
* Both can coexist without confusion because they live in different
|
|
1565
|
+
* namespaces.
|
|
1566
|
+
*
|
|
1567
|
+
* `walk()` is an async iterator so large scopes don't buffer in memory.
|
|
1568
|
+
* Each yielded `IRawNode` carries the full parsed frontmatter + body plus
|
|
1569
|
+
* the path relative to the scan root; the kernel computes hashes, bytes,
|
|
1570
|
+
* and tokens on top.
|
|
1571
|
+
*
|
|
1572
|
+
* **Spec 0.8.0**. Per-kind frontmatter schemas relocated from the spec
|
|
1573
|
+
* to the Provider that owns them. The flat
|
|
1574
|
+
* `defaultRefreshAction` map collapsed into the new `kinds` map: every
|
|
1575
|
+
* kind the Provider emits gets one entry that declares both its schema
|
|
1576
|
+
* and its refresh action. Spec keeps only `frontmatter/base.schema.json`
|
|
1577
|
+
* (universal); per-kind schemas live with the Provider.
|
|
1578
|
+
*/
|
|
1579
|
+
|
|
1580
|
+
interface IRawNode {
|
|
1581
|
+
/** Path relative to the scan root that produced this node. */
|
|
1582
|
+
path: string;
|
|
1583
|
+
/** Raw markdown body (everything after the frontmatter fence). */
|
|
1584
|
+
body: string;
|
|
1585
|
+
/** Raw frontmatter text (between `---` fences). Empty string when absent. */
|
|
1586
|
+
frontmatterRaw: string;
|
|
1587
|
+
/** Parsed frontmatter, or `{}` when absent / unparseable. */
|
|
1588
|
+
frontmatter: Record<string, unknown>;
|
|
1702
1589
|
/**
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1705
|
-
* through
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
* drift detection is NOT a scan concern, it lives at load time and
|
|
1709
|
-
* surfaces via `sm plugins doctor`. Empty array when no extension
|
|
1710
|
-
* declares view contributions; absent for legacy callers (older
|
|
1711
|
-
* runScan sites that never wired the catalog through).
|
|
1590
|
+
* Parser diagnostics (audit L1). Populated by the walker when the
|
|
1591
|
+
* parser surfaced `IParseIssue` entries (e.g. malformed YAML).
|
|
1592
|
+
* Carried through `processRawNode` and converted into warn-level
|
|
1593
|
+
* kernel `Issue` rows inside `buildFreshNodeAndValidateFrontmatter`.
|
|
1594
|
+
* Empty / undefined on the happy path.
|
|
1712
1595
|
*/
|
|
1713
|
-
|
|
1596
|
+
parseIssues?: readonly IParseIssue[];
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* One entry in a Provider's `kinds` map. Declares both the per-kind
|
|
1600
|
+
* frontmatter schema (path relative to the Provider's package dir, plus
|
|
1601
|
+
* the loaded JSON object the kernel passes to AJV) and the qualified
|
|
1602
|
+
* default refresh action id the UI dispatches for nodes of this kind.
|
|
1603
|
+
*
|
|
1604
|
+
* The split between `schema` (manifest-level path) and `schemaJson`
|
|
1605
|
+
* (runtime-loaded JSON) keeps the manifest shape spec-conformant while
|
|
1606
|
+
* letting the runtime instance carry the parsed schema without a second
|
|
1607
|
+
* filesystem read at scan time. Built-in Providers populate `schemaJson`
|
|
1608
|
+
* via `import schema from './schemas/skill.schema.json' with { type: 'json' }`;
|
|
1609
|
+
* user-plugin Providers loaded by `PluginLoader` will have it filled in
|
|
1610
|
+
* by the loader after manifest validation.
|
|
1611
|
+
*/
|
|
1612
|
+
interface IProviderKind {
|
|
1714
1613
|
/**
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
* issue. Pre-computed by the driving adapter (CLI / BFF) inside its
|
|
1719
|
-
* already-open storage transaction (mirrors the `orphanSidecars`
|
|
1720
|
-
* pattern: detection lives outside the analyzer, the analyzer only
|
|
1721
|
-
* projects). Absent (or empty) when the caller does not maintain a
|
|
1722
|
-
* jobs directory, when the storage path is unavailable, or when no
|
|
1723
|
-
* orphan files exist. Treat as read-only.
|
|
1614
|
+
* Path to the kind's frontmatter JSON Schema, relative to the
|
|
1615
|
+
* Provider's package directory. Mirrors the spec field of the same
|
|
1616
|
+
* name in `extensions/provider.schema.json#/properties/kinds/.../schema`.
|
|
1724
1617
|
*/
|
|
1725
|
-
|
|
1618
|
+
schema: string;
|
|
1726
1619
|
/**
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
* path-style link target falls into the set. Absent / empty when
|
|
1734
|
-
* the operator left `scan.referencePaths` empty or when the
|
|
1735
|
-
* adapter does not maintain the side index. Treat as read-only.
|
|
1736
|
-
*/
|
|
1737
|
-
referenceablePaths?: ReadonlySet<string>;
|
|
1738
|
-
/**
|
|
1739
|
-
* Absolute path of the scan's project root (cwd of the invocation).
|
|
1740
|
-
* Threaded into the analyzer pass so an analyzer that needs to
|
|
1741
|
-
* resolve a relative `link.target` to an absolute filesystem path
|
|
1742
|
-
* (today only `core/broken-ref`, when consulting
|
|
1743
|
-
* `referenceablePaths`) does not have to derive it from
|
|
1744
|
-
* `nodes[0].path` heuristics. Absent for legacy callers (older
|
|
1745
|
-
* `runScan` sites that never wired the field through). Always an
|
|
1746
|
-
* absolute path when present.
|
|
1747
|
-
*/
|
|
1748
|
-
cwd?: string;
|
|
1749
|
-
/**
|
|
1750
|
-
* Emit a per-node view contribution declared in this analyzer's
|
|
1751
|
-
* manifest `viewContributions` map. Sync, void return; the
|
|
1752
|
-
* orchestrator validates the payload against the slot's schema at
|
|
1753
|
-
* call time and silently drops invalid emissions with a logged
|
|
1754
|
-
* `extension.error` event (parallel to
|
|
1755
|
-
* `IExtractorCallbacks.emitContribution`).
|
|
1756
|
-
*
|
|
1757
|
-
* Unlike Extractor's emit (which binds `nodePath` from `ctx.node.path`
|
|
1758
|
-
* implicitly because Extractors run per-node), Analyzer's `evaluate()`
|
|
1759
|
-
* sees the full graph at once. The analyzer walks `ctx.nodes` itself
|
|
1760
|
-
* and MUST supply the target node path explicitly per emission.
|
|
1620
|
+
* Loaded JSON Schema document for the kind. The kernel registers this
|
|
1621
|
+
* with AJV at scan boot and validates each node's frontmatter against
|
|
1622
|
+
* it. The schema MUST extend the spec's
|
|
1623
|
+
* `frontmatter/base.schema.json` via `allOf` + `$ref` to base's
|
|
1624
|
+
* `$id`; the loader registers base into the same AJV instance so
|
|
1625
|
+
* cross-package `$ref`-by-`$id` resolves transparently.
|
|
1761
1626
|
*
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
* pipeline as Extractor emissions (`scan_contributions`).
|
|
1627
|
+
* `unknown` rather than a stronger type because AJV consumes any JSON
|
|
1628
|
+
* Schema object; tightening to a concrete shape would require mirroring
|
|
1629
|
+
* the JSON Schema vocabulary in TypeScript.
|
|
1766
1630
|
*/
|
|
1767
|
-
|
|
1768
|
-
}
|
|
1769
|
-
interface IAnalyzer extends IExtensionBase {
|
|
1770
|
-
kind: 'analyzer';
|
|
1631
|
+
schemaJson: unknown;
|
|
1771
1632
|
/**
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1633
|
+
* Qualified action id (`<plugin-id>/<action-id>`) the probabilistic-
|
|
1634
|
+
* refresh UI dispatches for nodes of this kind. The kernel resolves
|
|
1635
|
+
* the id against its qualified action registry; a dangling reference
|
|
1636
|
+
* disables the Provider with status `invalid-manifest`.
|
|
1774
1637
|
*/
|
|
1775
|
-
|
|
1638
|
+
defaultRefreshAction: string;
|
|
1776
1639
|
/**
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
* Analyzer fires from the Analyzer side. Actions are per-node by
|
|
1782
|
-
* design (project-level cleanup verbs like orphan file prune or
|
|
1783
|
-
* contribution relink are CLI verbs, not Actions) and are NOT
|
|
1784
|
-
* surfaced through this field. The UI consumes it in the node
|
|
1785
|
-
* inspector under "Recommended for issues". Optional; omit when no
|
|
1786
|
-
* Action resolves the finding (e.g. `core/superseded` surfaces
|
|
1787
|
-
* deliberate user declarations, not problems).
|
|
1640
|
+
* Presentation metadata the UI consumes to render nodes of this kind
|
|
1641
|
+
* (palette swatches, list tags, graph nodes, filter chips). Required
|
|
1642
|
+
* so the UI never has to invent visuals for a Provider-declared kind.
|
|
1643
|
+
* Mirrors `extensions/provider.schema.json#/properties/kinds/.../ui`.
|
|
1788
1644
|
*/
|
|
1789
|
-
|
|
1790
|
-
evaluate(ctx: IAnalyzerContext): Issue[] | Promise<Issue[]>;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
/**
|
|
1794
|
-
* Action runtime contract. The fourth plugin kind (spec § A.4 +
|
|
1795
|
-
* `spec/schemas/extensions/action.schema.json`).
|
|
1796
|
-
*
|
|
1797
|
-
* Actions operate on one or more nodes in one of two execution modes:
|
|
1798
|
-
*
|
|
1799
|
-
* - `deterministic`, code runs in-process; the action computes the
|
|
1800
|
-
* report synchronously and returns it. No job file, no runner.
|
|
1801
|
-
* - `probabilistic`, the kernel renders a prompt + preamble into a
|
|
1802
|
-
* job file; a runner executes it via `RunnerPort` against an LLM;
|
|
1803
|
-
* `sm record` closes the job and validates the report against
|
|
1804
|
-
* `reportSchemaRef`.
|
|
1805
|
-
*
|
|
1806
|
-
* **Deferred runtime invocation.** The dispatcher (`Action.run(ctx)` for
|
|
1807
|
-
* deterministic; the `RunnerPort` + `sm record` round-trip for
|
|
1808
|
-
* probabilistic) lands with the job subsystem (Decision #114 in
|
|
1809
|
-
* `ROADMAP.md`). Today the loader still validates `kind: 'action'`
|
|
1810
|
-
* manifests against `extension-action.schema.json` and the registry
|
|
1811
|
-
* holds them, `sm actions show` and the precondition gating UI consume
|
|
1812
|
-
* the manifest data. The runtime entry point is intentionally absent
|
|
1813
|
-
* from `IAction` so plugin authors don't ship a method the kernel will
|
|
1814
|
-
* not call until the job subsystem is in place; when it ships, the
|
|
1815
|
-
* method shape will land here without breaking the manifest contract.
|
|
1816
|
-
*
|
|
1817
|
-
* Mirrors `extensions/action.schema.json`:
|
|
1818
|
-
*
|
|
1819
|
-
* - `mode` (required), discriminator between the two modes.
|
|
1820
|
-
* - `reportSchemaRef` (required), JSON Schema reference the report
|
|
1821
|
-
* MUST validate against. MUST extend `report-base.schema.json`.
|
|
1822
|
-
* - `promptTemplateRef`, REQUIRED when `mode: 'probabilistic'`,
|
|
1823
|
-
* FORBIDDEN when `mode: 'deterministic'`. The schema's conditional
|
|
1824
|
-
* `allOf` enforces both directions; the runtime contract simply
|
|
1825
|
-
* surfaces the field as optional and lets the loader catch shape
|
|
1826
|
-
* violations at AJV time.
|
|
1827
|
-
* - `expectedDurationSeconds`, REQUIRED for probabilistic (drives
|
|
1828
|
-
* TTL); advisory for deterministic.
|
|
1829
|
-
* - `precondition`, declarative filter consumed by `--all` fan-out,
|
|
1830
|
-
* UI button gating, `sm actions show`.
|
|
1831
|
-
* - `expectedTools`, hint to Skill / CLI runners about expected
|
|
1832
|
-
* tools (no normative enforcement in v0).
|
|
1833
|
-
* - `fanOutPolicy`, `'per-node'` (default) vs `'batch'`.
|
|
1834
|
-
*/
|
|
1835
|
-
|
|
1836
|
-
/**
|
|
1837
|
-
* Single sidecar write payload an Action can return. Discriminated union so
|
|
1838
|
-
* future write kinds (storage rows, plugin KV, etc.) can land additively
|
|
1839
|
-
* without breaking consumers that only handle `kind: 'sidecar'`.
|
|
1840
|
-
*
|
|
1841
|
-
* - `path`, absolute path to the `.sm` file the kernel must materialise
|
|
1842
|
-
* the change into. Resolved by the Action from the node's absolute
|
|
1843
|
-
* path via `sidecarPathFor()`.
|
|
1844
|
-
* - `changes`, partial sidecar root used as a deep-merge patch (NOT a
|
|
1845
|
-
* full replacement). Arrays REPLACE; objects RECURSE. Reason:
|
|
1846
|
-
* sidecars are shared-write between skill-map core and plugins;
|
|
1847
|
-
* a full replace would clobber `<plugin-id>:` namespaced blocks.
|
|
1848
|
-
*/
|
|
1849
|
-
type TActionWrite = {
|
|
1850
|
-
kind: 'sidecar';
|
|
1851
|
-
path: string;
|
|
1852
|
-
changes: Record<string, unknown>;
|
|
1853
|
-
};
|
|
1854
|
-
/**
|
|
1855
|
-
* Result envelope returned by deterministic Actions. The `report` field
|
|
1856
|
-
* carries the typed report payload (each Action declares its shape via
|
|
1857
|
-
* `reportSchemaRef`); `writes` is opt-in, Actions that do not mutate
|
|
1858
|
-
* persistent state simply omit it.
|
|
1859
|
-
*/
|
|
1860
|
-
interface IActionResult<TReport = unknown> {
|
|
1861
|
-
report: TReport;
|
|
1862
|
-
writes?: TActionWrite[];
|
|
1863
|
-
}
|
|
1864
|
-
/**
|
|
1865
|
-
* Runtime context passed to a deterministic Action's `invoke()` method.
|
|
1866
|
-
* Minimal surface, Actions stay pure (no IO inside `invoke`); the kernel
|
|
1867
|
-
* materialises any returned `writes` after the call.
|
|
1868
|
-
*
|
|
1869
|
-
* - `node`, the target `Node` the Action operates on. Open-by-design;
|
|
1870
|
-
* batch / fan-out flows pick the matching nodes upstream.
|
|
1871
|
-
* - `nodeAbsolutePath`, absolute path to the node's `.md` file on
|
|
1872
|
-
* disk. The Action uses this to compute the sidecar path it returns
|
|
1873
|
-
* in a `TActionWrite`. Surfaced separately from `node.path` (which is
|
|
1874
|
-
* the relative scope-root form) so Actions never compose absolute
|
|
1875
|
-
* paths from `node.path` themselves.
|
|
1876
|
-
* - `invoker`, identity of the caller; written into the sidecar's
|
|
1877
|
-
* `audit.lastBumpedBy` when the Action chooses to. CLI invocations
|
|
1878
|
-
* pass `'cli'`; plugin-driven invocations pass `'plugin:<plugin-id>'`.
|
|
1879
|
-
* - `now`, clock function; tests inject a deterministic source.
|
|
1880
|
-
* Defaults to `() => new Date()` at the composition root.
|
|
1881
|
-
*/
|
|
1882
|
-
interface IActionContext {
|
|
1883
|
-
node: Node;
|
|
1884
|
-
nodeAbsolutePath: string;
|
|
1885
|
-
invoker: string;
|
|
1886
|
-
now: () => Date;
|
|
1645
|
+
ui: IProviderKindUi;
|
|
1887
1646
|
}
|
|
1888
1647
|
/**
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1648
|
+
* Presentation contract for one Provider kind. The Provider declares
|
|
1649
|
+
* intent (label + base color, optional dark variant + emoji + icon);
|
|
1650
|
+
* the UI derives `bg`/`fg` tints per theme via a deterministic helper
|
|
1651
|
+
* and reads the registry from the `kindRegistry` field embedded in REST
|
|
1652
|
+
* envelopes. Single source of truth for what a kind looks like, the
|
|
1653
|
+
* UI never hardcodes presentation for a built-in kind.
|
|
1892
1654
|
*/
|
|
1893
|
-
interface
|
|
1655
|
+
interface IProviderKindUi {
|
|
1894
1656
|
/**
|
|
1895
|
-
*
|
|
1896
|
-
* `
|
|
1897
|
-
*
|
|
1898
|
-
* into `cursorRule`. Omitted → any kind.
|
|
1657
|
+
* Plural human-readable label for groups of this kind (e.g. `'Skills'`,
|
|
1658
|
+
* `'Agents'`, `'Cursor Rules'`). Used in filter dropdowns, palette
|
|
1659
|
+
* tooltips, and any list grouping.
|
|
1899
1660
|
*/
|
|
1900
|
-
|
|
1901
|
-
/** Provider ids whose nodes this action accepts. Omitted → any Provider. */
|
|
1902
|
-
provider?: string[];
|
|
1903
|
-
/** Node stability filter. */
|
|
1904
|
-
stability?: Array<'experimental' | 'stable' | 'deprecated'>;
|
|
1661
|
+
label: string;
|
|
1905
1662
|
/**
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1663
|
+
* Base hex color (`#RRGGBB`) for the light theme. The UI derives `bg`
|
|
1664
|
+
* and `fg` tints from this value at runtime via a deterministic
|
|
1665
|
+
* helper. Declaring one base value (instead of three) keeps the
|
|
1666
|
+
* manifest small and centralises accessibility-driven contrast in the
|
|
1667
|
+
* UI.
|
|
1908
1668
|
*/
|
|
1909
|
-
|
|
1910
|
-
}
|
|
1911
|
-
interface IAction extends IExtensionBase {
|
|
1912
|
-
kind: 'action';
|
|
1669
|
+
color: string;
|
|
1913
1670
|
/**
|
|
1914
|
-
*
|
|
1915
|
-
* `
|
|
1671
|
+
* Optional dark-theme variant of `color`. When absent, the UI falls
|
|
1672
|
+
* back to `color`. Declared explicitly because a luminosity flip
|
|
1673
|
+
* rarely matches the brand intent for kinds that should stand out in
|
|
1674
|
+
* dark mode.
|
|
1916
1675
|
*/
|
|
1917
|
-
|
|
1676
|
+
colorDark?: string;
|
|
1918
1677
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
*
|
|
1922
|
-
* `report-invalid`.
|
|
1678
|
+
* Optional decorative emoji used as a fallback when `icon` is absent
|
|
1679
|
+
* or fails to render. Length-bound so the UI can lay it out
|
|
1680
|
+
* predictably alongside text.
|
|
1923
1681
|
*/
|
|
1924
|
-
|
|
1682
|
+
emoji?: string;
|
|
1925
1683
|
/**
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
* `deterministic`.
|
|
1684
|
+
* Optional discriminated icon descriptor. The UI prefers `icon` over
|
|
1685
|
+
* `emoji`; when both are absent, the UI falls back to the first
|
|
1686
|
+
* letter of `label` colored with `color`.
|
|
1930
1687
|
*/
|
|
1931
|
-
|
|
1688
|
+
icon?: TProviderKindIcon;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Discriminated icon contract. `pi` references a PrimeIcons identifier
|
|
1692
|
+
* (e.g. `'pi-cog'`); `svg` carries raw SVG path data the UI wraps in a
|
|
1693
|
+
* `<svg viewBox="0 0 24 24"><path d="…"/></svg>` element tinted with
|
|
1694
|
+
* `currentColor`. The discriminator (`kind`) keeps the UI dispatch
|
|
1695
|
+
* exhaustive without string-sniffing the payload.
|
|
1696
|
+
*/
|
|
1697
|
+
type TProviderKindIcon = {
|
|
1698
|
+
kind: 'pi';
|
|
1699
|
+
id: string;
|
|
1700
|
+
} | {
|
|
1701
|
+
kind: 'svg';
|
|
1702
|
+
path: string;
|
|
1703
|
+
};
|
|
1704
|
+
interface IProvider extends IExtensionBase {
|
|
1705
|
+
kind: 'provider';
|
|
1932
1706
|
/**
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
*
|
|
1707
|
+
* Catalog of node kinds this Provider emits. Keyed by kind name. Every
|
|
1708
|
+
* kind the Provider can `classify()` MUST have an entry; an entry is
|
|
1709
|
+
* the union of the kind's frontmatter schema and its default refresh
|
|
1710
|
+
* action.
|
|
1711
|
+
*
|
|
1712
|
+
* The string keys are typed loosely (`string`) rather than `NodeKind`
|
|
1713
|
+
* because the value space is open by design: a future Cursor Provider
|
|
1714
|
+
* could declare `rule`, an Obsidian Provider could declare `daily`.
|
|
1715
|
+
* The kernel's hard-coded `NodeKind` union represents the kinds the
|
|
1716
|
+
* built-in Claude Provider emits; it is NOT the kernel-wide kind type
|
|
1717
|
+
* (see `kernel/types.ts:NodeKind` docstring). `Node.kind`, the AJV
|
|
1718
|
+
* `node.schema.json` validator, and the SQLite `scan_nodes.kind`
|
|
1719
|
+
* column all accept any non-empty string an enabled Provider returns.
|
|
1943
1720
|
*/
|
|
1944
|
-
|
|
1721
|
+
kinds: Record<string, IProviderKind>;
|
|
1945
1722
|
/**
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1723
|
+
* Optional auxiliary JSON Schemas this Provider's per-kind schemas
|
|
1724
|
+
* `$ref` by `$id`. Registered with AJV via `addSchema` BEFORE the
|
|
1725
|
+
* per-kind schemas compile, so cross-file `$ref` resolution succeeds.
|
|
1726
|
+
*
|
|
1727
|
+
* Use case: when several kinds share a common base (e.g. Anthropic's
|
|
1728
|
+
* merged skill / command frontmatter, both extend a shared
|
|
1729
|
+
* `skill-base.schema.json`), the Provider declares the base here so
|
|
1730
|
+
* `skill.schema.json` and `command.schema.json` can `$ref` it without
|
|
1731
|
+
* duplicating fields.
|
|
1732
|
+
*
|
|
1733
|
+
* Runtime-only, does NOT appear in the spec's `provider.schema.json`
|
|
1734
|
+
* manifest. Manifest-validated schemas remain the per-kind ones in
|
|
1735
|
+
* `kinds[<kind>].schema`; auxiliary schemas are an implementation
|
|
1736
|
+
* concern of how the runtime composes those.
|
|
1949
1737
|
*/
|
|
1950
|
-
|
|
1738
|
+
schemas?: unknown[];
|
|
1951
1739
|
/**
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
*
|
|
1740
|
+
* Declarative file-discovery config consumed by the kernel walker.
|
|
1741
|
+
* When present, the kernel walks every root, includes files whose
|
|
1742
|
+
* extension matches `extensions`, parses each with the parser id
|
|
1743
|
+
* registered in the kernel-internal registry, and yields `IRawNode`
|
|
1744
|
+
* records the same shape `walk()` would.
|
|
1745
|
+
*
|
|
1746
|
+
* When neither `read` nor `walk` is declared, `resolveProviderWalk`
|
|
1747
|
+
* applies the default `{ extensions: ['.md'], parser: 'frontmatter-yaml' }`
|
|
1748
|
+
* so the most common Provider shape needs zero configuration.
|
|
1749
|
+
*
|
|
1750
|
+
* Precedence: when both `walk()` (runtime field) and `read` are
|
|
1751
|
+
* declared, `walk()` wins, `read` is ignored. The escape-hatch
|
|
1752
|
+
* relationship is intentional: most Providers should use `read`;
|
|
1753
|
+
* Providers with non-standard discovery requirements (custom file
|
|
1754
|
+
* naming, multi-pass walks, dynamic ignore logic) implement `walk()`
|
|
1755
|
+
* directly and accept the duplication of audit-cleared defences.
|
|
1756
|
+
*
|
|
1757
|
+
* Built-in parsers: `'frontmatter-yaml'` (markdown with `--- … ---`
|
|
1758
|
+
* YAML frontmatter; pollution-strip + JSON_SCHEMA-pinned), `'plain'`
|
|
1759
|
+
* (entire body, empty frontmatter). The set is closed; user plugins
|
|
1760
|
+
* cannot register their own.
|
|
1955
1761
|
*/
|
|
1956
|
-
|
|
1762
|
+
read?: IProviderReadConfig;
|
|
1957
1763
|
/**
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
* runner / record path leave it absent and the kernel never calls it.
|
|
1962
|
-
* Step 9.6.3 (Decision #125) introduces the first concrete consumer:
|
|
1963
|
-
* the built-in `bump` Action implements `invoke()` and returns a
|
|
1964
|
-
* `writes: [{ kind: 'sidecar', ... }]` payload that the kernel
|
|
1965
|
-
* materialises through `ISidecarStore`.
|
|
1764
|
+
* Walk the given roots and yield every node the Provider recognises.
|
|
1765
|
+
* Non-matching files are silently skipped. Unreadable files produce
|
|
1766
|
+
* a diagnostic via the emitter but do not abort the walk.
|
|
1966
1767
|
*
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1768
|
+
* `options.ignoreFilter`, when supplied, the Provider MUST
|
|
1769
|
+
* skip every directory and file whose path-relative-to-root the
|
|
1770
|
+
* filter reports as ignored. Providers MAY also keep their own
|
|
1771
|
+
* hard-coded skip list (e.g. `.git`) as a defensive measure, but the
|
|
1772
|
+
* filter is the canonical source of user intent.
|
|
1971
1773
|
*
|
|
1972
|
-
*
|
|
1973
|
-
*
|
|
1974
|
-
*
|
|
1774
|
+
* Optional. When omitted, the Provider MUST declare `read` (or rely
|
|
1775
|
+
* on the default config). The orchestrator never calls `walk()`
|
|
1776
|
+
* directly, it goes through `resolveProviderWalk(provider)` which
|
|
1777
|
+
* picks `walk` over `read`.
|
|
1975
1778
|
*/
|
|
1976
|
-
|
|
1779
|
+
walk?(roots: string[], options?: {
|
|
1780
|
+
ignoreFilter?: IIgnoreFilter;
|
|
1781
|
+
}): AsyncIterable<IRawNode>;
|
|
1782
|
+
/**
|
|
1783
|
+
* Given a path and its parsed frontmatter, decide the node kind, or
|
|
1784
|
+
* `null` to disclaim the file. The classifier is called after walk()
|
|
1785
|
+
* yields; with multiple Providers active, every Provider walks every
|
|
1786
|
+
* file matching its `read.extensions`, so each Provider MUST disclaim
|
|
1787
|
+
* paths it does not recognise. Returning the same path's kind from
|
|
1788
|
+
* two Providers fires the spec's `provider-ambiguous` issue and the
|
|
1789
|
+
* orchestrator drops the duplicate.
|
|
1790
|
+
*
|
|
1791
|
+
* Convention: a Provider's classify returns one of its own `kinds`
|
|
1792
|
+
* map keys for paths in its territory (`.claude/`, `.gemini/`,
|
|
1793
|
+
* `.agents/skills/`, etc.) and `null` elsewhere. External Providers
|
|
1794
|
+
* (Cursor, Obsidian, …) follow the same rule: claim what's yours,
|
|
1795
|
+
* disclaim everything else. The orchestrator does not validate the
|
|
1796
|
+
* kind against `NodeKind`.
|
|
1797
|
+
*/
|
|
1798
|
+
classify(path: string, frontmatter: Record<string, unknown>): string | null;
|
|
1977
1799
|
}
|
|
1978
|
-
|
|
1979
1800
|
/**
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
1982
|
-
*
|
|
1983
|
-
* Two adjacent names live on the same instance:
|
|
1984
|
-
*
|
|
1985
|
-
* - `formatId: string`, the manifest field consumed by the
|
|
1986
|
-
* `--format <name>` CLI flag. The kernel's lookup is
|
|
1987
|
-
* `formatters.find((f) => f.formatId === flag)`.
|
|
1988
|
-
* - `format(ctx) → string`, the runtime method. Receives the full
|
|
1989
|
-
* graph and returns the serialized output. Output MUST be
|
|
1990
|
-
* byte-deterministic for the same input (the snapshot-test suite
|
|
1991
|
-
* relies on this).
|
|
1992
|
-
*
|
|
1993
|
-
* The split (`formatId` vs `format`) is deliberate: it keeps the method
|
|
1994
|
-
* named after the kind (`Formatter.format()` reads naturally) while the
|
|
1995
|
-
* field carries the identifier the user types on the command line.
|
|
1801
|
+
* Declarative read config a Provider declares via `IProvider.read`.
|
|
1802
|
+
* Mirrors `extensions/provider.schema.json#/properties/read` at the
|
|
1803
|
+
* TypeScript level. Built-in parser ids: `'frontmatter-yaml'`, `'plain'`.
|
|
1996
1804
|
*/
|
|
1997
|
-
|
|
1998
|
-
interface IFormatterContext {
|
|
1999
|
-
nodes: Node[];
|
|
2000
|
-
links: Link[];
|
|
2001
|
-
issues: Issue[];
|
|
1805
|
+
interface IProviderReadConfig {
|
|
2002
1806
|
/**
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
* envelope (today: the built-in `json` formatter under
|
|
2007
|
-
* `built-in-plugins/formatters/json/`) read this to project the
|
|
2008
|
-
* canonical document verbatim. `undefined` when the caller has only
|
|
2009
|
-
* the three primary arrays (back-compat with older drivers).
|
|
1807
|
+
* File extensions the walker yields. Strings include the leading dot
|
|
1808
|
+
* (e.g. `'.md'`, `'.mdc'`, `'.toml'`). Match is suffix-based; the
|
|
1809
|
+
* comparison is case-sensitive.
|
|
2010
1810
|
*/
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1811
|
+
extensions: string[];
|
|
1812
|
+
/**
|
|
1813
|
+
* Parser id from the kernel-internal registry. Built-ins:
|
|
1814
|
+
* `'frontmatter-yaml'`, `'plain'`. Unknown ids surface as
|
|
1815
|
+
* `UnknownParserError` from the walker; the orchestrator translates
|
|
1816
|
+
* the error into a Provider issue with status `invalid-manifest`.
|
|
1817
|
+
*/
|
|
1818
|
+
parser: string;
|
|
2019
1819
|
}
|
|
2020
1820
|
|
|
2021
1821
|
/**
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
*
|
|
2026
|
-
* mutate the pipeline, block emission, or alter outputs. Use cases
|
|
2027
|
-
* are notification (Slack on `job.completed`), integration glue (CI
|
|
2028
|
-
* webhook on `job.failed`), and bookkeeping (per-extractor metrics).
|
|
1822
|
+
* Extractor runtime contract. Consumes a single node (frontmatter + body)
|
|
1823
|
+
* and emits its output through three context-supplied callbacks rather than
|
|
1824
|
+
* a return value. Extractors run in isolation: they MUST NOT read other
|
|
1825
|
+
* nodes, the graph, or the DB. Cross-node reasoning lives in rules.
|
|
2029
1826
|
*
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2033
|
-
*
|
|
2034
|
-
* `process.exit` is never blocked). The full `ProgressEmitterPort`
|
|
2035
|
-
* catalog (per-node `scan.progress`, `model.delta`, `run.*`, internal
|
|
2036
|
-
* job lifecycle) is deliberately not hookable: too verbose for a
|
|
2037
|
-
* reactive surface, internal to the runner, or covered elsewhere.
|
|
2038
|
-
* Declaring a trigger outside the curated set yields
|
|
2039
|
-
* `invalid-manifest` at load time.
|
|
1827
|
+
* Extractors are deterministic-only. They run synchronously inside the
|
|
1828
|
+
* scan loop; LLM-driven enrichment of a node is an Action concern, not
|
|
1829
|
+
* an Extractor concern. The Extractor context therefore exposes no
|
|
1830
|
+
* `RunnerPort`, see spec `architecture.md` §Execution modes.
|
|
2040
1831
|
*
|
|
2041
|
-
*
|
|
1832
|
+
* Output channels (all on the context):
|
|
2042
1833
|
*
|
|
2043
|
-
* - `
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
1834
|
+
* - `ctx.emitLink(link)`, persist a link in the kernel's `links` table.
|
|
1835
|
+
* Validated against `emitsLinkKinds` before insertion; an off-contract
|
|
1836
|
+
* kind drops the link and surfaces an `extension.error` event.
|
|
1837
|
+
* - `ctx.enrichNode(partial)`, merge canonical, kernel-curated properties
|
|
1838
|
+
* onto the node. Strictly separate from the author-supplied frontmatter
|
|
1839
|
+
* (the latter remains immutable and survives verbatim). Persistence
|
|
1840
|
+
* is spec'd in § A.8.
|
|
1841
|
+
* - `ctx.store`, plugin-scoped persistence. Present only when the
|
|
1842
|
+
* plugin declares `storage.mode` in `plugin.json`; shape depends on the
|
|
1843
|
+
* mode (`KvStore` for mode A, scoped `Database` for mode B). See
|
|
1844
|
+
* `plugin-kv-api.md` for the contract.
|
|
2051
1845
|
*
|
|
2052
|
-
*
|
|
1846
|
+
* The manifest's `scope` field tells the orchestrator which parts to feed:
|
|
1847
|
+
* `frontmatter` extractors receive an empty string for body and vice versa.
|
|
2053
1848
|
*
|
|
2054
|
-
*
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2057
|
-
* 3. `extractor.completed` , aggregated per-Extractor outputs.
|
|
2058
|
-
* 4. `analyzer.completed` , aggregated per-Rule outputs.
|
|
2059
|
-
* 5. `action.completed` , Action executed on a node.
|
|
2060
|
-
* 6. `job.spawning` , pre-spawn of runner subprocess.
|
|
2061
|
-
* 7. `job.completed` , most common trigger.
|
|
2062
|
-
* 8. `job.failed` , alerts, retry triggers.
|
|
2063
|
-
* 9. `shutdown` , once per CLI process, after the verb's
|
|
2064
|
-
* exit code resolves and before
|
|
2065
|
-
* `process.exit`.
|
|
1849
|
+
* Renamed from `Detector` in spec 0.8.x. The previous `detect(ctx) → Link[]`
|
|
1850
|
+
* signature is gone; everything now flows through `extract(ctx) → void`
|
|
1851
|
+
* and the callbacks above.
|
|
2066
1852
|
*/
|
|
2067
1853
|
|
|
2068
1854
|
/**
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
2072
|
-
*
|
|
2073
|
-
* verb runs). Anything outside this set is rejected at load time as
|
|
2074
|
-
* `invalid-manifest`.
|
|
2075
|
-
*/
|
|
2076
|
-
type THookTrigger = 'boot' | 'scan.started' | 'scan.completed' | 'extractor.completed' | 'analyzer.completed' | 'action.completed' | 'job.spawning' | 'job.completed' | 'job.failed' | 'shutdown';
|
|
2077
|
-
/**
|
|
2078
|
-
* Frozen list mirror of `THookTrigger` for runtime introspection. The
|
|
2079
|
-
* loader validates `manifest.triggers[]` against this set; the
|
|
2080
|
-
* dispatcher iterates it in order when fanning an event out to
|
|
2081
|
-
* subscribed hooks. `boot` first / `shutdown` last so a debug log of
|
|
2082
|
-
* the array reads in lifecycle order.
|
|
1855
|
+
* Output callbacks supplied by the kernel on the extractor context.
|
|
1856
|
+
* Split out so plugin authors can name the callback shape if they
|
|
1857
|
+
* want to mock it in unit tests without depending on the wider
|
|
1858
|
+
* `IExtractorContext`.
|
|
2083
1859
|
*/
|
|
2084
|
-
|
|
2085
|
-
/**
|
|
2086
|
-
* Context the dispatcher hands to `Hook.on()`. The shape is intentionally
|
|
2087
|
-
* narrow: a hook reacts to an event, it does not steer the pipeline.
|
|
2088
|
-
*
|
|
2089
|
-
* The `event` carries the raw `ProgressEvent` envelope (type, timestamp,
|
|
2090
|
-
* runId/jobId when applicable, data). Optional `node` / `extractorId`
|
|
2091
|
-
* / `analyzerId` / `actionId` are extracted from the event payload by the
|
|
2092
|
-
* dispatcher when present so authors don't have to walk `event.data`.
|
|
2093
|
-
*
|
|
2094
|
-
* Probabilistic hooks additionally receive `runner` for LLM dispatch.
|
|
2095
|
-
* Deterministic hooks SHOULD ignore the field.
|
|
2096
|
-
*/
|
|
2097
|
-
interface IHookContext {
|
|
2098
|
-
/** The raw event the dispatcher matched. */
|
|
2099
|
-
event: {
|
|
2100
|
-
type: THookTrigger;
|
|
2101
|
-
timestamp: string;
|
|
2102
|
-
runId?: string;
|
|
2103
|
-
jobId?: string;
|
|
2104
|
-
data?: unknown;
|
|
2105
|
-
};
|
|
1860
|
+
interface IExtractorCallbacks {
|
|
2106
1861
|
/**
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
2109
|
-
*
|
|
1862
|
+
* Emit a single Link. The orchestrator validates the link against the
|
|
1863
|
+
* extractor's declared `emitsLinkKinds` before inserting it; off-contract
|
|
1864
|
+
* links are silently dropped with an `extension.error` event.
|
|
2110
1865
|
*/
|
|
2111
|
-
|
|
1866
|
+
emitLink(link: Link): void;
|
|
2112
1867
|
/**
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
1868
|
+
* Merge canonical, kernel-curated properties onto the current node's
|
|
1869
|
+
* enrichment layer. The author-supplied frontmatter stays untouched
|
|
1870
|
+
* (Decision #109 in `ROADMAP.md`). Persistence and stale-tracking
|
|
1871
|
+
* semantics live in spec § A.8; the orchestrator already buffers the
|
|
1872
|
+
* partials and `persistScanResult` upserts them.
|
|
2115
1873
|
*/
|
|
2116
|
-
|
|
1874
|
+
enrichNode(partial: Partial<Node>): void;
|
|
2117
1875
|
/**
|
|
2118
|
-
*
|
|
1876
|
+
* Emit a per-node view contribution. The first argument is the
|
|
1877
|
+
* extension-local Record key declared under
|
|
1878
|
+
* `extension.viewContributions[<contributionId>]`; the second is a
|
|
1879
|
+
* payload that conforms to the slot's payload schema in
|
|
1880
|
+
* `spec/schemas/view-slots.schema.json#/$defs/payloads/<slot>`,
|
|
1881
|
+
* where `<slot>` is the slot the manifest declared for this
|
|
1882
|
+
* contribution. The orchestrator validates the payload against the
|
|
1883
|
+
* slot's schema before persisting to `scan_contributions`; off-shape
|
|
1884
|
+
* payloads are silently dropped with an `extension.error` event
|
|
1885
|
+
* (mirror of `emitLink` rejecting off-`emitsLinkKinds` links).
|
|
1886
|
+
* Calling `emitContribution` with a `contributionId` that is not
|
|
1887
|
+
* declared in the manifest is also dropped with an `extension.error`.
|
|
1888
|
+
* See `architecture.md` §View contribution system → Emit path.
|
|
2119
1889
|
*/
|
|
2120
|
-
|
|
1890
|
+
emitContribution(contributionId: string, payload: unknown): void;
|
|
1891
|
+
}
|
|
1892
|
+
interface IExtractorContext extends IExtractorCallbacks {
|
|
1893
|
+
node: Node;
|
|
1894
|
+
body: string;
|
|
1895
|
+
frontmatter: Record<string, unknown>;
|
|
2121
1896
|
/**
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
1897
|
+
* Plugin-scoped persistence. Optional because not every plugin declares
|
|
1898
|
+
* a `storage.mode` in `plugin.json`. Shape: `KvStoreWrapper` for mode A
|
|
1899
|
+
* (`set(key, value)`), `DedicatedStoreWrapper` for mode B
|
|
1900
|
+
* (`write(table, row)`). See `spec/plugin-kv-api.md`.
|
|
1901
|
+
*
|
|
1902
|
+
* Typed as `unknown` so this contract module stays free of any
|
|
1903
|
+
* adapter-side imports, the concrete `IPluginStore` lives in
|
|
1904
|
+
* `kernel/adapters/plugin-store.js`. Plugin authors narrow at the
|
|
1905
|
+
* call site based on the storage mode declared in their manifest.
|
|
1906
|
+
* The orchestrator looks up the wrapper per-extractor in
|
|
1907
|
+
* `RunScanOptions.pluginStores` (keyed by `pluginId`) and attaches
|
|
1908
|
+
* it here.
|
|
2124
1909
|
*/
|
|
2125
|
-
|
|
1910
|
+
store?: unknown;
|
|
1911
|
+
}
|
|
1912
|
+
interface IExtractor extends IExtensionBase {
|
|
1913
|
+
kind: 'extractor';
|
|
1914
|
+
emitsLinkKinds: LinkKind[];
|
|
1915
|
+
defaultConfidence: Confidence;
|
|
1916
|
+
scope: 'frontmatter' | 'body' | 'both';
|
|
2126
1917
|
/**
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
1918
|
+
* Optional opt-in filter on `node.kind`. When declared, the orchestrator
|
|
1919
|
+
* skips invocation of `extract()` for any node whose `kind` is NOT in
|
|
1920
|
+
* this list, fail-fast, before context construction, so the extractor
|
|
1921
|
+
* wastes zero CPU on inapplicable nodes.
|
|
1922
|
+
*
|
|
1923
|
+
* Absent (`undefined`) is the default: the extractor applies to every
|
|
1924
|
+
* kind. There are no wildcards, the absence of the field already
|
|
1925
|
+
* encodes "every kind". An empty array (`[]`) is rejected at load
|
|
1926
|
+
* time by AJV (`minItems: 1` in the schema).
|
|
1927
|
+
*
|
|
1928
|
+
* Unknown kinds (no installed Provider declares them) do NOT block
|
|
1929
|
+
* the load: the extractor keeps `loaded` status and `sm plugins doctor`
|
|
1930
|
+
* surfaces a warning. The Provider that declares the kind may arrive
|
|
1931
|
+
* later (e.g. a user installs the corresponding plugin).
|
|
1932
|
+
*
|
|
1933
|
+
* Spec: `spec/schemas/extensions/extractor.schema.json#/properties/applicableKinds`.
|
|
2130
1934
|
*/
|
|
2131
|
-
|
|
1935
|
+
applicableKinds?: string[];
|
|
2132
1936
|
/**
|
|
2133
|
-
*
|
|
2134
|
-
* `
|
|
2135
|
-
* the job subsystem; the field is reserved here so the runtime
|
|
2136
|
-
* contract is forward-compatible without a major bump.
|
|
1937
|
+
* Extractor entry point. Returns nothing; output flows through
|
|
1938
|
+
* `ctx.emitLink`, `ctx.enrichNode`, and `ctx.store`.
|
|
2137
1939
|
*/
|
|
2138
|
-
|
|
1940
|
+
extract(ctx: IExtractorContext): void | Promise<void>;
|
|
2139
1941
|
}
|
|
1942
|
+
|
|
2140
1943
|
/**
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
*
|
|
2147
|
-
*
|
|
2148
|
-
*
|
|
2149
|
-
* filter field, the loader surfaces `invalid-manifest`. The current
|
|
2150
|
-
* impl performs the basic enum check but defers full payload-shape
|
|
2151
|
-
* cross-validation to a follow-up, the dispatcher is permissive at
|
|
2152
|
-
* runtime (an unknown field never matches → the hook simply never
|
|
2153
|
-
* fires for that event, which is a correct interpretation of "filter
|
|
2154
|
-
* by a field that doesn't exist").
|
|
1944
|
+
* Analyzer runtime contract. Runs against the whole graph after every
|
|
1945
|
+
* Provider and extractor has completed; emits issues and MAY project
|
|
1946
|
+
* findings into the UI via view contributions. Deterministic analyzers
|
|
1947
|
+
* are pure (same graph in → same issues out) and run synchronously
|
|
1948
|
+
* inside `sm scan` / `sm check`. Probabilistic analyzers invoke an LLM
|
|
1949
|
+
* through the kernel's `RunnerPort` and dispatch only as queued jobs,
|
|
1950
|
+
* they never participate in scan-time pipelines. Mode is declared in
|
|
1951
|
+
* the manifest (default `deterministic`).
|
|
2155
1952
|
*/
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Step 9.6.2, orphan sidecar entry surfaced to analyzers. A `.sm` file
|
|
1956
|
+
* whose sibling `.md` does not exist on disk; the `annotation-orphan`
|
|
1957
|
+
* built-in analyzer emits one warning per entry. Other analyzers that
|
|
1958
|
+
* care about orphan sidecars MAY consume the list too.
|
|
1959
|
+
*/
|
|
1960
|
+
interface IAnalyzerOrphanSidecar {
|
|
1961
|
+
/** Relative path (POSIX-separated) of the orphan `.sm`. */
|
|
1962
|
+
relativePath: string;
|
|
1963
|
+
/** Absolute path of the missing `.md` the sidecar was anchored to. */
|
|
1964
|
+
expectedMdPath: string;
|
|
1965
|
+
}
|
|
1966
|
+
interface IAnalyzerContext {
|
|
1967
|
+
nodes: Node[];
|
|
1968
|
+
links: Link[];
|
|
2159
1969
|
/**
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
* until the job subsystem ships (Decision #114).
|
|
1970
|
+
* Step 9.6.2, orphaned sidecars discovered during the scan walk.
|
|
1971
|
+
* Empty when sidecar discovery did not run (legacy callers) or
|
|
1972
|
+
* when no orphans exist.
|
|
2164
1973
|
*/
|
|
2165
|
-
|
|
1974
|
+
orphanSidecars?: IAnalyzerOrphanSidecar[];
|
|
2166
1975
|
/**
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2170
|
-
* `
|
|
1976
|
+
* Step 9.6.6, raw parsed sidecar root keyed by `node.path`. Populated
|
|
1977
|
+
* by the orchestrator alongside the public `Node.sidecar` overlay so
|
|
1978
|
+
* analyzers that inspect plugin namespaces (e.g. the built-in
|
|
1979
|
+
* `core/unknown-field` Analyzer) can walk the full tree without
|
|
1980
|
+
* re-reading the file from disk. Absent (or `undefined` per node)
|
|
1981
|
+
* when no sidecar accompanies the node, or when the sidecar failed
|
|
1982
|
+
* to parse. Treat as read-only.
|
|
2171
1983
|
*/
|
|
2172
|
-
|
|
1984
|
+
sidecarRoots?: ReadonlyMap<string, Record<string, unknown>>;
|
|
2173
1985
|
/**
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
1986
|
+
* Step 9.6.6, runtime catalog of plugin-contributed annotation keys,
|
|
1987
|
+
* as exposed by `kernel.getRegisteredAnnotationKeys()`. Threaded
|
|
1988
|
+
* through so analyzers can reason about the registered-vs-unknown
|
|
1989
|
+
* split without reaching back into the kernel. Empty array when no
|
|
1990
|
+
* plugin declares contributions; absent for legacy callers (older
|
|
1991
|
+
* runScan sites that never wired the catalog through).
|
|
2176
1992
|
*/
|
|
2177
|
-
|
|
1993
|
+
annotationContributions?: readonly IRegisteredAnnotationKey[];
|
|
2178
1994
|
/**
|
|
2179
|
-
*
|
|
2180
|
-
*
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
1995
|
+
* Step 11.x, runtime catalog of plugin-contributed view contributions,
|
|
1996
|
+
* as exposed by `kernel.getRegisteredViewContributions()`. Threaded
|
|
1997
|
+
* through so analyzers can reason about emissions without reaching
|
|
1998
|
+
* back into the kernel (built-in `core/contribution-orphan` joins it
|
|
1999
|
+
* with the live node set to flag dangling emissions). Slot catalog
|
|
2000
|
+
* drift detection is NOT a scan concern, it lives at load time and
|
|
2001
|
+
* surfaces via `sm plugins doctor`. Empty array when no extension
|
|
2002
|
+
* declares view contributions; absent for legacy callers (older
|
|
2003
|
+
* runScan sites that never wired the catalog through).
|
|
2183
2004
|
*/
|
|
2184
|
-
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
/**
|
|
2188
|
-
* `ProgressEmitterPort`, emits progress events during long operations.
|
|
2189
|
-
*
|
|
2190
|
-
* Shape-only today. The full event catalog (`run.started`,
|
|
2191
|
-
* `job.claimed`, `model.delta`, etc.) is normative in
|
|
2192
|
-
* `spec/job-events.md`; this port carries an open `data` payload so
|
|
2193
|
-
* adapters can emit any documented event without type churn.
|
|
2194
|
-
*/
|
|
2195
|
-
interface ProgressEvent {
|
|
2196
|
-
type: string;
|
|
2197
|
-
timestamp: string;
|
|
2198
|
-
runId?: string;
|
|
2199
|
-
jobId?: string;
|
|
2200
|
-
data?: unknown;
|
|
2201
|
-
}
|
|
2202
|
-
type TProgressListener = (event: ProgressEvent) => void;
|
|
2203
|
-
interface ProgressEmitterPort {
|
|
2204
|
-
emit(event: ProgressEvent): void;
|
|
2205
|
-
subscribe(listener: TProgressListener): () => void;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
/**
|
|
2209
|
-
* Per-node extractor invocation: build a fresh `IExtractorContext` for
|
|
2210
|
-
* each extractor, validate every emitted link / contribution against
|
|
2211
|
-
* the declared catalog, fold enrichment partials into per-`(node,
|
|
2212
|
-
* extractor)` records, and surface emit-time drops as
|
|
2213
|
-
* `extension.error` events.
|
|
2214
|
-
*
|
|
2215
|
-
* Also hosts the post-walk recompute helpers that re-derive
|
|
2216
|
-
* `linksOutCount` / `linksInCount` / `externalRefsCount` on every node
|
|
2217
|
-
* from the final merged link buffer, plus the `IExtractorRunRecord`
|
|
2218
|
-
* and `IEnrichmentRecord` types those records eventually persist as.
|
|
2219
|
-
*/
|
|
2220
|
-
|
|
2221
|
-
/**
|
|
2222
|
-
* Spec § A.9, runs to persist into `scan_extractor_runs`. One entry
|
|
2223
|
-
* per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
|
|
2224
|
-
* "this extractor is current for this body". Includes both freshly-run
|
|
2225
|
-
* pairs (extractor invoked this scan) and reused pairs (cached node, the
|
|
2226
|
-
* extractor's prior run still applies to the same body hash). Excludes
|
|
2227
|
-
* obsolete pairs, extractors that ran in the prior but are no longer
|
|
2228
|
-
* registered, so a replace-all persist drops them automatically.
|
|
2229
|
-
*/
|
|
2230
|
-
interface IExtractorRunRecord {
|
|
2231
|
-
nodePath: string;
|
|
2232
|
-
extractorId: string;
|
|
2233
|
-
bodyHashAtRun: string;
|
|
2234
|
-
ranAt: number;
|
|
2005
|
+
viewContributions?: readonly IRegisteredViewContribution[];
|
|
2235
2006
|
/**
|
|
2236
|
-
*
|
|
2237
|
-
*
|
|
2238
|
-
* `
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
2007
|
+
* Absolute paths of `*.md` files under the project's
|
|
2008
|
+
* `.skill-map/jobs/` that no `state_jobs.filePath` references, the
|
|
2009
|
+
* built-in `core/job-orphan-file` analyzer projects each as a `warn`
|
|
2010
|
+
* issue. Pre-computed by the driving adapter (CLI / BFF) inside its
|
|
2011
|
+
* already-open storage transaction (mirrors the `orphanSidecars`
|
|
2012
|
+
* pattern: detection lives outside the analyzer, the analyzer only
|
|
2013
|
+
* projects). Absent (or empty) when the caller does not maintain a
|
|
2014
|
+
* jobs directory, when the storage path is unavailable, or when no
|
|
2015
|
+
* orphan files exist. Treat as read-only.
|
|
2241
2016
|
*/
|
|
2242
|
-
|
|
2243
|
-
|
|
2017
|
+
orphanJobFiles?: readonly string[];
|
|
2018
|
+
/**
|
|
2019
|
+
* Set of absolute file paths the operator has opted into for
|
|
2020
|
+
* link-validation purposes via `scan.referencePaths`. The driving
|
|
2021
|
+
* adapter walks each configured path before the scan and collects
|
|
2022
|
+
* every existing file's absolute path here. Files in this set are
|
|
2023
|
+
* NOT indexed as graph nodes, the only consumer is
|
|
2024
|
+
* `core/broken-ref`, which suppresses its `warn` issue when a
|
|
2025
|
+
* path-style link target falls into the set. Absent / empty when
|
|
2026
|
+
* the operator left `scan.referencePaths` empty or when the
|
|
2027
|
+
* adapter does not maintain the side index. Treat as read-only.
|
|
2028
|
+
*/
|
|
2029
|
+
referenceablePaths?: ReadonlySet<string>;
|
|
2030
|
+
/**
|
|
2031
|
+
* Absolute path of the scan's project root (cwd of the invocation).
|
|
2032
|
+
* Threaded into the analyzer pass so an analyzer that needs to
|
|
2033
|
+
* resolve a relative `link.target` to an absolute filesystem path
|
|
2034
|
+
* (today only `core/broken-ref`, when consulting
|
|
2035
|
+
* `referenceablePaths`) does not have to derive it from
|
|
2036
|
+
* `nodes[0].path` heuristics. Absent for legacy callers (older
|
|
2037
|
+
* `runScan` sites that never wired the field through). Always an
|
|
2038
|
+
* absolute path when present.
|
|
2039
|
+
*/
|
|
2040
|
+
cwd?: string;
|
|
2041
|
+
/**
|
|
2042
|
+
* Emit a per-node view contribution declared in this analyzer's
|
|
2043
|
+
* manifest `viewContributions` map. Sync, void return; the
|
|
2044
|
+
* orchestrator validates the payload against the slot's schema at
|
|
2045
|
+
* call time and silently drops invalid emissions with a logged
|
|
2046
|
+
* `extension.error` event (parallel to
|
|
2047
|
+
* `IExtractorCallbacks.emitContribution`).
|
|
2048
|
+
*
|
|
2049
|
+
* Unlike Extractor's emit (which binds `nodePath` from `ctx.node.path`
|
|
2050
|
+
* implicitly because Extractors run per-node), Analyzer's `evaluate()`
|
|
2051
|
+
* sees the full graph at once. The analyzer walks `ctx.nodes` itself
|
|
2052
|
+
* and MUST supply the target node path explicitly per emission.
|
|
2053
|
+
*
|
|
2054
|
+
* Calling `emitContribution` with a `contributionId` that is not
|
|
2055
|
+
* declared in the manifest is dropped with an `extension.error`. The
|
|
2056
|
+
* kernel routes emitted contributions to the same persistence
|
|
2057
|
+
* pipeline as Extractor emissions (`scan_contributions`).
|
|
2058
|
+
*/
|
|
2059
|
+
emitContribution(nodePath: string, contributionId: string, payload: unknown): void;
|
|
2060
|
+
}
|
|
2061
|
+
interface IAnalyzer extends IExtensionBase {
|
|
2062
|
+
kind: 'analyzer';
|
|
2063
|
+
/**
|
|
2064
|
+
* Execution mode. Optional in the manifest with a default of
|
|
2065
|
+
* `deterministic` per `spec/schemas/extensions/analyzer.schema.json`.
|
|
2066
|
+
*/
|
|
2067
|
+
mode?: TExecutionMode;
|
|
2068
|
+
/**
|
|
2069
|
+
* Qualified `<pluginId>/<id>` Action ids the analyzer recommends to
|
|
2070
|
+
* resolve its findings. Distinct from `Action.precondition` (which
|
|
2071
|
+
* declares which nodes an Action applies to from the Action side);
|
|
2072
|
+
* this field declares which Actions are relevant when this
|
|
2073
|
+
* Analyzer fires from the Analyzer side. Actions are per-node by
|
|
2074
|
+
* design (project-level cleanup verbs like orphan file prune or
|
|
2075
|
+
* contribution relink are CLI verbs, not Actions) and are NOT
|
|
2076
|
+
* surfaced through this field. The UI consumes it in the node
|
|
2077
|
+
* inspector under "Recommended for issues". Optional; omit when no
|
|
2078
|
+
* Action resolves the finding (e.g. `core/superseded` surfaces
|
|
2079
|
+
* deliberate user declarations, not problems).
|
|
2080
|
+
*/
|
|
2081
|
+
recommendedActions?: readonly string[];
|
|
2082
|
+
evaluate(ctx: IAnalyzerContext): Issue[] | Promise<Issue[]>;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2244
2085
|
/**
|
|
2245
|
-
*
|
|
2246
|
-
*
|
|
2247
|
-
* One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
|
|
2248
|
-
* produced via `ctx.enrichNode(...)` during the walk. Attribution is
|
|
2249
|
-
* preserved per-Extractor (rather than merged client-side as B.1 did)
|
|
2250
|
-
* so the persistence layer can:
|
|
2086
|
+
* Action runtime contract. The fourth plugin kind (spec § A.4 +
|
|
2087
|
+
* `spec/schemas/extensions/action.schema.json`).
|
|
2251
2088
|
*
|
|
2252
|
-
*
|
|
2253
|
-
* re-extract);
|
|
2254
|
-
* - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
|
|
2255
|
-
* for last-write-wins per field at read time.
|
|
2089
|
+
* Actions operate on one or more nodes in one of two execution modes:
|
|
2256
2090
|
*
|
|
2257
|
-
* `
|
|
2258
|
-
*
|
|
2259
|
-
* `
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
2091
|
+
* - `deterministic`, code runs in-process; the action computes the
|
|
2092
|
+
* report synchronously and returns it. No job file, no runner.
|
|
2093
|
+
* - `probabilistic`, the kernel renders a prompt + preamble into a
|
|
2094
|
+
* job file; a runner executes it via `RunnerPort` against an LLM;
|
|
2095
|
+
* `sm record` closes the job and validates the report against
|
|
2096
|
+
* `reportSchemaRef`.
|
|
2262
2097
|
*
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
*
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
bodyHashAtEnrichment: string;
|
|
2274
|
-
value: Partial<Node>;
|
|
2275
|
-
enrichedAt: number;
|
|
2276
|
-
isProbabilistic: boolean;
|
|
2277
|
-
}
|
|
2278
|
-
/**
|
|
2279
|
-
* Run a set of extractors against a single node, collecting their link
|
|
2280
|
-
* emissions and node-enrichment partials. Each extractor is invoked
|
|
2281
|
-
* exactly once with a fresh `IExtractorContext`. Caller decides what
|
|
2282
|
-
* to do with the returned arrays (push into per-scan buffers, write to
|
|
2283
|
-
* a focused refresh result, etc.).
|
|
2098
|
+
* **Deferred runtime invocation.** The dispatcher (`Action.run(ctx)` for
|
|
2099
|
+
* deterministic; the `RunnerPort` + `sm record` round-trip for
|
|
2100
|
+
* probabilistic) lands with the job subsystem (Decision #114 in
|
|
2101
|
+
* `ROADMAP.md`). Today the loader still validates `kind: 'action'`
|
|
2102
|
+
* manifests against `extension-action.schema.json` and the registry
|
|
2103
|
+
* holds them, `sm actions show` and the precondition gating UI consume
|
|
2104
|
+
* the manifest data. The runtime entry point is intentionally absent
|
|
2105
|
+
* from `IAction` so plugin authors don't ship a method the kernel will
|
|
2106
|
+
* not call until the job subsystem is in place; when it ships, the
|
|
2107
|
+
* method shape will land here without breaking the manifest contract.
|
|
2284
2108
|
*
|
|
2285
|
-
*
|
|
2286
|
-
* needs for re-running a single extractor against a single node, the
|
|
2287
|
-
* pre-extraction code in `refresh.ts` was hand-duplicating this loop
|
|
2288
|
-
* (audit item V4).
|
|
2109
|
+
* Mirrors `extensions/action.schema.json`:
|
|
2289
2110
|
*
|
|
2290
|
-
*
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2111
|
+
* - `mode` (required), discriminator between the two modes.
|
|
2112
|
+
* - `reportSchemaRef` (required), JSON Schema reference the report
|
|
2113
|
+
* MUST validate against. MUST extend `report-base.schema.json`.
|
|
2114
|
+
* - `promptTemplateRef`, REQUIRED when `mode: 'probabilistic'`,
|
|
2115
|
+
* FORBIDDEN when `mode: 'deterministic'`. The schema's conditional
|
|
2116
|
+
* `allOf` enforces both directions; the runtime contract simply
|
|
2117
|
+
* surfaces the field as optional and lets the loader catch shape
|
|
2118
|
+
* violations at AJV time.
|
|
2119
|
+
* - `expectedDurationSeconds`, REQUIRED for probabilistic (drives
|
|
2120
|
+
* TTL); advisory for deterministic.
|
|
2121
|
+
* - `precondition`, declarative filter consumed by `--all` fan-out,
|
|
2122
|
+
* UI button gating, `sm actions show`.
|
|
2123
|
+
* - `expectedTools`, hint to Skill / CLI runners about expected
|
|
2124
|
+
* tools (no normative enforcement in v0).
|
|
2125
|
+
* - `fanOutPolicy`, `'per-node'` (default) vs `'batch'`.
|
|
2293
2126
|
*/
|
|
2294
|
-
declare function runExtractorsForNode(opts: {
|
|
2295
|
-
extractors: IExtractor[];
|
|
2296
|
-
node: Node;
|
|
2297
|
-
body: string;
|
|
2298
|
-
frontmatter: Record<string, unknown>;
|
|
2299
|
-
bodyHash: string;
|
|
2300
|
-
emitter: ProgressEmitterPort;
|
|
2301
|
-
/**
|
|
2302
|
-
* Spec § A.12, per-plugin `ctx.store` wrappers keyed by `pluginId`.
|
|
2303
|
-
* The map's lookup is per-extractor inside the loop, so callers that
|
|
2304
|
-
* don't track plugin storage can omit it; the resulting `ctx.store`
|
|
2305
|
-
* stays `undefined` (the existing contract).
|
|
2306
|
-
*/
|
|
2307
|
-
pluginStores?: ReadonlyMap<string, IPluginStore>;
|
|
2308
|
-
}): Promise<{
|
|
2309
|
-
internalLinks: Link[];
|
|
2310
|
-
externalLinks: Link[];
|
|
2311
|
-
enrichments: IEnrichmentRecord[];
|
|
2312
|
-
contributions: IContributionRecord[];
|
|
2313
|
-
}>;
|
|
2314
2127
|
|
|
2315
2128
|
/**
|
|
2316
|
-
*
|
|
2317
|
-
*
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2320
|
-
* the
|
|
2129
|
+
* Single sidecar write payload an Action can return. Discriminated union so
|
|
2130
|
+
* future write kinds (storage rows, plugin KV, etc.) can land additively
|
|
2131
|
+
* without breaking consumers that only handle `kind: 'sidecar'`.
|
|
2132
|
+
*
|
|
2133
|
+
* - `path`, absolute path to the `.sm` file the kernel must materialise
|
|
2134
|
+
* the change into. Resolved by the Action from the node's absolute
|
|
2135
|
+
* path via `sidecarPathFor()`.
|
|
2136
|
+
* - `changes`, partial sidecar root used as a deep-merge patch (NOT a
|
|
2137
|
+
* full replacement). Arrays REPLACE; objects RECURSE. Reason:
|
|
2138
|
+
* sidecars are shared-write between skill-map core and plugins;
|
|
2139
|
+
* a full replace would clobber `<plugin-id>:` namespaced blocks.
|
|
2321
2140
|
*/
|
|
2322
|
-
|
|
2141
|
+
type TActionWrite = {
|
|
2142
|
+
kind: 'sidecar';
|
|
2143
|
+
path: string;
|
|
2144
|
+
changes: Record<string, unknown>;
|
|
2145
|
+
};
|
|
2323
2146
|
/**
|
|
2324
|
-
*
|
|
2325
|
-
*
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2147
|
+
* Result envelope returned by deterministic Actions. The `report` field
|
|
2148
|
+
* carries the typed report payload (each Action declares its shape via
|
|
2149
|
+
* `reportSchemaRef`); `writes` is opt-in, Actions that do not mutate
|
|
2150
|
+
* persistent state simply omit it.
|
|
2328
2151
|
*/
|
|
2329
|
-
interface
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
confidence: 'high' | 'medium';
|
|
2152
|
+
interface IActionResult<TReport = unknown> {
|
|
2153
|
+
report: TReport;
|
|
2154
|
+
writes?: TActionWrite[];
|
|
2333
2155
|
}
|
|
2334
2156
|
/**
|
|
2335
|
-
*
|
|
2336
|
-
*
|
|
2337
|
-
*
|
|
2338
|
-
* apply inside its tx.
|
|
2339
|
-
*
|
|
2340
|
-
* Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused
|
|
2341
|
-
* by another):
|
|
2342
|
-
*
|
|
2343
|
-
* 1. **High-confidence**: pair each `deletedPath` with a `newPath`
|
|
2344
|
-
* that has the same `bodyHash`. No issue, no prompt.
|
|
2345
|
-
* 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
|
|
2346
|
-
* each with the *unique* unclaimed `newPath` that shares its
|
|
2347
|
-
* `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
|
|
2348
|
-
* with `data: { from, to, confidence: 'medium' }`.
|
|
2349
|
-
* 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
|
|
2350
|
-
* remaining frontmatter-matching candidate, emit ONE
|
|
2351
|
-
* `auto-rename-ambiguous` issue per `newPath`, listing all
|
|
2352
|
-
* candidates in `data.candidates`. NO migration.
|
|
2353
|
-
* 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one
|
|
2354
|
-
* `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
|
|
2157
|
+
* Runtime context passed to a deterministic Action's `invoke()` method.
|
|
2158
|
+
* Minimal surface, Actions stay pure (no IO inside `invoke`); the kernel
|
|
2159
|
+
* materialises any returned `writes` after the call.
|
|
2355
2160
|
*
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2161
|
+
* - `node`, the target `Node` the Action operates on. Open-by-design;
|
|
2162
|
+
* batch / fan-out flows pick the matching nodes upstream.
|
|
2163
|
+
* - `nodeAbsolutePath`, absolute path to the node's `.md` file on
|
|
2164
|
+
* disk. The Action uses this to compute the sidecar path it returns
|
|
2165
|
+
* in a `TActionWrite`. Surfaced separately from `node.path` (which is
|
|
2166
|
+
* the relative scope-root form) so Actions never compose absolute
|
|
2167
|
+
* paths from `node.path` themselves.
|
|
2168
|
+
* - `invoker`, identity of the caller; written into the sidecar's
|
|
2169
|
+
* `audit.lastBumpedBy` when the Action chooses to. CLI invocations
|
|
2170
|
+
* pass `'cli'`; plugin-driven invocations pass `'plugin:<plugin-id>'`.
|
|
2171
|
+
* - `now`, clock function; tests inject a deterministic source.
|
|
2172
|
+
* Defaults to `() => new Date()` at the composition root.
|
|
2360
2173
|
*/
|
|
2361
|
-
|
|
2362
|
-
|
|
2174
|
+
interface IActionContext {
|
|
2175
|
+
node: Node;
|
|
2176
|
+
nodeAbsolutePath: string;
|
|
2177
|
+
invoker: string;
|
|
2178
|
+
now: () => Date;
|
|
2179
|
+
}
|
|
2363
2180
|
/**
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
2366
|
-
*
|
|
2367
|
-
* `RunScanOptions.extensions`, the Registry holds manifest metadata, the
|
|
2368
|
-
* callable set holds the runtime instances the orchestrator actually
|
|
2369
|
-
* invokes. Separating the two lets `sm plugins` and `sm help` introspect
|
|
2370
|
-
* the graph without loading code.
|
|
2371
|
-
*
|
|
2372
|
-
* With zero registered extensions (or a callable set that carries none)
|
|
2373
|
-
* the pipeline still produces a valid zero-filled `ScanResult`, the
|
|
2374
|
-
* kernel-empty-boot invariant.
|
|
2375
|
-
*
|
|
2376
|
-
* Roots are validated up front: each entry of `RunScanOptions.roots`
|
|
2377
|
-
* must exist on disk as a directory. The first failure throws a clear
|
|
2378
|
-
* `Error` naming the offending path. This guards every caller (CLI,
|
|
2379
|
-
* server, skill-agent) against silently producing a zero-filled
|
|
2380
|
-
* `ScanResult` when a Provider walks a non-existent path, the bug
|
|
2381
|
-
* that wiped a populated DB via `sm scan -- --dry-run` (clipanion's
|
|
2382
|
-
* `--` made `--dry-run` a positional root that did not exist).
|
|
2383
|
-
*
|
|
2384
|
-
* Incremental scans: when `priorSnapshot` is supplied, the
|
|
2385
|
-
* orchestrator walks the filesystem, hashes each file, and reuses the
|
|
2386
|
-
* prior node + its prior-extracted internal links whenever both
|
|
2387
|
-
* `bodyHash` and `frontmatterHash` match. New / modified files run
|
|
2388
|
-
* through the full extractor pipeline (including the external-url-counter
|
|
2389
|
-
* which produces ephemeral pseudo-links). Rules ALWAYS run over the
|
|
2390
|
-
* fully merged graph, issue state can change even for an unchanged node
|
|
2391
|
-
* (e.g. a previously broken `references` link now resolves because a new
|
|
2392
|
-
* node was added). For unchanged nodes the prior `externalRefsCount` is
|
|
2393
|
-
* preserved as-is (the external pseudo-links were never persisted, so
|
|
2394
|
-
* they cannot be reconstructed; the count survived in the node row).
|
|
2395
|
-
*
|
|
2396
|
-
* Extractor output model (B.1, post-rename from Detector): extractors
|
|
2397
|
-
* return `void` and emit through three callbacks injected on the context:
|
|
2398
|
-
* - `ctx.emitLink(link)` → orchestrator validates against
|
|
2399
|
-
* `emitsLinkKinds` then partitions into internal / external buckets.
|
|
2400
|
-
* - `ctx.enrichNode(partial)` → orchestrator records ONE enrichment
|
|
2401
|
-
* entry per `(node, extractor)` so attribution survives into the DB.
|
|
2402
|
-
* Persisted into `node_enrichments` (A.8). The author-supplied
|
|
2403
|
-
* frontmatter on `node.frontmatter` stays immutable from any Extractor
|
|
2404
|
-
* , the enrichment layer is the only writable surface, and rules /
|
|
2405
|
-
* formatters consume it via `mergeNodeWithEnrichments`.
|
|
2406
|
-
* - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).
|
|
2407
|
-
* Wired by the driving adapter via `RunScanOptions.pluginStores`,
|
|
2408
|
-
* which the orchestrator looks up per-extractor by `pluginId` and
|
|
2409
|
-
* attaches to the context. The orchestrator never inspects what
|
|
2410
|
-
* plugins write through it; the wrapper handles AJV validation
|
|
2411
|
-
* when the manifest declared an output schema.
|
|
2181
|
+
* Declarative filter applied by `--all` fan-out, UI button gating, and
|
|
2182
|
+
* `sm actions show`. All fields optional, an empty precondition matches
|
|
2183
|
+
* every node.
|
|
2412
2184
|
*/
|
|
2413
|
-
|
|
2414
|
-
interface IScanExtensions {
|
|
2415
|
-
providers: IProvider[];
|
|
2416
|
-
extractors: IExtractor[];
|
|
2417
|
-
analyzers: IAnalyzer[];
|
|
2418
|
-
/**
|
|
2419
|
-
* Optional hooks (spec § A.11). When supplied, the orchestrator's
|
|
2420
|
-
* lifecycle dispatcher invokes deterministic hooks subscribed to one
|
|
2421
|
-
* of the eight hookable triggers in canonical order with the matching
|
|
2422
|
-
* event payload. Absent → no hooks fire (the scan still emits its
|
|
2423
|
-
* lifecycle events to `ProgressEmitterPort` for observability).
|
|
2424
|
-
* Probabilistic hooks are loaded but skipped here with a stderr
|
|
2425
|
-
* advisory until the job subsystem ships once the job subsystem ships.
|
|
2426
|
-
*/
|
|
2427
|
-
hooks?: IHook[];
|
|
2428
|
-
}
|
|
2429
|
-
interface RunScanOptions {
|
|
2185
|
+
interface IActionPrecondition {
|
|
2430
2186
|
/**
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2187
|
+
* Node kinds this action accepts. Open-by-design (matches
|
|
2188
|
+
* `node.schema.json#/properties/kind`): an action declared with
|
|
2189
|
+
* `kind: ['cursorRule']` is valid as long as some Provider classifies
|
|
2190
|
+
* into `cursorRule`. Omitted → any kind.
|
|
2433
2191
|
*/
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2192
|
+
kind?: string[];
|
|
2193
|
+
/** Provider ids whose nodes this action accepts. Omitted → any Provider. */
|
|
2194
|
+
provider?: string[];
|
|
2195
|
+
/** Node stability filter. */
|
|
2196
|
+
stability?: Array<'experimental' | 'stable' | 'deprecated'>;
|
|
2438
2197
|
/**
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
* Threaded into the rule pass so `core/unknown-field` can
|
|
2442
|
-
* legitimise registered plugin namespaces / root keys without
|
|
2443
|
-
* re-walking the manifests. Absent → empty catalog (every plugin
|
|
2444
|
-
* key is treated as unknown). Built-in catalog from
|
|
2445
|
-
* `annotations.schema.json` is NOT included, that is hard-coded
|
|
2446
|
-
* inside the rule.
|
|
2198
|
+
* Free-form precondition strings the kernel forwards to the action for
|
|
2199
|
+
* runtime evaluation (example: `frontmatter.metadata.source != null`).
|
|
2447
2200
|
*/
|
|
2448
|
-
|
|
2201
|
+
custom?: string[];
|
|
2202
|
+
}
|
|
2203
|
+
interface IAction extends IExtensionBase {
|
|
2204
|
+
kind: 'action';
|
|
2449
2205
|
/**
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
* into the rule pass so:
|
|
2453
|
-
* - `core/contribution-orphan` can introspect the catalog
|
|
2454
|
-
* (read-only) and join it with the live node set to flag
|
|
2455
|
-
* dangling emissions. Slot catalog drift is NOT a scan concern,
|
|
2456
|
-
* it lives at load time and surfaces via `sm plugins doctor`
|
|
2457
|
-
* (the kernel rejects unknown slots as `invalid-manifest` first,
|
|
2458
|
-
* doctor catches the catalog-version-skew tail).
|
|
2459
|
-
* - The orchestrator's per-rule emit closure can look up each
|
|
2460
|
-
* declared `(contributionId → slot)` pairing for AJV
|
|
2461
|
-
* payload validation.
|
|
2462
|
-
* Absent → empty catalog. Rules that emit contributions silently
|
|
2463
|
-
* drop emissions when the catalog has no entry for the rule's
|
|
2464
|
-
* declared contributionId.
|
|
2206
|
+
* Execution mode discriminator. Required per
|
|
2207
|
+
* `extensions/action.schema.json`.
|
|
2465
2208
|
*/
|
|
2466
|
-
|
|
2209
|
+
mode: TExecutionMode;
|
|
2467
2210
|
/**
|
|
2468
|
-
*
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2211
|
+
* Reference to the JSON Schema the report MUST validate against. MUST
|
|
2212
|
+
* extend `report-base.schema.json` (directly or transitively).
|
|
2213
|
+
* Validation failure → job transitions to `failed` with reason
|
|
2214
|
+
* `report-invalid`.
|
|
2471
2215
|
*/
|
|
2472
|
-
|
|
2216
|
+
reportSchemaRef: string;
|
|
2473
2217
|
/**
|
|
2474
|
-
*
|
|
2475
|
-
*
|
|
2476
|
-
*
|
|
2477
|
-
*
|
|
2218
|
+
* Best-effort estimate of wall-clock duration in seconds. Drives TTL
|
|
2219
|
+
* (`ttl = max(expectedDurationSeconds × graceMultiplier,
|
|
2220
|
+
* minimumTtlSeconds)`). Required for `probabilistic`; advisory for
|
|
2221
|
+
* `deterministic`.
|
|
2478
2222
|
*/
|
|
2479
|
-
|
|
2223
|
+
expectedDurationSeconds?: number;
|
|
2480
2224
|
/**
|
|
2481
|
-
*
|
|
2482
|
-
*
|
|
2483
|
-
*
|
|
2484
|
-
*
|
|
2485
|
-
*
|
|
2486
|
-
*
|
|
2487
|
-
* runs on EVERY `sm scan` (with or without `--changed`) so
|
|
2488
|
-
* reorganising files always preserves history, never silently.
|
|
2489
|
-
*
|
|
2490
|
-
* 2. **Cache reuse** (`sm scan --changed`): only kicks in when
|
|
2491
|
-
* `enableCache: true` is also passed. With the flag set, nodes
|
|
2492
|
-
* whose `path` exists in the prior with both `bodyHash` and
|
|
2493
|
-
* `frontmatterHash` matching the freshly-computed hashes are
|
|
2494
|
-
* reused as-is (their internal links and `externalRefsCount`
|
|
2495
|
-
* survive); only new / modified nodes run through extractors.
|
|
2496
|
-
* Rules always re-run over the merged graph.
|
|
2497
|
-
*
|
|
2498
|
-
* Pass `null` (or omit) for a fresh scan with no rename detection.
|
|
2225
|
+
* Path (relative to the extension file) to the prompt template the
|
|
2226
|
+
* kernel renders at `sm job submit`. REQUIRED when `mode:
|
|
2227
|
+
* 'probabilistic'`; FORBIDDEN when `mode: 'deterministic'`. The
|
|
2228
|
+
* conditional shape is enforced by AJV at load time; the runtime
|
|
2229
|
+
* contract carries the field as optional so both modes share one
|
|
2230
|
+
* interface.
|
|
2499
2231
|
*/
|
|
2500
|
-
|
|
2232
|
+
promptTemplateRef?: string;
|
|
2501
2233
|
/**
|
|
2502
|
-
*
|
|
2503
|
-
* extractors over them. Defaults to `false` so a plain `sm scan`
|
|
2504
|
-
* always re-walks deterministically. `sm scan --changed` flips this
|
|
2505
|
-
* to `true` for the perf win on unchanged files.
|
|
2506
|
-
*
|
|
2507
|
-
* Has no effect without `priorSnapshot`; setting it to `true` with
|
|
2508
|
-
* a null prior is a no-op (every file is "new").
|
|
2234
|
+
* Optional declarative filter; absent → applies to every node.
|
|
2509
2235
|
*/
|
|
2510
|
-
|
|
2236
|
+
precondition?: IActionPrecondition;
|
|
2511
2237
|
/**
|
|
2512
|
-
*
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
2515
|
-
* their own defensive defaults (just enough to keep `.git` /
|
|
2516
|
-
* `node_modules` out).
|
|
2238
|
+
* Hint to Skill / CLI runners about what tools the rendered prompt
|
|
2239
|
+
* expects access to (`Bash`, `Read`, `WebSearch`, …). No normative
|
|
2240
|
+
* enforcement in v0.
|
|
2517
2241
|
*/
|
|
2518
|
-
|
|
2242
|
+
expectedTools?: string[];
|
|
2519
2243
|
/**
|
|
2520
|
-
*
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2523
|
-
* still emits a `frontmatter-invalid` issue per malformed file but
|
|
2524
|
-
* leaves the severity at `warn` so a clean scan exits 0; when true,
|
|
2525
|
-
* the same finding becomes `error` and the scan exits 1.
|
|
2244
|
+
* `'per-node'` (default): `sm job submit --all` produces one job per
|
|
2245
|
+
* matching node. `'batch'`: one job whose prompt template receives the
|
|
2246
|
+
* full list. Batch actions tend to hit context limits; use sparingly.
|
|
2526
2247
|
*/
|
|
2527
|
-
|
|
2248
|
+
fanOutPolicy?: 'per-node' | 'batch';
|
|
2528
2249
|
/**
|
|
2529
|
-
*
|
|
2530
|
-
*
|
|
2531
|
-
*
|
|
2532
|
-
*
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
* `
|
|
2250
|
+
* Deterministic invocation entry point. OPTIONAL on the runtime
|
|
2251
|
+
* contract for backward compatibility with the manifest-only era
|
|
2252
|
+
* (Decision #114), actions that ship for the future probabilistic
|
|
2253
|
+
* runner / record path leave it absent and the kernel never calls it.
|
|
2254
|
+
* Step 9.6.3 (Decision #125) introduces the first concrete consumer:
|
|
2255
|
+
* the built-in `bump` Action implements `invoke()` and returns a
|
|
2256
|
+
* `writes: [{ kind: 'sidecar', ... }]` payload that the kernel
|
|
2257
|
+
* materialises through `ISidecarStore`.
|
|
2536
2258
|
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
* - some applicable extractor lacks a matching row (newly registered,
|
|
2542
|
-
* or its prior run targeted a different body hash or sidecar
|
|
2543
|
-
* annotations hash) → run only the missing extractors, drop prior
|
|
2544
|
-
* links whose `sources` map to any missing extractor or to an
|
|
2545
|
-
* extractor that is no longer registered.
|
|
2546
|
-
*/
|
|
2547
|
-
priorExtractorRuns?: Map<string, Map<string, IPriorExtractorRun>>;
|
|
2548
|
-
/**
|
|
2549
|
-
* Spec § A.12, per-plugin storage wrappers exposed to extractors via
|
|
2550
|
-
* `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves
|
|
2551
|
-
* `ctx.store` undefined for that extractor (the existing contract).
|
|
2259
|
+
* Implementations MUST stay pure, no IO inside `invoke()`. The Action
|
|
2260
|
+
* computes the patch and returns it; the kernel reads the on-disk
|
|
2261
|
+
* sidecar, deep-merges, validates, and writes back inside its critical
|
|
2262
|
+
* section.
|
|
2552
2263
|
*
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
*
|
|
2556
|
-
* keeps the orchestrator persistence-agnostic (the wrapper supplies
|
|
2557
|
-
* its own persist callback) and lets tests inject a captured-call
|
|
2558
|
-
* mock without spinning up a DB.
|
|
2559
|
-
*/
|
|
2560
|
-
pluginStores?: ReadonlyMap<string, IPluginStore>;
|
|
2561
|
-
/**
|
|
2562
|
-
* Pre-computed absolute paths of orphan job MD files (files under
|
|
2563
|
-
* `.skill-map/jobs/` whose absolute path appears nowhere in
|
|
2564
|
-
* `state_jobs.filePath`). Threaded into the rule pass so the
|
|
2565
|
-
* built-in `core/job-orphan-file` rule can project each as a `warn`
|
|
2566
|
-
* issue without the kernel reaching for the storage port or doing
|
|
2567
|
-
* its own FS walk. The driving adapter (CLI, BFF) computes this
|
|
2568
|
-
* inside its already-open storage transaction via
|
|
2569
|
-
* `findOrphanJobFiles(jobsDir, await port.jobs.listReferencedFilePaths())`
|
|
2570
|
-
* mirrors the `orphanSidecars` model where detection lives
|
|
2571
|
-
* outside the rule and the rule only projects. Absent / empty when
|
|
2572
|
-
* the caller has no jobs context (out-of-band tests, fresh DB,
|
|
2573
|
-
* `--no-built-ins`).
|
|
2264
|
+
* `TInput` is action-specific; the built-in `bump` Action declares
|
|
2265
|
+
* `{ force?: boolean; reason?: string }`. The signature stays generic
|
|
2266
|
+
* so each Action narrows it locally without forcing a common base.
|
|
2574
2267
|
*/
|
|
2575
|
-
|
|
2576
|
-
/**
|
|
2577
|
-
* Side set of absolute file paths the operator opted into for
|
|
2578
|
-
* link-validation purposes via `scan.referencePaths`. Threaded
|
|
2579
|
-
* through to `IAnalyzerContext.referenceablePaths` so the built-in
|
|
2580
|
-
* `core/broken-ref` rule can suppress its `warn` for path-style
|
|
2581
|
-
* links whose target lands in the set. Files are NOT walked by
|
|
2582
|
-
* the kernel, the driving adapter populates the set before
|
|
2583
|
-
* calling `runScan`. Absent / empty when the operator left
|
|
2584
|
-
* `scan.referencePaths` unconfigured.
|
|
2585
|
-
*/
|
|
2586
|
-
referenceablePaths?: ReadonlySet<string>;
|
|
2587
|
-
/**
|
|
2588
|
-
* Absolute path of the scan's cwd / project root. Threaded onto
|
|
2589
|
-
* `IAnalyzerContext.cwd` so rules that need to resolve a relative
|
|
2590
|
-
* `link.target` to an absolute filesystem path can do so without
|
|
2591
|
-
* heuristics. Absent for callers that don't track a cwd
|
|
2592
|
-
* concept (out-of-band tests, embedders).
|
|
2593
|
-
*/
|
|
2594
|
-
cwd?: string;
|
|
2268
|
+
invoke?: <TInput, TReport>(input: TInput, ctx: IActionContext) => IActionResult<TReport>;
|
|
2595
2269
|
}
|
|
2270
|
+
|
|
2596
2271
|
/**
|
|
2597
|
-
*
|
|
2598
|
-
*
|
|
2599
|
-
* apply to `state_*` rows inside the same tx as the scan zone replace-
|
|
2600
|
-
* all (per `spec/db-schema.md` §Rename detection). Most callers want
|
|
2601
|
-
* `runScan` (which returns just `ScanResult`); the CLI's `sm scan`
|
|
2602
|
-
* uses this variant so it can hand the ops off to `persistScanResult`.
|
|
2272
|
+
* Formatter runtime contract. Turns the (nodes, links, issues) graph into
|
|
2273
|
+
* a textual representation for `sm graph --format <name>`.
|
|
2603
2274
|
*
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2607
|
-
*
|
|
2275
|
+
* Two adjacent names live on the same instance:
|
|
2276
|
+
*
|
|
2277
|
+
* - `formatId: string`, the manifest field consumed by the
|
|
2278
|
+
* `--format <name>` CLI flag. The kernel's lookup is
|
|
2279
|
+
* `formatters.find((f) => f.formatId === flag)`.
|
|
2280
|
+
* - `format(ctx) → string`, the runtime method. Receives the full
|
|
2281
|
+
* graph and returns the serialized output. Output MUST be
|
|
2282
|
+
* byte-deterministic for the same input (the snapshot-test suite
|
|
2283
|
+
* relies on this).
|
|
2284
|
+
*
|
|
2285
|
+
* The split (`formatId` vs `format`) is deliberate: it keeps the method
|
|
2286
|
+
* named after the kind (`Formatter.format()` reads naturally) while the
|
|
2287
|
+
* field carries the identifier the user types on the command line.
|
|
2608
2288
|
*/
|
|
2609
|
-
declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): Promise<{
|
|
2610
|
-
result: ScanResult;
|
|
2611
|
-
renameOps: RenameOp[];
|
|
2612
|
-
extractorRuns: IExtractorRunRecord[];
|
|
2613
|
-
enrichments: IEnrichmentRecord[];
|
|
2614
|
-
contributions: IContributionRecord[];
|
|
2615
|
-
freshlyRunTuples: ReadonlySet<string>;
|
|
2616
|
-
}>;
|
|
2617
|
-
declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
|
|
2618
2289
|
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2290
|
+
interface IFormatterContext {
|
|
2291
|
+
nodes: Node[];
|
|
2292
|
+
links: Link[];
|
|
2293
|
+
issues: Issue[];
|
|
2294
|
+
/**
|
|
2295
|
+
* Full persisted scan, when the caller has it on hand. Optional so
|
|
2296
|
+
* existing formatters that only consume (nodes, links, issues) keep
|
|
2297
|
+
* working unchanged; formatters whose output mirrors a `ScanResult`
|
|
2298
|
+
* envelope (today: the built-in `json` formatter under
|
|
2299
|
+
* `built-in-plugins/formatters/json/`) read this to project the
|
|
2300
|
+
* canonical document verbatim. `undefined` when the caller has only
|
|
2301
|
+
* the three primary arrays (back-compat with older drivers).
|
|
2302
|
+
*/
|
|
2303
|
+
scanResult?: ScanResult;
|
|
2304
|
+
}
|
|
2305
|
+
interface IFormatter extends IExtensionBase {
|
|
2306
|
+
kind: 'formatter';
|
|
2307
|
+
/** Format identifier consumed by `sm graph --format <name>`. */
|
|
2308
|
+
formatId: string;
|
|
2309
|
+
/** Serialize the graph into a string. Deterministic-only. */
|
|
2310
|
+
format(ctx: IFormatterContext): string;
|
|
2311
|
+
}
|
|
2627
2312
|
|
|
2628
2313
|
/**
|
|
2629
|
-
*
|
|
2314
|
+
* Hook runtime contract. The sixth plugin kind (spec § A.11).
|
|
2630
2315
|
*
|
|
2631
|
-
*
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2316
|
+
* Hooks subscribe declaratively to a curated set of kernel lifecycle
|
|
2317
|
+
* events and react to them. Reaction-only by design: a hook cannot
|
|
2318
|
+
* mutate the pipeline, block emission, or alter outputs. Use cases
|
|
2319
|
+
* are notification (Slack on `job.completed`), integration glue (CI
|
|
2320
|
+
* webhook on `job.failed`), and bookkeeping (per-extractor metrics).
|
|
2635
2321
|
*
|
|
2636
|
-
*
|
|
2322
|
+
* The hookable trigger set is INTENTIONALLY SMALL, ten events. Eight
|
|
2323
|
+
* are pipeline-driven (emitted from inside `runScan`); two
|
|
2324
|
+
* (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving
|
|
2325
|
+
* binary before / after the verb runs, fire-and-forget so
|
|
2326
|
+
* `process.exit` is never blocked). The full `ProgressEmitterPort`
|
|
2327
|
+
* catalog (per-node `scan.progress`, `model.delta`, `run.*`, internal
|
|
2328
|
+
* job lifecycle) is deliberately not hookable: too verbose for a
|
|
2329
|
+
* reactive surface, internal to the runner, or covered elsewhere.
|
|
2330
|
+
* Declaring a trigger outside the curated set yields
|
|
2331
|
+
* `invalid-manifest` at load time.
|
|
2637
2332
|
*
|
|
2638
|
-
*
|
|
2639
|
-
* flagged `stale`. With Extractors deterministic-only no row is
|
|
2640
|
-
* stale-flagged in this revision; the filter is preserved for the
|
|
2641
|
-
* future Action-issued enrichment revision (queued LLM jobs whose
|
|
2642
|
-
* output must survive body changes), where stale visibility
|
|
2643
|
-
* belongs to the UI layer next to the value.
|
|
2644
|
-
* 2. Sort the survivors by `enrichedAt` ASC so iteration order is
|
|
2645
|
-
* "oldest first". This makes the spread merge below
|
|
2646
|
-
* last-write-wins per field, the freshest Extractor's value
|
|
2647
|
-
* pisar the older one for any conflicting key.
|
|
2648
|
-
* 3. Spread-merge each row's `value` over `node.frontmatter`. The
|
|
2649
|
-
* author's keys are the base; enrichment keys overlay them.
|
|
2333
|
+
* Dual-mode (declared in manifest):
|
|
2650
2334
|
*
|
|
2651
|
-
*
|
|
2652
|
-
*
|
|
2653
|
-
*
|
|
2654
|
-
*
|
|
2335
|
+
* - `deterministic` (default): `on(ctx)` runs in-process during the
|
|
2336
|
+
* dispatch of the matching event, synchronously between the
|
|
2337
|
+
* event's emission and the next pipeline step. Errors are caught
|
|
2338
|
+
* by the dispatcher, logged via `extension.error`, and never
|
|
2339
|
+
* block the main flow.
|
|
2340
|
+
* - `probabilistic`: the hook is enqueued as a job. Until the job
|
|
2341
|
+
* subsystem ships, probabilistic hooks load but skip dispatch
|
|
2342
|
+
* with a stderr advisory (Decision #114 in `ROADMAP.md`).
|
|
2655
2343
|
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
*
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2661
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2344
|
+
* Curated trigger set (per spec § A.11):
|
|
2345
|
+
*
|
|
2346
|
+
* 0. `boot` , once per CLI process, before verb routing.
|
|
2347
|
+
* 1. `scan.started` , pre-scan setup (one per scan).
|
|
2348
|
+
* 2. `scan.completed` , post-scan reaction (one per scan).
|
|
2349
|
+
* 3. `extractor.completed` , aggregated per-Extractor outputs.
|
|
2350
|
+
* 4. `analyzer.completed` , aggregated per-Rule outputs.
|
|
2351
|
+
* 5. `action.completed` , Action executed on a node.
|
|
2352
|
+
* 6. `job.spawning` , pre-spawn of runner subprocess.
|
|
2353
|
+
* 7. `job.completed` , most common trigger.
|
|
2354
|
+
* 8. `job.failed` , alerts, retry triggers.
|
|
2355
|
+
* 9. `shutdown` , once per CLI process, after the verb's
|
|
2356
|
+
* exit code resolves and before
|
|
2357
|
+
* `process.exit`.
|
|
2665
2358
|
*/
|
|
2666
|
-
|
|
2667
|
-
includeStale?: boolean;
|
|
2668
|
-
}): Record<string, unknown>;
|
|
2359
|
+
|
|
2669
2360
|
/**
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
2361
|
+
* The ten hookable lifecycle events. Mirrors the `triggers[]` enum in
|
|
2362
|
+
* `spec/schemas/extensions/hook.schema.json`. Eight are pipeline-driven
|
|
2363
|
+
* (emitted from inside `runScan`); two (`boot`, `shutdown`) are
|
|
2364
|
+
* CLI-process-driven (emitted by the driving binary before / after the
|
|
2365
|
+
* verb runs). Anything outside this set is rejected at load time as
|
|
2366
|
+
* `invalid-manifest`.
|
|
2675
2367
|
*/
|
|
2676
|
-
|
|
2677
|
-
nodePath: string;
|
|
2678
|
-
extractorId: string;
|
|
2679
|
-
bodyHashAtEnrichment: string;
|
|
2680
|
-
value: Partial<Node>;
|
|
2681
|
-
stale: boolean;
|
|
2682
|
-
enrichedAt: number;
|
|
2683
|
-
isProbabilistic: boolean;
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2368
|
+
type THookTrigger = 'boot' | 'scan.started' | 'scan.completed' | 'extractor.completed' | 'analyzer.completed' | 'action.completed' | 'job.spawning' | 'job.completed' | 'job.failed' | 'shutdown';
|
|
2686
2369
|
/**
|
|
2687
|
-
*
|
|
2688
|
-
*
|
|
2689
|
-
*
|
|
2690
|
-
*
|
|
2370
|
+
* Frozen list mirror of `THookTrigger` for runtime introspection. The
|
|
2371
|
+
* loader validates `manifest.triggers[]` against this set; the
|
|
2372
|
+
* dispatcher iterates it in order when fanning an event out to
|
|
2373
|
+
* subscribed hooks. `boot` first / `shutdown` last so a debug log of
|
|
2374
|
+
* the array reads in lifecycle order.
|
|
2691
2375
|
*/
|
|
2692
|
-
|
|
2693
|
-
declare class InMemoryProgressEmitter implements ProgressEmitterPort {
|
|
2694
|
-
#private;
|
|
2695
|
-
emit(event: ProgressEvent): void;
|
|
2696
|
-
subscribe(listener: TProgressListener): () => void;
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2376
|
+
declare const HOOK_TRIGGERS: readonly THookTrigger[];
|
|
2699
2377
|
/**
|
|
2700
|
-
*
|
|
2701
|
-
*
|
|
2702
|
-
* Wraps `chokidar` behind a small `IFsWatcher` interface so:
|
|
2703
|
-
*
|
|
2704
|
-
* 1. The CLI command is impl-agnostic, swapping chokidar for a
|
|
2705
|
-
* different watcher later (Java? Rust port? a future `WatchPort`?)
|
|
2706
|
-
* doesn't ripple into the command.
|
|
2707
|
-
* 2. Debouncing, batching, and ignore-filter integration live in one
|
|
2708
|
-
* place. The CLI just gets `onBatch(paths)` callbacks and decides
|
|
2709
|
-
* whether to re-scan.
|
|
2378
|
+
* Context the dispatcher hands to `Hook.on()`. The shape is intentionally
|
|
2379
|
+
* narrow: a hook reacts to an event, it does not steer the pipeline.
|
|
2710
2380
|
*
|
|
2711
|
-
* The
|
|
2712
|
-
*
|
|
2713
|
-
* `
|
|
2714
|
-
*
|
|
2715
|
-
* would couple the kernel module to `SqliteStorageAdapter`, which the
|
|
2716
|
-
* Server wouldn't want. Keep this module side-effect free
|
|
2717
|
-
* apart from filesystem subscription.
|
|
2381
|
+
* The `event` carries the raw `ProgressEvent` envelope (type, timestamp,
|
|
2382
|
+
* runId/jobId when applicable, data). Optional `node` / `extractorId`
|
|
2383
|
+
* / `analyzerId` / `actionId` are extracted from the event payload by the
|
|
2384
|
+
* dispatcher when present so authors don't have to walk `event.data`.
|
|
2718
2385
|
*
|
|
2719
|
-
*
|
|
2720
|
-
*
|
|
2721
|
-
* We re-derive the path RELATIVE to the closest matching root before
|
|
2722
|
-
* passing it through `IIgnoreFilter.ignores`. This mirrors what the
|
|
2723
|
-
* scan walker does (`extensions/providers/claude/index.ts`) so both code
|
|
2724
|
-
* paths agree on what "ignored" means.
|
|
2386
|
+
* Probabilistic hooks additionally receive `runner` for LLM dispatch.
|
|
2387
|
+
* Deterministic hooks SHOULD ignore the field.
|
|
2725
2388
|
*/
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
events: IWatchEvent[];
|
|
2736
|
-
/** Convenience: deduplicated absolute paths across the batch. */
|
|
2737
|
-
paths: string[];
|
|
2738
|
-
}
|
|
2739
|
-
interface IFsWatcher {
|
|
2740
|
-
/** Resolves once chokidar has finished its initial directory scan and is ready to emit. */
|
|
2741
|
-
ready: Promise<void>;
|
|
2742
|
-
/** Tear down the watcher. Resolves after chokidar releases handles. */
|
|
2743
|
-
close: () => Promise<void>;
|
|
2744
|
-
}
|
|
2745
|
-
interface ICreateFsWatcherOptions {
|
|
2746
|
-
/** Roots to watch. Resolved relative to `cwd` if relative paths are passed. */
|
|
2747
|
-
roots: string[];
|
|
2748
|
-
/** Working directory used to resolve relative roots and the ignore-filter root. */
|
|
2749
|
-
cwd: string;
|
|
2750
|
-
/** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */
|
|
2751
|
-
debounceMs: number;
|
|
2389
|
+
interface IHookContext {
|
|
2390
|
+
/** The raw event the dispatcher matched. */
|
|
2391
|
+
event: {
|
|
2392
|
+
type: THookTrigger;
|
|
2393
|
+
timestamp: string;
|
|
2394
|
+
runId?: string;
|
|
2395
|
+
jobId?: string;
|
|
2396
|
+
data?: unknown;
|
|
2397
|
+
};
|
|
2752
2398
|
/**
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
2756
|
-
*
|
|
2757
|
-
* - **`IIgnoreFilter`** (the static one), captured by reference at
|
|
2758
|
-
* construction. Use this when the filter never changes for the
|
|
2759
|
-
* lifetime of the watcher (the typical CLI `sm watch` flow).
|
|
2760
|
-
*
|
|
2761
|
-
* - **`() => IIgnoreFilter | undefined`** (a getter), re-evaluated
|
|
2762
|
-
* on EVERY chokidar `ignored` predicate call. Use this when the
|
|
2763
|
-
* filter can change at runtime, e.g. the BFF rebuilds it after
|
|
2764
|
-
* a `.skillmapignore` or `.skill-map/settings.json` edit and
|
|
2765
|
-
* wants chokidar to immediately respect the new patterns without
|
|
2766
|
-
* tearing down and rebuilding the watcher. A getter that returns
|
|
2767
|
-
* `undefined` disables ignore filtering for that call.
|
|
2399
|
+
* Convenience extraction of the node payload when the event is
|
|
2400
|
+
* node-scoped (`action.completed`). Undefined for run-scoped or
|
|
2401
|
+
* scan-scoped events.
|
|
2768
2402
|
*/
|
|
2769
|
-
|
|
2403
|
+
node?: Node;
|
|
2770
2404
|
/**
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
* literal `roots` entries (no descent), which is the right setting
|
|
2774
|
-
* when watching a directory only to catch changes to specific
|
|
2775
|
-
* top-level files (see `subscribeMeta` in `core/watcher/runtime.ts`).
|
|
2776
|
-
* Forwarded verbatim to chokidar's `depth` option.
|
|
2405
|
+
* Set on `extractor.completed` events. Qualified extension id of the
|
|
2406
|
+
* Extractor whose work the event aggregates.
|
|
2777
2407
|
*/
|
|
2778
|
-
|
|
2779
|
-
/** Called once per debounced batch. Awaited; concurrent batches are serialised. */
|
|
2780
|
-
onBatch: (batch: IWatchBatch) => void | Promise<void>;
|
|
2408
|
+
extractorId?: string;
|
|
2781
2409
|
/**
|
|
2782
|
-
*
|
|
2783
|
-
* stays open, callers decide whether to log, keep going, or close.
|
|
2410
|
+
* Set on `analyzer.completed` events. Qualified extension id of the Rule.
|
|
2784
2411
|
*/
|
|
2785
|
-
|
|
2412
|
+
analyzerId?: string;
|
|
2413
|
+
/**
|
|
2414
|
+
* Set on `action.completed` events. Qualified extension id of the
|
|
2415
|
+
* Action that just ran.
|
|
2416
|
+
*/
|
|
2417
|
+
actionId?: string;
|
|
2418
|
+
/**
|
|
2419
|
+
* Set on `job.*` events once the job subsystem lands. Carries the
|
|
2420
|
+
* report payload for `job.completed`, the failure record for
|
|
2421
|
+
* `job.failed`, and the spawn metadata for `job.spawning`.
|
|
2422
|
+
*/
|
|
2423
|
+
jobResult?: unknown;
|
|
2424
|
+
/**
|
|
2425
|
+
* `RunnerPort` injection for `probabilistic` hooks. `undefined` for
|
|
2426
|
+
* `deterministic` mode (the default). Probabilistic hooks land with
|
|
2427
|
+
* the job subsystem; the field is reserved here so the runtime
|
|
2428
|
+
* contract is forward-compatible without a major bump.
|
|
2429
|
+
*/
|
|
2430
|
+
runner?: unknown;
|
|
2786
2431
|
}
|
|
2787
2432
|
/**
|
|
2788
|
-
*
|
|
2789
|
-
*
|
|
2790
|
-
*
|
|
2433
|
+
* Optional declarative filter applied by the dispatcher BEFORE
|
|
2434
|
+
* invoking `on(ctx)`. Keys are payload field paths (top-level only in
|
|
2435
|
+
* v0.x); values are the literal expected match. The dispatcher walks
|
|
2436
|
+
* `event.data` for the field and short-circuits the invocation if the
|
|
2437
|
+
* value disagrees.
|
|
2791
2438
|
*
|
|
2792
|
-
*
|
|
2793
|
-
*
|
|
2794
|
-
*
|
|
2795
|
-
*
|
|
2439
|
+
* Cross-field validation against declared `triggers` is best-effort
|
|
2440
|
+
* at load time: when none of the declared triggers carries a given
|
|
2441
|
+
* filter field, the loader surfaces `invalid-manifest`. The current
|
|
2442
|
+
* impl performs the basic enum check but defers full payload-shape
|
|
2443
|
+
* cross-validation to a follow-up, the dispatcher is permissive at
|
|
2444
|
+
* runtime (an unknown field never matches → the hook simply never
|
|
2445
|
+
* fires for that event, which is a correct interpretation of "filter
|
|
2446
|
+
* by a field that doesn't exist").
|
|
2796
2447
|
*/
|
|
2797
|
-
|
|
2448
|
+
type THookFilter = Record<string, string | number | boolean>;
|
|
2449
|
+
interface IHook extends IExtensionBase {
|
|
2450
|
+
kind: 'hook';
|
|
2451
|
+
/**
|
|
2452
|
+
* Execution mode. Optional in the manifest with a default of
|
|
2453
|
+
* `deterministic` per `spec/schemas/extensions/hook.schema.json`.
|
|
2454
|
+
* Probabilistic hooks load but skip dispatch with a stderr advisory
|
|
2455
|
+
* until the job subsystem ships (Decision #114).
|
|
2456
|
+
*/
|
|
2457
|
+
mode?: TExecutionMode;
|
|
2458
|
+
/**
|
|
2459
|
+
* Subset of the curated lifecycle trigger set this hook subscribes
|
|
2460
|
+
* to. MUST be non-empty; every entry MUST be a member of
|
|
2461
|
+
* `HOOK_TRIGGERS`. The loader validates both invariants and surfaces
|
|
2462
|
+
* `invalid-manifest` on violation.
|
|
2463
|
+
*/
|
|
2464
|
+
triggers: THookTrigger[];
|
|
2465
|
+
/**
|
|
2466
|
+
* Optional declarative filter. Absent → invoke on every dispatched
|
|
2467
|
+
* event of every declared trigger.
|
|
2468
|
+
*/
|
|
2469
|
+
filter?: THookFilter;
|
|
2470
|
+
/**
|
|
2471
|
+
* Hook entry point. Returns nothing; reactions are side effects.
|
|
2472
|
+
* Errors are caught by the dispatcher (logged as `extension.error`,
|
|
2473
|
+
* surfaced via `hook.failed` meta-event) and NEVER block the main
|
|
2474
|
+
* pipeline, a buggy hook degrades gracefully.
|
|
2475
|
+
*/
|
|
2476
|
+
on(ctx: IHookContext): void | Promise<void>;
|
|
2477
|
+
}
|
|
2798
2478
|
|
|
2799
2479
|
/**
|
|
2800
|
-
*
|
|
2801
|
-
* `sm scan --compare-with <path>` and is the single place the kernel
|
|
2802
|
-
* knows how to identify "the same" entity across two scans.
|
|
2803
|
-
*
|
|
2804
|
-
* **Identity contract** (mirrors decisions made at earlier sub-steps):
|
|
2805
|
-
*
|
|
2806
|
-
* - **Node**: `node.path`. The path is the only field stable across
|
|
2807
|
-
* edits, every other Node field is content-derived (hashes, counts,
|
|
2808
|
-
* denormalised frontmatter). Two nodes with the same path are the
|
|
2809
|
-
* "same" node; differences are reported as a `changed` entry with
|
|
2810
|
-
* a reason narrowing what diverged.
|
|
2811
|
-
*
|
|
2812
|
-
* - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This
|
|
2813
|
-
* mirrors the link-conflict rule and `sm show` aggregation,
|
|
2814
|
-
* two links with identical endpoints, kind, and (optional) trigger
|
|
2815
|
-
* are the same link, even if emitted by different extractors. The
|
|
2816
|
-
* `sources[]` union and confidence are NOT part of identity; they
|
|
2817
|
-
* are presentation facets that can churn without making the link
|
|
2818
|
-
* "different" for delta purposes.
|
|
2819
|
-
*
|
|
2820
|
-
* - **Issue**: `(analyzerId, sorted nodeIds, message)`. Mirrors
|
|
2821
|
-
* `spec/job-events.md` §issue.*, same key → same issue, even when
|
|
2822
|
-
* `data` / `severity` / `linkIndices` shift. A meaningful change in
|
|
2823
|
-
* `message` (or a different set of node ids) is a different issue.
|
|
2824
|
-
* This is the same key future job events will use; keep it aligned
|
|
2825
|
-
* so consumers can reuse logic.
|
|
2826
|
-
*
|
|
2827
|
-
* No "changed" bucket for links / issues, identity already captures
|
|
2828
|
-
* everything that matters there. Nodes get a "changed" bucket because
|
|
2829
|
-
* the path stays stable while the body / frontmatter rewrite, and that
|
|
2830
|
-
* change is meaningful (formatters, summarisers, downstream consumers
|
|
2831
|
-
* all care about it).
|
|
2480
|
+
* `ProgressEmitterPort`, emits progress events during long operations.
|
|
2832
2481
|
*
|
|
2833
|
-
*
|
|
2834
|
-
*
|
|
2482
|
+
* Shape-only today. The full event catalog (`run.started`,
|
|
2483
|
+
* `job.claimed`, `model.delta`, etc.) is normative in
|
|
2484
|
+
* `spec/job-events.md`; this port carries an open `data` payload so
|
|
2485
|
+
* adapters can emit any documented event without type churn.
|
|
2835
2486
|
*/
|
|
2836
|
-
|
|
2837
|
-
type
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
* Which hash diverged. `'body'` means body rewritten, frontmatter
|
|
2843
|
-
* untouched; `'frontmatter'` means metadata rewritten, body
|
|
2844
|
-
* untouched; `'both'` means both rewritten in the same edit.
|
|
2845
|
-
*/
|
|
2846
|
-
reason: TNodeChangeReason;
|
|
2487
|
+
interface ProgressEvent {
|
|
2488
|
+
type: string;
|
|
2489
|
+
timestamp: string;
|
|
2490
|
+
runId?: string;
|
|
2491
|
+
jobId?: string;
|
|
2492
|
+
data?: unknown;
|
|
2847
2493
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
added: Node[];
|
|
2853
|
-
removed: Node[];
|
|
2854
|
-
changed: INodeChange[];
|
|
2855
|
-
};
|
|
2856
|
-
links: {
|
|
2857
|
-
added: Link[];
|
|
2858
|
-
removed: Link[];
|
|
2859
|
-
};
|
|
2860
|
-
issues: {
|
|
2861
|
-
added: Issue[];
|
|
2862
|
-
removed: Issue[];
|
|
2863
|
-
};
|
|
2494
|
+
type TProgressListener = (event: ProgressEvent) => void;
|
|
2495
|
+
interface ProgressEmitterPort {
|
|
2496
|
+
emit(event: ProgressEvent): void;
|
|
2497
|
+
subscribe(listener: TProgressListener): () => void;
|
|
2864
2498
|
}
|
|
2865
|
-
|
|
2499
|
+
|
|
2866
2500
|
/**
|
|
2867
|
-
*
|
|
2868
|
-
*
|
|
2501
|
+
* Per-node extractor invocation: build a fresh `IExtractorContext` for
|
|
2502
|
+
* each extractor, validate every emitted link / contribution against
|
|
2503
|
+
* the declared catalog, fold enrichment partials into per-`(node,
|
|
2504
|
+
* extractor)` records, and surface emit-time drops as
|
|
2505
|
+
* `extension.error` events.
|
|
2506
|
+
*
|
|
2507
|
+
* Also hosts the post-walk recompute helpers that re-derive
|
|
2508
|
+
* `linksOutCount` / `linksInCount` / `externalRefsCount` on every node
|
|
2509
|
+
* from the final merged link buffer, plus the `IExtractorRunRecord`
|
|
2510
|
+
* and `IEnrichmentRecord` types those records eventually persist as.
|
|
2869
2511
|
*/
|
|
2870
|
-
declare function isEmptyDelta(delta: IScanDelta): boolean;
|
|
2871
2512
|
|
|
2872
2513
|
/**
|
|
2873
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
*
|
|
2876
|
-
*
|
|
2877
|
-
*
|
|
2878
|
-
*
|
|
2879
|
-
*
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2514
|
+
* Spec § A.9, runs to persist into `scan_extractor_runs`. One entry
|
|
2515
|
+
* per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
|
|
2516
|
+
* "this extractor is current for this body". Includes both freshly-run
|
|
2517
|
+
* pairs (extractor invoked this scan) and reused pairs (cached node, the
|
|
2518
|
+
* extractor's prior run still applies to the same body hash). Excludes
|
|
2519
|
+
* obsolete pairs, extractors that ran in the prior but are no longer
|
|
2520
|
+
* registered, so a replace-all persist drops them automatically.
|
|
2521
|
+
*/
|
|
2522
|
+
interface IExtractorRunRecord {
|
|
2523
|
+
nodePath: string;
|
|
2524
|
+
extractorId: string;
|
|
2525
|
+
bodyHashAtRun: string;
|
|
2526
|
+
ranAt: number;
|
|
2527
|
+
/**
|
|
2528
|
+
* sha256 of the canonical-form sidecar annotations the Extractor saw
|
|
2529
|
+
* at run time. Always populated (an absent sidecar canonicalises to
|
|
2530
|
+
* `{}` so the hash is stable). Used unconditionally by the cache
|
|
2531
|
+
* decision alongside `bodyHashAtRun`: a sidecar-only edit invalidates
|
|
2532
|
+
* the cached run for every applicable Extractor on that node.
|
|
2533
|
+
*/
|
|
2534
|
+
sidecarAnnotationsHashAtRun: string;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Spec § A.8, universal enrichment layer.
|
|
2885
2538
|
*
|
|
2886
|
-
*
|
|
2887
|
-
*
|
|
2539
|
+
* One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
|
|
2540
|
+
* produced via `ctx.enrichNode(...)` during the walk. Attribution is
|
|
2541
|
+
* preserved per-Extractor (rather than merged client-side as B.1 did)
|
|
2542
|
+
* so the persistence layer can:
|
|
2888
2543
|
*
|
|
2889
|
-
*
|
|
2544
|
+
* - upsert a single row per pair (stable PRIMARY KEY conflict on
|
|
2545
|
+
* re-extract);
|
|
2546
|
+
* - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
|
|
2547
|
+
* for last-write-wins per field at read time.
|
|
2890
2548
|
*
|
|
2891
|
-
*
|
|
2892
|
-
*
|
|
2893
|
-
*
|
|
2894
|
-
*
|
|
2895
|
-
*
|
|
2896
|
-
* - `path=foo/*` / `path=.claude/agents/**`, POSIX glob over `node.path`.
|
|
2897
|
-
* Supports `*` (any chars except `/`) and `**` (any chars including `/`).
|
|
2549
|
+
* `value` is the cumulative merge across every `enrichNode` call that
|
|
2550
|
+
* Extractor made for this node within this scan, multiple
|
|
2551
|
+
* `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
|
|
2552
|
+
* fold into a single row, but two different Extractors hitting the
|
|
2553
|
+
* same node yield two distinct rows.
|
|
2898
2554
|
*
|
|
2899
|
-
*
|
|
2555
|
+
* `isProbabilistic` is reserved: Extractors are deterministic-only, so
|
|
2556
|
+
* every record produced by the orchestrator sets it to `false`. The
|
|
2557
|
+
* field is kept on the record (and the row in `node_enrichments`) so a
|
|
2558
|
+
* future Action-issued enrichment can populate it without reshaping
|
|
2559
|
+
* the persistence contract, see spec `architecture.md`
|
|
2560
|
+
* §Extractor · enrichment layer.
|
|
2561
|
+
*/
|
|
2562
|
+
interface IEnrichmentRecord {
|
|
2563
|
+
nodePath: string;
|
|
2564
|
+
extractorId: string;
|
|
2565
|
+
bodyHashAtEnrichment: string;
|
|
2566
|
+
value: Partial<Node>;
|
|
2567
|
+
enrichedAt: number;
|
|
2568
|
+
isProbabilistic: boolean;
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Run a set of extractors against a single node, collecting their link
|
|
2572
|
+
* emissions and node-enrichment partials. Each extractor is invoked
|
|
2573
|
+
* exactly once with a fresh `IExtractorContext`. Caller decides what
|
|
2574
|
+
* to do with the returned arrays (push into per-scan buffers, write to
|
|
2575
|
+
* a focused refresh result, etc.).
|
|
2900
2576
|
*
|
|
2901
|
-
*
|
|
2902
|
-
*
|
|
2903
|
-
*
|
|
2904
|
-
*
|
|
2905
|
-
* unfiltered nodes" would be confusing, the user asked for a focused
|
|
2906
|
-
* subgraph, not its boundary. External-URL pseudo-links are already
|
|
2907
|
-
* stripped by the orchestrator and never reach this layer.
|
|
2908
|
-
* - Issues survive when ANY of the issue's `nodeIds` is in the filtered
|
|
2909
|
-
* set. Issues span multiple nodes (e.g. `trigger-collision` over two
|
|
2910
|
-
* advertisers); dropping an issue when one of its nodes is outside
|
|
2911
|
-
* would hide cross-cutting problems the user is investigating.
|
|
2577
|
+
* Exported so `cli/commands/refresh.ts` can reuse the same wiring it
|
|
2578
|
+
* needs for re-running a single extractor against a single node, the
|
|
2579
|
+
* pre-extraction code in `refresh.ts` was hand-duplicating this loop
|
|
2580
|
+
* (audit item V4).
|
|
2912
2581
|
*
|
|
2913
|
-
*
|
|
2582
|
+
* Within this call, multiple `enrichNode(partial)` calls from the same
|
|
2583
|
+
* extractor against the same node fold into one record (last-write-wins
|
|
2584
|
+
* per field), same contract as the in-scan path.
|
|
2914
2585
|
*/
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2586
|
+
declare function runExtractorsForNode(opts: {
|
|
2587
|
+
extractors: IExtractor[];
|
|
2588
|
+
node: Node;
|
|
2589
|
+
body: string;
|
|
2590
|
+
frontmatter: Record<string, unknown>;
|
|
2591
|
+
bodyHash: string;
|
|
2592
|
+
emitter: ProgressEmitterPort;
|
|
2919
2593
|
/**
|
|
2920
|
-
*
|
|
2921
|
-
*
|
|
2922
|
-
*
|
|
2923
|
-
*
|
|
2924
|
-
* yields zero matches at filter time.
|
|
2594
|
+
* Spec § A.12, per-plugin `ctx.store` wrappers keyed by `pluginId`.
|
|
2595
|
+
* The map's lookup is per-extractor inside the loop, so callers that
|
|
2596
|
+
* don't track plugin storage can omit it; the resulting `ctx.store`
|
|
2597
|
+
* stays `undefined` (the existing contract).
|
|
2925
2598
|
*/
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
links: Link[];
|
|
2934
|
-
issues: Issue[];
|
|
2935
|
-
}
|
|
2936
|
-
declare class ExportQueryError extends Error {
|
|
2937
|
-
constructor(message: string);
|
|
2938
|
-
}
|
|
2939
|
-
declare function parseExportQuery(raw: string): IExportQuery;
|
|
2940
|
-
declare function applyExportQuery(scan: {
|
|
2941
|
-
nodes: Node[];
|
|
2942
|
-
links: Link[];
|
|
2943
|
-
issues: Issue[];
|
|
2944
|
-
}, query: IExportQuery): IExportSubset;
|
|
2599
|
+
pluginStores?: ReadonlyMap<string, IPluginStore>;
|
|
2600
|
+
}): Promise<{
|
|
2601
|
+
internalLinks: Link[];
|
|
2602
|
+
externalLinks: Link[];
|
|
2603
|
+
enrichments: IEnrichmentRecord[];
|
|
2604
|
+
contributions: IContributionRecord[];
|
|
2605
|
+
}>;
|
|
2945
2606
|
|
|
2946
2607
|
/**
|
|
2947
|
-
*
|
|
2948
|
-
*
|
|
2949
|
-
*
|
|
2950
|
-
*
|
|
2951
|
-
*
|
|
2952
|
-
* string MAY appear under both sources for the same node, the PK
|
|
2953
|
-
* accepts the pair; search returns the node once via DISTINCT, the
|
|
2954
|
-
* UI renders both chips with their attribution.
|
|
2955
|
-
*
|
|
2956
|
-
* Belongs to the `scan_*` family, replaced wholesale per scan.
|
|
2957
|
-
* Cached nodes' tag rows are projected from the cached
|
|
2958
|
-
* `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both
|
|
2959
|
-
* already in memory at persist time), so the rebuild is cheap
|
|
2960
|
-
* regardless of cache hit / miss. See `spec/db-schema.md`
|
|
2961
|
-
* § scan_node_tags for the normative shape and replace-all semantics.
|
|
2608
|
+
* Rename + orphan classification per `spec/db-schema.md` §Rename
|
|
2609
|
+
* detection. Pure: takes the prior `ScanResult` and the current node
|
|
2610
|
+
* set, mutates the supplied `issues` array in place, and returns the
|
|
2611
|
+
* `RenameOp[]` the persistence layer must apply inside the same tx as
|
|
2612
|
+
* the scan zone replace-all.
|
|
2962
2613
|
*/
|
|
2963
2614
|
|
|
2964
2615
|
/**
|
|
2965
|
-
*
|
|
2966
|
-
*
|
|
2967
|
-
*
|
|
2968
|
-
*
|
|
2969
|
-
* (`source: 'user'`).
|
|
2616
|
+
* Confidence-tagged plan to repoint `state_*` references from one node
|
|
2617
|
+
* path to another. Emitted by the rename heuristic during `runScan` and
|
|
2618
|
+
* consumed by `persistScanResult` so the FK migration runs inside the
|
|
2619
|
+
* same transaction as the scan zone replace-all.
|
|
2970
2620
|
*/
|
|
2971
|
-
interface
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2621
|
+
interface RenameOp {
|
|
2622
|
+
from: string;
|
|
2623
|
+
to: string;
|
|
2624
|
+
confidence: 'high' | 'medium';
|
|
2975
2625
|
}
|
|
2976
|
-
|
|
2977
2626
|
/**
|
|
2978
|
-
* Pure
|
|
2627
|
+
* Pure rename / orphan classification per `spec/db-schema.md` §Rename
|
|
2628
|
+
* detection. Mutates `issues` in place, caller passes the in-progress
|
|
2629
|
+
* issue list; returns the `RenameOp[]` for the persistence layer to
|
|
2630
|
+
* apply inside its tx.
|
|
2979
2631
|
*
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
2982
|
-
* with `AbortController` + timeout. Throws on
|
|
2983
|
-
* non-200 / parse failure / abort.
|
|
2984
|
-
* - `compareVersions` , semver compare (-1 / 0 / 1). Pre-1.0 aware:
|
|
2985
|
-
* treats prereleases via the standard rules
|
|
2986
|
-
* (release > prerelease at the same triple).
|
|
2987
|
-
* - `isOutdated` , sugar over `compareVersions` for the common
|
|
2988
|
-
* "is `latest` strictly greater than `current`"
|
|
2989
|
-
* check the banner runs against.
|
|
2632
|
+
* Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused
|
|
2633
|
+
* by another):
|
|
2990
2634
|
*
|
|
2991
|
-
*
|
|
2992
|
-
*
|
|
2993
|
-
*
|
|
2994
|
-
*
|
|
2635
|
+
* 1. **High-confidence**: pair each `deletedPath` with a `newPath`
|
|
2636
|
+
* that has the same `bodyHash`. No issue, no prompt.
|
|
2637
|
+
* 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
|
|
2638
|
+
* each with the *unique* unclaimed `newPath` that shares its
|
|
2639
|
+
* `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
|
|
2640
|
+
* with `data: { from, to, confidence: 'medium' }`.
|
|
2641
|
+
* 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
|
|
2642
|
+
* remaining frontmatter-matching candidate, emit ONE
|
|
2643
|
+
* `auto-rename-ambiguous` issue per `newPath`, listing all
|
|
2644
|
+
* candidates in `data.candidates`. NO migration.
|
|
2645
|
+
* 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one
|
|
2646
|
+
* `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
|
|
2995
2647
|
*
|
|
2996
|
-
*
|
|
2997
|
-
*
|
|
2998
|
-
*
|
|
2999
|
-
*
|
|
3000
|
-
* (from `VERSION`) into the cache so the UI can render without a
|
|
3001
|
-
* second lookup. Both stay flat, no nested objects, so JSON
|
|
3002
|
-
* serialization is trivial.
|
|
2648
|
+
* Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
|
|
2649
|
+
* order so the same input always produces the same matches,
|
|
2650
|
+
* required for reproducible tests and conformance fixtures (the spec
|
|
2651
|
+
* does not prescribe an order, but stability is the obvious contract).
|
|
3003
2652
|
*/
|
|
3004
|
-
|
|
3005
|
-
latestVersion: string;
|
|
3006
|
-
/** Epoch ms, when the registry was last successfully probed. */
|
|
3007
|
-
checkedAt: number;
|
|
3008
|
-
/** Epoch ms, when the banner was last printed; null = never shown yet. */
|
|
3009
|
-
shownAt: number | null;
|
|
3010
|
-
}
|
|
2653
|
+
declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
|
|
3011
2654
|
|
|
3012
2655
|
/**
|
|
3013
|
-
*
|
|
3014
|
-
*
|
|
3015
|
-
*
|
|
3016
|
-
* the
|
|
3017
|
-
*
|
|
3018
|
-
*
|
|
3019
|
-
*
|
|
2656
|
+
* Scan orchestrator, runs the Provider → extractor → analyzer pipeline across
|
|
2657
|
+
* every registered extension and emits `ProgressEmitterPort` events in
|
|
2658
|
+
* canonical order. The callable extension set is injected via
|
|
2659
|
+
* `RunScanOptions.extensions`, the Registry holds manifest metadata, the
|
|
2660
|
+
* callable set holds the runtime instances the orchestrator actually
|
|
2661
|
+
* invokes. Separating the two lets `sm plugins` and `sm help` introspect
|
|
2662
|
+
* the graph without loading code.
|
|
3020
2663
|
*
|
|
3021
|
-
*
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
*
|
|
2664
|
+
* With zero registered extensions (or a callable set that carries none)
|
|
2665
|
+
* the pipeline still produces a valid zero-filled `ScanResult`, the
|
|
2666
|
+
* kernel-empty-boot invariant.
|
|
2667
|
+
*
|
|
2668
|
+
* Roots are validated up front: each entry of `RunScanOptions.roots`
|
|
2669
|
+
* must exist on disk as a directory. The first failure throws a clear
|
|
2670
|
+
* `Error` naming the offending path. This guards every caller (CLI,
|
|
2671
|
+
* server, skill-agent) against silently producing a zero-filled
|
|
2672
|
+
* `ScanResult` when a Provider walks a non-existent path, the bug
|
|
2673
|
+
* that wiped a populated DB via `sm scan -- --dry-run` (clipanion's
|
|
2674
|
+
* `--` made `--dry-run` a positional root that did not exist).
|
|
2675
|
+
*
|
|
2676
|
+
* Incremental scans: when `priorSnapshot` is supplied, the
|
|
2677
|
+
* orchestrator walks the filesystem, hashes each file, and reuses the
|
|
2678
|
+
* prior node + its prior-extracted internal links whenever both
|
|
2679
|
+
* `bodyHash` and `frontmatterHash` match. New / modified files run
|
|
2680
|
+
* through the full extractor pipeline (including the external-url-counter
|
|
2681
|
+
* which produces ephemeral pseudo-links). Rules ALWAYS run over the
|
|
2682
|
+
* fully merged graph, issue state can change even for an unchanged node
|
|
2683
|
+
* (e.g. a previously broken `references` link now resolves because a new
|
|
2684
|
+
* node was added). For unchanged nodes the prior `externalRefsCount` is
|
|
2685
|
+
* preserved as-is (the external pseudo-links were never persisted, so
|
|
2686
|
+
* they cannot be reconstructed; the count survived in the node row).
|
|
2687
|
+
*
|
|
2688
|
+
* Extractor output model (B.1, post-rename from Detector): extractors
|
|
2689
|
+
* return `void` and emit through three callbacks injected on the context:
|
|
2690
|
+
* - `ctx.emitLink(link)` → orchestrator validates against
|
|
2691
|
+
* `emitsLinkKinds` then partitions into internal / external buckets.
|
|
2692
|
+
* - `ctx.enrichNode(partial)` → orchestrator records ONE enrichment
|
|
2693
|
+
* entry per `(node, extractor)` so attribution survives into the DB.
|
|
2694
|
+
* Persisted into `node_enrichments` (A.8). The author-supplied
|
|
2695
|
+
* frontmatter on `node.frontmatter` stays immutable from any Extractor
|
|
2696
|
+
* , the enrichment layer is the only writable surface, and rules /
|
|
2697
|
+
* formatters consume it via `mergeNodeWithEnrichments`.
|
|
2698
|
+
* - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).
|
|
2699
|
+
* Wired by the driving adapter via `RunScanOptions.pluginStores`,
|
|
2700
|
+
* which the orchestrator looks up per-extractor by `pluginId` and
|
|
2701
|
+
* attaches to the context. The orchestrator never inspects what
|
|
2702
|
+
* plugins write through it; the wrapper handles AJV validation
|
|
2703
|
+
* when the manifest declared an output schema.
|
|
3026
2704
|
*/
|
|
3027
2705
|
|
|
3028
|
-
interface
|
|
2706
|
+
interface IScanExtensions {
|
|
2707
|
+
providers: IProvider[];
|
|
2708
|
+
extractors: IExtractor[];
|
|
2709
|
+
analyzers: IAnalyzer[];
|
|
3029
2710
|
/**
|
|
3030
|
-
*
|
|
3031
|
-
*
|
|
2711
|
+
* Optional hooks (spec § A.11). When supplied, the orchestrator's
|
|
2712
|
+
* lifecycle dispatcher invokes deterministic hooks subscribed to one
|
|
2713
|
+
* of the eight hookable triggers in canonical order with the matching
|
|
2714
|
+
* event payload. Absent → no hooks fire (the scan still emits its
|
|
2715
|
+
* lifecycle events to `ProgressEmitterPort` for observability).
|
|
2716
|
+
* Probabilistic hooks are loaded but skipped here with a stderr
|
|
2717
|
+
* advisory until the job subsystem ships once the job subsystem ships.
|
|
3032
2718
|
*/
|
|
3033
|
-
|
|
2719
|
+
hooks?: IHook[];
|
|
2720
|
+
}
|
|
2721
|
+
interface RunScanOptions {
|
|
3034
2722
|
/**
|
|
3035
|
-
*
|
|
3036
|
-
*
|
|
3037
|
-
* via `IDiscoveredPlugin.status`.
|
|
2723
|
+
* Filesystem roots to walk. Spec requires `minItems: 1`; passing an
|
|
2724
|
+
* empty array makes `runScan` throw before any work happens.
|
|
3038
2725
|
*/
|
|
3039
|
-
|
|
2726
|
+
roots: string[];
|
|
2727
|
+
emitter?: ProgressEmitterPort;
|
|
2728
|
+
/** Runtime extension instances. Absent → empty pipeline. */
|
|
2729
|
+
extensions?: IScanExtensions;
|
|
3040
2730
|
/**
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
2731
|
+
* Step 9.6.6, runtime catalog of plugin-contributed annotation keys
|
|
2732
|
+
* (the same shape `kernel.getRegisteredAnnotationKeys()` returns).
|
|
2733
|
+
* Threaded into the rule pass so `core/unknown-field` can
|
|
2734
|
+
* legitimise registered plugin namespaces / root keys without
|
|
2735
|
+
* re-walking the manifests. Absent → empty catalog (every plugin
|
|
2736
|
+
* key is treated as unknown). Built-in catalog from
|
|
2737
|
+
* `annotations.schema.json` is NOT included, that is hard-coded
|
|
2738
|
+
* inside the rule.
|
|
3043
2739
|
*/
|
|
3044
|
-
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
/**
|
|
3048
|
-
* Row-level filter for `port.scans.findNodes(...)` (driven by
|
|
3049
|
-
* `sm list`'s flags). All fields are optional, an empty filter
|
|
3050
|
-
* returns every node sorted by `path` asc.
|
|
3051
|
-
*/
|
|
3052
|
-
interface INodeFilter {
|
|
3053
|
-
/** Restrict to a single node kind. Open string (matches `Node.kind`). */
|
|
3054
|
-
kind?: string;
|
|
2740
|
+
annotationContributions?: readonly IRegisteredAnnotationKey[];
|
|
3055
2741
|
/**
|
|
3056
|
-
*
|
|
3057
|
-
*
|
|
2742
|
+
* Runtime catalog of plugin-contributed view contributions (the same
|
|
2743
|
+
* shape `kernel.getRegisteredViewContributions()` returns). Threaded
|
|
2744
|
+
* into the rule pass so:
|
|
2745
|
+
* - `core/contribution-orphan` can introspect the catalog
|
|
2746
|
+
* (read-only) and join it with the live node set to flag
|
|
2747
|
+
* dangling emissions. Slot catalog drift is NOT a scan concern,
|
|
2748
|
+
* it lives at load time and surfaces via `sm plugins doctor`
|
|
2749
|
+
* (the kernel rejects unknown slots as `invalid-manifest` first,
|
|
2750
|
+
* doctor catches the catalog-version-skew tail).
|
|
2751
|
+
* - The orchestrator's per-rule emit closure can look up each
|
|
2752
|
+
* declared `(contributionId → slot)` pairing for AJV
|
|
2753
|
+
* payload validation.
|
|
2754
|
+
* Absent → empty catalog. Rules that emit contributions silently
|
|
2755
|
+
* drop emissions when the catalog has no entry for the rule's
|
|
2756
|
+
* declared contributionId.
|
|
3058
2757
|
*/
|
|
3059
|
-
|
|
2758
|
+
viewContributions?: readonly IRegisteredViewContribution[];
|
|
3060
2759
|
/**
|
|
3061
|
-
*
|
|
3062
|
-
*
|
|
3063
|
-
*
|
|
3064
|
-
* defends in depth).
|
|
2760
|
+
* Scan scope. Defaults to `'project'`. The CLI flag wiring lands in
|
|
2761
|
+
* the config layer wiring; `runScan` already accepts the override
|
|
2762
|
+
* so plugins / tests can opt into `'global'` today.
|
|
3065
2763
|
*/
|
|
3066
|
-
|
|
3067
|
-
/** `'asc'` or `'desc'`. Defaults to the adapter's per-column convention. */
|
|
3068
|
-
sortDirection?: 'asc' | 'desc';
|
|
3069
|
-
/** Cap the result. Positive integer; absent → no limit. */
|
|
3070
|
-
limit?: number;
|
|
3071
|
-
}
|
|
3072
|
-
/**
|
|
3073
|
-
* Bundled fetch for `port.scans.findNode(path)`, one node and
|
|
3074
|
-
* everything `sm show <path>` displays alongside it. Every field is
|
|
3075
|
-
* computed from `scan_*` zone reads only; per-domain data (history,
|
|
3076
|
-
* jobs, plugin enrichments) ships through other namespaces.
|
|
3077
|
-
*/
|
|
3078
|
-
interface INodeBundle {
|
|
3079
|
-
node: Node;
|
|
3080
|
-
linksOut: Link[];
|
|
3081
|
-
linksIn: Link[];
|
|
3082
|
-
issues: Issue[];
|
|
3083
|
-
}
|
|
3084
|
-
/**
|
|
3085
|
-
* Output of `port.scans.countRows()`. Used by `sm scan` to decide
|
|
3086
|
-
* whether the persist would wipe a populated DB (the "refusing to
|
|
3087
|
-
* wipe" guard) and by `sm db status` for the human summary.
|
|
3088
|
-
*/
|
|
3089
|
-
interface INodeCounts {
|
|
3090
|
-
nodes: number;
|
|
3091
|
-
links: number;
|
|
3092
|
-
issues: number;
|
|
3093
|
-
}
|
|
3094
|
-
/**
|
|
3095
|
-
* Lightweight option bag for `port.scans.persist`. Mirrors the trailing
|
|
3096
|
-
* arguments of the legacy `persistScanResult(db, result, renameOps,
|
|
3097
|
-
* extractorRuns, enrichments)` free function so the adapter
|
|
3098
|
-
* implementation is a one-line delegation today; the named-bag shape
|
|
3099
|
-
* tomorrow lets new optional inputs land without breaking callers.
|
|
3100
|
-
*/
|
|
3101
|
-
interface IPersistOptions {
|
|
3102
|
-
renameOps?: RenameOp[];
|
|
3103
|
-
extractorRuns?: IExtractorRunRecord[];
|
|
3104
|
-
enrichments?: IEnrichmentRecord[];
|
|
3105
|
-
contributions?: IContributionRecord[];
|
|
2764
|
+
scope?: 'project' | 'global';
|
|
3106
2765
|
/**
|
|
3107
|
-
*
|
|
3108
|
-
*
|
|
3109
|
-
*
|
|
3110
|
-
*
|
|
3111
|
-
* belonging to plugins / extensions that are no longer in the
|
|
3112
|
-
* catalog (uninstalled plugins, disabled bundles, removed
|
|
3113
|
-
* contributions). Empty / absent set = no catalog sweep (legacy
|
|
3114
|
-
* behaviour, leaves disabled-plugin rows stale per design F24
|
|
3115
|
-
* pre-fix).
|
|
2766
|
+
* Compute per-node token counts (frontmatter / body / total) using the
|
|
2767
|
+
* cl100k_base BPE (the modern OpenAI tokenizer used by GPT-4 / GPT-3.5).
|
|
2768
|
+
* Defaults to true. Set false to skip tokenization; `node.tokens` is
|
|
2769
|
+
* left undefined (spec-valid: the field is optional).
|
|
3116
2770
|
*/
|
|
3117
|
-
|
|
2771
|
+
tokenize?: boolean;
|
|
3118
2772
|
/**
|
|
3119
|
-
*
|
|
3120
|
-
* node)` tuples where the extension actually RAN against that node
|
|
3121
|
-
* in this scan. Format: `<pluginId>/<extensionId>/<nodePath>` (no
|
|
3122
|
-
* contribution-id segment, the sweep operates at the (plugin,
|
|
3123
|
-
* extension, node) level and inspects the buffer to decide which
|
|
3124
|
-
* contribution-ids survive).
|
|
2773
|
+
* Prior snapshot for two purposes (decoupled by design):
|
|
3125
2774
|
*
|
|
3126
|
-
*
|
|
3127
|
-
*
|
|
3128
|
-
*
|
|
3129
|
-
*
|
|
3130
|
-
*
|
|
3131
|
-
*
|
|
2775
|
+
* 1. **Rename heuristic** (`spec/db-schema.md` §Rename detection):
|
|
2776
|
+
* always evaluated when `priorSnapshot` is supplied. The
|
|
2777
|
+
* heuristic compares prior vs current node paths and emits
|
|
2778
|
+
* high / medium / ambiguous / orphan classifications. This
|
|
2779
|
+
* runs on EVERY `sm scan` (with or without `--changed`) so
|
|
2780
|
+
* reorganising files always preserves history, never silently.
|
|
3132
2781
|
*
|
|
3133
|
-
*
|
|
3134
|
-
*
|
|
3135
|
-
* `
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
3139
|
-
*
|
|
3140
|
-
*
|
|
2782
|
+
* 2. **Cache reuse** (`sm scan --changed`): only kicks in when
|
|
2783
|
+
* `enableCache: true` is also passed. With the flag set, nodes
|
|
2784
|
+
* whose `path` exists in the prior with both `bodyHash` and
|
|
2785
|
+
* `frontmatterHash` matching the freshly-computed hashes are
|
|
2786
|
+
* reused as-is (their internal links and `externalRefsCount`
|
|
2787
|
+
* survive); only new / modified nodes run through extractors.
|
|
2788
|
+
* Rules always re-run over the merged graph.
|
|
2789
|
+
*
|
|
2790
|
+
* Pass `null` (or omit) for a fresh scan with no rename detection.
|
|
3141
2791
|
*/
|
|
3142
|
-
|
|
3143
|
-
}
|
|
3144
|
-
/**
|
|
3145
|
-
* Issue row as the storage layer sees it, paired with its DB-assigned
|
|
3146
|
-
* id so `port.issues.deleteById(id)` can target it inside a
|
|
3147
|
-
* transaction. The runtime `Issue` shape (per `issue.schema.json`) does
|
|
3148
|
-
* not carry `id` because the spec models issues as ephemeral findings
|
|
3149
|
-
* scoped to a scan; the DB does need the synthetic id to update / delete
|
|
3150
|
-
* a single row.
|
|
3151
|
-
*/
|
|
3152
|
-
interface IIssueRow {
|
|
3153
|
-
id: number;
|
|
3154
|
-
issue: Issue;
|
|
3155
|
-
}
|
|
3156
|
-
/**
|
|
3157
|
-
* Filter + pagination shape for `port.issues.list(...)`, driven by the
|
|
3158
|
-
* BFF's `/api/issues` route. Every field is optional, an empty filter
|
|
3159
|
-
* returns every issue ordered by `id` ASC (insertion order, stable
|
|
3160
|
-
* across pages so `offset` / `limit` paging is deterministic).
|
|
3161
|
-
*
|
|
3162
|
-
* The three semantic filters mirror `/api/issues`'s query params:
|
|
3163
|
-
*
|
|
3164
|
-
* - `severities`, narrowed list of `Severity` values. Empty / absent
|
|
3165
|
-
* matches every severity.
|
|
3166
|
-
* - `analyzerIds`, accepts qualified (`<plugin>/<id>`) AND short
|
|
3167
|
-
* (`<id>`) forms; the suffix-match semantics live in
|
|
3168
|
-
* `matchesAnalyzerFilter`. Each entry generates two SQL clauses
|
|
3169
|
-
* (`= ?` and `LIKE '%/' || ?`) ORed together so the filter remains
|
|
3170
|
-
* a single SQL pass with parameterised values, no string
|
|
3171
|
-
* interpolation. Empty / absent matches every analyzer id.
|
|
3172
|
-
* - `nodePath`, keeps issues whose `nodeIds` JSON array contains the
|
|
3173
|
-
* given path (correlated EXISTS over `json_each`). Absent / null
|
|
3174
|
-
* skips the filter.
|
|
3175
|
-
*
|
|
3176
|
-
* Pagination is mandatory; the route layer fills the defaults via
|
|
3177
|
-
* `parsePagination`. `total` in `IIssueListResult` reports the total
|
|
3178
|
-
* MATCHING the filters (not just the page slice) so the SPA can
|
|
3179
|
-
* surface a correct page-count without a second round-trip.
|
|
3180
|
-
*/
|
|
3181
|
-
interface IIssueListFilter {
|
|
2792
|
+
priorSnapshot?: ScanResult | null;
|
|
3182
2793
|
/**
|
|
3183
|
-
*
|
|
3184
|
-
*
|
|
3185
|
-
*
|
|
3186
|
-
*
|
|
3187
|
-
*
|
|
2794
|
+
* Reuse unchanged nodes from `priorSnapshot` instead of re-running
|
|
2795
|
+
* extractors over them. Defaults to `false` so a plain `sm scan`
|
|
2796
|
+
* always re-walks deterministically. `sm scan --changed` flips this
|
|
2797
|
+
* to `true` for the perf win on unchanged files.
|
|
2798
|
+
*
|
|
2799
|
+
* Has no effect without `priorSnapshot`; setting it to `true` with
|
|
2800
|
+
* a null prior is a no-op (every file is "new").
|
|
3188
2801
|
*/
|
|
3189
|
-
|
|
3190
|
-
analyzerIds?: readonly string[];
|
|
3191
|
-
nodePath?: string | null;
|
|
3192
|
-
offset: number;
|
|
3193
|
-
limit: number;
|
|
3194
|
-
}
|
|
3195
|
-
/**
|
|
3196
|
-
* Output of `port.issues.list(...)`. `items` is the page slice (length
|
|
3197
|
-
* ≤ `filter.limit`); `total` is the count of rows matching the filters
|
|
3198
|
-
* before pagination was applied.
|
|
3199
|
-
*/
|
|
3200
|
-
interface IIssueListResult {
|
|
3201
|
-
items: Issue[];
|
|
3202
|
-
total: number;
|
|
3203
|
-
}
|
|
3204
|
-
/** Output of `port.jobs.pruneTerminal` / `listTerminalCandidates`. */
|
|
3205
|
-
interface IPruneResult {
|
|
3206
|
-
/** How many `state_jobs` rows were deleted (or would be, in dry-run). */
|
|
3207
|
-
deletedCount: number;
|
|
3208
|
-
/** Job-file paths from the affected rows; the CLI unlinks these from disk. `null` `filePath` rows contribute nothing here. */
|
|
3209
|
-
filePaths: string[];
|
|
3210
|
-
}
|
|
3211
|
-
/** Filter shape for `port.history.list`. All fields optional. */
|
|
3212
|
-
interface IListExecutionsFilter {
|
|
3213
|
-
/** Restrict to executions whose `nodeIds` array contains this path. */
|
|
3214
|
-
nodePath?: string;
|
|
3215
|
-
/** Exact match on `extension_id`. */
|
|
3216
|
-
actionId?: string;
|
|
3217
|
-
/** Subset of {`completed`,`failed`,`cancelled`}. */
|
|
3218
|
-
statuses?: ExecutionStatus[];
|
|
3219
|
-
/** Lower bound (inclusive) on `started_at`. Unix ms. */
|
|
3220
|
-
sinceMs?: number;
|
|
3221
|
-
/** Upper bound (exclusive) on `started_at`. Unix ms. */
|
|
3222
|
-
untilMs?: number;
|
|
3223
|
-
/** Cap result count. No default. */
|
|
3224
|
-
limit?: number;
|
|
3225
|
-
}
|
|
3226
|
-
/** Window shape for `port.history.aggregateStats`. */
|
|
3227
|
-
interface IHistoryStatsRange {
|
|
3228
|
-
/** Inclusive lower bound. `null` = all-time. */
|
|
3229
|
-
sinceMs: number | null;
|
|
3230
|
-
/** Exclusive upper bound. */
|
|
3231
|
-
untilMs: number;
|
|
3232
|
-
}
|
|
3233
|
-
/** Period bucket granularity for `port.history.aggregateStats`. */
|
|
3234
|
-
type THistoryStatsPeriod = 'day' | 'week' | 'month';
|
|
3235
|
-
/**
|
|
3236
|
-
* Output of `port.transaction(tx => tx.history.migrateNodeFks(from, to))`.
|
|
3237
|
-
* Lists how many rows in each `state_*` table were repointed plus any
|
|
3238
|
-
* composite-PK collisions that forced a drop instead of an update.
|
|
3239
|
-
*/
|
|
3240
|
-
interface IMigrateNodeFksReport {
|
|
3241
|
-
jobs: number;
|
|
3242
|
-
executions: number;
|
|
3243
|
-
summaries: number;
|
|
3244
|
-
enrichments: number;
|
|
3245
|
-
pluginKvs: number;
|
|
3246
|
-
nodeFavorites: number;
|
|
2802
|
+
enableCache?: boolean;
|
|
3247
2803
|
/**
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
*
|
|
3251
|
-
*
|
|
3252
|
-
*
|
|
3253
|
-
* for diagnostic output. `state_node_favorites` has no composite key
|
|
3254
|
-
* so its `keys` is the empty object.
|
|
2804
|
+
* Filter that decides which paths the Providers skip. Composed by the
|
|
2805
|
+
* caller (typically the CLI) from bundled defaults + `config.ignore`
|
|
2806
|
+
* + `.skillmapignore`. Providers that omit this option fall back to
|
|
2807
|
+
* their own defensive defaults (just enough to keep `.git` /
|
|
2808
|
+
* `node_modules` out).
|
|
3255
2809
|
*/
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
2810
|
+
ignoreFilter?: IIgnoreFilter;
|
|
2811
|
+
/**
|
|
2812
|
+
* Promote frontmatter-validation findings from `warn` to `error`.
|
|
2813
|
+
* Defaults to false. The CLI surfaces this via `--strict` on `sm scan`
|
|
2814
|
+
* and the `scan.strict` config key. When false, the orchestrator
|
|
2815
|
+
* still emits a `frontmatter-invalid` issue per malformed file but
|
|
2816
|
+
* leaves the severity at `warn` so a clean scan exits 0; when true,
|
|
2817
|
+
* the same finding becomes `error` and the scan exits 1.
|
|
2818
|
+
*/
|
|
2819
|
+
strict?: boolean;
|
|
2820
|
+
/**
|
|
2821
|
+
* Spec § A.9, fine-grained Extractor cache breadcrumbs from the
|
|
2822
|
+
* prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
|
|
2823
|
+
* Loaded from the `scan_extractor_runs` table by the CLI before
|
|
2824
|
+
* invoking `runScan`; absent / empty for a fresh DB or an out-of-band
|
|
2825
|
+
* caller that does not maintain a cache. Decoupled from `priorSnapshot`
|
|
2826
|
+
* because the runs live in a sibling table and are useful only when
|
|
2827
|
+
* `enableCache` is also set.
|
|
2828
|
+
*
|
|
2829
|
+
* Cache decision per `(node, extractor)`:
|
|
2830
|
+
* - body+frontmatter hashes match the prior node AND every currently-
|
|
2831
|
+
* registered extractor that applies to this kind has a matching
|
|
2832
|
+
* row → full skip, all prior outbound links reused.
|
|
2833
|
+
* - some applicable extractor lacks a matching row (newly registered,
|
|
2834
|
+
* or its prior run targeted a different body hash or sidecar
|
|
2835
|
+
* annotations hash) → run only the missing extractors, drop prior
|
|
2836
|
+
* links whose `sources` map to any missing extractor or to an
|
|
2837
|
+
* extractor that is no longer registered.
|
|
2838
|
+
*/
|
|
2839
|
+
priorExtractorRuns?: Map<string, Map<string, IPriorExtractorRun>>;
|
|
2840
|
+
/**
|
|
2841
|
+
* Spec § A.12, per-plugin storage wrappers exposed to extractors via
|
|
2842
|
+
* `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves
|
|
2843
|
+
* `ctx.store` undefined for that extractor (the existing contract).
|
|
2844
|
+
*
|
|
2845
|
+
* The kernel does not construct these, the driving adapter (CLI,
|
|
2846
|
+
* future server) builds them with `makePluginStore` from
|
|
2847
|
+
* `kernel/adapters/plugin-store.js` and threads them through. This
|
|
2848
|
+
* keeps the orchestrator persistence-agnostic (the wrapper supplies
|
|
2849
|
+
* its own persist callback) and lets tests inject a captured-call
|
|
2850
|
+
* mock without spinning up a DB.
|
|
2851
|
+
*/
|
|
2852
|
+
pluginStores?: ReadonlyMap<string, IPluginStore>;
|
|
2853
|
+
/**
|
|
2854
|
+
* Pre-computed absolute paths of orphan job MD files (files under
|
|
2855
|
+
* `.skill-map/jobs/` whose absolute path appears nowhere in
|
|
2856
|
+
* `state_jobs.filePath`). Threaded into the rule pass so the
|
|
2857
|
+
* built-in `core/job-orphan-file` rule can project each as a `warn`
|
|
2858
|
+
* issue without the kernel reaching for the storage port or doing
|
|
2859
|
+
* its own FS walk. The driving adapter (CLI, BFF) computes this
|
|
2860
|
+
* inside its already-open storage transaction via
|
|
2861
|
+
* `findOrphanJobFiles(jobsDir, await port.jobs.listReferencedFilePaths())`
|
|
2862
|
+
* mirrors the `orphanSidecars` model where detection lives
|
|
2863
|
+
* outside the rule and the rule only projects. Absent / empty when
|
|
2864
|
+
* the caller has no jobs context (out-of-band tests, fresh DB,
|
|
2865
|
+
* `--no-built-ins`).
|
|
2866
|
+
*/
|
|
2867
|
+
orphanJobFiles?: readonly string[];
|
|
2868
|
+
/**
|
|
2869
|
+
* Side set of absolute file paths the operator opted into for
|
|
2870
|
+
* link-validation purposes via `scan.referencePaths`. Threaded
|
|
2871
|
+
* through to `IAnalyzerContext.referenceablePaths` so the built-in
|
|
2872
|
+
* `core/broken-ref` rule can suppress its `warn` for path-style
|
|
2873
|
+
* links whose target lands in the set. Files are NOT walked by
|
|
2874
|
+
* the kernel, the driving adapter populates the set before
|
|
2875
|
+
* calling `runScan`. Absent / empty when the operator left
|
|
2876
|
+
* `scan.referencePaths` unconfigured.
|
|
2877
|
+
*/
|
|
2878
|
+
referenceablePaths?: ReadonlySet<string>;
|
|
2879
|
+
/**
|
|
2880
|
+
* Absolute path of the scan's cwd / project root. Threaded onto
|
|
2881
|
+
* `IAnalyzerContext.cwd` so rules that need to resolve a relative
|
|
2882
|
+
* `link.target` to an absolute filesystem path can do so without
|
|
2883
|
+
* heuristics. Absent for callers that don't track a cwd
|
|
2884
|
+
* concept (out-of-band tests, embedders).
|
|
2885
|
+
*/
|
|
2886
|
+
cwd?: string;
|
|
3262
2887
|
}
|
|
3263
|
-
/**
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
2888
|
+
/**
|
|
2889
|
+
* Same as `runScan` but also returns the rename heuristic's `RenameOp[]`
|
|
2890
|
+
* the high- and medium-confidence renames the persistence layer must
|
|
2891
|
+
* apply to `state_*` rows inside the same tx as the scan zone replace-
|
|
2892
|
+
* all (per `spec/db-schema.md` §Rename detection). Most callers want
|
|
2893
|
+
* `runScan` (which returns just `ScanResult`); the CLI's `sm scan`
|
|
2894
|
+
* uses this variant so it can hand the ops off to `persistScanResult`.
|
|
2895
|
+
*
|
|
2896
|
+
* Also returns `extractorRuns`, the Spec § A.9 fine-grained cache
|
|
2897
|
+
* breadcrumbs the CLI persists into `scan_extractor_runs` so the next
|
|
2898
|
+
* incremental scan can decide per-(node, extractor) whether re-running
|
|
2899
|
+
* is required.
|
|
2900
|
+
*/
|
|
2901
|
+
declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): Promise<{
|
|
2902
|
+
result: ScanResult;
|
|
2903
|
+
renameOps: RenameOp[];
|
|
2904
|
+
extractorRuns: IExtractorRunRecord[];
|
|
2905
|
+
enrichments: IEnrichmentRecord[];
|
|
2906
|
+
contributions: IContributionRecord[];
|
|
2907
|
+
freshlyRunTuples: ReadonlySet<string>;
|
|
2908
|
+
}>;
|
|
2909
|
+
declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
|
|
2910
|
+
|
|
2911
|
+
/**
|
|
2912
|
+
* Node-construction helpers: hash a body, canonicalise frontmatter /
|
|
2913
|
+
* sidecar annotations, resolve the sidecar overlay for a given relative
|
|
2914
|
+
* path, and produce a fresh `Node` (validating its frontmatter on the
|
|
2915
|
+
* way out). Also hosts `mergeNodeWithEnrichments` + `IPersistedEnrichment`
|
|
2916
|
+
* the read-time merge of author frontmatter with the A.8 enrichment
|
|
2917
|
+
* layer.
|
|
2918
|
+
*/
|
|
2919
|
+
|
|
2920
|
+
/**
|
|
2921
|
+
* Spec § A.8, produce the merged read-time view of a Node.
|
|
2922
|
+
*
|
|
2923
|
+
* Rules / `sm check` / `sm export` consume `node.frontmatter` directly
|
|
2924
|
+
* (deterministic CI-safe baseline, author intent, byte-stable). UI / future
|
|
2925
|
+
* rules that opt into enrichment context call this helper to merge the
|
|
2926
|
+
* author frontmatter with the live enrichment layer.
|
|
2927
|
+
*
|
|
2928
|
+
* Algorithm:
|
|
2929
|
+
*
|
|
2930
|
+
* 1. Filter `enrichments` down to rows targeting this node AND not
|
|
2931
|
+
* flagged `stale`. With Extractors deterministic-only no row is
|
|
2932
|
+
* stale-flagged in this revision; the filter is preserved for the
|
|
2933
|
+
* future Action-issued enrichment revision (queued LLM jobs whose
|
|
2934
|
+
* output must survive body changes), where stale visibility
|
|
2935
|
+
* belongs to the UI layer next to the value.
|
|
2936
|
+
* 2. Sort the survivors by `enrichedAt` ASC so iteration order is
|
|
2937
|
+
* "oldest first". This makes the spread merge below
|
|
2938
|
+
* last-write-wins per field, the freshest Extractor's value
|
|
2939
|
+
* pisar the older one for any conflicting key.
|
|
2940
|
+
* 3. Spread-merge each row's `value` over `node.frontmatter`. The
|
|
2941
|
+
* author's keys are the base; enrichment keys overlay them.
|
|
2942
|
+
*
|
|
2943
|
+
* The returned object is a fresh shallow copy, mutating it does not
|
|
2944
|
+
* touch the caller's node. The original `node.frontmatter` reference
|
|
2945
|
+
* remains accessible via `node.frontmatter` for callers that want the
|
|
2946
|
+
* pristine author baseline.
|
|
2947
|
+
*
|
|
2948
|
+
* @param node Node to merge against; `node.frontmatter` is the base.
|
|
2949
|
+
* @param enrichments Per-(node, extractor) enrichment records, typically
|
|
2950
|
+
* loaded via `loadNodeEnrichments(db, node.path)` or
|
|
2951
|
+
* pre-filtered to this node by the caller.
|
|
2952
|
+
* @param opts.includeStale When true, include rows flagged stale. Defaults
|
|
2953
|
+
* to false (the safe, CI-deterministic default).
|
|
2954
|
+
* UIs that want to display "stale (last value: …)"
|
|
2955
|
+
* pass `true` and consult `enrichment.stale`
|
|
2956
|
+
* on the source rows.
|
|
2957
|
+
*/
|
|
2958
|
+
declare function mergeNodeWithEnrichments(node: Node, enrichments: IPersistedEnrichment[], opts?: {
|
|
2959
|
+
includeStale?: boolean;
|
|
2960
|
+
}): Record<string, unknown>;
|
|
2961
|
+
/**
|
|
2962
|
+
* A persisted enrichment row, post-load. Mirrors the DB row shape
|
|
2963
|
+
* but with `value` already deserialised from JSON and `stale` /
|
|
2964
|
+
* `isProbabilistic` already decoded from `0 | 1`. Surfaced via
|
|
2965
|
+
* `loadNodeEnrichments` (driven adapter) and consumed by
|
|
2966
|
+
* `mergeNodeWithEnrichments` and the `sm refresh` command.
|
|
2967
|
+
*/
|
|
2968
|
+
interface IPersistedEnrichment {
|
|
2969
|
+
nodePath: string;
|
|
2970
|
+
extractorId: string;
|
|
2971
|
+
bodyHashAtEnrichment: string;
|
|
2972
|
+
value: Partial<Node>;
|
|
2973
|
+
stale: boolean;
|
|
2974
|
+
enrichedAt: number;
|
|
2975
|
+
isProbabilistic: boolean;
|
|
3269
2976
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
2977
|
+
|
|
2978
|
+
/**
|
|
2979
|
+
* In-memory `ProgressEmitterPort` adapter. No network, no DB, just a
|
|
2980
|
+
* synchronous fan-out to registered listeners. Used by the default scan
|
|
2981
|
+
* orchestrator; the WebSocket-backed emitter that streams to
|
|
2982
|
+
* the Web UI lands.
|
|
2983
|
+
*/
|
|
2984
|
+
|
|
2985
|
+
declare class InMemoryProgressEmitter implements ProgressEmitterPort {
|
|
2986
|
+
#private;
|
|
2987
|
+
emit(event: ProgressEvent): void;
|
|
2988
|
+
subscribe(listener: TProgressListener): () => void;
|
|
3275
2989
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
2990
|
+
|
|
2991
|
+
/**
|
|
2992
|
+
* File watcher for `sm watch` / `sm scan --watch`.
|
|
2993
|
+
*
|
|
2994
|
+
* Wraps `chokidar` behind a small `IFsWatcher` interface so:
|
|
2995
|
+
*
|
|
2996
|
+
* 1. The CLI command is impl-agnostic, swapping chokidar for a
|
|
2997
|
+
* different watcher later (Java? Rust port? a future `WatchPort`?)
|
|
2998
|
+
* doesn't ripple into the command.
|
|
2999
|
+
* 2. Debouncing, batching, and ignore-filter integration live in one
|
|
3000
|
+
* place. The CLI just gets `onBatch(paths)` callbacks and decides
|
|
3001
|
+
* whether to re-scan.
|
|
3002
|
+
*
|
|
3003
|
+
* The watcher does NOT call into the orchestrator itself. That decision
|
|
3004
|
+
* is deliberate: the CLI owns the scan-and-persist pipeline (`runScan`,
|
|
3005
|
+
* `persistScanResult`, optional rebuild of the ignore filter when
|
|
3006
|
+
* `.skillmapignore` itself changes). Pulling that into the watcher
|
|
3007
|
+
* would couple the kernel module to `SqliteStorageAdapter`, which the
|
|
3008
|
+
* Server wouldn't want. Keep this module side-effect free
|
|
3009
|
+
* apart from filesystem subscription.
|
|
3010
|
+
*
|
|
3011
|
+
* Ignore filter integration: the supplied `IIgnoreFilter` is consulted
|
|
3012
|
+
* via chokidar's `ignored` predicate, which receives an absolute path.
|
|
3013
|
+
* We re-derive the path RELATIVE to the closest matching root before
|
|
3014
|
+
* passing it through `IIgnoreFilter.ignores`. This mirrors what the
|
|
3015
|
+
* scan walker does (`extensions/providers/claude/index.ts`) so both code
|
|
3016
|
+
* paths agree on what "ignored" means.
|
|
3017
|
+
*/
|
|
3018
|
+
|
|
3019
|
+
type TWatchEventKind = 'add' | 'change' | 'unlink';
|
|
3020
|
+
interface IWatchEvent {
|
|
3021
|
+
kind: TWatchEventKind;
|
|
3022
|
+
/** Absolute path. */
|
|
3023
|
+
absolutePath: string;
|
|
3283
3024
|
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3025
|
+
interface IWatchBatch {
|
|
3026
|
+
/** Events that arrived inside the debounce window, in arrival order. */
|
|
3027
|
+
events: IWatchEvent[];
|
|
3028
|
+
/** Convenience: deduplicated absolute paths across the batch. */
|
|
3029
|
+
paths: string[];
|
|
3030
|
+
}
|
|
3031
|
+
interface IFsWatcher {
|
|
3032
|
+
/** Resolves once chokidar has finished its initial directory scan and is ready to emit. */
|
|
3033
|
+
ready: Promise<void>;
|
|
3034
|
+
/** Tear down the watcher. Resolves after chokidar releases handles. */
|
|
3035
|
+
close: () => Promise<void>;
|
|
3036
|
+
}
|
|
3037
|
+
interface ICreateFsWatcherOptions {
|
|
3038
|
+
/** Roots to watch. Resolved relative to `cwd` if relative paths are passed. */
|
|
3039
|
+
roots: string[];
|
|
3040
|
+
/** Working directory used to resolve relative roots and the ignore-filter root. */
|
|
3041
|
+
cwd: string;
|
|
3042
|
+
/** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */
|
|
3043
|
+
debounceMs: number;
|
|
3044
|
+
/**
|
|
3045
|
+
* Optional ignore filter, same instance the scan walker uses.
|
|
3046
|
+
*
|
|
3047
|
+
* Two shapes are accepted:
|
|
3048
|
+
*
|
|
3049
|
+
* - **`IIgnoreFilter`** (the static one), captured by reference at
|
|
3050
|
+
* construction. Use this when the filter never changes for the
|
|
3051
|
+
* lifetime of the watcher (the typical CLI `sm watch` flow).
|
|
3052
|
+
*
|
|
3053
|
+
* - **`() => IIgnoreFilter | undefined`** (a getter), re-evaluated
|
|
3054
|
+
* on EVERY chokidar `ignored` predicate call. Use this when the
|
|
3055
|
+
* filter can change at runtime, e.g. the BFF rebuilds it after
|
|
3056
|
+
* a `.skillmapignore` or `.skill-map/settings.json` edit and
|
|
3057
|
+
* wants chokidar to immediately respect the new patterns without
|
|
3058
|
+
* tearing down and rebuilding the watcher. A getter that returns
|
|
3059
|
+
* `undefined` disables ignore filtering for that call.
|
|
3060
|
+
*/
|
|
3061
|
+
ignoreFilter?: IIgnoreFilter | (() => IIgnoreFilter | undefined) | undefined;
|
|
3062
|
+
/**
|
|
3063
|
+
* Maximum directory traversal depth. `undefined` (default) walks the
|
|
3064
|
+
* tree recursively without bound; `0` limits the watch to the
|
|
3065
|
+
* literal `roots` entries (no descent), which is the right setting
|
|
3066
|
+
* when watching a directory only to catch changes to specific
|
|
3067
|
+
* top-level files (see `subscribeMeta` in `core/watcher/runtime.ts`).
|
|
3068
|
+
* Forwarded verbatim to chokidar's `depth` option.
|
|
3069
|
+
*/
|
|
3070
|
+
depth?: number;
|
|
3071
|
+
/** Called once per debounced batch. Awaited; concurrent batches are serialised. */
|
|
3072
|
+
onBatch: (batch: IWatchBatch) => void | Promise<void>;
|
|
3073
|
+
/**
|
|
3074
|
+
* Called when the underlying watcher surfaces an error. The watcher
|
|
3075
|
+
* stays open, callers decide whether to log, keep going, or close.
|
|
3076
|
+
*/
|
|
3077
|
+
onError?: (err: Error) => void;
|
|
3078
|
+
}
|
|
3079
|
+
/**
|
|
3080
|
+
* Construct a chokidar-backed watcher. Subscribes immediately; the
|
|
3081
|
+
* returned `ready` promise resolves once chokidar's initial directory
|
|
3082
|
+
* walk completes, at which point only NEW events fire `onBatch`.
|
|
3083
|
+
*
|
|
3084
|
+
* The initial directory walk is deliberately silent, we set
|
|
3085
|
+
* `ignoreInitial: true`. The CLI runs a one-shot scan before flipping
|
|
3086
|
+
* the watcher on, so re-emitting an `add` for every existing file
|
|
3087
|
+
* would be redundant churn.
|
|
3088
|
+
*/
|
|
3089
|
+
declare function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatcher;
|
|
3090
|
+
|
|
3091
|
+
/**
|
|
3092
|
+
* Scan delta, pure comparison of two `ScanResult` snapshots. Drives
|
|
3093
|
+
* `sm scan --compare-with <path>` and is the single place the kernel
|
|
3094
|
+
* knows how to identify "the same" entity across two scans.
|
|
3095
|
+
*
|
|
3096
|
+
* **Identity contract** (mirrors decisions made at earlier sub-steps):
|
|
3097
|
+
*
|
|
3098
|
+
* - **Node**: `node.path`. The path is the only field stable across
|
|
3099
|
+
* edits, every other Node field is content-derived (hashes, counts,
|
|
3100
|
+
* denormalised frontmatter). Two nodes with the same path are the
|
|
3101
|
+
* "same" node; differences are reported as a `changed` entry with
|
|
3102
|
+
* a reason narrowing what diverged.
|
|
3103
|
+
*
|
|
3104
|
+
* - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This
|
|
3105
|
+
* mirrors the link-conflict rule and `sm show` aggregation,
|
|
3106
|
+
* two links with identical endpoints, kind, and (optional) trigger
|
|
3107
|
+
* are the same link, even if emitted by different extractors. The
|
|
3108
|
+
* `sources[]` union and confidence are NOT part of identity; they
|
|
3109
|
+
* are presentation facets that can churn without making the link
|
|
3110
|
+
* "different" for delta purposes.
|
|
3111
|
+
*
|
|
3112
|
+
* - **Issue**: `(analyzerId, sorted nodeIds, message)`. Mirrors
|
|
3113
|
+
* `spec/job-events.md` §issue.*, same key → same issue, even when
|
|
3114
|
+
* `data` / `severity` / `linkIndices` shift. A meaningful change in
|
|
3115
|
+
* `message` (or a different set of node ids) is a different issue.
|
|
3116
|
+
* This is the same key future job events will use; keep it aligned
|
|
3117
|
+
* so consumers can reuse logic.
|
|
3118
|
+
*
|
|
3119
|
+
* No "changed" bucket for links / issues, identity already captures
|
|
3120
|
+
* everything that matters there. Nodes get a "changed" bucket because
|
|
3121
|
+
* the path stays stable while the body / frontmatter rewrite, and that
|
|
3122
|
+
* change is meaningful (formatters, summarisers, downstream consumers
|
|
3123
|
+
* all care about it).
|
|
3124
|
+
*
|
|
3125
|
+
* Pure: no IO, no DB, no FS. Safe to run in-memory inside `sm scan`
|
|
3126
|
+
* without polluting the persisted snapshot.
|
|
3127
|
+
*/
|
|
3128
|
+
|
|
3129
|
+
type TNodeChangeReason = 'body' | 'frontmatter' | 'both';
|
|
3130
|
+
interface INodeChange {
|
|
3131
|
+
before: Node;
|
|
3132
|
+
after: Node;
|
|
3133
|
+
/**
|
|
3134
|
+
* Which hash diverged. `'body'` means body rewritten, frontmatter
|
|
3135
|
+
* untouched; `'frontmatter'` means metadata rewritten, body
|
|
3136
|
+
* untouched; `'both'` means both rewritten in the same edit.
|
|
3137
|
+
*/
|
|
3138
|
+
reason: TNodeChangeReason;
|
|
3288
3139
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3140
|
+
interface IScanDelta {
|
|
3141
|
+
/** Path the current scan was compared against (echoed for the report header). */
|
|
3142
|
+
comparedWith: string;
|
|
3143
|
+
nodes: {
|
|
3144
|
+
added: Node[];
|
|
3145
|
+
removed: Node[];
|
|
3146
|
+
changed: INodeChange[];
|
|
3147
|
+
};
|
|
3148
|
+
links: {
|
|
3149
|
+
added: Link[];
|
|
3150
|
+
removed: Link[];
|
|
3151
|
+
};
|
|
3152
|
+
issues: {
|
|
3153
|
+
added: Issue[];
|
|
3154
|
+
removed: Issue[];
|
|
3155
|
+
};
|
|
3294
3156
|
}
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3157
|
+
declare function computeScanDelta(prior: ScanResult, current: ScanResult, comparedWith: string): IScanDelta;
|
|
3158
|
+
/**
|
|
3159
|
+
* `true` iff every bucket is empty. Callers use this to decide the
|
|
3160
|
+
* exit code (`0` clean, `1` non-empty delta).
|
|
3161
|
+
*/
|
|
3162
|
+
declare function isEmptyDelta(delta: IScanDelta): boolean;
|
|
3163
|
+
|
|
3164
|
+
/**
|
|
3165
|
+
* Export query, minimal filter language for `sm export <query>` (Step 8.3).
|
|
3166
|
+
*
|
|
3167
|
+
* Spec contract: `spec/cli-contract.md` line 190 says "Query syntax is
|
|
3168
|
+
* implementation-defined pre-1.0". This module defines the v0.5.0 syntax.
|
|
3169
|
+
*
|
|
3170
|
+
* **Grammar** (BNF-ish, intentionally tiny):
|
|
3171
|
+
*
|
|
3172
|
+
* query := token (WS+ token)*
|
|
3173
|
+
* token := key "=" value-list
|
|
3174
|
+
* key := "kind" | "has" | "path"
|
|
3175
|
+
* value-list := value ("," value)*
|
|
3176
|
+
* value := non-comma, non-whitespace string
|
|
3177
|
+
*
|
|
3178
|
+
* Tokens AND together; values within one token OR. An empty / whitespace-only
|
|
3179
|
+
* query is valid and matches every node ("export everything").
|
|
3180
|
+
*
|
|
3181
|
+
* **Filters**:
|
|
3182
|
+
*
|
|
3183
|
+
* - `kind=skill` / `kind=skill,agent`, node kind whitelist.
|
|
3184
|
+
* - `has=issues`, node must appear in some issue's `nodeIds`. (Future
|
|
3185
|
+
* expansion: `has=findings` / `has=summary` once Step 10 / 11 land.
|
|
3186
|
+
* Unknown values are a parse error today; we'll ratchet up the
|
|
3187
|
+
* accepted set additively.)
|
|
3188
|
+
* - `path=foo/*` / `path=.claude/agents/**`, POSIX glob over `node.path`.
|
|
3189
|
+
* Supports `*` (any chars except `/`) and `**` (any chars including `/`).
|
|
3190
|
+
*
|
|
3191
|
+
* **Subset semantics** (`applyExportQuery`):
|
|
3192
|
+
*
|
|
3193
|
+
* - Nodes pass when every specified filter matches (AND across keys,
|
|
3194
|
+
* OR within values).
|
|
3195
|
+
* - Links survive only when BOTH endpoints (`source` + `target`) belong
|
|
3196
|
+
* to the filtered node set. A subset that includes "edges out to
|
|
3197
|
+
* unfiltered nodes" would be confusing, the user asked for a focused
|
|
3198
|
+
* subgraph, not its boundary. External-URL pseudo-links are already
|
|
3199
|
+
* stripped by the orchestrator and never reach this layer.
|
|
3200
|
+
* - Issues survive when ANY of the issue's `nodeIds` is in the filtered
|
|
3201
|
+
* set. Issues span multiple nodes (e.g. `trigger-collision` over two
|
|
3202
|
+
* advertisers); dropping an issue when one of its nodes is outside
|
|
3203
|
+
* would hide cross-cutting problems the user is investigating.
|
|
3204
|
+
*
|
|
3205
|
+
* Pure: no IO, no DB, no FS.
|
|
3206
|
+
*/
|
|
3207
|
+
|
|
3208
|
+
interface IExportQuery {
|
|
3209
|
+
/** Original query string echoed back so consumers can render the header. */
|
|
3210
|
+
raw: string;
|
|
3211
|
+
/**
|
|
3212
|
+
* Whitelist of node kinds (`node.kind` is open string, built-in
|
|
3213
|
+
* Claude catalog `skill` / `agent` / `command` / `hook` / `note`,
|
|
3214
|
+
* plus whatever external Providers declare). The query parser does
|
|
3215
|
+
* not validate values against a closed enum; an unknown kind simply
|
|
3216
|
+
* yields zero matches at filter time.
|
|
3217
|
+
*/
|
|
3218
|
+
kinds?: string[];
|
|
3219
|
+
hasIssues?: boolean;
|
|
3220
|
+
pathGlobs?: string[];
|
|
3299
3221
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3222
|
+
interface IExportSubset {
|
|
3223
|
+
query: IExportQuery;
|
|
3224
|
+
nodes: Node[];
|
|
3225
|
+
links: Link[];
|
|
3226
|
+
issues: Issue[];
|
|
3305
3227
|
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
version: number;
|
|
3309
|
-
description: string;
|
|
3310
|
-
appliedAt: number;
|
|
3228
|
+
declare class ExportQueryError extends Error {
|
|
3229
|
+
constructor(message: string);
|
|
3311
3230
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3231
|
+
declare function parseExportQuery(raw: string): IExportQuery;
|
|
3232
|
+
declare function applyExportQuery(scan: {
|
|
3233
|
+
nodes: Node[];
|
|
3234
|
+
links: Link[];
|
|
3235
|
+
issues: Issue[];
|
|
3236
|
+
}, query: IExportQuery): IExportSubset;
|
|
3237
|
+
|
|
3238
|
+
/**
|
|
3239
|
+
* `scan_node_tags` adapter, tags · dual-source persistence layer.
|
|
3240
|
+
*
|
|
3241
|
+
* One row per `(node_path, tag, source)` triple. Projected at persist
|
|
3242
|
+
* time from BOTH `frontmatter.tags` (with `source='author'`) and
|
|
3243
|
+
* `sidecar.annotations.tags` (with `source='user'`). The same tag
|
|
3244
|
+
* string MAY appear under both sources for the same node, the PK
|
|
3245
|
+
* accepts the pair; search returns the node once via DISTINCT, the
|
|
3246
|
+
* UI renders both chips with their attribution.
|
|
3247
|
+
*
|
|
3248
|
+
* Belongs to the `scan_*` family, replaced wholesale per scan.
|
|
3249
|
+
* Cached nodes' tag rows are projected from the cached
|
|
3250
|
+
* `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both
|
|
3251
|
+
* already in memory at persist time), so the rebuild is cheap
|
|
3252
|
+
* regardless of cache hit / miss. See `spec/db-schema.md`
|
|
3253
|
+
* § scan_node_tags for the normative shape and replace-all semantics.
|
|
3254
|
+
*/
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* In-memory tag record buffered during scan and flushed to
|
|
3258
|
+
* `scan_node_tags` by `persistScanResult`. One entry per
|
|
3259
|
+
* `(node_path, tag, source)` projected from a node's frontmatter tags
|
|
3260
|
+
* (`source: 'author'`) or sidecar annotations tags
|
|
3261
|
+
* (`source: 'user'`).
|
|
3262
|
+
*/
|
|
3263
|
+
interface ITagRecord {
|
|
3264
|
+
nodePath: string;
|
|
3265
|
+
tag: string;
|
|
3266
|
+
source: 'author' | 'user';
|
|
3317
3267
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3268
|
+
|
|
3269
|
+
/**
|
|
3270
|
+
* Pure helpers for the "update available" notification feature.
|
|
3271
|
+
*
|
|
3272
|
+
* Three responsibilities:
|
|
3273
|
+
* - `fetchLatestVersion` , query `https://registry.npmjs.org/<pkg>/latest`
|
|
3274
|
+
* with `AbortController` + timeout. Throws on
|
|
3275
|
+
* non-200 / parse failure / abort.
|
|
3276
|
+
* - `compareVersions` , semver compare (-1 / 0 / 1). Pre-1.0 aware:
|
|
3277
|
+
* treats prereleases via the standard rules
|
|
3278
|
+
* (release > prerelease at the same triple).
|
|
3279
|
+
* - `isOutdated` , sugar over `compareVersions` for the common
|
|
3280
|
+
* "is `latest` strictly greater than `current`"
|
|
3281
|
+
* check the banner runs against.
|
|
3282
|
+
*
|
|
3283
|
+
* Pure kernel module, NO `process.env` reads, NO Node globals beyond the
|
|
3284
|
+
* built-in `fetch` / `AbortController` (Node 22+). Every env / settings
|
|
3285
|
+
* lookup happens in `src/cli/util/update-check-banner.ts`, the CLI-side
|
|
3286
|
+
* adapter that owns side effects.
|
|
3287
|
+
*
|
|
3288
|
+
* The shared cache type (`IUpdateCheckCache`) is used by the storage
|
|
3289
|
+
* helpers under `kernel/storage/update-check.ts` and by the BFF's
|
|
3290
|
+
* `GET /api/update-status` projection. A second type
|
|
3291
|
+
* (`IUpdateStatus`) shapes the BFF response, it merges `current`
|
|
3292
|
+
* (from `VERSION`) into the cache so the UI can render without a
|
|
3293
|
+
* second lookup. Both stay flat, no nested objects, so JSON
|
|
3294
|
+
* serialization is trivial.
|
|
3295
|
+
*/
|
|
3296
|
+
interface IUpdateCheckCache {
|
|
3297
|
+
latestVersion: string;
|
|
3298
|
+
/** Epoch ms, when the registry was last successfully probed. */
|
|
3299
|
+
checkedAt: number;
|
|
3300
|
+
/** Epoch ms, when the banner was last printed; null = never shown yet. */
|
|
3301
|
+
shownAt: number | null;
|
|
3322
3302
|
}
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* `PluginLoaderPort`, discovers plugin directories and loads their
|
|
3306
|
+
* extensions. The shape mirrors what the concrete loader actually
|
|
3307
|
+
* exposes (see `kernel/adapters/plugin-loader.ts`); the port exists so
|
|
3308
|
+
* the CLI consumes the abstract contract via `createPluginLoader(...)`
|
|
3309
|
+
* instead of `new PluginLoader(...)` and so the concrete adapter is
|
|
3310
|
+
* structurally pinned to the port (`implements PluginLoaderPort` makes
|
|
3311
|
+
* any drift a compile error).
|
|
3312
|
+
*
|
|
3313
|
+
* Domain types (`IPluginManifest`, `ILoadedExtension`, `IDiscoveredPlugin`,
|
|
3314
|
+
* `TPluginStorage`, `TPluginLoadStatus`, `TGranularity`) live in
|
|
3315
|
+
* `kernel/types/plugin.ts` because they are spec-mirroring DTOs, not
|
|
3316
|
+
* port-shape types. The port re-exports them for callers that import
|
|
3317
|
+
* from the ports barrel.
|
|
3318
|
+
*/
|
|
3319
|
+
|
|
3320
|
+
interface PluginLoaderPort {
|
|
3321
|
+
/**
|
|
3322
|
+
* Synchronously enumerate every directory containing a `plugin.json`
|
|
3323
|
+
* across the configured search paths. Non-existent paths are skipped.
|
|
3324
|
+
*/
|
|
3325
|
+
discoverPaths(): string[];
|
|
3326
|
+
/**
|
|
3327
|
+
* Discover every plugin, attempt to load each, then apply the
|
|
3328
|
+
* cross-root id-collision pass. Never throws, failures are reported
|
|
3329
|
+
* via `IDiscoveredPlugin.status`.
|
|
3330
|
+
*/
|
|
3331
|
+
discoverAndLoadAll(): Promise<IDiscoveredPlugin[]>;
|
|
3332
|
+
/**
|
|
3333
|
+
* Load a single plugin from its directory. Never throws, failure is
|
|
3334
|
+
* reported via the returned `status`.
|
|
3335
|
+
*/
|
|
3336
|
+
loadOne(pluginPath: string): Promise<IDiscoveredPlugin>;
|
|
3329
3337
|
}
|
|
3330
3338
|
|
|
3331
3339
|
/**
|