@skill-map/cli 0.20.1 → 0.22.0

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.
Files changed (37) hide show
  1. package/dist/cli/tutorial/sm-tutorial.md +93 -14
  2. package/dist/cli.js +7660 -6354
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +1244 -1065
  5. package/dist/index.js.map +1 -1
  6. package/dist/kernel/index.d.ts +300 -194
  7. package/dist/kernel/index.js +1244 -1065
  8. package/dist/kernel/index.js.map +1 -1
  9. package/dist/migrations/001_initial.sql +13 -0
  10. package/dist/ui/chunk-25AWRVIC.js +965 -0
  11. package/dist/ui/chunk-GETTEQ3S.js +123 -0
  12. package/dist/ui/{chunk-4NLC7QD2.js → chunk-GXRWH2VL.js} +1 -1
  13. package/dist/ui/chunk-HC6PNQMW.js +251 -0
  14. package/dist/ui/chunk-HJHWJTFH.js +1 -0
  15. package/dist/ui/chunk-MF2M6GYF.js +1 -0
  16. package/dist/ui/{chunk-EZZF5RL5.js → chunk-MPMBTIUR.js} +2 -2
  17. package/dist/ui/{chunk-6GUHSAP5.js → chunk-OPPQMCMQ.js} +1 -1
  18. package/dist/ui/chunk-V3SZQETX.js +61 -0
  19. package/dist/ui/{chunk-E4ALROJS.js → chunk-VVOEPDQD.js} +1 -1
  20. package/dist/ui/{chunk-6BZZQV42.js → chunk-W2EFGI3J.js} +1 -1
  21. package/dist/ui/index.html +2 -10
  22. package/dist/ui/main-Q2WC254P.js +2 -0
  23. package/dist/ui/media/fa-brands-400-AHOAZHCU.woff2 +0 -0
  24. package/dist/ui/media/fa-regular-400-VRZYIBIZ.woff2 +0 -0
  25. package/dist/ui/media/fa-solid-900-MDEYK55F.woff2 +0 -0
  26. package/dist/ui/media/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
  27. package/dist/ui/styles-M2FETVAG.css +1 -0
  28. package/migrations/001_initial.sql +13 -0
  29. package/package.json +6 -5
  30. package/dist/ui/chunk-FWX4RRDF.js +0 -125
  31. package/dist/ui/chunk-GGMXMGRJ.js +0 -1
  32. package/dist/ui/chunk-K5PULFK7.js +0 -1
  33. package/dist/ui/chunk-OJ6W6OIB.js +0 -61
  34. package/dist/ui/chunk-PTCD42GB.js +0 -247
  35. package/dist/ui/chunk-ZSRIBCAW.js +0 -965
  36. package/dist/ui/main-5FJWWH5I.js +0 -1
  37. package/dist/ui/styles-VJ5Q6D2X.css +0 -1
@@ -484,7 +484,7 @@ interface IRegisteredAnnotationKey {
484
484
  * validates each pick at load time (`invalid-manifest` on miss); the
485
485
  * slot fixes both the renderer and the payload shape.
486
486
  */
487
- type TSlotName = 'card.title.right' | 'card.subtitle.left' | 'card.footer.left.counter' | 'card.footer.right' | 'graph.node.alert' | 'inspector.header.badge.counter' | 'inspector.header.badge.tag' | 'inspector.body.panel.breakdown' | 'inspector.body.panel.records' | 'inspector.body.panel.tree' | 'inspector.body.panel.key-values' | 'inspector.body.panel.link-list' | 'inspector.body.panel.markdown' | 'topbar.actions.indicator';
487
+ type TSlotName = 'card.title.right' | 'card.subtitle.left' | 'card.footer.left' | 'card.footer.right' | 'graph.node.alert' | 'inspector.header.badge.counter' | 'inspector.header.badge.tag' | 'inspector.body.panel.breakdown' | 'inspector.body.panel.records' | 'inspector.body.panel.tree' | 'inspector.body.panel.key-values' | 'inspector.body.panel.link-list' | 'inspector.body.panel.markdown' | 'topbar.nav.start';
488
488
  /**
489
489
  * Closed enum of input-type names for plugin settings. Mirror of
490
490
  * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.
@@ -759,55 +759,6 @@ interface IExtensionBase {
759
759
  viewContributions?: Record<string, IViewContribution>;
760
760
  }
761
761
 
762
- /**
763
- * `.skillmapignore` parser + filter facade. Wraps `ignore` (kaelzhang)
764
- * with the project-local layering: bundled defaults → `config.ignore`
765
- * (from `.skill-map/settings.json`) → `.skillmapignore` file content.
766
- *
767
- * Why a wrapper instead of exposing `ignore` directly:
768
- *
769
- * 1. Single-source defaults — `src/config/defaults/skillmapignore` is
770
- * the canonical default list, loaded once at module init (or at
771
- * explicit build time, depending on bundling). The runtime never
772
- * re-reads it per scan.
773
- * 2. Stable interface — Providers and the orchestrator depend on a
774
- * minimal `IIgnoreFilter` shape, so the underlying library can be
775
- * swapped without touching every consumer.
776
- * 3. Path normalization — every consumer passes the path RELATIVE to
777
- * the scan root (POSIX separators); the wrapper guarantees that
778
- * contract before delegating to `ignore`.
779
- */
780
- interface IIgnoreFilter {
781
- /**
782
- * Returns `true` when `relativePath` should be skipped. The caller
783
- * MUST pass paths relative to the scan root, with POSIX separators
784
- * (forward slashes), no leading `/`. Directories MAY be passed with
785
- * or without trailing `/`; the wrapper does not require it.
786
- */
787
- ignores(relativePath: string): boolean;
788
- }
789
-
790
- /**
791
- * `ProgressEmitterPort` — emits progress events during long operations.
792
- *
793
- * Shape-only today. The full event catalog (`run.started`,
794
- * `job.claimed`, `model.delta`, etc.) is normative in
795
- * `spec/job-events.md`; this port carries an open `data` payload so
796
- * adapters can emit any documented event without type churn.
797
- */
798
- interface ProgressEvent {
799
- type: string;
800
- timestamp: string;
801
- runId?: string;
802
- jobId?: string;
803
- data?: unknown;
804
- }
805
- type TProgressListener = (event: ProgressEvent) => void;
806
- interface ProgressEmitterPort {
807
- emit(event: ProgressEvent): void;
808
- subscribe(listener: TProgressListener): () => void;
809
- }
810
-
811
762
  /**
812
763
  * Plugin-surface types, hand-written to mirror
813
764
  * `spec/schemas/plugins-registry.schema.json#/$defs/PluginManifest` and the
@@ -913,9 +864,11 @@ interface IPluginManifest {
913
864
  *
914
865
  * - `incompatible-spec`: manifest parsed fine but `semver.satisfies` failed
915
866
  * against the installed `@skill-map/spec` version.
916
- * - `invalid-manifest`: `plugin.json` missing, unparseable, or failing AJV.
917
- * - `load-error`: manifest passed but an extension module failed to import
918
- * or the imported manifest failed its extension-kind schema.
867
+ * - `invalid-manifest`: `plugin.json` missing, unparseable, failing AJV on
868
+ * the base manifest schema, OR the exported extension shape failed its
869
+ * kind-specific schema (per spec/architecture.md §Plugin discovery
870
+ * "AJV rejects unknown `slot` names with `invalid-manifest`").
871
+ * - `load-error`: manifest parsed but an extension module failed to import.
919
872
  */
920
873
  /**
921
874
  * Possible outcomes after the loader sees a plugin.json. Mirrors the
@@ -1191,6 +1144,95 @@ interface IPersistedContribution {
1191
1144
  emittedAt: number;
1192
1145
  }
1193
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
+ /**
1192
+ * Spec § A.9 — load the fine-grained Extractor cache as a per-node map
1193
+ * from qualified extractor id (`<pluginId>/<id>`) to the run-time
1194
+ * hashes the extractor recorded on its last run. Empty map is the
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).
1202
+ */
1203
+ interface IPriorExtractorRun {
1204
+ bodyHash: string;
1205
+ sidecarAnnotationsHash: string;
1206
+ }
1207
+
1208
+ /**
1209
+ * `.skillmapignore` parser + filter facade. Wraps `ignore` (kaelzhang)
1210
+ * with the project-local layering: bundled defaults → `config.ignore`
1211
+ * (from `.skill-map/settings.json`) → `.skillmapignore` file content.
1212
+ *
1213
+ * Why a wrapper instead of exposing `ignore` directly:
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`.
1225
+ */
1226
+ interface IIgnoreFilter {
1227
+ /**
1228
+ * Returns `true` when `relativePath` should be skipped. The caller
1229
+ * MUST pass paths relative to the scan root, with POSIX separators
1230
+ * (forward slashes), no leading `/`. Directories MAY be passed with
1231
+ * or without trailing `/`; the wrapper does not require it.
1232
+ */
1233
+ ignores(relativePath: string): boolean;
1234
+ }
1235
+
1194
1236
  /**
1195
1237
  * Provider runtime contract. Walks filesystem roots and emits raw node
1196
1238
  * records; classification maps path conventions to a node kind.
@@ -1334,15 +1376,6 @@ type TProviderKindIcon = {
1334
1376
  };
1335
1377
  interface IProvider extends IExtensionBase {
1336
1378
  kind: 'provider';
1337
- /**
1338
- * Filesystem directory (relative to user home or project root) where this
1339
- * Provider's content lives. Required. Examples: `'~/.claude'` for the
1340
- * Claude Provider, `'~/.cursor'` for a hypothetical Cursor Provider.
1341
- * The kernel walks this directory during boot/scan to discover nodes;
1342
- * `sm doctor` validates the directory exists and emits a non-blocking
1343
- * warning when it does not.
1344
- */
1345
- explorationDir: string;
1346
1379
  /**
1347
1380
  * Catalog of node kinds this Provider emits. Keyed by kind name. Every
1348
1381
  * kind the Provider can `classify()` MUST have an entry; an entry is
@@ -2092,6 +2125,182 @@ interface IHook extends IExtensionBase {
2092
2125
  on(ctx: IHookContext): void | Promise<void>;
2093
2126
  }
2094
2127
 
2128
+ /**
2129
+ * `ProgressEmitterPort` — emits progress events during long operations.
2130
+ *
2131
+ * Shape-only today. The full event catalog (`run.started`,
2132
+ * `job.claimed`, `model.delta`, etc.) is normative in
2133
+ * `spec/job-events.md`; this port carries an open `data` payload so
2134
+ * adapters can emit any documented event without type churn.
2135
+ */
2136
+ interface ProgressEvent {
2137
+ type: string;
2138
+ timestamp: string;
2139
+ runId?: string;
2140
+ jobId?: string;
2141
+ data?: unknown;
2142
+ }
2143
+ type TProgressListener = (event: ProgressEvent) => void;
2144
+ interface ProgressEmitterPort {
2145
+ emit(event: ProgressEvent): void;
2146
+ subscribe(listener: TProgressListener): () => void;
2147
+ }
2148
+
2149
+ /**
2150
+ * Per-node extractor invocation: build a fresh `IExtractorContext` for
2151
+ * each extractor, validate every emitted link / contribution against
2152
+ * the declared catalog, fold enrichment partials into per-`(node,
2153
+ * extractor)` records, and surface emit-time drops as
2154
+ * `extension.error` events.
2155
+ *
2156
+ * Also hosts the post-walk recompute helpers that re-derive
2157
+ * `linksOutCount` / `linksInCount` / `externalRefsCount` on every node
2158
+ * from the final merged link buffer, plus the `IExtractorRunRecord`
2159
+ * and `IEnrichmentRecord` types those records eventually persist as.
2160
+ */
2161
+
2162
+ /**
2163
+ * Spec § A.9 — runs to persist into `scan_extractor_runs`. One entry
2164
+ * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
2165
+ * "this extractor is current for this body". Includes both freshly-run
2166
+ * pairs (extractor invoked this scan) and reused pairs (cached node, the
2167
+ * extractor's prior run still applies to the same body hash). Excludes
2168
+ * obsolete pairs — extractors that ran in the prior but are no longer
2169
+ * registered — so a replace-all persist drops them automatically.
2170
+ */
2171
+ interface IExtractorRunRecord {
2172
+ nodePath: string;
2173
+ extractorId: string;
2174
+ bodyHashAtRun: string;
2175
+ ranAt: number;
2176
+ /**
2177
+ * sha256 of the canonical-form sidecar annotations the Extractor saw
2178
+ * at run time. Always populated (an absent sidecar canonicalises to
2179
+ * `{}` so the hash is stable). Used unconditionally by the cache
2180
+ * decision alongside `bodyHashAtRun`: a sidecar-only edit invalidates
2181
+ * the cached run for every applicable Extractor on that node.
2182
+ */
2183
+ sidecarAnnotationsHashAtRun: string;
2184
+ }
2185
+ /**
2186
+ * Spec § A.8 — universal enrichment layer.
2187
+ *
2188
+ * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
2189
+ * produced via `ctx.enrichNode(...)` during the walk. Attribution is
2190
+ * preserved per-Extractor (rather than merged client-side as B.1 did)
2191
+ * so the persistence layer can:
2192
+ *
2193
+ * - upsert a single row per pair (stable PRIMARY KEY conflict on
2194
+ * re-extract);
2195
+ * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
2196
+ * for last-write-wins per field at read time.
2197
+ *
2198
+ * `value` is the cumulative merge across every `enrichNode` call that
2199
+ * Extractor made for this node within this scan — multiple
2200
+ * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
2201
+ * fold into a single row, but two different Extractors hitting the
2202
+ * same node yield two distinct rows.
2203
+ *
2204
+ * `isProbabilistic` is reserved: Extractors are deterministic-only, so
2205
+ * every record produced by the orchestrator sets it to `false`. The
2206
+ * field is kept on the record (and the row in `node_enrichments`) so a
2207
+ * future Action-issued enrichment can populate it without reshaping
2208
+ * the persistence contract — see spec `architecture.md`
2209
+ * §Extractor · enrichment layer.
2210
+ */
2211
+ interface IEnrichmentRecord {
2212
+ nodePath: string;
2213
+ extractorId: string;
2214
+ bodyHashAtEnrichment: string;
2215
+ value: Partial<Node>;
2216
+ enrichedAt: number;
2217
+ isProbabilistic: boolean;
2218
+ }
2219
+ /**
2220
+ * Run a set of extractors against a single node, collecting their link
2221
+ * emissions and node-enrichment partials. Each extractor is invoked
2222
+ * exactly once with a fresh `IExtractorContext`. Caller decides what
2223
+ * to do with the returned arrays (push into per-scan buffers, write to
2224
+ * a focused refresh result, etc.).
2225
+ *
2226
+ * Exported so `cli/commands/refresh.ts` can reuse the same wiring it
2227
+ * needs for re-running a single extractor against a single node — the
2228
+ * pre-extraction code in `refresh.ts` was hand-duplicating this loop
2229
+ * (audit item V4).
2230
+ *
2231
+ * Within this call, multiple `enrichNode(partial)` calls from the same
2232
+ * extractor against the same node fold into one record (last-write-wins
2233
+ * per field) — same contract as the in-scan path.
2234
+ */
2235
+ declare function runExtractorsForNode(opts: {
2236
+ extractors: IExtractor[];
2237
+ node: Node;
2238
+ body: string;
2239
+ frontmatter: Record<string, unknown>;
2240
+ bodyHash: string;
2241
+ emitter: ProgressEmitterPort;
2242
+ /**
2243
+ * Spec § A.12 — per-plugin `ctx.store` wrappers keyed by `pluginId`.
2244
+ * The map's lookup is per-extractor inside the loop, so callers that
2245
+ * don't track plugin storage can omit it; the resulting `ctx.store`
2246
+ * stays `undefined` (the existing contract).
2247
+ */
2248
+ pluginStores?: ReadonlyMap<string, IPluginStore>;
2249
+ }): Promise<{
2250
+ internalLinks: Link[];
2251
+ externalLinks: Link[];
2252
+ enrichments: IEnrichmentRecord[];
2253
+ contributions: IContributionRecord[];
2254
+ }>;
2255
+
2256
+ /**
2257
+ * Rename + orphan classification per `spec/db-schema.md` §Rename
2258
+ * detection. Pure: takes the prior `ScanResult` and the current node
2259
+ * set, mutates the supplied `issues` array in place, and returns the
2260
+ * `RenameOp[]` the persistence layer must apply inside the same tx as
2261
+ * the scan zone replace-all.
2262
+ */
2263
+
2264
+ /**
2265
+ * Confidence-tagged plan to repoint `state_*` references from one node
2266
+ * path to another. Emitted by the rename heuristic during `runScan` and
2267
+ * consumed by `persistScanResult` so the FK migration runs inside the
2268
+ * same transaction as the scan zone replace-all.
2269
+ */
2270
+ interface RenameOp {
2271
+ from: string;
2272
+ to: string;
2273
+ confidence: 'high' | 'medium';
2274
+ }
2275
+ /**
2276
+ * Pure rename / orphan classification per `spec/db-schema.md` §Rename
2277
+ * detection. Mutates `issues` in place — caller passes the in-progress
2278
+ * issue list; returns the `RenameOp[]` for the persistence layer to
2279
+ * apply inside its tx.
2280
+ *
2281
+ * Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused
2282
+ * by another):
2283
+ *
2284
+ * 1. **High-confidence**: pair each `deletedPath` with a `newPath`
2285
+ * that has the same `bodyHash`. No issue, no prompt.
2286
+ * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
2287
+ * each with the *unique* unclaimed `newPath` that shares its
2288
+ * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
2289
+ * with `data: { from, to, confidence: 'medium' }`.
2290
+ * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
2291
+ * remaining frontmatter-matching candidate, emit ONE
2292
+ * `auto-rename-ambiguous` issue per `newPath`, listing all
2293
+ * candidates in `data.candidates`. NO migration.
2294
+ * 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one
2295
+ * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
2296
+ *
2297
+ * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
2298
+ * order so the same input always produces the same matches —
2299
+ * required for reproducible tests and conformance fixtures (the spec
2300
+ * does not prescribe an order, but stability is the obvious contract).
2301
+ */
2302
+ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
2303
+
2095
2304
  /**
2096
2305
  * Scan orchestrator — runs the Provider → extractor → analyzer pipeline across
2097
2306
  * every registered extension and emits `ProgressEmitterPort` events in
@@ -2158,17 +2367,6 @@ interface IScanExtensions {
2158
2367
  */
2159
2368
  hooks?: IHook[];
2160
2369
  }
2161
- /**
2162
- * Confidence-tagged plan to repoint `state_*` references from one node
2163
- * path to another. Emitted by the rename heuristic during `runScan` and
2164
- * consumed by `persistScanResult` so the FK migration runs inside the
2165
- * same transaction as the scan zone replace-all.
2166
- */
2167
- interface RenameOp {
2168
- from: string;
2169
- to: string;
2170
- confidence: 'high' | 'medium';
2171
- }
2172
2370
  interface RunScanOptions {
2173
2371
  /**
2174
2372
  * Filesystem roots to walk. Spec requires `minItems: 1`; passing an
@@ -2266,7 +2464,7 @@ interface RunScanOptions {
2266
2464
  strict?: boolean;
2267
2465
  /**
2268
2466
  * Spec § A.9 — fine-grained Extractor cache breadcrumbs from the
2269
- * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, bodyHashAtRun>>`.
2467
+ * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
2270
2468
  * Loaded from the `scan_extractor_runs` table by the CLI before
2271
2469
  * invoking `runScan`; absent / empty for a fresh DB or an out-of-band
2272
2470
  * caller that does not maintain a cache. Decoupled from `priorSnapshot`
@@ -2278,11 +2476,12 @@ interface RunScanOptions {
2278
2476
  * registered extractor that applies to this kind has a matching
2279
2477
  * row → full skip, all prior outbound links reused.
2280
2478
  * - some applicable extractor lacks a matching row (newly registered,
2281
- * or its prior run targeted a different body hash) run only the
2282
- * missing extractors, drop prior links whose `sources` map to any
2283
- * missing extractor or to an extractor that is no longer registered.
2479
+ * or its prior run targeted a different body hash or sidecar
2480
+ * annotations hash) run only the missing extractors, drop prior
2481
+ * links whose `sources` map to any missing extractor or to an
2482
+ * extractor that is no longer registered.
2284
2483
  */
2285
- priorExtractorRuns?: Map<string, Map<string, string>>;
2484
+ priorExtractorRuns?: Map<string, Map<string, IPriorExtractorRun>>;
2286
2485
  /**
2287
2486
  * Spec § A.12 — per-plugin storage wrappers exposed to extractors via
2288
2487
  * `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves
@@ -2331,55 +2530,6 @@ interface RunScanOptions {
2331
2530
  */
2332
2531
  cwd?: string;
2333
2532
  }
2334
- /**
2335
- * Spec § A.9 — runs to persist into `scan_extractor_runs`. One entry
2336
- * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
2337
- * "this extractor is current for this body". Includes both freshly-run
2338
- * pairs (extractor invoked this scan) and reused pairs (cached node, the
2339
- * extractor's prior run still applies to the same body hash). Excludes
2340
- * obsolete pairs — extractors that ran in the prior but are no longer
2341
- * registered — so a replace-all persist drops them automatically.
2342
- */
2343
- interface IExtractorRunRecord {
2344
- nodePath: string;
2345
- extractorId: string;
2346
- bodyHashAtRun: string;
2347
- ranAt: number;
2348
- }
2349
- /**
2350
- * Spec § A.8 — universal enrichment layer.
2351
- *
2352
- * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
2353
- * produced via `ctx.enrichNode(...)` during the walk. Attribution is
2354
- * preserved per-Extractor (rather than merged client-side as B.1 did)
2355
- * so the persistence layer can:
2356
- *
2357
- * - upsert a single row per pair (stable PRIMARY KEY conflict on
2358
- * re-extract);
2359
- * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
2360
- * for last-write-wins per field at read time.
2361
- *
2362
- * `value` is the cumulative merge across every `enrichNode` call that
2363
- * Extractor made for this node within this scan — multiple
2364
- * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
2365
- * fold into a single row, but two different Extractors hitting the
2366
- * same node yield two distinct rows.
2367
- *
2368
- * `isProbabilistic` is reserved: Extractors are deterministic-only, so
2369
- * every record produced by the orchestrator sets it to `false`. The
2370
- * field is kept on the record (and the row in `node_enrichments`) so a
2371
- * future Action-issued enrichment can populate it without reshaping
2372
- * the persistence contract — see spec `architecture.md`
2373
- * §Extractor · enrichment layer.
2374
- */
2375
- interface IEnrichmentRecord {
2376
- nodePath: string;
2377
- extractorId: string;
2378
- bodyHashAtEnrichment: string;
2379
- value: Partial<Node>;
2380
- enrichedAt: number;
2381
- isProbabilistic: boolean;
2382
- }
2383
2533
  /**
2384
2534
  * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`
2385
2535
  * — the high- and medium-confidence renames the persistence layer must
@@ -2402,70 +2552,16 @@ declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): P
2402
2552
  freshlyRunTuples: ReadonlySet<string>;
2403
2553
  }>;
2404
2554
  declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
2555
+
2405
2556
  /**
2406
- * Run a set of extractors against a single node, collecting their link
2407
- * emissions and node-enrichment partials. Each extractor is invoked
2408
- * exactly once with a fresh `IExtractorContext`. Caller decides what
2409
- * to do with the returned arrays (push into per-scan buffers, write to
2410
- * a focused refresh result, etc.).
2411
- *
2412
- * Exported so `cli/commands/refresh.ts` can reuse the same wiring it
2413
- * needs for re-running a single extractor against a single node — the
2414
- * pre-extraction code in `refresh.ts` was hand-duplicating this loop
2415
- * (audit item V4).
2416
- *
2417
- * Within this call, multiple `enrichNode(partial)` calls from the same
2418
- * extractor against the same node fold into one record (last-write-wins
2419
- * per field) — same contract as the in-scan path.
2420
- */
2421
- declare function runExtractorsForNode(opts: {
2422
- extractors: IExtractor[];
2423
- node: Node;
2424
- body: string;
2425
- frontmatter: Record<string, unknown>;
2426
- bodyHash: string;
2427
- emitter: ProgressEmitterPort;
2428
- /**
2429
- * Spec § A.12 — per-plugin `ctx.store` wrappers keyed by `pluginId`.
2430
- * The map's lookup is per-extractor inside the loop, so callers that
2431
- * don't track plugin storage can omit it; the resulting `ctx.store`
2432
- * stays `undefined` (the existing contract).
2433
- */
2434
- pluginStores?: ReadonlyMap<string, IPluginStore>;
2435
- }): Promise<{
2436
- internalLinks: Link[];
2437
- externalLinks: Link[];
2438
- enrichments: IEnrichmentRecord[];
2439
- contributions: IContributionRecord[];
2440
- }>;
2441
- /**
2442
- * Pure rename / orphan classification per `spec/db-schema.md` §Rename
2443
- * detection. Mutates `issues` in place — caller passes the in-progress
2444
- * issue list; returns the `RenameOp[]` for the persistence layer to
2445
- * apply inside its tx.
2446
- *
2447
- * Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused
2448
- * by another):
2449
- *
2450
- * 1. **High-confidence**: pair each `deletedPath` with a `newPath`
2451
- * that has the same `bodyHash`. No issue, no prompt.
2452
- * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
2453
- * each with the *unique* unclaimed `newPath` that shares its
2454
- * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
2455
- * with `data: { from, to, confidence: 'medium' }`.
2456
- * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
2457
- * remaining frontmatter-matching candidate, emit ONE
2458
- * `auto-rename-ambiguous` issue per `newPath`, listing all
2459
- * candidates in `data.candidates`. NO migration.
2460
- * 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one
2461
- * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
2462
- *
2463
- * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
2464
- * order so the same input always produces the same matches —
2465
- * required for reproducible tests and conformance fixtures (the spec
2466
- * does not prescribe an order, but stability is the obvious contract).
2557
+ * Node-construction helpers: hash a body, canonicalise frontmatter /
2558
+ * sidecar annotations, resolve the sidecar overlay for a given relative
2559
+ * path, and produce a fresh `Node` (validating its frontmatter on the
2560
+ * way out). Also hosts `mergeNodeWithEnrichments` + `IPersistedEnrichment`
2561
+ * the read-time merge of author frontmatter with the A.8 enrichment
2562
+ * layer.
2467
2563
  */
2468
- declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
2564
+
2469
2565
  /**
2470
2566
  * Spec § A.8 — produce the merged read-time view of a Node.
2471
2567
  *
@@ -3170,9 +3266,11 @@ interface StoragePort {
3170
3266
  load(): Promise<ScanResult>;
3171
3267
  /**
3172
3268
  * Spec § A.9 — fine-grained extractor-runs cache breadcrumbs.
3173
- * Returns `Map<nodePath, Map<qualifiedExtractorId, bodyHashAtRun>>`.
3269
+ * Returns `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
3270
+ * Inner value carries `bodyHash` AND `sidecarAnnotationsHash`; both
3271
+ * participate in the cache hit condition for every Extractor.
3174
3272
  */
3175
- loadExtractorRuns(): Promise<Map<string, Map<string, string>>>;
3273
+ loadExtractorRuns(): Promise<Map<string, Map<string, IPriorExtractorRun>>>;
3176
3274
  /** Universal enrichment layer — every persisted `(node, extractor)` pair. */
3177
3275
  loadNodeEnrichments(): Promise<IPersistedEnrichment[]>;
3178
3276
  /**
@@ -3190,9 +3288,10 @@ interface StoragePort {
3190
3288
  };
3191
3289
  /**
3192
3290
  * Phase 3 / View contribution system — read access to
3193
- * `scan_contributions`. Writes happen exclusively via
3194
- * `scans.persist({ contributions })` to keep the replace-all
3195
- * semantics intact; this namespace is read-only.
3291
+ * `scan_contributions`, plus the targeted purge used by
3292
+ * `sm plugins disable` to clear stale rows immediately at toggle time.
3293
+ * Bulk writes still happen exclusively via
3294
+ * `scans.persist({ contributions })` (replace-all semantics).
3196
3295
  */
3197
3296
  contributions: {
3198
3297
  /** Every contribution row for a single node. Stable order. */
@@ -3208,6 +3307,13 @@ interface StoragePort {
3208
3307
  * `GET /api/contributions/:pluginId/:contributionId?path=...`.
3209
3308
  */
3210
3309
  lookup(pluginId: string, contributionId: string, nodePath: string, extensionId?: string): Promise<IPersistedContribution[]>;
3310
+ /**
3311
+ * Drop rows for a plugin (optionally narrowed to a single
3312
+ * extension within the bundle). Returns the number of deleted
3313
+ * rows. Called by `sm plugins disable` so the UI stops rendering
3314
+ * the disabled plugin's chips before the next scan.
3315
+ */
3316
+ purgeByPlugin(pluginId: string, extensionId?: string): Promise<number>;
3211
3317
  };
3212
3318
  /**
3213
3319
  * Read-only access to `scan_node_tags`. Writes happen exclusively