@skill-map/cli 0.21.0 → 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.
@@ -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
@@ -1252,6 +1205,34 @@ interface IPriorExtractorRun {
1252
1205
  sidecarAnnotationsHash: string;
1253
1206
  }
1254
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
+
1255
1236
  /**
1256
1237
  * Provider runtime contract. Walks filesystem roots and emits raw node
1257
1238
  * records; classification maps path conventions to a node kind.
@@ -1395,15 +1376,6 @@ type TProviderKindIcon = {
1395
1376
  };
1396
1377
  interface IProvider extends IExtensionBase {
1397
1378
  kind: 'provider';
1398
- /**
1399
- * Filesystem directory (relative to user home or project root) where this
1400
- * Provider's content lives. Required. Examples: `'~/.claude'` for the
1401
- * Claude Provider, `'~/.cursor'` for a hypothetical Cursor Provider.
1402
- * The kernel walks this directory during boot/scan to discover nodes;
1403
- * `sm doctor` validates the directory exists and emits a non-blocking
1404
- * warning when it does not.
1405
- */
1406
- explorationDir: string;
1407
1379
  /**
1408
1380
  * Catalog of node kinds this Provider emits. Keyed by kind name. Every
1409
1381
  * kind the Provider can `classify()` MUST have an entry; an entry is
@@ -2153,6 +2125,182 @@ interface IHook extends IExtensionBase {
2153
2125
  on(ctx: IHookContext): void | Promise<void>;
2154
2126
  }
2155
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
+
2156
2304
  /**
2157
2305
  * Scan orchestrator — runs the Provider → extractor → analyzer pipeline across
2158
2306
  * every registered extension and emits `ProgressEmitterPort` events in
@@ -2219,17 +2367,6 @@ interface IScanExtensions {
2219
2367
  */
2220
2368
  hooks?: IHook[];
2221
2369
  }
2222
- /**
2223
- * Confidence-tagged plan to repoint `state_*` references from one node
2224
- * path to another. Emitted by the rename heuristic during `runScan` and
2225
- * consumed by `persistScanResult` so the FK migration runs inside the
2226
- * same transaction as the scan zone replace-all.
2227
- */
2228
- interface RenameOp {
2229
- from: string;
2230
- to: string;
2231
- confidence: 'high' | 'medium';
2232
- }
2233
2370
  interface RunScanOptions {
2234
2371
  /**
2235
2372
  * Filesystem roots to walk. Spec requires `minItems: 1`; passing an
@@ -2393,63 +2530,6 @@ interface RunScanOptions {
2393
2530
  */
2394
2531
  cwd?: string;
2395
2532
  }
2396
- /**
2397
- * Spec § A.9 — runs to persist into `scan_extractor_runs`. One entry
2398
- * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
2399
- * "this extractor is current for this body". Includes both freshly-run
2400
- * pairs (extractor invoked this scan) and reused pairs (cached node, the
2401
- * extractor's prior run still applies to the same body hash). Excludes
2402
- * obsolete pairs — extractors that ran in the prior but are no longer
2403
- * registered — so a replace-all persist drops them automatically.
2404
- */
2405
- interface IExtractorRunRecord {
2406
- nodePath: string;
2407
- extractorId: string;
2408
- bodyHashAtRun: string;
2409
- ranAt: number;
2410
- /**
2411
- * sha256 of the canonical-form sidecar annotations the Extractor saw
2412
- * at run time. Always populated (an absent sidecar canonicalises to
2413
- * `{}` so the hash is stable). Used unconditionally by the cache
2414
- * decision alongside `bodyHashAtRun`: a sidecar-only edit invalidates
2415
- * the cached run for every applicable Extractor on that node.
2416
- */
2417
- sidecarAnnotationsHashAtRun: string;
2418
- }
2419
- /**
2420
- * Spec § A.8 — universal enrichment layer.
2421
- *
2422
- * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
2423
- * produced via `ctx.enrichNode(...)` during the walk. Attribution is
2424
- * preserved per-Extractor (rather than merged client-side as B.1 did)
2425
- * so the persistence layer can:
2426
- *
2427
- * - upsert a single row per pair (stable PRIMARY KEY conflict on
2428
- * re-extract);
2429
- * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
2430
- * for last-write-wins per field at read time.
2431
- *
2432
- * `value` is the cumulative merge across every `enrichNode` call that
2433
- * Extractor made for this node within this scan — multiple
2434
- * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
2435
- * fold into a single row, but two different Extractors hitting the
2436
- * same node yield two distinct rows.
2437
- *
2438
- * `isProbabilistic` is reserved: Extractors are deterministic-only, so
2439
- * every record produced by the orchestrator sets it to `false`. The
2440
- * field is kept on the record (and the row in `node_enrichments`) so a
2441
- * future Action-issued enrichment can populate it without reshaping
2442
- * the persistence contract — see spec `architecture.md`
2443
- * §Extractor · enrichment layer.
2444
- */
2445
- interface IEnrichmentRecord {
2446
- nodePath: string;
2447
- extractorId: string;
2448
- bodyHashAtEnrichment: string;
2449
- value: Partial<Node>;
2450
- enrichedAt: number;
2451
- isProbabilistic: boolean;
2452
- }
2453
2533
  /**
2454
2534
  * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`
2455
2535
  * — the high- and medium-confidence renames the persistence layer must
@@ -2472,70 +2552,16 @@ declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): P
2472
2552
  freshlyRunTuples: ReadonlySet<string>;
2473
2553
  }>;
2474
2554
  declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
2555
+
2475
2556
  /**
2476
- * Run a set of extractors against a single node, collecting their link
2477
- * emissions and node-enrichment partials. Each extractor is invoked
2478
- * exactly once with a fresh `IExtractorContext`. Caller decides what
2479
- * to do with the returned arrays (push into per-scan buffers, write to
2480
- * a focused refresh result, etc.).
2481
- *
2482
- * Exported so `cli/commands/refresh.ts` can reuse the same wiring it
2483
- * needs for re-running a single extractor against a single node — the
2484
- * pre-extraction code in `refresh.ts` was hand-duplicating this loop
2485
- * (audit item V4).
2486
- *
2487
- * Within this call, multiple `enrichNode(partial)` calls from the same
2488
- * extractor against the same node fold into one record (last-write-wins
2489
- * per field) — same contract as the in-scan path.
2490
- */
2491
- declare function runExtractorsForNode(opts: {
2492
- extractors: IExtractor[];
2493
- node: Node;
2494
- body: string;
2495
- frontmatter: Record<string, unknown>;
2496
- bodyHash: string;
2497
- emitter: ProgressEmitterPort;
2498
- /**
2499
- * Spec § A.12 — per-plugin `ctx.store` wrappers keyed by `pluginId`.
2500
- * The map's lookup is per-extractor inside the loop, so callers that
2501
- * don't track plugin storage can omit it; the resulting `ctx.store`
2502
- * stays `undefined` (the existing contract).
2503
- */
2504
- pluginStores?: ReadonlyMap<string, IPluginStore>;
2505
- }): Promise<{
2506
- internalLinks: Link[];
2507
- externalLinks: Link[];
2508
- enrichments: IEnrichmentRecord[];
2509
- contributions: IContributionRecord[];
2510
- }>;
2511
- /**
2512
- * Pure rename / orphan classification per `spec/db-schema.md` §Rename
2513
- * detection. Mutates `issues` in place — caller passes the in-progress
2514
- * issue list; returns the `RenameOp[]` for the persistence layer to
2515
- * apply inside its tx.
2516
- *
2517
- * Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused
2518
- * by another):
2519
- *
2520
- * 1. **High-confidence**: pair each `deletedPath` with a `newPath`
2521
- * that has the same `bodyHash`. No issue, no prompt.
2522
- * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
2523
- * each with the *unique* unclaimed `newPath` that shares its
2524
- * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
2525
- * with `data: { from, to, confidence: 'medium' }`.
2526
- * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
2527
- * remaining frontmatter-matching candidate, emit ONE
2528
- * `auto-rename-ambiguous` issue per `newPath`, listing all
2529
- * candidates in `data.candidates`. NO migration.
2530
- * 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one
2531
- * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
2532
- *
2533
- * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
2534
- * order so the same input always produces the same matches —
2535
- * required for reproducible tests and conformance fixtures (the spec
2536
- * 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.
2537
2563
  */
2538
- declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
2564
+
2539
2565
  /**
2540
2566
  * Spec § A.8 — produce the merged read-time view of a Node.
2541
2567
  *