@skill-map/cli 0.21.0 → 0.23.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Domain types byte-aligned with `spec/schemas/{node,link,issue,scan-result}.schema.json`.
2
+ * Domain types, byte-aligned with `spec/schemas/{node,link,issue,scan-result}.schema.json`.
3
3
  *
4
4
  * The kernel is the reference consumer of the spec; these types are therefore
5
5
  * derived from the schemas, not invented. When a schema changes, this file
@@ -11,27 +11,27 @@
11
11
  * Five categories with distinct prefix rules; the rules are deliberate
12
12
  * even though they look mixed at first read:
13
13
  *
14
- * 1. **Domain types** every shape that mirrors a `spec/schemas/*.json`
14
+ * 1. **Domain types**, every shape that mirrors a `spec/schemas/*.json`
15
15
  * file: `Node`, `Link`, `Issue`, `ScanResult`, `ScanStats`,
16
16
  * `ExecutionRecord`, `HistoryStats`, …. **No prefix.** Names track
17
17
  * the spec verbatim because the spec is the source of truth.
18
18
  * Renaming any of these is a spec change.
19
19
  *
20
- * 2. **Hexagonal ports** the abstract boundaries the kernel calls
20
+ * 2. **Hexagonal ports**, the abstract boundaries the kernel calls
21
21
  * out to (`StoragePort`, `RunnerPort`, `ProgressEmitterPort`,
22
22
  * `FilesystemPort`, `PluginLoaderPort`). **`Port` suffix.** The
23
23
  * suffix calls out the architectural role and avoids name clashes
24
24
  * with the concrete adapter classes (`SqliteStorageAdapter`
25
25
  * implements `StoragePort`).
26
26
  *
27
- * 3. **Runtime extension contracts** what a plugin author
27
+ * 3. **Runtime extension contracts**, what a plugin author
28
28
  * implements: `IProvider`, `IExtractor`, `IAnalyzer`, `IFormatter`,
29
29
  * `IExtensionBase`. **`I` prefix.** The prefix flags "this is a
30
- * contract you supply, not a value the kernel hands you" same
30
+ * contract you supply, not a value the kernel hands you", same
31
31
  * reading as the rest of TypeScript's plugin ecosystems where a
32
32
  * shape is implementable.
33
33
  *
34
- * 4. **Internal interfaces** option bags, result records, config
34
+ * 4. **Internal interfaces**, option bags, result records, config
35
35
  * slices, anything declared as `interface` and passed across
36
36
  * function boundaries inside the kernel / CLI but not part of the
37
37
  * spec: `IPluginRuntimeBundle`, `IPruneResult`, `IMigrationFile`,
@@ -39,7 +39,7 @@
39
39
  * category 3 because both are "shapes that live in TypeScript
40
40
  * only, never in JSON".
41
41
  *
42
- * 5. **Internal type aliases** anything declared as `type` (string-
42
+ * 5. **Internal type aliases**, anything declared as `type` (string-
43
43
  * literal unions, function types, mapped/derived types) that lives
44
44
  * only in TS: `TLogLevel`, `TLogMethodLevel`, `TProgressListener`,
45
45
  * `TLogFormatter`, `TActionWrite`, `TExecutionMode`, `TGranularity`,
@@ -67,7 +67,7 @@
67
67
  * for `interface`, `T` prefix for `type` aliases.
68
68
  */
69
69
  /**
70
- * The four node kinds the **built-in Claude Provider** declares `skill`,
70
+ * The four node kinds the **built-in Claude Provider** declares, `skill`,
71
71
  * `agent`, `command`, `note`. **NOT** the kernel-wide kind type.
72
72
  *
73
73
  * `Node.kind` is `string`. An external Provider (Cursor, Obsidian, …)
@@ -78,13 +78,13 @@
78
78
  * (matches `IProvider.kinds` "open by design" docstring).
79
79
  *
80
80
  * Step 9.5 dropped `hook` from the catalog: `.claude/hooks/*.md` is NOT
81
- * an Anthropic-defined node type hooks live in `settings.json` or as
81
+ * an Anthropic-defined node type, hooks live in `settings.json` or as
82
82
  * sub-objects of agent / skill frontmatter (see
83
83
  * https://code.claude.com/docs/en/hooks.md). Files at the old path
84
84
  * classify as `markdown` via the Provider's fallback. The fallback is
85
85
  * named after the *format* because the file is generic markdown with
86
86
  * no specific role; format-named kinds apply only as the generic
87
- * fallback a file that matches a specific role (agent / command /
87
+ * fallback, a file that matches a specific role (agent / command /
88
88
  * skill) classifies under that role, not under `markdown`.
89
89
  *
90
90
  * This alias survives because:
@@ -108,9 +108,9 @@ type Stability = 'experimental' | 'stable' | 'deprecated';
108
108
  * Execution mode of an analytical extension. Mirrors the per-kind capability
109
109
  * matrix in `spec/architecture.md` §Execution modes:
110
110
  *
111
- * - `deterministic` pure code, runs synchronously inside `sm scan` /
111
+ * - `deterministic`, pure code, runs synchronously inside `sm scan` /
112
112
  * `sm check`. Same input → same output, every run.
113
- * - `probabilistic` calls an LLM through `RunnerPort`, dispatches only
113
+ * - `probabilistic`, calls an LLM through `RunnerPort`, dispatches only
114
114
  * as a queued job (`sm job submit <kind>:<id>`); never participates in
115
115
  * scan-time pipelines.
116
116
  *
@@ -154,7 +154,7 @@ interface Node {
154
154
  frontmatter?: Record<string, unknown>;
155
155
  tokens?: TripleSplit;
156
156
  /**
157
- * Step 9.6.2 sidecar denormalisation surface. Populated by the
157
+ * Step 9.6.2, sidecar denormalisation surface. Populated by the
158
158
  * orchestrator at scan time; absent when the orchestrator did not
159
159
  * inspect sidecars (legacy code paths) or when no sidecar accompanies
160
160
  * the node. Read by `annotation-stale` rule and the persistence layer.
@@ -165,7 +165,7 @@ interface Node {
165
165
  * `/api/nodes/:pathB64` responses via in-memory `Set` lookup against
166
166
  * `state_node_favorites`. Absent on emissions that don't carry per-user
167
167
  * state (e.g. `sm export --json`); consumers that don't recognise the
168
- * field MUST treat the absence as "unknown" rather than "false" a
168
+ * field MUST treat the absence as "unknown" rather than "false", a
169
169
  * truthy `isFavorite` only ever lands when the BFF set it.
170
170
  */
171
171
  isFavorite?: boolean;
@@ -187,25 +187,25 @@ interface ISidecarOverlay {
187
187
  present: boolean;
188
188
  status?: SidecarStatus | null;
189
189
  /**
190
- * Parsed `annotations:` block. Untyped object schema lives in
190
+ * Parsed `annotations:` block. Untyped object, schema lives in
191
191
  * `spec/schemas/annotations.schema.json`. Null when no sidecar or
192
192
  * the block is empty/absent.
193
193
  */
194
194
  annotations?: Record<string, unknown> | null;
195
195
  /**
196
- * R15 closure (2026-05-07) full parsed YAML root of the sidecar
196
+ * R15 closure (2026-05-07), full parsed YAML root of the sidecar
197
197
  * (the entire `.sm` payload, mirroring `sidecar.schema.json`). Surfaced
198
198
  * so the UI inspector can render `for:`, `audit:`, `settings:`, and
199
199
  * `<plugin-id>:` namespace blocks without re-reading the file. NULL
200
200
  * when no sidecar is present, or when the sidecar exists but failed
201
- * to parse / validate. The `annotations` field above stays it
201
+ * to parse / validate. The `annotations` field above stays, it
202
202
  * duplicates `root.annotations` intentionally so existing consumers
203
203
  * keep working unchanged.
204
204
  */
205
205
  root?: Record<string, unknown> | null;
206
206
  }
207
207
  interface Link {
208
- /** The originating node the path of the file the extractor was reading
208
+ /** The originating node, the path of the file the extractor was reading
209
209
  * when it emitted this link. Singular, NOT to be confused with
210
210
  * `sources` (plural) below. */
211
211
  source: string;
@@ -353,7 +353,7 @@ interface ScanResult {
353
353
  scope: 'project' | 'global';
354
354
  /**
355
355
  * Filesystem roots that were walked during this scan. Spec requires
356
- * `minItems: 1` `runScan` throws if `roots: []` is supplied.
356
+ * `minItems: 1`, `runScan` throws if `roots: []` is supplied.
357
357
  */
358
358
  roots: string[];
359
359
  /** Provider ids that participated in classification. Empty if no Provider matched. */
@@ -367,14 +367,14 @@ interface ScanResult {
367
367
  }
368
368
 
369
369
  /**
370
- * Extension registry six kinds, first-class, loaded through a single API.
370
+ * Extension registry, six kinds, first-class, loaded through a single API.
371
371
  *
372
372
  * The `Extension` shape is aligned with `spec/schemas/extensions/base.schema.json`.
373
373
  * Kind-specific manifests (provider / extractor / analyzer / action / formatter /
374
374
  * hook) extend this base structurally; the registry stores the base view
375
375
  * and each kind's code carries its own fuller type where needed.
376
376
  *
377
- * **Spec § A.6 qualified ids.** Every extension is keyed in the registry
377
+ * **Spec § A.6, qualified ids.** Every extension is keyed in the registry
378
378
  * by `<pluginId>/<id>` (e.g. `core/annotations`, `core/slash`,
379
379
  * `hello-world/greet`). `Extension.id` carries the **short** id as authored;
380
380
  * `Extension.pluginId` carries the namespace; the registry composes the
@@ -429,18 +429,18 @@ declare class Registry {
429
429
  }
430
430
 
431
431
  /**
432
- * Step 9.6.6 runtime annotation-contribution catalog types.
432
+ * Step 9.6.6, runtime annotation-contribution catalog types.
433
433
  *
434
434
  * Lives in its own module (rather than `kernel/index.ts`) so consumers
435
- * deep inside the kernel `IAnalyzerContext`, the BFF route factories,
436
- * future Action contexts can depend on the catalog shape without
435
+ * deep inside the kernel, `IAnalyzerContext`, the BFF route factories,
436
+ * future Action contexts, can depend on the catalog shape without
437
437
  * dragging the whole kernel barrel and risking a cycle.
438
438
  */
439
439
  /**
440
440
  * Single row of the runtime annotation-contribution catalog surfaced by
441
441
  * `kernel.getRegisteredAnnotationKeys()`. One row per (plugin × key)
442
442
  * tuple. Built-in catalog keys from `annotations.schema.json` are NOT
443
- * included this catalog is plugin-only; the UI knows the built-in
443
+ * included, this catalog is plugin-only; the UI knows the built-in
444
444
  * catalog via the schema bundle.
445
445
  */
446
446
  interface IRegisteredAnnotationKey {
@@ -453,17 +453,17 @@ interface IRegisteredAnnotationKey {
453
453
  }
454
454
 
455
455
  /**
456
- * Step 11.x runtime view-contribution catalog types.
456
+ * Step 11.x, runtime view-contribution catalog types.
457
457
  *
458
458
  * Lives in its own module (rather than `kernel/index.ts`) so consumers
459
- * deep inside the kernel `IAnalyzerContext`, the BFF route factories,
460
- * future Action contexts can depend on the catalog shape without
459
+ * deep inside the kernel, `IAnalyzerContext`, the BFF route factories,
460
+ * future Action contexts, can depend on the catalog shape without
461
461
  * dragging the whole kernel barrel and risking a cycle.
462
462
  *
463
463
  * Mirrors `annotation-catalog.ts` for the annotation contribution side
464
464
  * (Step 9.6.6). The two systems share the "plugin contributes data,
465
465
  * kernel exposes catalog, UI renders" pattern but never overlap in
466
- * storage or routing see `architecture.md` §View contribution system
466
+ * storage or routing, see `architecture.md` §View contribution system
467
467
  * for the comparison table.
468
468
  *
469
469
  * **Closed catalog by design.** Both `TSlotName` and `TInputTypeName`
@@ -545,7 +545,7 @@ interface IViewContribution {
545
545
  * `order: 'priority'` sort contributions ASC by this value, with
546
546
  * alphabetical tie-break by qualified id. The plugin uses this to
547
547
  * suggest where its contribution belongs relative to others sharing
548
- * the same slot the slot has the final say.
548
+ * the same slot, the slot has the final say.
549
549
  */
550
550
  priority?: number;
551
551
  }
@@ -556,7 +556,7 @@ interface IViewContribution {
556
556
  * by `loadPluginRuntime` from every loaded extension's
557
557
  * `viewContributions` map.
558
558
  *
559
- * The qualified id is `<pluginId>/<extensionId>/<contributionId>` —
559
+ * The qualified id is `<pluginId>/<extensionId>/<contributionId>`,
560
560
  * matches the qualified id pattern used elsewhere in the kernel
561
561
  * (`<pluginId>/<extensionId>` for extensions; this adds the third
562
562
  * segment for per-contribution identity).
@@ -662,7 +662,7 @@ interface ISetting_KeyValueList extends ISettingCommon {
662
662
  }
663
663
  /**
664
664
  * Discriminated union of every setting declaration shape. The plugin
665
- * author NEVER writes JSON Schema for settings they pick one of
665
+ * author NEVER writes JSON Schema for settings, they pick one of
666
666
  * these `type` values and supply per-type parameters.
667
667
  *
668
668
  * Mirror of `input-types.schema.json#/$defs/ISettingDeclaration`.
@@ -680,7 +680,7 @@ type TSettingValue = string | string[] | boolean | number | ISetting_KeyValueLis
680
680
  * Base manifest shape shared by every extension kind. Mirrors
681
681
  * `spec/schemas/extensions/base.schema.json` at the TypeScript level.
682
682
  *
683
- * Spec § A.6 every extension is identified in the registry by the
683
+ * Spec § A.6, every extension is identified in the registry by the
684
684
  * qualified id `<pluginId>/<id>`. The `pluginId` field is required at the
685
685
  * runtime / TS level: built-ins declare it directly in
686
686
  * `src/extensions/built-ins.ts`; user plugins have it injected by the
@@ -688,7 +688,7 @@ type TSettingValue = string | string[] | boolean | number | ISetting_KeyValueLis
688
688
  * registry. A plugin author who hand-codes a `pluginId` that disagrees
689
689
  * with the manifest's `id` is rejected as `invalid-manifest`.
690
690
  *
691
- * The JSON Schema deliberately does NOT model `pluginId` the qualifier
691
+ * The JSON Schema deliberately does NOT model `pluginId`, the qualifier
692
692
  * is a runtime concern composed by the loader, not a manifest field
693
693
  * authors are expected to set. Stripping it before AJV validation in
694
694
  * the loader keeps the spec contract clean ("authors declare only the
@@ -696,7 +696,7 @@ type TSettingValue = string | string[] | boolean | number | ISetting_KeyValueLis
696
696
  */
697
697
 
698
698
  /**
699
- * Step 9.6.6 single entry of an extension's `annotationContributions`
699
+ * Step 9.6.6, single entry of an extension's `annotationContributions`
700
700
  * map. Mirrors `spec/schemas/extensions/base.schema.json#/properties/annotationContributions/additionalProperties`.
701
701
  *
702
702
  * `schema` is an INLINE JSON Schema (object literal in the manifest),
@@ -707,14 +707,14 @@ interface IAnnotationContribution {
707
707
  /** Inline JSON Schema describing the value written under this key. */
708
708
  schema: Record<string, unknown>;
709
709
  /**
710
- * Conflict policy. `shared` (default) multiple plugins MAY write
711
- * the key; `exclusive` only this plugin may. REQUIRED to be
710
+ * Conflict policy. `shared` (default), multiple plugins MAY write
711
+ * the key; `exclusive`, only this plugin may. REQUIRED to be
712
712
  * `'exclusive'` when `location: 'root'`.
713
713
  */
714
714
  ownership?: 'exclusive' | 'shared';
715
715
  /**
716
- * Where the key lands. `namespaced` (default) under the plugin's
717
- * `<plugin-id>:` block; `root` top-level, alongside `for` /
716
+ * Where the key lands. `namespaced` (default), under the plugin's
717
+ * `<plugin-id>:` block; `root`, top-level, alongside `for` /
718
718
  * `annotations` / `settings` / `audit`. Cross-plugin root-key
719
719
  * collisions on `exclusive` are a fatal startup error.
720
720
  */
@@ -735,7 +735,7 @@ interface IExtensionBase {
735
735
  preconditions?: string[];
736
736
  entry?: string;
737
737
  /**
738
- * Step 9.6.6 plugin-contributed annotation keys. Each entry maps a
738
+ * Step 9.6.6, plugin-contributed annotation keys. Each entry maps a
739
739
  * key name to an inline JSON Schema + ownership + location triple.
740
740
  * The kernel surfaces the aggregate via `kernel.getRegisteredAnnotationKeys()`.
741
741
  * See `IAnnotationContribution` for the field semantics and
@@ -747,7 +747,7 @@ interface IExtensionBase {
747
747
  * contribution id (kebab-case, unique within the extension) to a
748
748
  * `IViewContribution` declaration that picks a view slot by name
749
749
  * from the closed kernel catalog (`view-catalog.ts#TSlotName`).
750
- * The slot fixes both the renderer and the payload shape there
750
+ * The slot fixes both the renderer and the payload shape, there
751
751
  * is no separate "contract" abstraction. The kernel validates each
752
752
  * `slot` pick at load time (`invalid-manifest` on miss); the plugin
753
753
  * emits per-node payloads via `ctx.emitContribution(<contributionId>,
@@ -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
@@ -817,7 +768,7 @@ interface ProgressEmitterPort {
817
768
  * typed DTOs from `@skill-map/spec` is deferred to a future iteration when a
818
769
  * third consumer (real providers / extractors / rules) forces a single
819
770
  * source of truth. Until then, both `ui/src/models/` and `src/kernel/types/`
820
- * hand-curate their own local mirror the risk of drift is accepted at
771
+ * hand-curate their own local mirror, the risk of drift is accepted at
821
772
  * this scale (17 schemas) and flagged in the roadmap.
822
773
  */
823
774
 
@@ -827,7 +778,7 @@ interface ProgressEmitterPort {
827
778
  * tables with explicit migrations (mode `dedicated`). Absent = the plugin
828
779
  * does not persist state at all.
829
780
  *
830
- * Optional output-schema declarations (spec § A.12 opt-in correctness
781
+ * Optional output-schema declarations (spec § A.12, opt-in correctness
831
782
  * for plugin custom storage):
832
783
  * - Mode `kv` → `schema` (single relative path). Validates the value
833
784
  * written by `ctx.store.set(key, value)`.
@@ -851,13 +802,13 @@ type TPluginStorage = {
851
802
  /**
852
803
  * Toggle granularity for a plugin / built-in bundle.
853
804
  *
854
- * - `'bundle'` the plugin id is the only enable/disable key. The whole
805
+ * - `'bundle'` , the plugin id is the only enable/disable key. The whole
855
806
  * bundle of extensions follows the toggle; the user cannot
856
807
  * enable some extensions of the bundle and disable others.
857
808
  * Default for plugins (and for the built-in `claude`
858
809
  * bundle, where the provider and its kind-aware extractors
859
810
  * form a coherent provider).
860
- * - `'extension'` each extension is independently toggle-able under its
811
+ * - `'extension'`, each extension is independently toggle-able under its
861
812
  * qualified id `<plugin-id>/<extension-id>`. Used for
862
813
  * the built-in `core` bundle (every kernel built-in
863
814
  * rule / formatter is removable per spec
@@ -894,7 +845,7 @@ interface IPluginManifest {
894
845
  * Plugin user-configurable settings. Each entry picks an `input-type`
895
846
  * from the closed catalog at
896
847
  * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.
897
- * The plugin author NEVER writes JSON Schema they pick `type` by
848
+ * The plugin author NEVER writes JSON Schema, they pick `type` by
898
849
  * name and supply per-type parameters. The kernel exposes resolved
899
850
  * settings to extractors via `ctx.settings.<settingId>`; settings
900
851
  * are read once at extractor invocation; changing a setting requires
@@ -913,29 +864,31 @@ 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
922
875
  * `status` enum in `spec/schemas/plugins-registry.schema.json`.
923
876
  *
924
- * - `enabled` manifest valid, specCompat satisfied, every
877
+ * - `enabled` , manifest valid, specCompat satisfied, every
925
878
  * extension imported and validated.
926
- * - `disabled` user-toggled off via `sm plugins disable` or
879
+ * - `disabled` , user-toggled off via `sm plugins disable` or
927
880
  * `settings.json#/plugins/<id>/enabled`. Manifest
928
881
  * is parsed and surfaced (so `sm plugins list`
929
882
  * shows it), but extensions are not imported.
930
- * - `incompatible-spec` manifest parsed but `semver.satisfies` failed.
931
- * - `invalid-manifest` `plugin.json` missing, unparseable, AJV-fails,
883
+ * - `incompatible-spec` , manifest parsed but `semver.satisfies` failed.
884
+ * - `invalid-manifest` , `plugin.json` missing, unparseable, AJV-fails,
932
885
  * OR the directory name does not equal the
933
886
  * manifest id (a cheap structural rule that
934
887
  * rules out same-root collisions by construction:
935
888
  * a filesystem cannot contain two siblings with
936
889
  * the same name).
937
- * - `load-error` manifest passed, an extension module failed.
938
- * - `id-collision` two plugins reachable from different roots
890
+ * - `load-error` , manifest passed, an extension module failed.
891
+ * - `id-collision` , two plugins reachable from different roots
939
892
  * (project + global, or any `--plugin-dir`
940
893
  * combination) declared the same `id`. Both
941
894
  * collided plugins receive this status; no
@@ -947,7 +900,7 @@ interface ILoadedExtension {
947
900
  kind: ExtensionKind;
948
901
  id: string;
949
902
  /**
950
- * Owning plugin namespace `manifest.id` of the `plugin.json` that
903
+ * Owning plugin namespace, `manifest.id` of the `plugin.json` that
951
904
  * declared this extension. Composed with `id` to form the qualified
952
905
  * registry key `<pluginId>/<id>`. Per spec § A.6 the loader injects
953
906
  * this from the manifest; an extension that hand-declares a
@@ -959,7 +912,7 @@ interface ILoadedExtension {
959
912
  /** Raw module namespace as returned by the dynamic `import()`. */
960
913
  module: unknown;
961
914
  /**
962
- * Runtime extension instance ready for the registry / orchestrator
915
+ * Runtime extension instance ready for the registry / orchestrator,
963
916
  * the `default` export of `module` (or the module itself when no
964
917
  * default), shallow-cloned with `pluginId` injected per spec § A.6.
965
918
  *
@@ -973,7 +926,7 @@ interface ILoadedExtension {
973
926
  interface IDiscoveredPlugin {
974
927
  /** Absolute path to the plugin directory. */
975
928
  path: string;
976
- /** Plugin id populated from the manifest if it parsed, else a path hint. */
929
+ /** Plugin id, populated from the manifest if it parsed, else a path hint. */
977
930
  id: string;
978
931
  status: TPluginLoadStatus;
979
932
  /** Only present when status === 'enabled' or 'incompatible-spec'. */
@@ -987,20 +940,20 @@ interface IDiscoveredPlugin {
987
940
  */
988
941
  granularity?: TGranularity;
989
942
  /**
990
- * Runtime-only never persisted, never spec-modeled.
943
+ * Runtime-only, never persisted, never spec-modeled.
991
944
  *
992
- * Spec § A.12 opt-in JSON Schema validation for plugin custom storage.
945
+ * Spec § A.12, opt-in JSON Schema validation for plugin custom storage.
993
946
  * Populated by the loader when `manifest.storage.schemas` (Mode B) or
994
947
  * `manifest.storage.schema` (Mode A) declares schema paths the loader
995
948
  * successfully read and AJV-compiled. Consumed by the runtime store
996
949
  * wrapper to validate `ctx.store.write(table, row)` (Mode B) and
997
950
  * `ctx.store.set(key, value)` (Mode A) before persisting.
998
951
  *
999
- * Mode B layout keyed by logical table name (without the
952
+ * Mode B layout, keyed by logical table name (without the
1000
953
  * `plugin_<normalizedId>_` prefix), matching the manifest's `schemas`
1001
954
  * map. Tables not present in the map accept any shape (permissive).
1002
955
  *
1003
- * Mode A layout uses the sentinel key `__kv__` for the single
956
+ * Mode A layout, uses the sentinel key `__kv__` for the single
1004
957
  * value-shape schema. The sentinel survives the runtime contract change
1005
958
  * if Mode A ever grows multiple namespaces.
1006
959
  *
@@ -1013,7 +966,7 @@ interface IDiscoveredPlugin {
1013
966
  reason?: string;
1014
967
  }
1015
968
  /**
1016
- * Runtime-only a single AJV-compiled storage schema attached to a
969
+ * Runtime-only, a single AJV-compiled storage schema attached to a
1017
970
  * loaded plugin. The schema path (relative to the plugin directory) is
1018
971
  * preserved so error messages can name the offending file. `validate`
1019
972
  * is the AJV `ValidateFunction` itself: it returns `true` on shape
@@ -1035,20 +988,20 @@ interface IPluginStorageSchema {
1035
988
  }
1036
989
 
1037
990
  /**
1038
- * Plugin store wrappers runtime injection for `ctx.store` per spec
991
+ * Plugin store wrappers, runtime injection for `ctx.store` per spec
1039
992
  * § A.12 (opt-in `outputSchema` for plugin custom storage).
1040
993
  *
1041
994
  * Two shapes, mirroring the manifest's storage modes documented in
1042
995
  * `spec/plugin-kv-api.md`:
1043
996
  *
1044
- * - Mode A `KvStore.set(key, value)`. AJV-validates `value` against
997
+ * - Mode A, `KvStore.set(key, value)`. AJV-validates `value` against
1045
998
  * the schema declared by `manifest.storage.schema` (single
1046
999
  * value-shape) when present. Absent = permissive.
1047
- * - Mode B `DedicatedStore.write(table, row)`. AJV-validates `row`
1000
+ * - Mode B, `DedicatedStore.write(table, row)`. AJV-validates `row`
1048
1001
  * against the per-table schema declared in `manifest.storage.schemas`
1049
1002
  * when present. Tables absent from the map accept any shape.
1050
1003
  *
1051
- * Both wrappers are storage-engine agnostic they accept a `persist`
1004
+ * Both wrappers are storage-engine agnostic, they accept a `persist`
1052
1005
  * callback the caller supplies. The persistence side (SQLite, in-memory,
1053
1006
  * mock) is the caller's concern; this wrapper's only job is the
1054
1007
  * AJV gate. That separation lets the test suite exercise the validator
@@ -1057,7 +1010,7 @@ interface IPluginStorageSchema {
1057
1010
  * unchanged.
1058
1011
  *
1059
1012
  * Universal validation (`emitLink` against `link.schema.json`,
1060
- * `enrichNode` against `node.schema.json`) is unaffected it lives on
1013
+ * `enrichNode` against `node.schema.json`) is unaffected, it lives on
1061
1014
  * the orchestrator side and runs regardless of the plugin's
1062
1015
  * `outputSchema` opt-in.
1063
1016
  */
@@ -1083,7 +1036,7 @@ interface IDedicatedStorePersist {
1083
1036
  * schema path and AJV errors; persistence is skipped on failure.
1084
1037
  *
1085
1038
  * `pluginId` is captured for diagnostics (the throw message names the
1086
- * plugin). The wrapper does NOT itself scope by plugin id that is
1039
+ * plugin). The wrapper does NOT itself scope by plugin id, that is
1087
1040
  * the persistence layer's job (the spec's `state_plugin_kvs` PK includes
1088
1041
  * `pluginId` and the kernel-side adapter prepends it before write).
1089
1042
  */
@@ -1091,7 +1044,7 @@ interface IKvStoreWrapper {
1091
1044
  set(key: string, value: unknown): Promise<void>;
1092
1045
  }
1093
1046
  /**
1094
- * Union shape exposed to extractors via `ctx.store`. Spec § A.12 Mode A
1047
+ * Union shape exposed to extractors via `ctx.store`. Spec § A.12, Mode A
1095
1048
  * (`kv`) returns a `set(key, value)` surface; Mode B (`dedicated`) returns
1096
1049
  * `write(table, row)`. Plugin authors narrow at the call site based on
1097
1050
  * the storage mode declared in their `plugin.json`.
@@ -1105,7 +1058,7 @@ declare function makeKvStoreWrapper(opts: {
1105
1058
  /**
1106
1059
  * Mode B wrapper. `write(table, row)` AJV-validates `row` against
1107
1060
  * `storageSchemas[table]` when declared, then forwards to `persist`.
1108
- * Tables absent from the map are permissive the wrapper forwards
1061
+ * Tables absent from the map are permissive, the wrapper forwards
1109
1062
  * straight to `persist` without validation.
1110
1063
  *
1111
1064
  * The wrapper accepts the full `storageSchemas` map (rather than a
@@ -1135,7 +1088,7 @@ declare function makePluginStore(opts: {
1135
1088
  }): IPluginStore | undefined;
1136
1089
 
1137
1090
  /**
1138
- * `scan_contributions` adapter replace-all writer used by
1091
+ * `scan_contributions` adapter, replace-all writer used by
1139
1092
  * `persistScanResult`, plus read helpers consumed by the BFF
1140
1093
  * (`/api/contributions/...`) and rules (`core/contribution-orphan`).
1141
1094
  *
@@ -1148,7 +1101,7 @@ declare function makePluginStore(opts: {
1148
1101
  * scan is a fresh snapshot, so prior rows are deleted before insert.
1149
1102
  * Wrapped in the same transaction `persistScanResult` opens.
1150
1103
  *
1151
- * The rename heuristic does NOT need to migrate `node_path` here
1104
+ * The rename heuristic does NOT need to migrate `node_path` here,
1152
1105
  * because of replace-all, every contribution is re-emitted on the new
1153
1106
  * path automatically. Keeping the rename path lighter than `state_*`
1154
1107
  * (which IS rename-migrated because state survives across scans).
@@ -1168,7 +1121,7 @@ interface IContributionRecord {
1168
1121
  contributionId: string;
1169
1122
  /**
1170
1123
  * Closed enum value mirroring `view-slots.schema.json#/$defs/SlotName`.
1171
- * Persisted as TEXT (no SQL CHECK by design see migration comment).
1124
+ * Persisted as TEXT (no SQL CHECK by design, see migration comment).
1172
1125
  */
1173
1126
  slot: string;
1174
1127
  /** Already-validated payload. Serialised via `JSON.stringify` at write. */
@@ -1192,7 +1145,7 @@ interface IPersistedContribution {
1192
1145
  }
1193
1146
 
1194
1147
  /**
1195
- * `loadScanResult` driving inverse of `persistScanResult`. Reads the
1148
+ * `loadScanResult`, driving inverse of `persistScanResult`. Reads the
1196
1149
  * `scan_*` tables and reconstructs a `ScanResult` shape so the
1197
1150
  * orchestrator can run an incremental scan (`sm scan --changed`) on
1198
1151
  * top of a prior snapshot.
@@ -1205,7 +1158,7 @@ interface IPersistedContribution {
1205
1158
  *
1206
1159
  * **Documented omission**: external pseudo-links (those whose target is
1207
1160
  * an `http://` / `https://` URL emitted by the external-url-counter
1208
- * extractor) are NEVER persisted to `scan_links` only their per-node
1161
+ * extractor) are NEVER persisted to `scan_links`, only their per-node
1209
1162
  * count survives in `scan_nodes.external_refs_count`. Therefore the
1210
1163
  * `result.links` returned by `loadScanResult` contains only internal
1211
1164
  * graph links, and `node.externalRefsCount` is the authoritative count
@@ -1232,11 +1185,11 @@ interface IPersistedContribution {
1232
1185
  * `durationMs`; the three count fields derive from row counts.
1233
1186
  *
1234
1187
  * Both branches keep `nodesCount` / `linksCount` / `issuesCount` derived
1235
- * from `COUNT(*)` of the loaded rows never persisted, always recomputed.
1188
+ * from `COUNT(*)` of the loaded rows, never persisted, always recomputed.
1236
1189
  */
1237
1190
 
1238
1191
  /**
1239
- * Spec § A.9 load the fine-grained Extractor cache as a per-node map
1192
+ * Spec § A.9, load the fine-grained Extractor cache as a per-node map
1240
1193
  * from qualified extractor id (`<pluginId>/<id>`) to the run-time
1241
1194
  * hashes the extractor recorded on its last run. Empty map is the
1242
1195
  * default when the table is empty (fresh DB, never-scanned scope, or
@@ -1252,6 +1205,61 @@ 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
+
1236
+ /**
1237
+ * Diagnostic surfaced by a parser when the raw input was structurally
1238
+ * malformed (e.g. YAML parse error). The parser MUST still return a
1239
+ * usable `{ frontmatter, frontmatterRaw, body }` triple (defaults are
1240
+ * fine) so the scan keeps making progress; this carries the message
1241
+ * the orchestrator translates into a kernel `Issue` with severity
1242
+ * `warn` (and `error` under `--strict`).
1243
+ *
1244
+ * Pure data: parsers never log or throw; they describe the failure
1245
+ * here and let the orchestrator decide how to surface it.
1246
+ */
1247
+ interface IParseIssue {
1248
+ /**
1249
+ * Stable tag describing the failure class. The only emitter today
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;
1261
+ }
1262
+
1255
1263
  /**
1256
1264
  * Provider runtime contract. Walks filesystem roots and emits raw node
1257
1265
  * records; classification maps path conventions to a node kind.
@@ -1286,6 +1294,14 @@ interface IRawNode {
1286
1294
  frontmatterRaw: string;
1287
1295
  /** Parsed frontmatter, or `{}` when absent / unparseable. */
1288
1296
  frontmatter: Record<string, unknown>;
1297
+ /**
1298
+ * Parser diagnostics (audit L1). Populated by the walker when the
1299
+ * parser surfaced `IParseIssue` entries (e.g. malformed YAML).
1300
+ * Carried through `processRawNode` and converted into warn-level
1301
+ * kernel `Issue` rows inside `buildFreshNodeAndValidateFrontmatter`.
1302
+ * Empty / undefined on the happy path.
1303
+ */
1304
+ parseIssues?: readonly IParseIssue[];
1289
1305
  }
1290
1306
  /**
1291
1307
  * One entry in a Provider's `kinds` map. Declares both the per-kind
@@ -1341,7 +1357,7 @@ interface IProviderKind {
1341
1357
  * intent (label + base color, optional dark variant + emoji + icon);
1342
1358
  * the UI derives `bg`/`fg` tints per theme via a deterministic helper
1343
1359
  * and reads the registry from the `kindRegistry` field embedded in REST
1344
- * envelopes. Single source of truth for what a kind looks like the
1360
+ * envelopes. Single source of truth for what a kind looks like, the
1345
1361
  * UI never hardcodes presentation for a built-in kind.
1346
1362
  */
1347
1363
  interface IProviderKindUi {
@@ -1395,15 +1411,6 @@ type TProviderKindIcon = {
1395
1411
  };
1396
1412
  interface IProvider extends IExtensionBase {
1397
1413
  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
1414
  /**
1408
1415
  * Catalog of node kinds this Provider emits. Keyed by kind name. Every
1409
1416
  * kind the Provider can `classify()` MUST have an entry; an entry is
@@ -1426,12 +1433,12 @@ interface IProvider extends IExtensionBase {
1426
1433
  * per-kind schemas compile, so cross-file `$ref` resolution succeeds.
1427
1434
  *
1428
1435
  * Use case: when several kinds share a common base (e.g. Anthropic's
1429
- * merged skill / command frontmatter both extend a shared
1436
+ * merged skill / command frontmatter, both extend a shared
1430
1437
  * `skill-base.schema.json`), the Provider declares the base here so
1431
1438
  * `skill.schema.json` and `command.schema.json` can `$ref` it without
1432
1439
  * duplicating fields.
1433
1440
  *
1434
- * Runtime-only does NOT appear in the spec's `provider.schema.json`
1441
+ * Runtime-only, does NOT appear in the spec's `provider.schema.json`
1435
1442
  * manifest. Manifest-validated schemas remain the per-kind ones in
1436
1443
  * `kinds[<kind>].schema`; auxiliary schemas are an implementation
1437
1444
  * concern of how the runtime composes those.
@@ -1449,7 +1456,7 @@ interface IProvider extends IExtensionBase {
1449
1456
  * so the most common Provider shape needs zero configuration.
1450
1457
  *
1451
1458
  * Precedence: when both `walk()` (runtime field) and `read` are
1452
- * declared, `walk()` wins `read` is ignored. The escape-hatch
1459
+ * declared, `walk()` wins, `read` is ignored. The escape-hatch
1453
1460
  * relationship is intentional: most Providers should use `read`;
1454
1461
  * Providers with non-standard discovery requirements (custom file
1455
1462
  * naming, multi-pass walks, dynamic ignore logic) implement `walk()`
@@ -1466,7 +1473,7 @@ interface IProvider extends IExtensionBase {
1466
1473
  * Non-matching files are silently skipped. Unreadable files produce
1467
1474
  * a diagnostic via the emitter but do not abort the walk.
1468
1475
  *
1469
- * `options.ignoreFilter` when supplied, the Provider MUST
1476
+ * `options.ignoreFilter`, when supplied, the Provider MUST
1470
1477
  * skip every directory and file whose path-relative-to-root the
1471
1478
  * filter reports as ignored. Providers MAY also keep their own
1472
1479
  * hard-coded skip list (e.g. `.git`) as a defensive measure, but the
@@ -1474,14 +1481,14 @@ interface IProvider extends IExtensionBase {
1474
1481
  *
1475
1482
  * Optional. When omitted, the Provider MUST declare `read` (or rely
1476
1483
  * on the default config). The orchestrator never calls `walk()`
1477
- * directly it goes through `resolveProviderWalk(provider)` which
1484
+ * directly, it goes through `resolveProviderWalk(provider)` which
1478
1485
  * picks `walk` over `read`.
1479
1486
  */
1480
1487
  walk?(roots: string[], options?: {
1481
1488
  ignoreFilter?: IIgnoreFilter;
1482
1489
  }): AsyncIterable<IRawNode>;
1483
1490
  /**
1484
- * Given a path and its parsed frontmatter, decide the node kind or
1491
+ * Given a path and its parsed frontmatter, decide the node kind, or
1485
1492
  * `null` to disclaim the file. The classifier is called after walk()
1486
1493
  * yields; with multiple Providers active, every Provider walks every
1487
1494
  * file matching its `read.extensions`, so each Provider MUST disclaim
@@ -1528,18 +1535,18 @@ interface IProviderReadConfig {
1528
1535
  * Extractors are deterministic-only. They run synchronously inside the
1529
1536
  * scan loop; LLM-driven enrichment of a node is an Action concern, not
1530
1537
  * an Extractor concern. The Extractor context therefore exposes no
1531
- * `RunnerPort` see spec `architecture.md` §Execution modes.
1538
+ * `RunnerPort`, see spec `architecture.md` §Execution modes.
1532
1539
  *
1533
1540
  * Output channels (all on the context):
1534
1541
  *
1535
- * - `ctx.emitLink(link)` persist a link in the kernel's `links` table.
1542
+ * - `ctx.emitLink(link)`, persist a link in the kernel's `links` table.
1536
1543
  * Validated against `emitsLinkKinds` before insertion; an off-contract
1537
1544
  * kind drops the link and surfaces an `extension.error` event.
1538
- * - `ctx.enrichNode(partial)` merge canonical, kernel-curated properties
1545
+ * - `ctx.enrichNode(partial)`, merge canonical, kernel-curated properties
1539
1546
  * onto the node. Strictly separate from the author-supplied frontmatter
1540
1547
  * (the latter remains immutable and survives verbatim). Persistence
1541
1548
  * is spec'd in § A.8.
1542
- * - `ctx.store` plugin-scoped persistence. Present only when the
1549
+ * - `ctx.store`, plugin-scoped persistence. Present only when the
1543
1550
  * plugin declares `storage.mode` in `plugin.json`; shape depends on the
1544
1551
  * mode (`KvStore` for mode A, scoped `Database` for mode B). See
1545
1552
  * `plugin-kv-api.md` for the contract.
@@ -1578,7 +1585,7 @@ interface IExtractorCallbacks {
1578
1585
  * extension-local Record key declared under
1579
1586
  * `extension.viewContributions[<contributionId>]`; the second is a
1580
1587
  * payload that conforms to the slot's payload schema in
1581
- * `spec/schemas/view-slots.schema.json#/$defs/payloads/<slot>` —
1588
+ * `spec/schemas/view-slots.schema.json#/$defs/payloads/<slot>`,
1582
1589
  * where `<slot>` is the slot the manifest declared for this
1583
1590
  * contribution. The orchestrator validates the payload against the
1584
1591
  * slot's schema before persisting to `scan_contributions`; off-shape
@@ -1601,7 +1608,7 @@ interface IExtractorContext extends IExtractorCallbacks {
1601
1608
  * (`write(table, row)`). See `spec/plugin-kv-api.md`.
1602
1609
  *
1603
1610
  * Typed as `unknown` so this contract module stays free of any
1604
- * adapter-side imports the concrete `IPluginStore` lives in
1611
+ * adapter-side imports, the concrete `IPluginStore` lives in
1605
1612
  * `kernel/adapters/plugin-store.js`. Plugin authors narrow at the
1606
1613
  * call site based on the storage mode declared in their manifest.
1607
1614
  * The orchestrator looks up the wrapper per-extractor in
@@ -1618,11 +1625,11 @@ interface IExtractor extends IExtensionBase {
1618
1625
  /**
1619
1626
  * Optional opt-in filter on `node.kind`. When declared, the orchestrator
1620
1627
  * skips invocation of `extract()` for any node whose `kind` is NOT in
1621
- * this list fail-fast, before context construction, so the extractor
1628
+ * this list, fail-fast, before context construction, so the extractor
1622
1629
  * wastes zero CPU on inapplicable nodes.
1623
1630
  *
1624
1631
  * Absent (`undefined`) is the default: the extractor applies to every
1625
- * kind. There are no wildcards the absence of the field already
1632
+ * kind. There are no wildcards, the absence of the field already
1626
1633
  * encodes "every kind". An empty array (`[]`) is rejected at load
1627
1634
  * time by AJV (`minItems: 1` in the schema).
1628
1635
  *
@@ -1647,13 +1654,13 @@ interface IExtractor extends IExtensionBase {
1647
1654
  * findings into the UI via view contributions. Deterministic analyzers
1648
1655
  * are pure (same graph in → same issues out) and run synchronously
1649
1656
  * inside `sm scan` / `sm check`. Probabilistic analyzers invoke an LLM
1650
- * through the kernel's `RunnerPort` and dispatch only as queued jobs
1657
+ * through the kernel's `RunnerPort` and dispatch only as queued jobs,
1651
1658
  * they never participate in scan-time pipelines. Mode is declared in
1652
1659
  * the manifest (default `deterministic`).
1653
1660
  */
1654
1661
 
1655
1662
  /**
1656
- * Step 9.6.2 orphan sidecar entry surfaced to analyzers. A `.sm` file
1663
+ * Step 9.6.2, orphan sidecar entry surfaced to analyzers. A `.sm` file
1657
1664
  * whose sibling `.md` does not exist on disk; the `annotation-orphan`
1658
1665
  * built-in analyzer emits one warning per entry. Other analyzers that
1659
1666
  * care about orphan sidecars MAY consume the list too.
@@ -1668,13 +1675,13 @@ interface IAnalyzerContext {
1668
1675
  nodes: Node[];
1669
1676
  links: Link[];
1670
1677
  /**
1671
- * Step 9.6.2 orphaned sidecars discovered during the scan walk.
1678
+ * Step 9.6.2, orphaned sidecars discovered during the scan walk.
1672
1679
  * Empty when sidecar discovery did not run (legacy callers) or
1673
1680
  * when no orphans exist.
1674
1681
  */
1675
1682
  orphanSidecars?: IAnalyzerOrphanSidecar[];
1676
1683
  /**
1677
- * Step 9.6.6 raw parsed sidecar root keyed by `node.path`. Populated
1684
+ * Step 9.6.6, raw parsed sidecar root keyed by `node.path`. Populated
1678
1685
  * by the orchestrator alongside the public `Node.sidecar` overlay so
1679
1686
  * analyzers that inspect plugin namespaces (e.g. the built-in
1680
1687
  * `core/unknown-field` Analyzer) can walk the full tree without
@@ -1684,7 +1691,7 @@ interface IAnalyzerContext {
1684
1691
  */
1685
1692
  sidecarRoots?: ReadonlyMap<string, Record<string, unknown>>;
1686
1693
  /**
1687
- * Step 9.6.6 runtime catalog of plugin-contributed annotation keys,
1694
+ * Step 9.6.6, runtime catalog of plugin-contributed annotation keys,
1688
1695
  * as exposed by `kernel.getRegisteredAnnotationKeys()`. Threaded
1689
1696
  * through so analyzers can reason about the registered-vs-unknown
1690
1697
  * split without reaching back into the kernel. Empty array when no
@@ -1693,20 +1700,20 @@ interface IAnalyzerContext {
1693
1700
  */
1694
1701
  annotationContributions?: readonly IRegisteredAnnotationKey[];
1695
1702
  /**
1696
- * Step 11.x runtime catalog of plugin-contributed view contributions,
1703
+ * Step 11.x, runtime catalog of plugin-contributed view contributions,
1697
1704
  * as exposed by `kernel.getRegisteredViewContributions()`. Threaded
1698
1705
  * through so analyzers can reason about emissions without reaching
1699
- * back into the kernel: built-in `core/unknown-slot` walks this list
1700
- * to detect deprecated slots in use, and `core/contribution-orphan`
1701
- * joins it with the live node set to flag dangling emissions. Empty
1702
- * array when no extension declares view contributions; absent for
1703
- * legacy callers (older runScan sites that never wired the catalog
1704
- * through).
1706
+ * back into the kernel (built-in `core/contribution-orphan` joins it
1707
+ * with the live node set to flag dangling emissions). Slot catalog
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).
1705
1712
  */
1706
1713
  viewContributions?: readonly IRegisteredViewContribution[];
1707
1714
  /**
1708
1715
  * Absolute paths of `*.md` files under the project's
1709
- * `.skill-map/jobs/` that no `state_jobs.filePath` references the
1716
+ * `.skill-map/jobs/` that no `state_jobs.filePath` references, the
1710
1717
  * built-in `core/job-orphan-file` analyzer projects each as a `warn`
1711
1718
  * issue. Pre-computed by the driving adapter (CLI / BFF) inside its
1712
1719
  * already-open storage transaction (mirrors the `orphanSidecars`
@@ -1721,7 +1728,7 @@ interface IAnalyzerContext {
1721
1728
  * link-validation purposes via `scan.referencePaths`. The driving
1722
1729
  * adapter walks each configured path before the scan and collects
1723
1730
  * every existing file's absolute path here. Files in this set are
1724
- * NOT indexed as graph nodes the only consumer is
1731
+ * NOT indexed as graph nodes, the only consumer is
1725
1732
  * `core/broken-ref`, which suppresses its `warn` issue when a
1726
1733
  * path-style link target falls into the set. Absent / empty when
1727
1734
  * the operator left `scan.referencePaths` empty or when the
@@ -1766,6 +1773,20 @@ interface IAnalyzer extends IExtensionBase {
1766
1773
  * `deterministic` per `spec/schemas/extensions/analyzer.schema.json`.
1767
1774
  */
1768
1775
  mode?: TExecutionMode;
1776
+ /**
1777
+ * Qualified `<pluginId>/<id>` Action ids the analyzer recommends to
1778
+ * resolve its findings. Distinct from `Action.precondition` (which
1779
+ * declares which nodes an Action applies to from the Action side);
1780
+ * this field declares which Actions are relevant when this
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).
1788
+ */
1789
+ recommendedActions?: readonly string[];
1769
1790
  evaluate(ctx: IAnalyzerContext): Issue[] | Promise<Issue[]>;
1770
1791
  }
1771
1792
 
@@ -1775,9 +1796,9 @@ interface IAnalyzer extends IExtensionBase {
1775
1796
  *
1776
1797
  * Actions operate on one or more nodes in one of two execution modes:
1777
1798
  *
1778
- * - `deterministic` code runs in-process; the action computes the
1799
+ * - `deterministic`, code runs in-process; the action computes the
1779
1800
  * report synchronously and returns it. No job file, no runner.
1780
- * - `probabilistic` the kernel renders a prompt + preamble into a
1801
+ * - `probabilistic`, the kernel renders a prompt + preamble into a
1781
1802
  * job file; a runner executes it via `RunnerPort` against an LLM;
1782
1803
  * `sm record` closes the job and validates the report against
1783
1804
  * `reportSchemaRef`.
@@ -1787,7 +1808,7 @@ interface IAnalyzer extends IExtensionBase {
1787
1808
  * probabilistic) lands with the job subsystem (Decision #114 in
1788
1809
  * `ROADMAP.md`). Today the loader still validates `kind: 'action'`
1789
1810
  * manifests against `extension-action.schema.json` and the registry
1790
- * holds them `sm actions show` and the precondition gating UI consume
1811
+ * holds them, `sm actions show` and the precondition gating UI consume
1791
1812
  * the manifest data. The runtime entry point is intentionally absent
1792
1813
  * from `IAction` so plugin authors don't ship a method the kernel will
1793
1814
  * not call until the job subsystem is in place; when it ships, the
@@ -1795,21 +1816,21 @@ interface IAnalyzer extends IExtensionBase {
1795
1816
  *
1796
1817
  * Mirrors `extensions/action.schema.json`:
1797
1818
  *
1798
- * - `mode` (required) discriminator between the two modes.
1799
- * - `reportSchemaRef` (required) JSON Schema reference the report
1819
+ * - `mode` (required), discriminator between the two modes.
1820
+ * - `reportSchemaRef` (required), JSON Schema reference the report
1800
1821
  * MUST validate against. MUST extend `report-base.schema.json`.
1801
- * - `promptTemplateRef` REQUIRED when `mode: 'probabilistic'`,
1822
+ * - `promptTemplateRef`, REQUIRED when `mode: 'probabilistic'`,
1802
1823
  * FORBIDDEN when `mode: 'deterministic'`. The schema's conditional
1803
1824
  * `allOf` enforces both directions; the runtime contract simply
1804
1825
  * surfaces the field as optional and lets the loader catch shape
1805
1826
  * violations at AJV time.
1806
- * - `expectedDurationSeconds` REQUIRED for probabilistic (drives
1827
+ * - `expectedDurationSeconds`, REQUIRED for probabilistic (drives
1807
1828
  * TTL); advisory for deterministic.
1808
- * - `precondition` declarative filter consumed by `--all` fan-out,
1829
+ * - `precondition`, declarative filter consumed by `--all` fan-out,
1809
1830
  * UI button gating, `sm actions show`.
1810
- * - `expectedTools` hint to Skill / CLI runners about expected
1831
+ * - `expectedTools`, hint to Skill / CLI runners about expected
1811
1832
  * tools (no normative enforcement in v0).
1812
- * - `fanOutPolicy` `'per-node'` (default) vs `'batch'`.
1833
+ * - `fanOutPolicy`, `'per-node'` (default) vs `'batch'`.
1813
1834
  */
1814
1835
 
1815
1836
  /**
@@ -1817,10 +1838,10 @@ interface IAnalyzer extends IExtensionBase {
1817
1838
  * future write kinds (storage rows, plugin KV, etc.) can land additively
1818
1839
  * without breaking consumers that only handle `kind: 'sidecar'`.
1819
1840
  *
1820
- * - `path` absolute path to the `.sm` file the kernel must materialise
1841
+ * - `path`, absolute path to the `.sm` file the kernel must materialise
1821
1842
  * the change into. Resolved by the Action from the node's absolute
1822
1843
  * path via `sidecarPathFor()`.
1823
- * - `changes` partial sidecar root used as a deep-merge patch (NOT a
1844
+ * - `changes`, partial sidecar root used as a deep-merge patch (NOT a
1824
1845
  * full replacement). Arrays REPLACE; objects RECURSE. Reason:
1825
1846
  * sidecars are shared-write between skill-map core and plugins;
1826
1847
  * a full replace would clobber `<plugin-id>:` namespaced blocks.
@@ -1833,7 +1854,7 @@ type TActionWrite = {
1833
1854
  /**
1834
1855
  * Result envelope returned by deterministic Actions. The `report` field
1835
1856
  * carries the typed report payload (each Action declares its shape via
1836
- * `reportSchemaRef`); `writes` is opt-in Actions that do not mutate
1857
+ * `reportSchemaRef`); `writes` is opt-in, Actions that do not mutate
1837
1858
  * persistent state simply omit it.
1838
1859
  */
1839
1860
  interface IActionResult<TReport = unknown> {
@@ -1842,20 +1863,20 @@ interface IActionResult<TReport = unknown> {
1842
1863
  }
1843
1864
  /**
1844
1865
  * Runtime context passed to a deterministic Action's `invoke()` method.
1845
- * Minimal surface Actions stay pure (no IO inside `invoke`); the kernel
1866
+ * Minimal surface, Actions stay pure (no IO inside `invoke`); the kernel
1846
1867
  * materialises any returned `writes` after the call.
1847
1868
  *
1848
- * - `node` the target `Node` the Action operates on. Open-by-design;
1869
+ * - `node`, the target `Node` the Action operates on. Open-by-design;
1849
1870
  * batch / fan-out flows pick the matching nodes upstream.
1850
- * - `nodeAbsolutePath` absolute path to the node's `.md` file on
1871
+ * - `nodeAbsolutePath`, absolute path to the node's `.md` file on
1851
1872
  * disk. The Action uses this to compute the sidecar path it returns
1852
1873
  * in a `TActionWrite`. Surfaced separately from `node.path` (which is
1853
1874
  * the relative scope-root form) so Actions never compose absolute
1854
1875
  * paths from `node.path` themselves.
1855
- * - `invoker` identity of the caller; written into the sidecar's
1876
+ * - `invoker`, identity of the caller; written into the sidecar's
1856
1877
  * `audit.lastBumpedBy` when the Action chooses to. CLI invocations
1857
1878
  * pass `'cli'`; plugin-driven invocations pass `'plugin:<plugin-id>'`.
1858
- * - `now` clock function; tests inject a deterministic source.
1879
+ * - `now`, clock function; tests inject a deterministic source.
1859
1880
  * Defaults to `() => new Date()` at the composition root.
1860
1881
  */
1861
1882
  interface IActionContext {
@@ -1866,7 +1887,7 @@ interface IActionContext {
1866
1887
  }
1867
1888
  /**
1868
1889
  * Declarative filter applied by `--all` fan-out, UI button gating, and
1869
- * `sm actions show`. All fields optional an empty precondition matches
1890
+ * `sm actions show`. All fields optional, an empty precondition matches
1870
1891
  * every node.
1871
1892
  */
1872
1893
  interface IActionPrecondition {
@@ -1936,14 +1957,14 @@ interface IAction extends IExtensionBase {
1936
1957
  /**
1937
1958
  * Deterministic invocation entry point. OPTIONAL on the runtime
1938
1959
  * contract for backward compatibility with the manifest-only era
1939
- * (Decision #114) actions that ship for the future probabilistic
1960
+ * (Decision #114), actions that ship for the future probabilistic
1940
1961
  * runner / record path leave it absent and the kernel never calls it.
1941
1962
  * Step 9.6.3 (Decision #125) introduces the first concrete consumer:
1942
1963
  * the built-in `bump` Action implements `invoke()` and returns a
1943
1964
  * `writes: [{ kind: 'sidecar', ... }]` payload that the kernel
1944
1965
  * materialises through `ISidecarStore`.
1945
1966
  *
1946
- * Implementations MUST stay pure no IO inside `invoke()`. The Action
1967
+ * Implementations MUST stay pure, no IO inside `invoke()`. The Action
1947
1968
  * computes the patch and returns it; the kernel reads the on-disk
1948
1969
  * sidecar, deep-merges, validates, and writes back inside its critical
1949
1970
  * section.
@@ -1961,10 +1982,10 @@ interface IAction extends IExtensionBase {
1961
1982
  *
1962
1983
  * Two adjacent names live on the same instance:
1963
1984
  *
1964
- * - `formatId: string` the manifest field consumed by the
1985
+ * - `formatId: string`, the manifest field consumed by the
1965
1986
  * `--format <name>` CLI flag. The kernel's lookup is
1966
1987
  * `formatters.find((f) => f.formatId === flag)`.
1967
- * - `format(ctx) → string` the runtime method. Receives the full
1988
+ * - `format(ctx) → string`, the runtime method. Receives the full
1968
1989
  * graph and returns the serialized output. Output MUST be
1969
1990
  * byte-deterministic for the same input (the snapshot-test suite
1970
1991
  * relies on this).
@@ -1978,6 +1999,16 @@ interface IFormatterContext {
1978
1999
  nodes: Node[];
1979
2000
  links: Link[];
1980
2001
  issues: Issue[];
2002
+ /**
2003
+ * Full persisted scan, when the caller has it on hand. Optional so
2004
+ * existing formatters that only consume (nodes, links, issues) keep
2005
+ * working unchanged; formatters whose output mirrors a `ScanResult`
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).
2010
+ */
2011
+ scanResult?: ScanResult;
1981
2012
  }
1982
2013
  interface IFormatter extends IExtensionBase {
1983
2014
  kind: 'formatter';
@@ -1996,7 +2027,7 @@ interface IFormatter extends IExtensionBase {
1996
2027
  * are notification (Slack on `job.completed`), integration glue (CI
1997
2028
  * webhook on `job.failed`), and bookkeeping (per-extractor metrics).
1998
2029
  *
1999
- * The hookable trigger set is INTENTIONALLY SMALL ten events. Eight
2030
+ * The hookable trigger set is INTENTIONALLY SMALL, ten events. Eight
2000
2031
  * are pipeline-driven (emitted from inside `runScan`); two
2001
2032
  * (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving
2002
2033
  * binary before / after the verb runs, fire-and-forget so
@@ -2020,16 +2051,16 @@ interface IFormatter extends IExtensionBase {
2020
2051
  *
2021
2052
  * Curated trigger set (per spec § A.11):
2022
2053
  *
2023
- * 0. `boot` once per CLI process, before verb routing.
2024
- * 1. `scan.started` pre-scan setup (one per scan).
2025
- * 2. `scan.completed` post-scan reaction (one per scan).
2026
- * 3. `extractor.completed` aggregated per-Extractor outputs.
2027
- * 4. `analyzer.completed` aggregated per-Rule outputs.
2028
- * 5. `action.completed` Action executed on a node.
2029
- * 6. `job.spawning` pre-spawn of runner subprocess.
2030
- * 7. `job.completed` most common trigger.
2031
- * 8. `job.failed` alerts, retry triggers.
2032
- * 9. `shutdown` once per CLI process, after the verb's
2054
+ * 0. `boot` , once per CLI process, before verb routing.
2055
+ * 1. `scan.started` , pre-scan setup (one per scan).
2056
+ * 2. `scan.completed` , post-scan reaction (one per scan).
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
2033
2064
  * exit code resolves and before
2034
2065
  * `process.exit`.
2035
2066
  */
@@ -2117,7 +2148,7 @@ interface IHookContext {
2117
2148
  * at load time: when none of the declared triggers carries a given
2118
2149
  * filter field, the loader surfaces `invalid-manifest`. The current
2119
2150
  * impl performs the basic enum check but defers full payload-shape
2120
- * cross-validation to a follow-up the dispatcher is permissive at
2151
+ * cross-validation to a follow-up, the dispatcher is permissive at
2121
2152
  * runtime (an unknown field never matches → the hook simply never
2122
2153
  * fires for that event, which is a correct interpretation of "filter
2123
2154
  * by a field that doesn't exist").
@@ -2148,29 +2179,205 @@ interface IHook extends IExtensionBase {
2148
2179
  * Hook entry point. Returns nothing; reactions are side effects.
2149
2180
  * Errors are caught by the dispatcher (logged as `extension.error`,
2150
2181
  * surfaced via `hook.failed` meta-event) and NEVER block the main
2151
- * pipeline a buggy hook degrades gracefully.
2182
+ * pipeline, a buggy hook degrades gracefully.
2152
2183
  */
2153
2184
  on(ctx: IHookContext): void | Promise<void>;
2154
2185
  }
2155
2186
 
2156
2187
  /**
2157
- * Scan orchestrator runs the Provider → extractor → analyzer pipeline across
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;
2235
+ /**
2236
+ * sha256 of the canonical-form sidecar annotations the Extractor saw
2237
+ * at run time. Always populated (an absent sidecar canonicalises to
2238
+ * `{}` so the hash is stable). Used unconditionally by the cache
2239
+ * decision alongside `bodyHashAtRun`: a sidecar-only edit invalidates
2240
+ * the cached run for every applicable Extractor on that node.
2241
+ */
2242
+ sidecarAnnotationsHashAtRun: string;
2243
+ }
2244
+ /**
2245
+ * Spec § A.8, universal enrichment layer.
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:
2251
+ *
2252
+ * - upsert a single row per pair (stable PRIMARY KEY conflict on
2253
+ * re-extract);
2254
+ * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
2255
+ * for last-write-wins per field at read time.
2256
+ *
2257
+ * `value` is the cumulative merge across every `enrichNode` call that
2258
+ * Extractor made for this node within this scan, multiple
2259
+ * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
2260
+ * fold into a single row, but two different Extractors hitting the
2261
+ * same node yield two distinct rows.
2262
+ *
2263
+ * `isProbabilistic` is reserved: Extractors are deterministic-only, so
2264
+ * every record produced by the orchestrator sets it to `false`. The
2265
+ * field is kept on the record (and the row in `node_enrichments`) so a
2266
+ * future Action-issued enrichment can populate it without reshaping
2267
+ * the persistence contract, see spec `architecture.md`
2268
+ * §Extractor · enrichment layer.
2269
+ */
2270
+ interface IEnrichmentRecord {
2271
+ nodePath: string;
2272
+ extractorId: string;
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.).
2284
+ *
2285
+ * Exported so `cli/commands/refresh.ts` can reuse the same wiring it
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).
2289
+ *
2290
+ * Within this call, multiple `enrichNode(partial)` calls from the same
2291
+ * extractor against the same node fold into one record (last-write-wins
2292
+ * per field), same contract as the in-scan path.
2293
+ */
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
+
2315
+ /**
2316
+ * Rename + orphan classification per `spec/db-schema.md` §Rename
2317
+ * detection. Pure: takes the prior `ScanResult` and the current node
2318
+ * set, mutates the supplied `issues` array in place, and returns the
2319
+ * `RenameOp[]` the persistence layer must apply inside the same tx as
2320
+ * the scan zone replace-all.
2321
+ */
2322
+
2323
+ /**
2324
+ * Confidence-tagged plan to repoint `state_*` references from one node
2325
+ * path to another. Emitted by the rename heuristic during `runScan` and
2326
+ * consumed by `persistScanResult` so the FK migration runs inside the
2327
+ * same transaction as the scan zone replace-all.
2328
+ */
2329
+ interface RenameOp {
2330
+ from: string;
2331
+ to: string;
2332
+ confidence: 'high' | 'medium';
2333
+ }
2334
+ /**
2335
+ * Pure rename / orphan classification per `spec/db-schema.md` §Rename
2336
+ * detection. Mutates `issues` in place, caller passes the in-progress
2337
+ * issue list; returns the `RenameOp[]` for the persistence layer to
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> }`.
2355
+ *
2356
+ * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
2357
+ * order so the same input always produces the same matches,
2358
+ * required for reproducible tests and conformance fixtures (the spec
2359
+ * does not prescribe an order, but stability is the obvious contract).
2360
+ */
2361
+ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
2362
+
2363
+ /**
2364
+ * Scan orchestrator, runs the Provider → extractor → analyzer pipeline across
2158
2365
  * every registered extension and emits `ProgressEmitterPort` events in
2159
2366
  * canonical order. The callable extension set is injected via
2160
- * `RunScanOptions.extensions` the Registry holds manifest metadata, the
2367
+ * `RunScanOptions.extensions`, the Registry holds manifest metadata, the
2161
2368
  * callable set holds the runtime instances the orchestrator actually
2162
2369
  * invokes. Separating the two lets `sm plugins` and `sm help` introspect
2163
2370
  * the graph without loading code.
2164
2371
  *
2165
2372
  * With zero registered extensions (or a callable set that carries none)
2166
- * the pipeline still produces a valid zero-filled `ScanResult` the
2373
+ * the pipeline still produces a valid zero-filled `ScanResult`, the
2167
2374
  * kernel-empty-boot invariant.
2168
2375
  *
2169
2376
  * Roots are validated up front: each entry of `RunScanOptions.roots`
2170
2377
  * must exist on disk as a directory. The first failure throws a clear
2171
2378
  * `Error` naming the offending path. This guards every caller (CLI,
2172
2379
  * server, skill-agent) against silently producing a zero-filled
2173
- * `ScanResult` when a Provider walks a non-existent path the bug
2380
+ * `ScanResult` when a Provider walks a non-existent path, the bug
2174
2381
  * that wiped a populated DB via `sm scan -- --dry-run` (clipanion's
2175
2382
  * `--` made `--dry-run` a positional root that did not exist).
2176
2383
  *
@@ -2180,7 +2387,7 @@ interface IHook extends IExtensionBase {
2180
2387
  * `bodyHash` and `frontmatterHash` match. New / modified files run
2181
2388
  * through the full extractor pipeline (including the external-url-counter
2182
2389
  * which produces ephemeral pseudo-links). Rules ALWAYS run over the
2183
- * fully merged graph issue state can change even for an unchanged node
2390
+ * fully merged graph, issue state can change even for an unchanged node
2184
2391
  * (e.g. a previously broken `references` link now resolves because a new
2185
2392
  * node was added). For unchanged nodes the prior `externalRefsCount` is
2186
2393
  * preserved as-is (the external pseudo-links were never persisted, so
@@ -2194,7 +2401,7 @@ interface IHook extends IExtensionBase {
2194
2401
  * entry per `(node, extractor)` so attribution survives into the DB.
2195
2402
  * Persisted into `node_enrichments` (A.8). The author-supplied
2196
2403
  * frontmatter on `node.frontmatter` stays immutable from any Extractor
2197
- * the enrichment layer is the only writable surface, and rules /
2404
+ * , the enrichment layer is the only writable surface, and rules /
2198
2405
  * formatters consume it via `mergeNodeWithEnrichments`.
2199
2406
  * - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).
2200
2407
  * Wired by the driving adapter via `RunScanOptions.pluginStores`,
@@ -2219,17 +2426,6 @@ interface IScanExtensions {
2219
2426
  */
2220
2427
  hooks?: IHook[];
2221
2428
  }
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
2429
  interface RunScanOptions {
2234
2430
  /**
2235
2431
  * Filesystem roots to walk. Spec requires `minItems: 1`; passing an
@@ -2240,13 +2436,13 @@ interface RunScanOptions {
2240
2436
  /** Runtime extension instances. Absent → empty pipeline. */
2241
2437
  extensions?: IScanExtensions;
2242
2438
  /**
2243
- * Step 9.6.6 runtime catalog of plugin-contributed annotation keys
2439
+ * Step 9.6.6, runtime catalog of plugin-contributed annotation keys
2244
2440
  * (the same shape `kernel.getRegisteredAnnotationKeys()` returns).
2245
2441
  * Threaded into the rule pass so `core/unknown-field` can
2246
2442
  * legitimise registered plugin namespaces / root keys without
2247
2443
  * re-walking the manifests. Absent → empty catalog (every plugin
2248
2444
  * key is treated as unknown). Built-in catalog from
2249
- * `annotations.schema.json` is NOT included that is hard-coded
2445
+ * `annotations.schema.json` is NOT included, that is hard-coded
2250
2446
  * inside the rule.
2251
2447
  */
2252
2448
  annotationContributions?: readonly IRegisteredAnnotationKey[];
@@ -2254,8 +2450,12 @@ interface RunScanOptions {
2254
2450
  * Runtime catalog of plugin-contributed view contributions (the same
2255
2451
  * shape `kernel.getRegisteredViewContributions()` returns). Threaded
2256
2452
  * into the rule pass so:
2257
- * - `core/unknown-slot` and `core/contribution-orphan` can
2258
- * introspect the catalog (read-only).
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).
2259
2459
  * - The orchestrator's per-rule emit closure can look up each
2260
2460
  * declared `(contributionId → slot)` pairing for AJV
2261
2461
  * payload validation.
@@ -2326,7 +2526,7 @@ interface RunScanOptions {
2326
2526
  */
2327
2527
  strict?: boolean;
2328
2528
  /**
2329
- * Spec § A.9 fine-grained Extractor cache breadcrumbs from the
2529
+ * Spec § A.9, fine-grained Extractor cache breadcrumbs from the
2330
2530
  * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
2331
2531
  * Loaded from the `scan_extractor_runs` table by the CLI before
2332
2532
  * invoking `runScan`; absent / empty for a fresh DB or an out-of-band
@@ -2346,11 +2546,11 @@ interface RunScanOptions {
2346
2546
  */
2347
2547
  priorExtractorRuns?: Map<string, Map<string, IPriorExtractorRun>>;
2348
2548
  /**
2349
- * Spec § A.12 per-plugin storage wrappers exposed to extractors via
2549
+ * Spec § A.12, per-plugin storage wrappers exposed to extractors via
2350
2550
  * `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves
2351
2551
  * `ctx.store` undefined for that extractor (the existing contract).
2352
2552
  *
2353
- * The kernel does not construct these the driving adapter (CLI,
2553
+ * The kernel does not construct these, the driving adapter (CLI,
2354
2554
  * future server) builds them with `makePluginStore` from
2355
2555
  * `kernel/adapters/plugin-store.js` and threads them through. This
2356
2556
  * keeps the orchestrator persistence-agnostic (the wrapper supplies
@@ -2367,7 +2567,7 @@ interface RunScanOptions {
2367
2567
  * its own FS walk. The driving adapter (CLI, BFF) computes this
2368
2568
  * inside its already-open storage transaction via
2369
2569
  * `findOrphanJobFiles(jobsDir, await port.jobs.listReferencedFilePaths())`
2370
- * mirrors the `orphanSidecars` model where detection lives
2570
+ * mirrors the `orphanSidecars` model where detection lives
2371
2571
  * outside the rule and the rule only projects. Absent / empty when
2372
2572
  * the caller has no jobs context (out-of-band tests, fresh DB,
2373
2573
  * `--no-built-ins`).
@@ -2379,7 +2579,7 @@ interface RunScanOptions {
2379
2579
  * through to `IAnalyzerContext.referenceablePaths` so the built-in
2380
2580
  * `core/broken-ref` rule can suppress its `warn` for path-style
2381
2581
  * links whose target lands in the set. Files are NOT walked by
2382
- * the kernel the driving adapter populates the set before
2582
+ * the kernel, the driving adapter populates the set before
2383
2583
  * calling `runScan`. Absent / empty when the operator left
2384
2584
  * `scan.referencePaths` unconfigured.
2385
2585
  */
@@ -2393,72 +2593,15 @@ interface RunScanOptions {
2393
2593
  */
2394
2594
  cwd?: string;
2395
2595
  }
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
2596
  /**
2454
2597
  * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`
2455
- * the high- and medium-confidence renames the persistence layer must
2598
+ * the high- and medium-confidence renames the persistence layer must
2456
2599
  * apply to `state_*` rows inside the same tx as the scan zone replace-
2457
2600
  * all (per `spec/db-schema.md` §Rename detection). Most callers want
2458
2601
  * `runScan` (which returns just `ScanResult`); the CLI's `sm scan`
2459
2602
  * uses this variant so it can hand the ops off to `persistScanResult`.
2460
2603
  *
2461
- * Also returns `extractorRuns` the Spec § A.9 fine-grained cache
2604
+ * Also returns `extractorRuns`, the Spec § A.9 fine-grained cache
2462
2605
  * breadcrumbs the CLI persists into `scan_extractor_runs` so the next
2463
2606
  * incremental scan can decide per-(node, extractor) whether re-running
2464
2607
  * is required.
@@ -2472,75 +2615,21 @@ declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): P
2472
2615
  freshlyRunTuples: ReadonlySet<string>;
2473
2616
  }>;
2474
2617
  declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
2618
+
2475
2619
  /**
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).
2620
+ * Node-construction helpers: hash a body, canonicalise frontmatter /
2621
+ * sidecar annotations, resolve the sidecar overlay for a given relative
2622
+ * path, and produce a fresh `Node` (validating its frontmatter on the
2623
+ * way out). Also hosts `mergeNodeWithEnrichments` + `IPersistedEnrichment`
2624
+ * the read-time merge of author frontmatter with the A.8 enrichment
2625
+ * layer.
2537
2626
  */
2538
- declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
2627
+
2539
2628
  /**
2540
- * Spec § A.8 produce the merged read-time view of a Node.
2629
+ * Spec § A.8, produce the merged read-time view of a Node.
2541
2630
  *
2542
2631
  * Rules / `sm check` / `sm export` consume `node.frontmatter` directly
2543
- * (deterministic CI-safe baseline author intent, byte-stable). UI / future
2632
+ * (deterministic CI-safe baseline, author intent, byte-stable). UI / future
2544
2633
  * rules that opt into enrichment context call this helper to merge the
2545
2634
  * author frontmatter with the live enrichment layer.
2546
2635
  *
@@ -2554,18 +2643,18 @@ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], iss
2554
2643
  * belongs to the UI layer next to the value.
2555
2644
  * 2. Sort the survivors by `enrichedAt` ASC so iteration order is
2556
2645
  * "oldest first". This makes the spread merge below
2557
- * last-write-wins per field the freshest Extractor's value
2646
+ * last-write-wins per field, the freshest Extractor's value
2558
2647
  * pisar the older one for any conflicting key.
2559
2648
  * 3. Spread-merge each row's `value` over `node.frontmatter`. The
2560
2649
  * author's keys are the base; enrichment keys overlay them.
2561
2650
  *
2562
- * The returned object is a fresh shallow copy mutating it does not
2651
+ * The returned object is a fresh shallow copy, mutating it does not
2563
2652
  * touch the caller's node. The original `node.frontmatter` reference
2564
2653
  * remains accessible via `node.frontmatter` for callers that want the
2565
2654
  * pristine author baseline.
2566
2655
  *
2567
2656
  * @param node Node to merge against; `node.frontmatter` is the base.
2568
- * @param enrichments Per-(node, extractor) enrichment records typically
2657
+ * @param enrichments Per-(node, extractor) enrichment records, typically
2569
2658
  * loaded via `loadNodeEnrichments(db, node.path)` or
2570
2659
  * pre-filtered to this node by the caller.
2571
2660
  * @param opts.includeStale When true, include rows flagged stale. Defaults
@@ -2595,7 +2684,7 @@ interface IPersistedEnrichment {
2595
2684
  }
2596
2685
 
2597
2686
  /**
2598
- * In-memory `ProgressEmitterPort` adapter. No network, no DB just a
2687
+ * In-memory `ProgressEmitterPort` adapter. No network, no DB, just a
2599
2688
  * synchronous fan-out to registered listeners. Used by the default scan
2600
2689
  * orchestrator; the WebSocket-backed emitter that streams to
2601
2690
  * the Web UI lands.
@@ -2612,7 +2701,7 @@ declare class InMemoryProgressEmitter implements ProgressEmitterPort {
2612
2701
  *
2613
2702
  * Wraps `chokidar` behind a small `IFsWatcher` interface so:
2614
2703
  *
2615
- * 1. The CLI command is impl-agnostic swapping chokidar for a
2704
+ * 1. The CLI command is impl-agnostic, swapping chokidar for a
2616
2705
  * different watcher later (Java? Rust port? a future `WatchPort`?)
2617
2706
  * doesn't ripple into the command.
2618
2707
  * 2. Debouncing, batching, and ignore-filter integration live in one
@@ -2661,28 +2750,37 @@ interface ICreateFsWatcherOptions {
2661
2750
  /** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */
2662
2751
  debounceMs: number;
2663
2752
  /**
2664
- * Optional ignore filter same instance the scan walker uses.
2753
+ * Optional ignore filter, same instance the scan walker uses.
2665
2754
  *
2666
2755
  * Two shapes are accepted:
2667
2756
  *
2668
- * - **`IIgnoreFilter`** (the static one) captured by reference at
2757
+ * - **`IIgnoreFilter`** (the static one), captured by reference at
2669
2758
  * construction. Use this when the filter never changes for the
2670
2759
  * lifetime of the watcher (the typical CLI `sm watch` flow).
2671
2760
  *
2672
- * - **`() => IIgnoreFilter | undefined`** (a getter) re-evaluated
2761
+ * - **`() => IIgnoreFilter | undefined`** (a getter), re-evaluated
2673
2762
  * on EVERY chokidar `ignored` predicate call. Use this when the
2674
- * filter can change at runtime e.g. the BFF rebuilds it after
2763
+ * filter can change at runtime, e.g. the BFF rebuilds it after
2675
2764
  * a `.skillmapignore` or `.skill-map/settings.json` edit and
2676
2765
  * wants chokidar to immediately respect the new patterns without
2677
2766
  * tearing down and rebuilding the watcher. A getter that returns
2678
2767
  * `undefined` disables ignore filtering for that call.
2679
2768
  */
2680
2769
  ignoreFilter?: IIgnoreFilter | (() => IIgnoreFilter | undefined) | undefined;
2770
+ /**
2771
+ * Maximum directory traversal depth. `undefined` (default) walks the
2772
+ * tree recursively without bound; `0` limits the watch to the
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.
2777
+ */
2778
+ depth?: number;
2681
2779
  /** Called once per debounced batch. Awaited; concurrent batches are serialised. */
2682
2780
  onBatch: (batch: IWatchBatch) => void | Promise<void>;
2683
2781
  /**
2684
2782
  * Called when the underlying watcher surfaces an error. The watcher
2685
- * stays open callers decide whether to log, keep going, or close.
2783
+ * stays open, callers decide whether to log, keep going, or close.
2686
2784
  */
2687
2785
  onError?: (err: Error) => void;
2688
2786
  }
@@ -2691,7 +2789,7 @@ interface ICreateFsWatcherOptions {
2691
2789
  * returned `ready` promise resolves once chokidar's initial directory
2692
2790
  * walk completes, at which point only NEW events fire `onBatch`.
2693
2791
  *
2694
- * The initial directory walk is deliberately silent we set
2792
+ * The initial directory walk is deliberately silent, we set
2695
2793
  * `ignoreInitial: true`. The CLI runs a one-shot scan before flipping
2696
2794
  * the watcher on, so re-emitting an `add` for every existing file
2697
2795
  * would be redundant churn.
@@ -2699,20 +2797,20 @@ interface ICreateFsWatcherOptions {
2699
2797
  declare function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatcher;
2700
2798
 
2701
2799
  /**
2702
- * Scan delta pure comparison of two `ScanResult` snapshots. Drives
2800
+ * Scan delta, pure comparison of two `ScanResult` snapshots. Drives
2703
2801
  * `sm scan --compare-with <path>` and is the single place the kernel
2704
2802
  * knows how to identify "the same" entity across two scans.
2705
2803
  *
2706
2804
  * **Identity contract** (mirrors decisions made at earlier sub-steps):
2707
2805
  *
2708
2806
  * - **Node**: `node.path`. The path is the only field stable across
2709
- * edits every other Node field is content-derived (hashes, counts,
2807
+ * edits, every other Node field is content-derived (hashes, counts,
2710
2808
  * denormalised frontmatter). Two nodes with the same path are the
2711
2809
  * "same" node; differences are reported as a `changed` entry with
2712
2810
  * a reason narrowing what diverged.
2713
2811
  *
2714
2812
  * - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This
2715
- * mirrors the link-conflict rule and `sm show` aggregation
2813
+ * mirrors the link-conflict rule and `sm show` aggregation,
2716
2814
  * two links with identical endpoints, kind, and (optional) trigger
2717
2815
  * are the same link, even if emitted by different extractors. The
2718
2816
  * `sources[]` union and confidence are NOT part of identity; they
@@ -2720,13 +2818,13 @@ declare function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatche
2720
2818
  * "different" for delta purposes.
2721
2819
  *
2722
2820
  * - **Issue**: `(analyzerId, sorted nodeIds, message)`. Mirrors
2723
- * `spec/job-events.md` §issue.* same key → same issue, even when
2821
+ * `spec/job-events.md` §issue.*, same key → same issue, even when
2724
2822
  * `data` / `severity` / `linkIndices` shift. A meaningful change in
2725
2823
  * `message` (or a different set of node ids) is a different issue.
2726
2824
  * This is the same key future job events will use; keep it aligned
2727
2825
  * so consumers can reuse logic.
2728
2826
  *
2729
- * No "changed" bucket for links / issues identity already captures
2827
+ * No "changed" bucket for links / issues, identity already captures
2730
2828
  * everything that matters there. Nodes get a "changed" bucket because
2731
2829
  * the path stays stable while the body / frontmatter rewrite, and that
2732
2830
  * change is meaningful (formatters, summarisers, downstream consumers
@@ -2772,7 +2870,7 @@ declare function computeScanDelta(prior: ScanResult, current: ScanResult, compar
2772
2870
  declare function isEmptyDelta(delta: IScanDelta): boolean;
2773
2871
 
2774
2872
  /**
2775
- * Export query minimal filter language for `sm export <query>` (Step 8.3).
2873
+ * Export query, minimal filter language for `sm export <query>` (Step 8.3).
2776
2874
  *
2777
2875
  * Spec contract: `spec/cli-contract.md` line 190 says "Query syntax is
2778
2876
  * implementation-defined pre-1.0". This module defines the v0.5.0 syntax.
@@ -2790,12 +2888,12 @@ declare function isEmptyDelta(delta: IScanDelta): boolean;
2790
2888
  *
2791
2889
  * **Filters**:
2792
2890
  *
2793
- * - `kind=skill` / `kind=skill,agent` node kind whitelist.
2794
- * - `has=issues` node must appear in some issue's `nodeIds`. (Future
2891
+ * - `kind=skill` / `kind=skill,agent`, node kind whitelist.
2892
+ * - `has=issues`, node must appear in some issue's `nodeIds`. (Future
2795
2893
  * expansion: `has=findings` / `has=summary` once Step 10 / 11 land.
2796
2894
  * Unknown values are a parse error today; we'll ratchet up the
2797
2895
  * accepted set additively.)
2798
- * - `path=foo/*` / `path=.claude/agents/**` POSIX glob over `node.path`.
2896
+ * - `path=foo/*` / `path=.claude/agents/**`, POSIX glob over `node.path`.
2799
2897
  * Supports `*` (any chars except `/`) and `**` (any chars including `/`).
2800
2898
  *
2801
2899
  * **Subset semantics** (`applyExportQuery`):
@@ -2804,7 +2902,7 @@ declare function isEmptyDelta(delta: IScanDelta): boolean;
2804
2902
  * OR within values).
2805
2903
  * - Links survive only when BOTH endpoints (`source` + `target`) belong
2806
2904
  * to the filtered node set. A subset that includes "edges out to
2807
- * unfiltered nodes" would be confusing the user asked for a focused
2905
+ * unfiltered nodes" would be confusing, the user asked for a focused
2808
2906
  * subgraph, not its boundary. External-URL pseudo-links are already
2809
2907
  * stripped by the orchestrator and never reach this layer.
2810
2908
  * - Issues survive when ANY of the issue's `nodeIds` is in the filtered
@@ -2819,7 +2917,7 @@ interface IExportQuery {
2819
2917
  /** Original query string echoed back so consumers can render the header. */
2820
2918
  raw: string;
2821
2919
  /**
2822
- * Whitelist of node kinds (`node.kind` is open string built-in
2920
+ * Whitelist of node kinds (`node.kind` is open string, built-in
2823
2921
  * Claude catalog `skill` / `agent` / `command` / `hook` / `note`,
2824
2922
  * plus whatever external Providers declare). The query parser does
2825
2923
  * not validate values against a closed enum; an unknown kind simply
@@ -2846,16 +2944,16 @@ declare function applyExportQuery(scan: {
2846
2944
  }, query: IExportQuery): IExportSubset;
2847
2945
 
2848
2946
  /**
2849
- * `scan_node_tags` adapter tags · dual-source persistence layer.
2947
+ * `scan_node_tags` adapter, tags · dual-source persistence layer.
2850
2948
  *
2851
2949
  * One row per `(node_path, tag, source)` triple. Projected at persist
2852
2950
  * time from BOTH `frontmatter.tags` (with `source='author'`) and
2853
2951
  * `sidecar.annotations.tags` (with `source='user'`). The same tag
2854
- * string MAY appear under both sources for the same node the PK
2952
+ * string MAY appear under both sources for the same node, the PK
2855
2953
  * accepts the pair; search returns the node once via DISTINCT, the
2856
2954
  * UI renders both chips with their attribution.
2857
2955
  *
2858
- * Belongs to the `scan_*` family replaced wholesale per scan.
2956
+ * Belongs to the `scan_*` family, replaced wholesale per scan.
2859
2957
  * Cached nodes' tag rows are projected from the cached
2860
2958
  * `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both
2861
2959
  * already in memory at persist time), so the rebuild is cheap
@@ -2880,17 +2978,17 @@ interface ITagRecord {
2880
2978
  * Pure helpers for the "update available" notification feature.
2881
2979
  *
2882
2980
  * Three responsibilities:
2883
- * - `fetchLatestVersion` query `https://registry.npmjs.org/<pkg>/latest`
2981
+ * - `fetchLatestVersion` , query `https://registry.npmjs.org/<pkg>/latest`
2884
2982
  * with `AbortController` + timeout. Throws on
2885
2983
  * non-200 / parse failure / abort.
2886
- * - `compareVersions` semver compare (-1 / 0 / 1). Pre-1.0 aware:
2984
+ * - `compareVersions` , semver compare (-1 / 0 / 1). Pre-1.0 aware:
2887
2985
  * treats prereleases via the standard rules
2888
2986
  * (release > prerelease at the same triple).
2889
- * - `isOutdated` sugar over `compareVersions` for the common
2987
+ * - `isOutdated` , sugar over `compareVersions` for the common
2890
2988
  * "is `latest` strictly greater than `current`"
2891
2989
  * check the banner runs against.
2892
2990
  *
2893
- * Pure kernel module NO `process.env` reads, NO Node globals beyond the
2991
+ * Pure kernel module, NO `process.env` reads, NO Node globals beyond the
2894
2992
  * built-in `fetch` / `AbortController` (Node 22+). Every env / settings
2895
2993
  * lookup happens in `src/cli/util/update-check-banner.ts`, the CLI-side
2896
2994
  * adapter that owns side effects.
@@ -2898,21 +2996,21 @@ interface ITagRecord {
2898
2996
  * The shared cache type (`IUpdateCheckCache`) is used by the storage
2899
2997
  * helpers under `kernel/storage/update-check.ts` and by the BFF's
2900
2998
  * `GET /api/update-status` projection. A second type
2901
- * (`IUpdateStatus`) shapes the BFF response it merges `current`
2999
+ * (`IUpdateStatus`) shapes the BFF response, it merges `current`
2902
3000
  * (from `VERSION`) into the cache so the UI can render without a
2903
- * second lookup. Both stay flat no nested objects so JSON
3001
+ * second lookup. Both stay flat, no nested objects, so JSON
2904
3002
  * serialization is trivial.
2905
3003
  */
2906
3004
  interface IUpdateCheckCache {
2907
3005
  latestVersion: string;
2908
- /** Epoch ms when the registry was last successfully probed. */
3006
+ /** Epoch ms, when the registry was last successfully probed. */
2909
3007
  checkedAt: number;
2910
- /** Epoch ms when the banner was last printed; null = never shown yet. */
3008
+ /** Epoch ms, when the banner was last printed; null = never shown yet. */
2911
3009
  shownAt: number | null;
2912
3010
  }
2913
3011
 
2914
3012
  /**
2915
- * `PluginLoaderPort` discovers plugin directories and loads their
3013
+ * `PluginLoaderPort`, discovers plugin directories and loads their
2916
3014
  * extensions. The shape mirrors what the concrete loader actually
2917
3015
  * exposes (see `kernel/adapters/plugin-loader.ts`); the port exists so
2918
3016
  * the CLI consumes the abstract contract via `createPluginLoader(...)`
@@ -2935,12 +3033,12 @@ interface PluginLoaderPort {
2935
3033
  discoverPaths(): string[];
2936
3034
  /**
2937
3035
  * Discover every plugin, attempt to load each, then apply the
2938
- * cross-root id-collision pass. Never throws failures are reported
3036
+ * cross-root id-collision pass. Never throws, failures are reported
2939
3037
  * via `IDiscoveredPlugin.status`.
2940
3038
  */
2941
3039
  discoverAndLoadAll(): Promise<IDiscoveredPlugin[]>;
2942
3040
  /**
2943
- * Load a single plugin from its directory. Never throws failure is
3041
+ * Load a single plugin from its directory. Never throws, failure is
2944
3042
  * reported via the returned `status`.
2945
3043
  */
2946
3044
  loadOne(pluginPath: string): Promise<IDiscoveredPlugin>;
@@ -2948,7 +3046,7 @@ interface PluginLoaderPort {
2948
3046
 
2949
3047
  /**
2950
3048
  * Row-level filter for `port.scans.findNodes(...)` (driven by
2951
- * `sm list`'s flags). All fields are optional an empty filter
3049
+ * `sm list`'s flags). All fields are optional, an empty filter
2952
3050
  * returns every node sorted by `path` asc.
2953
3051
  */
2954
3052
  interface INodeFilter {
@@ -2972,7 +3070,7 @@ interface INodeFilter {
2972
3070
  limit?: number;
2973
3071
  }
2974
3072
  /**
2975
- * Bundled fetch for `port.scans.findNode(path)` one node and
3073
+ * Bundled fetch for `port.scans.findNode(path)`, one node and
2976
3074
  * everything `sm show <path>` displays alongside it. Every field is
2977
3075
  * computed from `scan_*` zone reads only; per-domain data (history,
2978
3076
  * jobs, plugin enrichments) ships through other namespaces.
@@ -3006,7 +3104,7 @@ interface IPersistOptions {
3006
3104
  enrichments?: IEnrichmentRecord[];
3007
3105
  contributions?: IContributionRecord[];
3008
3106
  /**
3009
- * Phase 3 / View contribution system active runtime catalog of
3107
+ * Phase 3 / View contribution system, active runtime catalog of
3010
3108
  * registered view contributions, keyed by qualified id
3011
3109
  * `<pluginId>/<extensionId>/<contributionId>`. Passed to the
3012
3110
  * `scan_contributions` upsert so the catalog sweep can drop rows
@@ -3018,10 +3116,10 @@ interface IPersistOptions {
3018
3116
  */
3019
3117
  registeredContributionKeys?: ReadonlySet<string>;
3020
3118
  /**
3021
- * Phase 3 / View contribution system set of `(plugin, extension,
3119
+ * Phase 3 / View contribution system, set of `(plugin, extension,
3022
3120
  * node)` tuples where the extension actually RAN against that node
3023
3121
  * in this scan. Format: `<pluginId>/<extensionId>/<nodePath>` (no
3024
- * contribution-id segment the sweep operates at the (plugin,
3122
+ * contribution-id segment, the sweep operates at the (plugin,
3025
3123
  * extension, node) level and inspects the buffer to decide which
3026
3124
  * contribution-ids survive).
3027
3125
  *
@@ -3044,7 +3142,7 @@ interface IPersistOptions {
3044
3142
  freshlyRunTuples?: ReadonlySet<string>;
3045
3143
  }
3046
3144
  /**
3047
- * Issue row as the storage layer sees it paired with its DB-assigned
3145
+ * Issue row as the storage layer sees it, paired with its DB-assigned
3048
3146
  * id so `port.issues.deleteById(id)` can target it inside a
3049
3147
  * transaction. The runtime `Issue` shape (per `issue.schema.json`) does
3050
3148
  * not carry `id` because the spec models issues as ephemeral findings
@@ -3055,6 +3153,54 @@ interface IIssueRow {
3055
3153
  id: number;
3056
3154
  issue: Issue;
3057
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 {
3182
+ /**
3183
+ * Severity tokens to match. Typed as open `string` (not the
3184
+ * `Severity` union) so an unknown value from a URL query string
3185
+ * surfaces as a zero-match SQL query, not a kernel validation
3186
+ * error. The adapter parameterises each entry into the `IN(...)`
3187
+ * clause; unrecognised severities simply match no rows.
3188
+ */
3189
+ severities?: readonly string[];
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
+ }
3058
3204
  /** Output of `port.jobs.pruneTerminal` / `listTerminalCandidates`. */
3059
3205
  interface IPruneResult {
3060
3206
  /** How many `state_jobs` rows were deleted (or would be, in dry-run). */
@@ -3101,7 +3247,7 @@ interface IMigrateNodeFksReport {
3101
3247
  /**
3102
3248
  * Collisions encountered when migrating any of the keyed-by-node
3103
3249
  * `state_*` tables because a row already existed at the destination
3104
- * PK. The pre-existing rows are preserved the migrating rows are
3250
+ * PK. The pre-existing rows are preserved, the migrating rows are
3105
3251
  * dropped (deleted from `fromPath` without a corresponding INSERT).
3106
3252
  * One entry per dropped row, with the affected PK fields included
3107
3253
  * for diagnostic output. `state_node_favorites` has no composite key
@@ -3184,7 +3330,7 @@ interface IPluginApplyResult {
3184
3330
 
3185
3331
  /**
3186
3332
  * Subset of `StoragePort` exposed inside a `transaction(fn)` callback.
3187
- * Lifecycle methods are intentionally omitted a transaction that
3333
+ * Lifecycle methods are intentionally omitted, a transaction that
3188
3334
  * tries to `init()` the adapter mid-flight is a category error.
3189
3335
  *
3190
3336
  * Every callable in the subset MUST run on the same underlying
@@ -3205,7 +3351,7 @@ interface ITransactionalStorage {
3205
3351
  * Upsert a batch of fresh enrichment records produced by an
3206
3352
  * extractor pass. Composite PK is `(nodePath, extractorId)`;
3207
3353
  * conflict → replace. Every row lands with `stale = 0` (the
3208
- * caller just refreshed it; ROADMAP §B.10 staleness is
3354
+ * caller just refreshed it; ROADMAP §B.10, staleness is
3209
3355
  * computed downstream when the body hash changes again).
3210
3356
  */
3211
3357
  upsertMany(records: IEnrichmentRecord[]): Promise<void>;
@@ -3229,23 +3375,23 @@ interface StoragePort {
3229
3375
  * Persist a fresh `ScanResult` (replace-all on the scan zone).
3230
3376
  * Called by `sm scan` after the orchestrator returns. The renames /
3231
3377
  * extractor-runs / enrichments side bags ride along inside the
3232
- * same transaction the call is atomic from the caller's view.
3378
+ * same transaction, the call is atomic from the caller's view.
3233
3379
  */
3234
3380
  persist(result: ScanResult, opts?: IPersistOptions): Promise<void>;
3235
3381
  /**
3236
3382
  * Hydrate the persisted `ScanResult`. Returns the snapshot the
3237
- * scan zone holds today (including external-Provider kinds
3383
+ * scan zone holds today (including external-Provider kinds,
3238
3384
  * `node.kind` is open string per `node.schema.json`).
3239
3385
  */
3240
3386
  load(): Promise<ScanResult>;
3241
3387
  /**
3242
- * Spec § A.9 fine-grained extractor-runs cache breadcrumbs.
3388
+ * Spec § A.9, fine-grained extractor-runs cache breadcrumbs.
3243
3389
  * Returns `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
3244
3390
  * Inner value carries `bodyHash` AND `sidecarAnnotationsHash`; both
3245
3391
  * participate in the cache hit condition for every Extractor.
3246
3392
  */
3247
3393
  loadExtractorRuns(): Promise<Map<string, Map<string, IPriorExtractorRun>>>;
3248
- /** Universal enrichment layer every persisted `(node, extractor)` pair. */
3394
+ /** Universal enrichment layer, every persisted `(node, extractor)` pair. */
3249
3395
  loadNodeEnrichments(): Promise<IPersistedEnrichment[]>;
3250
3396
  /**
3251
3397
  * Row counts for `scan_nodes` / `scan_links` / `scan_issues`.
@@ -3261,7 +3407,7 @@ interface StoragePort {
3261
3407
  findNode(path: string): Promise<INodeBundle | null>;
3262
3408
  };
3263
3409
  /**
3264
- * Phase 3 / View contribution system read access to
3410
+ * Phase 3 / View contribution system, read access to
3265
3411
  * `scan_contributions`, plus the targeted purge used by
3266
3412
  * `sm plugins disable` to clear stale rows immediately at toggle time.
3267
3413
  * Bulk writes still happen exclusively via
@@ -3315,6 +3461,22 @@ interface StoragePort {
3315
3461
  issues: {
3316
3462
  /** Every issue from the latest scan, in insertion order. */
3317
3463
  listAll(): Promise<Issue[]>;
3464
+ /**
3465
+ * Paginated, filtered issue read. Drives `/api/issues` (the BFF
3466
+ * route used to call `listAll()` and filter in JS, which loaded
3467
+ * every persisted issue into memory before paging; the audit
3468
+ * L6 fix pushes both filtering AND pagination into SQL).
3469
+ *
3470
+ * `total` in the result is the count matching the filters BEFORE
3471
+ * pagination is applied; `items` is the page slice (length ≤
3472
+ * `filter.limit`). Order is `id` ASC (insertion order, stable
3473
+ * across pages so the route's `offset` / `limit` is deterministic).
3474
+ *
3475
+ * Empty filters match every row (the route still passes
3476
+ * `offset` + `limit` so pagination always applies). See
3477
+ * `IIssueListFilter` for the per-field semantics.
3478
+ */
3479
+ list(filter: IIssueListFilter): Promise<IIssueListResult>;
3318
3480
  /**
3319
3481
  * Issue rows whose runtime `Issue` shape passes `predicate`.
3320
3482
  * `port.issues.findActive((i) => i.analyzerId === 'orphan')` is the
@@ -3364,19 +3526,19 @@ interface StoragePort {
3364
3526
  * `path.resolve()`. The CLI's `sm job prune --orphan-files` flow
3365
3527
  * pairs this set with `kernel/jobs/orphan-files.ts:findOrphanJobFiles`
3366
3528
  * (which walks the directory) to compute the MD files on disk that
3367
- * no row references keeps the storage layer FS-free.
3529
+ * no row references, keeps the storage layer FS-free.
3368
3530
  */
3369
3531
  listReferencedFilePaths(): Promise<Set<string>>;
3370
3532
  };
3371
3533
  /**
3372
3534
  * Generic key/value preferences keyed by a stable string. Backs the
3373
- * `config_preferences` table one row per `key`, `value_json` is a
3535
+ * `config_preferences` table, one row per `key`, `value_json` is a
3374
3536
  * single JSON blob the caller serialises. Keys with the `_kernel.`
3375
3537
  * prefix are reserved for kernel-managed entries (today: the
3376
3538
  * update-check cache); user-set preferences land under unprefixed
3377
3539
  * keys when those ship.
3378
3540
  *
3379
- * Read-only by design at the port level the only writer is the
3541
+ * Read-only by design at the port level, the only writer is the
3380
3542
  * CLI's post-run hook (`cli/util/update-check-banner.ts`), which
3381
3543
  * reaches the persistence helpers directly. The port surfaces the
3382
3544
  * read so the BFF's `GET /api/update-status` projection can stay
@@ -3386,31 +3548,31 @@ interface StoragePort {
3386
3548
  /**
3387
3549
  * Load the update-check cache row. Returns `null` when the row
3388
3550
  * is absent, malformed JSON, or fails the shape guard. Never
3389
- * throws read failures degrade silently because the banner is
3551
+ * throws, read failures degrade silently because the banner is
3390
3552
  * a non-essential surface.
3391
3553
  */
3392
3554
  loadUpdateCheckCache(): Promise<IUpdateCheckCache | null>;
3393
3555
  /**
3394
3556
  * Upsert the update-check cache row. Always overwrites the
3395
3557
  * existing JSON blob in place. `updated_at` tracks wall-clock
3396
- * now separate from the embedded `checkedAt` field, which
3558
+ * now, separate from the embedded `checkedAt` field, which
3397
3559
  * the caller controls.
3398
3560
  */
3399
3561
  saveUpdateCheckCache(cache: IUpdateCheckCache): Promise<void>;
3400
3562
  };
3401
3563
  favorites: {
3402
3564
  /**
3403
- * Mark `path` as favorited. Idempotent a second call refreshes
3565
+ * Mark `path` as favorited. Idempotent, a second call refreshes
3404
3566
  * `favoritedAt` but does not error. The path is FK-semantic to
3405
3567
  * `scan_nodes.path`; the route layer is responsible for confirming
3406
3568
  * the path exists in the live scan before calling.
3407
3569
  */
3408
3570
  set(path: string): Promise<void>;
3409
- /** Drop the favorite row for `path`. Idempotent no-op when absent. */
3571
+ /** Drop the favorite row for `path`. Idempotent, no-op when absent. */
3410
3572
  unset(path: string): Promise<void>;
3411
3573
  /**
3412
3574
  * Load every favorited path as a `Set<string>` ready for `O(1)`
3413
- * membership checks. Used by the BFF's `/api/nodes` decorator
3575
+ * membership checks. Used by the BFF's `/api/nodes` decorator,
3414
3576
  * one query per request, no SQL JOIN against `scan_nodes`.
3415
3577
  */
3416
3578
  listPaths(): Promise<Set<string>>;
@@ -3484,7 +3646,7 @@ interface StoragePort {
3484
3646
  }
3485
3647
 
3486
3648
  /**
3487
- * `FilesystemPort` walks roots, reads nodes, writes job files.
3649
+ * `FilesystemPort`, walks roots, reads nodes, writes job files.
3488
3650
  *
3489
3651
  * Shape-only. The real adapter ships with the scan end-to-end pipeline.
3490
3652
  */
@@ -3505,7 +3667,7 @@ interface FilesystemPort {
3505
3667
  }
3506
3668
 
3507
3669
  /**
3508
- * `RunnerPort` executes an action against a rendered job file.
3670
+ * `RunnerPort`, executes an action against a rendered job file.
3509
3671
  *
3510
3672
  * Shape-only. `ClaudeCliRunner` + `MockRunner` land with the job subsystem
3511
3673
  * (job subsystem + first summarizer).
@@ -3526,7 +3688,7 @@ interface RunnerPort {
3526
3688
  }
3527
3689
 
3528
3690
  /**
3529
- * `LoggerPort` structured logging port for the kernel.
3691
+ * `LoggerPort`, structured logging port for the kernel.
3530
3692
  *
3531
3693
  * The kernel must NOT write to stdout/stderr directly. Anything that
3532
3694
  * would historically have been a `console.log` / `console.error` goes
@@ -3537,7 +3699,7 @@ interface RunnerPort {
3537
3699
  *
3538
3700
  * trace < debug < info < warn < error < silent
3539
3701
  *
3540
- * `silent` is a sentinel for filtering only it never appears as a
3702
+ * `silent` is a sentinel for filtering only, it never appears as a
3541
3703
  * `LogRecord.level`. Setting an adapter to `silent` disables every
3542
3704
  * method.
3543
3705
  */
@@ -3574,7 +3736,7 @@ interface LoggerPort {
3574
3736
  * `InMemoryProgressEmitter`: callers that don't care get a working
3575
3737
  * implementation that does nothing.
3576
3738
  *
3577
- * Every method is intentionally empty that IS the contract of this
3739
+ * Every method is intentionally empty, that IS the contract of this
3578
3740
  * class. We disable `no-empty-function` for the whole file because
3579
3741
  * adding `// eslint-disable-next-line` to each method would be noise.
3580
3742
  */
@@ -3599,7 +3761,7 @@ declare class SilentLogger implements LoggerPort {
3599
3761
  * side-channel concern.
3600
3762
  * - The active impl is a pointer; the exported `log` is a stable
3601
3763
  * proxy. Imports made before `configureLogger` runs still see the
3602
- * new impl on every call no "captured stale logger" bugs.
3764
+ * new impl on every call, no "captured stale logger" bugs.
3603
3765
  *
3604
3766
  * Tradeoffs accepted:
3605
3767
  * - Tests must call `resetLogger()` (or replace the active impl) in
@@ -3614,7 +3776,7 @@ declare const log: LoggerPort;
3614
3776
  declare function configureLogger(impl: LoggerPort): void;
3615
3777
  /** Restore the default `SilentLogger`. Call from test teardown. */
3616
3778
  declare function resetLogger(): void;
3617
- /** Inspect the active logger. Test-only production code uses `log`. */
3779
+ /** Inspect the active logger. Test-only, production code uses `log`. */
3618
3780
  declare function getActiveLogger(): LoggerPort;
3619
3781
 
3620
3782
  /**
@@ -3633,7 +3795,7 @@ declare function getActiveLogger(): LoggerPort;
3633
3795
  * Error policy: a hook that throws is caught here, logged through a
3634
3796
  * synthetic `extension.error` event with kind `hook-error`, and the
3635
3797
  * caller continues. A buggy hook MUST NOT block the main pipeline (or
3636
- * the CLI exit path) that would invert the design intent (hooks
3798
+ * the CLI exit path), that would invert the design intent (hooks
3637
3799
  * REACT to events, they never steer them).
3638
3800
  *
3639
3801
  * The module lives under `kernel/extensions/` (alongside the `IHook`
@@ -3642,7 +3804,7 @@ declare function getActiveLogger(): LoggerPort;
3642
3804
  * `analyzer.completed`, `action.completed`, `job.*`) and the CLI entry
3643
3805
  * for the two CLI-process-driven triggers (`boot`, `shutdown`).
3644
3806
  * Pulling the dispatcher out of the orchestrator keeps both consumers
3645
- * symmetric same indexing, same filter semantics, same error
3807
+ * symmetric, same indexing, same filter semantics, same error
3646
3808
  * policy.
3647
3809
  */
3648
3810
 
@@ -3671,20 +3833,20 @@ declare function makeEvent(type: string, data: unknown): ProgressEvent;
3671
3833
  interface Kernel {
3672
3834
  registry: Registry;
3673
3835
  /**
3674
- * Step 9.6.6 read-only catalog of plugin-contributed annotation
3836
+ * Step 9.6.6, read-only catalog of plugin-contributed annotation
3675
3837
  * keys, keyed by `(pluginId, key)`. Populated at plugin-load time;
3676
3838
  * pure read with no side effects. Built-in catalog (from
3677
3839
  * `annotations.schema.json`) is NOT included here.
3678
3840
  */
3679
3841
  getRegisteredAnnotationKeys: () => readonly IRegisteredAnnotationKey[];
3680
3842
  /**
3681
- * Internal replace the frozen catalog. Called once by the
3843
+ * Internal, replace the frozen catalog. Called once by the
3682
3844
  * plugin runtime composer after every plugin has loaded; consumers
3683
3845
  * MUST treat the resulting array as immutable.
3684
3846
  */
3685
3847
  setRegisteredAnnotationKeys: (entries: readonly IRegisteredAnnotationKey[]) => void;
3686
3848
  /**
3687
- * Step 11.x read-only catalog of plugin-contributed view
3849
+ * Step 11.x, read-only catalog of plugin-contributed view
3688
3850
  * contributions, keyed by `(pluginId, extensionId, contributionId)`.
3689
3851
  * Populated at plugin-load time; pure read with no side effects.
3690
3852
  * Mirror of `getRegisteredAnnotationKeys` for the view contribution
@@ -3693,7 +3855,7 @@ interface Kernel {
3693
3855
  */
3694
3856
  getRegisteredViewContributions: () => readonly IRegisteredViewContribution[];
3695
3857
  /**
3696
- * Internal replace the frozen view-contribution catalog. Called
3858
+ * Internal, replace the frozen view-contribution catalog. Called
3697
3859
  * once by the plugin runtime composer after every plugin has loaded;
3698
3860
  * consumers MUST treat the resulting array as immutable.
3699
3861
  */