@skill-map/cli 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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>,
@@ -768,7 +768,7 @@ interface IExtensionBase {
768
768
  * typed DTOs from `@skill-map/spec` is deferred to a future iteration when a
769
769
  * third consumer (real providers / extractors / rules) forces a single
770
770
  * source of truth. Until then, both `ui/src/models/` and `src/kernel/types/`
771
- * 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
772
772
  * this scale (17 schemas) and flagged in the roadmap.
773
773
  */
774
774
 
@@ -778,7 +778,7 @@ interface IExtensionBase {
778
778
  * tables with explicit migrations (mode `dedicated`). Absent = the plugin
779
779
  * does not persist state at all.
780
780
  *
781
- * Optional output-schema declarations (spec § A.12 opt-in correctness
781
+ * Optional output-schema declarations (spec § A.12, opt-in correctness
782
782
  * for plugin custom storage):
783
783
  * - Mode `kv` → `schema` (single relative path). Validates the value
784
784
  * written by `ctx.store.set(key, value)`.
@@ -802,13 +802,13 @@ type TPluginStorage = {
802
802
  /**
803
803
  * Toggle granularity for a plugin / built-in bundle.
804
804
  *
805
- * - `'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
806
806
  * bundle of extensions follows the toggle; the user cannot
807
807
  * enable some extensions of the bundle and disable others.
808
808
  * Default for plugins (and for the built-in `claude`
809
809
  * bundle, where the provider and its kind-aware extractors
810
810
  * form a coherent provider).
811
- * - `'extension'` each extension is independently toggle-able under its
811
+ * - `'extension'`, each extension is independently toggle-able under its
812
812
  * qualified id `<plugin-id>/<extension-id>`. Used for
813
813
  * the built-in `core` bundle (every kernel built-in
814
814
  * rule / formatter is removable per spec
@@ -845,7 +845,7 @@ interface IPluginManifest {
845
845
  * Plugin user-configurable settings. Each entry picks an `input-type`
846
846
  * from the closed catalog at
847
847
  * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.
848
- * The plugin author NEVER writes JSON Schema they pick `type` by
848
+ * The plugin author NEVER writes JSON Schema, they pick `type` by
849
849
  * name and supply per-type parameters. The kernel exposes resolved
850
850
  * settings to extractors via `ctx.settings.<settingId>`; settings
851
851
  * are read once at extractor invocation; changing a setting requires
@@ -866,7 +866,7 @@ interface IPluginManifest {
866
866
  * against the installed `@skill-map/spec` version.
867
867
  * - `invalid-manifest`: `plugin.json` missing, unparseable, failing AJV on
868
868
  * the base manifest schema, OR the exported extension shape failed its
869
- * kind-specific schema (per spec/architecture.md §Plugin discovery
869
+ * kind-specific schema (per spec/architecture.md §Plugin discovery,
870
870
  * "AJV rejects unknown `slot` names with `invalid-manifest`").
871
871
  * - `load-error`: manifest parsed but an extension module failed to import.
872
872
  */
@@ -874,21 +874,21 @@ interface IPluginManifest {
874
874
  * Possible outcomes after the loader sees a plugin.json. Mirrors the
875
875
  * `status` enum in `spec/schemas/plugins-registry.schema.json`.
876
876
  *
877
- * - `enabled` manifest valid, specCompat satisfied, every
877
+ * - `enabled` , manifest valid, specCompat satisfied, every
878
878
  * extension imported and validated.
879
- * - `disabled` user-toggled off via `sm plugins disable` or
879
+ * - `disabled` , user-toggled off via `sm plugins disable` or
880
880
  * `settings.json#/plugins/<id>/enabled`. Manifest
881
881
  * is parsed and surfaced (so `sm plugins list`
882
882
  * shows it), but extensions are not imported.
883
- * - `incompatible-spec` manifest parsed but `semver.satisfies` failed.
884
- * - `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,
885
885
  * OR the directory name does not equal the
886
886
  * manifest id (a cheap structural rule that
887
887
  * rules out same-root collisions by construction:
888
888
  * a filesystem cannot contain two siblings with
889
889
  * the same name).
890
- * - `load-error` manifest passed, an extension module failed.
891
- * - `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
892
892
  * (project + global, or any `--plugin-dir`
893
893
  * combination) declared the same `id`. Both
894
894
  * collided plugins receive this status; no
@@ -900,7 +900,7 @@ interface ILoadedExtension {
900
900
  kind: ExtensionKind;
901
901
  id: string;
902
902
  /**
903
- * Owning plugin namespace `manifest.id` of the `plugin.json` that
903
+ * Owning plugin namespace, `manifest.id` of the `plugin.json` that
904
904
  * declared this extension. Composed with `id` to form the qualified
905
905
  * registry key `<pluginId>/<id>`. Per spec § A.6 the loader injects
906
906
  * this from the manifest; an extension that hand-declares a
@@ -912,7 +912,7 @@ interface ILoadedExtension {
912
912
  /** Raw module namespace as returned by the dynamic `import()`. */
913
913
  module: unknown;
914
914
  /**
915
- * Runtime extension instance ready for the registry / orchestrator
915
+ * Runtime extension instance ready for the registry / orchestrator,
916
916
  * the `default` export of `module` (or the module itself when no
917
917
  * default), shallow-cloned with `pluginId` injected per spec § A.6.
918
918
  *
@@ -926,7 +926,7 @@ interface ILoadedExtension {
926
926
  interface IDiscoveredPlugin {
927
927
  /** Absolute path to the plugin directory. */
928
928
  path: string;
929
- /** 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. */
930
930
  id: string;
931
931
  status: TPluginLoadStatus;
932
932
  /** Only present when status === 'enabled' or 'incompatible-spec'. */
@@ -940,20 +940,20 @@ interface IDiscoveredPlugin {
940
940
  */
941
941
  granularity?: TGranularity;
942
942
  /**
943
- * Runtime-only never persisted, never spec-modeled.
943
+ * Runtime-only, never persisted, never spec-modeled.
944
944
  *
945
- * 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.
946
946
  * Populated by the loader when `manifest.storage.schemas` (Mode B) or
947
947
  * `manifest.storage.schema` (Mode A) declares schema paths the loader
948
948
  * successfully read and AJV-compiled. Consumed by the runtime store
949
949
  * wrapper to validate `ctx.store.write(table, row)` (Mode B) and
950
950
  * `ctx.store.set(key, value)` (Mode A) before persisting.
951
951
  *
952
- * Mode B layout keyed by logical table name (without the
952
+ * Mode B layout, keyed by logical table name (without the
953
953
  * `plugin_<normalizedId>_` prefix), matching the manifest's `schemas`
954
954
  * map. Tables not present in the map accept any shape (permissive).
955
955
  *
956
- * Mode A layout uses the sentinel key `__kv__` for the single
956
+ * Mode A layout, uses the sentinel key `__kv__` for the single
957
957
  * value-shape schema. The sentinel survives the runtime contract change
958
958
  * if Mode A ever grows multiple namespaces.
959
959
  *
@@ -966,7 +966,7 @@ interface IDiscoveredPlugin {
966
966
  reason?: string;
967
967
  }
968
968
  /**
969
- * Runtime-only a single AJV-compiled storage schema attached to a
969
+ * Runtime-only, a single AJV-compiled storage schema attached to a
970
970
  * loaded plugin. The schema path (relative to the plugin directory) is
971
971
  * preserved so error messages can name the offending file. `validate`
972
972
  * is the AJV `ValidateFunction` itself: it returns `true` on shape
@@ -988,20 +988,20 @@ interface IPluginStorageSchema {
988
988
  }
989
989
 
990
990
  /**
991
- * Plugin store wrappers runtime injection for `ctx.store` per spec
991
+ * Plugin store wrappers, runtime injection for `ctx.store` per spec
992
992
  * § A.12 (opt-in `outputSchema` for plugin custom storage).
993
993
  *
994
994
  * Two shapes, mirroring the manifest's storage modes documented in
995
995
  * `spec/plugin-kv-api.md`:
996
996
  *
997
- * - Mode A `KvStore.set(key, value)`. AJV-validates `value` against
997
+ * - Mode A, `KvStore.set(key, value)`. AJV-validates `value` against
998
998
  * the schema declared by `manifest.storage.schema` (single
999
999
  * value-shape) when present. Absent = permissive.
1000
- * - Mode B `DedicatedStore.write(table, row)`. AJV-validates `row`
1000
+ * - Mode B, `DedicatedStore.write(table, row)`. AJV-validates `row`
1001
1001
  * against the per-table schema declared in `manifest.storage.schemas`
1002
1002
  * when present. Tables absent from the map accept any shape.
1003
1003
  *
1004
- * Both wrappers are storage-engine agnostic they accept a `persist`
1004
+ * Both wrappers are storage-engine agnostic, they accept a `persist`
1005
1005
  * callback the caller supplies. The persistence side (SQLite, in-memory,
1006
1006
  * mock) is the caller's concern; this wrapper's only job is the
1007
1007
  * AJV gate. That separation lets the test suite exercise the validator
@@ -1010,7 +1010,7 @@ interface IPluginStorageSchema {
1010
1010
  * unchanged.
1011
1011
  *
1012
1012
  * Universal validation (`emitLink` against `link.schema.json`,
1013
- * `enrichNode` against `node.schema.json`) is unaffected it lives on
1013
+ * `enrichNode` against `node.schema.json`) is unaffected, it lives on
1014
1014
  * the orchestrator side and runs regardless of the plugin's
1015
1015
  * `outputSchema` opt-in.
1016
1016
  */
@@ -1036,7 +1036,7 @@ interface IDedicatedStorePersist {
1036
1036
  * schema path and AJV errors; persistence is skipped on failure.
1037
1037
  *
1038
1038
  * `pluginId` is captured for diagnostics (the throw message names the
1039
- * 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
1040
1040
  * the persistence layer's job (the spec's `state_plugin_kvs` PK includes
1041
1041
  * `pluginId` and the kernel-side adapter prepends it before write).
1042
1042
  */
@@ -1044,7 +1044,7 @@ interface IKvStoreWrapper {
1044
1044
  set(key: string, value: unknown): Promise<void>;
1045
1045
  }
1046
1046
  /**
1047
- * 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
1048
1048
  * (`kv`) returns a `set(key, value)` surface; Mode B (`dedicated`) returns
1049
1049
  * `write(table, row)`. Plugin authors narrow at the call site based on
1050
1050
  * the storage mode declared in their `plugin.json`.
@@ -1058,7 +1058,7 @@ declare function makeKvStoreWrapper(opts: {
1058
1058
  /**
1059
1059
  * Mode B wrapper. `write(table, row)` AJV-validates `row` against
1060
1060
  * `storageSchemas[table]` when declared, then forwards to `persist`.
1061
- * Tables absent from the map are permissive the wrapper forwards
1061
+ * Tables absent from the map are permissive, the wrapper forwards
1062
1062
  * straight to `persist` without validation.
1063
1063
  *
1064
1064
  * The wrapper accepts the full `storageSchemas` map (rather than a
@@ -1088,7 +1088,7 @@ declare function makePluginStore(opts: {
1088
1088
  }): IPluginStore | undefined;
1089
1089
 
1090
1090
  /**
1091
- * `scan_contributions` adapter replace-all writer used by
1091
+ * `scan_contributions` adapter, replace-all writer used by
1092
1092
  * `persistScanResult`, plus read helpers consumed by the BFF
1093
1093
  * (`/api/contributions/...`) and rules (`core/contribution-orphan`).
1094
1094
  *
@@ -1101,7 +1101,7 @@ declare function makePluginStore(opts: {
1101
1101
  * scan is a fresh snapshot, so prior rows are deleted before insert.
1102
1102
  * Wrapped in the same transaction `persistScanResult` opens.
1103
1103
  *
1104
- * The rename heuristic does NOT need to migrate `node_path` here
1104
+ * The rename heuristic does NOT need to migrate `node_path` here,
1105
1105
  * because of replace-all, every contribution is re-emitted on the new
1106
1106
  * path automatically. Keeping the rename path lighter than `state_*`
1107
1107
  * (which IS rename-migrated because state survives across scans).
@@ -1121,7 +1121,7 @@ interface IContributionRecord {
1121
1121
  contributionId: string;
1122
1122
  /**
1123
1123
  * Closed enum value mirroring `view-slots.schema.json#/$defs/SlotName`.
1124
- * Persisted as TEXT (no SQL CHECK by design see migration comment).
1124
+ * Persisted as TEXT (no SQL CHECK by design, see migration comment).
1125
1125
  */
1126
1126
  slot: string;
1127
1127
  /** Already-validated payload. Serialised via `JSON.stringify` at write. */
@@ -1145,7 +1145,7 @@ interface IPersistedContribution {
1145
1145
  }
1146
1146
 
1147
1147
  /**
1148
- * `loadScanResult` driving inverse of `persistScanResult`. Reads the
1148
+ * `loadScanResult`, driving inverse of `persistScanResult`. Reads the
1149
1149
  * `scan_*` tables and reconstructs a `ScanResult` shape so the
1150
1150
  * orchestrator can run an incremental scan (`sm scan --changed`) on
1151
1151
  * top of a prior snapshot.
@@ -1158,7 +1158,7 @@ interface IPersistedContribution {
1158
1158
  *
1159
1159
  * **Documented omission**: external pseudo-links (those whose target is
1160
1160
  * an `http://` / `https://` URL emitted by the external-url-counter
1161
- * extractor) are NEVER persisted to `scan_links` only their per-node
1161
+ * extractor) are NEVER persisted to `scan_links`, only their per-node
1162
1162
  * count survives in `scan_nodes.external_refs_count`. Therefore the
1163
1163
  * `result.links` returned by `loadScanResult` contains only internal
1164
1164
  * graph links, and `node.externalRefsCount` is the authoritative count
@@ -1185,11 +1185,11 @@ interface IPersistedContribution {
1185
1185
  * `durationMs`; the three count fields derive from row counts.
1186
1186
  *
1187
1187
  * Both branches keep `nodesCount` / `linksCount` / `issuesCount` derived
1188
- * from `COUNT(*)` of the loaded rows never persisted, always recomputed.
1188
+ * from `COUNT(*)` of the loaded rows, never persisted, always recomputed.
1189
1189
  */
1190
1190
 
1191
1191
  /**
1192
- * 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
1193
1193
  * from qualified extractor id (`<pluginId>/<id>`) to the run-time
1194
1194
  * hashes the extractor recorded on its last run. Empty map is the
1195
1195
  * default when the table is empty (fresh DB, never-scanned scope, or
@@ -1212,14 +1212,14 @@ interface IPriorExtractorRun {
1212
1212
  *
1213
1213
  * Why a wrapper instead of exposing `ignore` directly:
1214
1214
  *
1215
- * 1. Single-source defaults `src/config/defaults/skillmapignore` is
1215
+ * 1. Single-source defaults, `src/config/defaults/skillmapignore` is
1216
1216
  * the canonical default list, loaded once at module init (or at
1217
1217
  * explicit build time, depending on bundling). The runtime never
1218
1218
  * re-reads it per scan.
1219
- * 2. Stable interface Providers and the orchestrator depend on a
1219
+ * 2. Stable interface, Providers and the orchestrator depend on a
1220
1220
  * minimal `IIgnoreFilter` shape, so the underlying library can be
1221
1221
  * swapped without touching every consumer.
1222
- * 3. Path normalization every consumer passes the path RELATIVE to
1222
+ * 3. Path normalization, every consumer passes the path RELATIVE to
1223
1223
  * the scan root (POSIX separators); the wrapper guarantees that
1224
1224
  * contract before delegating to `ignore`.
1225
1225
  */
@@ -1233,6 +1233,33 @@ interface IIgnoreFilter {
1233
1233
  ignores(relativePath: string): boolean;
1234
1234
  }
1235
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
+
1236
1263
  /**
1237
1264
  * Provider runtime contract. Walks filesystem roots and emits raw node
1238
1265
  * records; classification maps path conventions to a node kind.
@@ -1267,6 +1294,14 @@ interface IRawNode {
1267
1294
  frontmatterRaw: string;
1268
1295
  /** Parsed frontmatter, or `{}` when absent / unparseable. */
1269
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[];
1270
1305
  }
1271
1306
  /**
1272
1307
  * One entry in a Provider's `kinds` map. Declares both the per-kind
@@ -1322,7 +1357,7 @@ interface IProviderKind {
1322
1357
  * intent (label + base color, optional dark variant + emoji + icon);
1323
1358
  * the UI derives `bg`/`fg` tints per theme via a deterministic helper
1324
1359
  * and reads the registry from the `kindRegistry` field embedded in REST
1325
- * 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
1326
1361
  * UI never hardcodes presentation for a built-in kind.
1327
1362
  */
1328
1363
  interface IProviderKindUi {
@@ -1398,12 +1433,12 @@ interface IProvider extends IExtensionBase {
1398
1433
  * per-kind schemas compile, so cross-file `$ref` resolution succeeds.
1399
1434
  *
1400
1435
  * Use case: when several kinds share a common base (e.g. Anthropic's
1401
- * merged skill / command frontmatter both extend a shared
1436
+ * merged skill / command frontmatter, both extend a shared
1402
1437
  * `skill-base.schema.json`), the Provider declares the base here so
1403
1438
  * `skill.schema.json` and `command.schema.json` can `$ref` it without
1404
1439
  * duplicating fields.
1405
1440
  *
1406
- * 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`
1407
1442
  * manifest. Manifest-validated schemas remain the per-kind ones in
1408
1443
  * `kinds[<kind>].schema`; auxiliary schemas are an implementation
1409
1444
  * concern of how the runtime composes those.
@@ -1421,7 +1456,7 @@ interface IProvider extends IExtensionBase {
1421
1456
  * so the most common Provider shape needs zero configuration.
1422
1457
  *
1423
1458
  * Precedence: when both `walk()` (runtime field) and `read` are
1424
- * declared, `walk()` wins `read` is ignored. The escape-hatch
1459
+ * declared, `walk()` wins, `read` is ignored. The escape-hatch
1425
1460
  * relationship is intentional: most Providers should use `read`;
1426
1461
  * Providers with non-standard discovery requirements (custom file
1427
1462
  * naming, multi-pass walks, dynamic ignore logic) implement `walk()`
@@ -1438,7 +1473,7 @@ interface IProvider extends IExtensionBase {
1438
1473
  * Non-matching files are silently skipped. Unreadable files produce
1439
1474
  * a diagnostic via the emitter but do not abort the walk.
1440
1475
  *
1441
- * `options.ignoreFilter` when supplied, the Provider MUST
1476
+ * `options.ignoreFilter`, when supplied, the Provider MUST
1442
1477
  * skip every directory and file whose path-relative-to-root the
1443
1478
  * filter reports as ignored. Providers MAY also keep their own
1444
1479
  * hard-coded skip list (e.g. `.git`) as a defensive measure, but the
@@ -1446,14 +1481,14 @@ interface IProvider extends IExtensionBase {
1446
1481
  *
1447
1482
  * Optional. When omitted, the Provider MUST declare `read` (or rely
1448
1483
  * on the default config). The orchestrator never calls `walk()`
1449
- * directly it goes through `resolveProviderWalk(provider)` which
1484
+ * directly, it goes through `resolveProviderWalk(provider)` which
1450
1485
  * picks `walk` over `read`.
1451
1486
  */
1452
1487
  walk?(roots: string[], options?: {
1453
1488
  ignoreFilter?: IIgnoreFilter;
1454
1489
  }): AsyncIterable<IRawNode>;
1455
1490
  /**
1456
- * 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
1457
1492
  * `null` to disclaim the file. The classifier is called after walk()
1458
1493
  * yields; with multiple Providers active, every Provider walks every
1459
1494
  * file matching its `read.extensions`, so each Provider MUST disclaim
@@ -1500,18 +1535,18 @@ interface IProviderReadConfig {
1500
1535
  * Extractors are deterministic-only. They run synchronously inside the
1501
1536
  * scan loop; LLM-driven enrichment of a node is an Action concern, not
1502
1537
  * an Extractor concern. The Extractor context therefore exposes no
1503
- * `RunnerPort` see spec `architecture.md` §Execution modes.
1538
+ * `RunnerPort`, see spec `architecture.md` §Execution modes.
1504
1539
  *
1505
1540
  * Output channels (all on the context):
1506
1541
  *
1507
- * - `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.
1508
1543
  * Validated against `emitsLinkKinds` before insertion; an off-contract
1509
1544
  * kind drops the link and surfaces an `extension.error` event.
1510
- * - `ctx.enrichNode(partial)` merge canonical, kernel-curated properties
1545
+ * - `ctx.enrichNode(partial)`, merge canonical, kernel-curated properties
1511
1546
  * onto the node. Strictly separate from the author-supplied frontmatter
1512
1547
  * (the latter remains immutable and survives verbatim). Persistence
1513
1548
  * is spec'd in § A.8.
1514
- * - `ctx.store` plugin-scoped persistence. Present only when the
1549
+ * - `ctx.store`, plugin-scoped persistence. Present only when the
1515
1550
  * plugin declares `storage.mode` in `plugin.json`; shape depends on the
1516
1551
  * mode (`KvStore` for mode A, scoped `Database` for mode B). See
1517
1552
  * `plugin-kv-api.md` for the contract.
@@ -1550,7 +1585,7 @@ interface IExtractorCallbacks {
1550
1585
  * extension-local Record key declared under
1551
1586
  * `extension.viewContributions[<contributionId>]`; the second is a
1552
1587
  * payload that conforms to the slot's payload schema in
1553
- * `spec/schemas/view-slots.schema.json#/$defs/payloads/<slot>` —
1588
+ * `spec/schemas/view-slots.schema.json#/$defs/payloads/<slot>`,
1554
1589
  * where `<slot>` is the slot the manifest declared for this
1555
1590
  * contribution. The orchestrator validates the payload against the
1556
1591
  * slot's schema before persisting to `scan_contributions`; off-shape
@@ -1573,7 +1608,7 @@ interface IExtractorContext extends IExtractorCallbacks {
1573
1608
  * (`write(table, row)`). See `spec/plugin-kv-api.md`.
1574
1609
  *
1575
1610
  * Typed as `unknown` so this contract module stays free of any
1576
- * adapter-side imports the concrete `IPluginStore` lives in
1611
+ * adapter-side imports, the concrete `IPluginStore` lives in
1577
1612
  * `kernel/adapters/plugin-store.js`. Plugin authors narrow at the
1578
1613
  * call site based on the storage mode declared in their manifest.
1579
1614
  * The orchestrator looks up the wrapper per-extractor in
@@ -1590,11 +1625,11 @@ interface IExtractor extends IExtensionBase {
1590
1625
  /**
1591
1626
  * Optional opt-in filter on `node.kind`. When declared, the orchestrator
1592
1627
  * skips invocation of `extract()` for any node whose `kind` is NOT in
1593
- * this list fail-fast, before context construction, so the extractor
1628
+ * this list, fail-fast, before context construction, so the extractor
1594
1629
  * wastes zero CPU on inapplicable nodes.
1595
1630
  *
1596
1631
  * Absent (`undefined`) is the default: the extractor applies to every
1597
- * kind. There are no wildcards the absence of the field already
1632
+ * kind. There are no wildcards, the absence of the field already
1598
1633
  * encodes "every kind". An empty array (`[]`) is rejected at load
1599
1634
  * time by AJV (`minItems: 1` in the schema).
1600
1635
  *
@@ -1619,13 +1654,13 @@ interface IExtractor extends IExtensionBase {
1619
1654
  * findings into the UI via view contributions. Deterministic analyzers
1620
1655
  * are pure (same graph in → same issues out) and run synchronously
1621
1656
  * inside `sm scan` / `sm check`. Probabilistic analyzers invoke an LLM
1622
- * through the kernel's `RunnerPort` and dispatch only as queued jobs
1657
+ * through the kernel's `RunnerPort` and dispatch only as queued jobs,
1623
1658
  * they never participate in scan-time pipelines. Mode is declared in
1624
1659
  * the manifest (default `deterministic`).
1625
1660
  */
1626
1661
 
1627
1662
  /**
1628
- * 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
1629
1664
  * whose sibling `.md` does not exist on disk; the `annotation-orphan`
1630
1665
  * built-in analyzer emits one warning per entry. Other analyzers that
1631
1666
  * care about orphan sidecars MAY consume the list too.
@@ -1640,13 +1675,13 @@ interface IAnalyzerContext {
1640
1675
  nodes: Node[];
1641
1676
  links: Link[];
1642
1677
  /**
1643
- * Step 9.6.2 orphaned sidecars discovered during the scan walk.
1678
+ * Step 9.6.2, orphaned sidecars discovered during the scan walk.
1644
1679
  * Empty when sidecar discovery did not run (legacy callers) or
1645
1680
  * when no orphans exist.
1646
1681
  */
1647
1682
  orphanSidecars?: IAnalyzerOrphanSidecar[];
1648
1683
  /**
1649
- * 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
1650
1685
  * by the orchestrator alongside the public `Node.sidecar` overlay so
1651
1686
  * analyzers that inspect plugin namespaces (e.g. the built-in
1652
1687
  * `core/unknown-field` Analyzer) can walk the full tree without
@@ -1656,7 +1691,7 @@ interface IAnalyzerContext {
1656
1691
  */
1657
1692
  sidecarRoots?: ReadonlyMap<string, Record<string, unknown>>;
1658
1693
  /**
1659
- * Step 9.6.6 runtime catalog of plugin-contributed annotation keys,
1694
+ * Step 9.6.6, runtime catalog of plugin-contributed annotation keys,
1660
1695
  * as exposed by `kernel.getRegisteredAnnotationKeys()`. Threaded
1661
1696
  * through so analyzers can reason about the registered-vs-unknown
1662
1697
  * split without reaching back into the kernel. Empty array when no
@@ -1665,20 +1700,20 @@ interface IAnalyzerContext {
1665
1700
  */
1666
1701
  annotationContributions?: readonly IRegisteredAnnotationKey[];
1667
1702
  /**
1668
- * Step 11.x runtime catalog of plugin-contributed view contributions,
1703
+ * Step 11.x, runtime catalog of plugin-contributed view contributions,
1669
1704
  * as exposed by `kernel.getRegisteredViewContributions()`. Threaded
1670
1705
  * through so analyzers can reason about emissions without reaching
1671
- * back into the kernel: built-in `core/unknown-slot` walks this list
1672
- * to detect deprecated slots in use, and `core/contribution-orphan`
1673
- * joins it with the live node set to flag dangling emissions. Empty
1674
- * array when no extension declares view contributions; absent for
1675
- * legacy callers (older runScan sites that never wired the catalog
1676
- * 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).
1677
1712
  */
1678
1713
  viewContributions?: readonly IRegisteredViewContribution[];
1679
1714
  /**
1680
1715
  * Absolute paths of `*.md` files under the project's
1681
- * `.skill-map/jobs/` that no `state_jobs.filePath` references the
1716
+ * `.skill-map/jobs/` that no `state_jobs.filePath` references, the
1682
1717
  * built-in `core/job-orphan-file` analyzer projects each as a `warn`
1683
1718
  * issue. Pre-computed by the driving adapter (CLI / BFF) inside its
1684
1719
  * already-open storage transaction (mirrors the `orphanSidecars`
@@ -1693,7 +1728,7 @@ interface IAnalyzerContext {
1693
1728
  * link-validation purposes via `scan.referencePaths`. The driving
1694
1729
  * adapter walks each configured path before the scan and collects
1695
1730
  * every existing file's absolute path here. Files in this set are
1696
- * NOT indexed as graph nodes the only consumer is
1731
+ * NOT indexed as graph nodes, the only consumer is
1697
1732
  * `core/broken-ref`, which suppresses its `warn` issue when a
1698
1733
  * path-style link target falls into the set. Absent / empty when
1699
1734
  * the operator left `scan.referencePaths` empty or when the
@@ -1738,6 +1773,20 @@ interface IAnalyzer extends IExtensionBase {
1738
1773
  * `deterministic` per `spec/schemas/extensions/analyzer.schema.json`.
1739
1774
  */
1740
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[];
1741
1790
  evaluate(ctx: IAnalyzerContext): Issue[] | Promise<Issue[]>;
1742
1791
  }
1743
1792
 
@@ -1747,9 +1796,9 @@ interface IAnalyzer extends IExtensionBase {
1747
1796
  *
1748
1797
  * Actions operate on one or more nodes in one of two execution modes:
1749
1798
  *
1750
- * - `deterministic` code runs in-process; the action computes the
1799
+ * - `deterministic`, code runs in-process; the action computes the
1751
1800
  * report synchronously and returns it. No job file, no runner.
1752
- * - `probabilistic` the kernel renders a prompt + preamble into a
1801
+ * - `probabilistic`, the kernel renders a prompt + preamble into a
1753
1802
  * job file; a runner executes it via `RunnerPort` against an LLM;
1754
1803
  * `sm record` closes the job and validates the report against
1755
1804
  * `reportSchemaRef`.
@@ -1759,7 +1808,7 @@ interface IAnalyzer extends IExtensionBase {
1759
1808
  * probabilistic) lands with the job subsystem (Decision #114 in
1760
1809
  * `ROADMAP.md`). Today the loader still validates `kind: 'action'`
1761
1810
  * manifests against `extension-action.schema.json` and the registry
1762
- * holds them `sm actions show` and the precondition gating UI consume
1811
+ * holds them, `sm actions show` and the precondition gating UI consume
1763
1812
  * the manifest data. The runtime entry point is intentionally absent
1764
1813
  * from `IAction` so plugin authors don't ship a method the kernel will
1765
1814
  * not call until the job subsystem is in place; when it ships, the
@@ -1767,21 +1816,21 @@ interface IAnalyzer extends IExtensionBase {
1767
1816
  *
1768
1817
  * Mirrors `extensions/action.schema.json`:
1769
1818
  *
1770
- * - `mode` (required) discriminator between the two modes.
1771
- * - `reportSchemaRef` (required) JSON Schema reference the report
1819
+ * - `mode` (required), discriminator between the two modes.
1820
+ * - `reportSchemaRef` (required), JSON Schema reference the report
1772
1821
  * MUST validate against. MUST extend `report-base.schema.json`.
1773
- * - `promptTemplateRef` REQUIRED when `mode: 'probabilistic'`,
1822
+ * - `promptTemplateRef`, REQUIRED when `mode: 'probabilistic'`,
1774
1823
  * FORBIDDEN when `mode: 'deterministic'`. The schema's conditional
1775
1824
  * `allOf` enforces both directions; the runtime contract simply
1776
1825
  * surfaces the field as optional and lets the loader catch shape
1777
1826
  * violations at AJV time.
1778
- * - `expectedDurationSeconds` REQUIRED for probabilistic (drives
1827
+ * - `expectedDurationSeconds`, REQUIRED for probabilistic (drives
1779
1828
  * TTL); advisory for deterministic.
1780
- * - `precondition` declarative filter consumed by `--all` fan-out,
1829
+ * - `precondition`, declarative filter consumed by `--all` fan-out,
1781
1830
  * UI button gating, `sm actions show`.
1782
- * - `expectedTools` hint to Skill / CLI runners about expected
1831
+ * - `expectedTools`, hint to Skill / CLI runners about expected
1783
1832
  * tools (no normative enforcement in v0).
1784
- * - `fanOutPolicy` `'per-node'` (default) vs `'batch'`.
1833
+ * - `fanOutPolicy`, `'per-node'` (default) vs `'batch'`.
1785
1834
  */
1786
1835
 
1787
1836
  /**
@@ -1789,10 +1838,10 @@ interface IAnalyzer extends IExtensionBase {
1789
1838
  * future write kinds (storage rows, plugin KV, etc.) can land additively
1790
1839
  * without breaking consumers that only handle `kind: 'sidecar'`.
1791
1840
  *
1792
- * - `path` absolute path to the `.sm` file the kernel must materialise
1841
+ * - `path`, absolute path to the `.sm` file the kernel must materialise
1793
1842
  * the change into. Resolved by the Action from the node's absolute
1794
1843
  * path via `sidecarPathFor()`.
1795
- * - `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
1796
1845
  * full replacement). Arrays REPLACE; objects RECURSE. Reason:
1797
1846
  * sidecars are shared-write between skill-map core and plugins;
1798
1847
  * a full replace would clobber `<plugin-id>:` namespaced blocks.
@@ -1805,7 +1854,7 @@ type TActionWrite = {
1805
1854
  /**
1806
1855
  * Result envelope returned by deterministic Actions. The `report` field
1807
1856
  * carries the typed report payload (each Action declares its shape via
1808
- * `reportSchemaRef`); `writes` is opt-in Actions that do not mutate
1857
+ * `reportSchemaRef`); `writes` is opt-in, Actions that do not mutate
1809
1858
  * persistent state simply omit it.
1810
1859
  */
1811
1860
  interface IActionResult<TReport = unknown> {
@@ -1814,20 +1863,20 @@ interface IActionResult<TReport = unknown> {
1814
1863
  }
1815
1864
  /**
1816
1865
  * Runtime context passed to a deterministic Action's `invoke()` method.
1817
- * Minimal surface Actions stay pure (no IO inside `invoke`); the kernel
1866
+ * Minimal surface, Actions stay pure (no IO inside `invoke`); the kernel
1818
1867
  * materialises any returned `writes` after the call.
1819
1868
  *
1820
- * - `node` the target `Node` the Action operates on. Open-by-design;
1869
+ * - `node`, the target `Node` the Action operates on. Open-by-design;
1821
1870
  * batch / fan-out flows pick the matching nodes upstream.
1822
- * - `nodeAbsolutePath` absolute path to the node's `.md` file on
1871
+ * - `nodeAbsolutePath`, absolute path to the node's `.md` file on
1823
1872
  * disk. The Action uses this to compute the sidecar path it returns
1824
1873
  * in a `TActionWrite`. Surfaced separately from `node.path` (which is
1825
1874
  * the relative scope-root form) so Actions never compose absolute
1826
1875
  * paths from `node.path` themselves.
1827
- * - `invoker` identity of the caller; written into the sidecar's
1876
+ * - `invoker`, identity of the caller; written into the sidecar's
1828
1877
  * `audit.lastBumpedBy` when the Action chooses to. CLI invocations
1829
1878
  * pass `'cli'`; plugin-driven invocations pass `'plugin:<plugin-id>'`.
1830
- * - `now` clock function; tests inject a deterministic source.
1879
+ * - `now`, clock function; tests inject a deterministic source.
1831
1880
  * Defaults to `() => new Date()` at the composition root.
1832
1881
  */
1833
1882
  interface IActionContext {
@@ -1838,7 +1887,7 @@ interface IActionContext {
1838
1887
  }
1839
1888
  /**
1840
1889
  * Declarative filter applied by `--all` fan-out, UI button gating, and
1841
- * `sm actions show`. All fields optional an empty precondition matches
1890
+ * `sm actions show`. All fields optional, an empty precondition matches
1842
1891
  * every node.
1843
1892
  */
1844
1893
  interface IActionPrecondition {
@@ -1908,14 +1957,14 @@ interface IAction extends IExtensionBase {
1908
1957
  /**
1909
1958
  * Deterministic invocation entry point. OPTIONAL on the runtime
1910
1959
  * contract for backward compatibility with the manifest-only era
1911
- * (Decision #114) actions that ship for the future probabilistic
1960
+ * (Decision #114), actions that ship for the future probabilistic
1912
1961
  * runner / record path leave it absent and the kernel never calls it.
1913
1962
  * Step 9.6.3 (Decision #125) introduces the first concrete consumer:
1914
1963
  * the built-in `bump` Action implements `invoke()` and returns a
1915
1964
  * `writes: [{ kind: 'sidecar', ... }]` payload that the kernel
1916
1965
  * materialises through `ISidecarStore`.
1917
1966
  *
1918
- * Implementations MUST stay pure no IO inside `invoke()`. The Action
1967
+ * Implementations MUST stay pure, no IO inside `invoke()`. The Action
1919
1968
  * computes the patch and returns it; the kernel reads the on-disk
1920
1969
  * sidecar, deep-merges, validates, and writes back inside its critical
1921
1970
  * section.
@@ -1933,10 +1982,10 @@ interface IAction extends IExtensionBase {
1933
1982
  *
1934
1983
  * Two adjacent names live on the same instance:
1935
1984
  *
1936
- * - `formatId: string` the manifest field consumed by the
1985
+ * - `formatId: string`, the manifest field consumed by the
1937
1986
  * `--format <name>` CLI flag. The kernel's lookup is
1938
1987
  * `formatters.find((f) => f.formatId === flag)`.
1939
- * - `format(ctx) → string` the runtime method. Receives the full
1988
+ * - `format(ctx) → string`, the runtime method. Receives the full
1940
1989
  * graph and returns the serialized output. Output MUST be
1941
1990
  * byte-deterministic for the same input (the snapshot-test suite
1942
1991
  * relies on this).
@@ -1950,6 +1999,16 @@ interface IFormatterContext {
1950
1999
  nodes: Node[];
1951
2000
  links: Link[];
1952
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;
1953
2012
  }
1954
2013
  interface IFormatter extends IExtensionBase {
1955
2014
  kind: 'formatter';
@@ -1968,7 +2027,7 @@ interface IFormatter extends IExtensionBase {
1968
2027
  * are notification (Slack on `job.completed`), integration glue (CI
1969
2028
  * webhook on `job.failed`), and bookkeeping (per-extractor metrics).
1970
2029
  *
1971
- * The hookable trigger set is INTENTIONALLY SMALL ten events. Eight
2030
+ * The hookable trigger set is INTENTIONALLY SMALL, ten events. Eight
1972
2031
  * are pipeline-driven (emitted from inside `runScan`); two
1973
2032
  * (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving
1974
2033
  * binary before / after the verb runs, fire-and-forget so
@@ -1992,16 +2051,16 @@ interface IFormatter extends IExtensionBase {
1992
2051
  *
1993
2052
  * Curated trigger set (per spec § A.11):
1994
2053
  *
1995
- * 0. `boot` once per CLI process, before verb routing.
1996
- * 1. `scan.started` pre-scan setup (one per scan).
1997
- * 2. `scan.completed` post-scan reaction (one per scan).
1998
- * 3. `extractor.completed` aggregated per-Extractor outputs.
1999
- * 4. `analyzer.completed` aggregated per-Rule outputs.
2000
- * 5. `action.completed` Action executed on a node.
2001
- * 6. `job.spawning` pre-spawn of runner subprocess.
2002
- * 7. `job.completed` most common trigger.
2003
- * 8. `job.failed` alerts, retry triggers.
2004
- * 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
2005
2064
  * exit code resolves and before
2006
2065
  * `process.exit`.
2007
2066
  */
@@ -2089,7 +2148,7 @@ interface IHookContext {
2089
2148
  * at load time: when none of the declared triggers carries a given
2090
2149
  * filter field, the loader surfaces `invalid-manifest`. The current
2091
2150
  * impl performs the basic enum check but defers full payload-shape
2092
- * cross-validation to a follow-up the dispatcher is permissive at
2151
+ * cross-validation to a follow-up, the dispatcher is permissive at
2093
2152
  * runtime (an unknown field never matches → the hook simply never
2094
2153
  * fires for that event, which is a correct interpretation of "filter
2095
2154
  * by a field that doesn't exist").
@@ -2120,13 +2179,13 @@ interface IHook extends IExtensionBase {
2120
2179
  * Hook entry point. Returns nothing; reactions are side effects.
2121
2180
  * Errors are caught by the dispatcher (logged as `extension.error`,
2122
2181
  * surfaced via `hook.failed` meta-event) and NEVER block the main
2123
- * pipeline a buggy hook degrades gracefully.
2182
+ * pipeline, a buggy hook degrades gracefully.
2124
2183
  */
2125
2184
  on(ctx: IHookContext): void | Promise<void>;
2126
2185
  }
2127
2186
 
2128
2187
  /**
2129
- * `ProgressEmitterPort` emits progress events during long operations.
2188
+ * `ProgressEmitterPort`, emits progress events during long operations.
2130
2189
  *
2131
2190
  * Shape-only today. The full event catalog (`run.started`,
2132
2191
  * `job.claimed`, `model.delta`, etc.) is normative in
@@ -2160,13 +2219,13 @@ interface ProgressEmitterPort {
2160
2219
  */
2161
2220
 
2162
2221
  /**
2163
- * Spec § A.9 runs to persist into `scan_extractor_runs`. One entry
2222
+ * Spec § A.9, runs to persist into `scan_extractor_runs`. One entry
2164
2223
  * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
2165
2224
  * "this extractor is current for this body". Includes both freshly-run
2166
2225
  * pairs (extractor invoked this scan) and reused pairs (cached node, the
2167
2226
  * extractor's prior run still applies to the same body hash). Excludes
2168
- * obsolete pairs extractors that ran in the prior but are no longer
2169
- * registered so a replace-all persist drops them automatically.
2227
+ * obsolete pairs, extractors that ran in the prior but are no longer
2228
+ * registered, so a replace-all persist drops them automatically.
2170
2229
  */
2171
2230
  interface IExtractorRunRecord {
2172
2231
  nodePath: string;
@@ -2183,7 +2242,7 @@ interface IExtractorRunRecord {
2183
2242
  sidecarAnnotationsHashAtRun: string;
2184
2243
  }
2185
2244
  /**
2186
- * Spec § A.8 universal enrichment layer.
2245
+ * Spec § A.8, universal enrichment layer.
2187
2246
  *
2188
2247
  * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
2189
2248
  * produced via `ctx.enrichNode(...)` during the walk. Attribution is
@@ -2196,7 +2255,7 @@ interface IExtractorRunRecord {
2196
2255
  * for last-write-wins per field at read time.
2197
2256
  *
2198
2257
  * `value` is the cumulative merge across every `enrichNode` call that
2199
- * Extractor made for this node within this scan multiple
2258
+ * Extractor made for this node within this scan, multiple
2200
2259
  * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
2201
2260
  * fold into a single row, but two different Extractors hitting the
2202
2261
  * same node yield two distinct rows.
@@ -2205,7 +2264,7 @@ interface IExtractorRunRecord {
2205
2264
  * every record produced by the orchestrator sets it to `false`. The
2206
2265
  * field is kept on the record (and the row in `node_enrichments`) so a
2207
2266
  * future Action-issued enrichment can populate it without reshaping
2208
- * the persistence contract see spec `architecture.md`
2267
+ * the persistence contract, see spec `architecture.md`
2209
2268
  * §Extractor · enrichment layer.
2210
2269
  */
2211
2270
  interface IEnrichmentRecord {
@@ -2224,13 +2283,13 @@ interface IEnrichmentRecord {
2224
2283
  * a focused refresh result, etc.).
2225
2284
  *
2226
2285
  * Exported so `cli/commands/refresh.ts` can reuse the same wiring it
2227
- * needs for re-running a single extractor against a single node the
2286
+ * needs for re-running a single extractor against a single node, the
2228
2287
  * pre-extraction code in `refresh.ts` was hand-duplicating this loop
2229
2288
  * (audit item V4).
2230
2289
  *
2231
2290
  * Within this call, multiple `enrichNode(partial)` calls from the same
2232
2291
  * extractor against the same node fold into one record (last-write-wins
2233
- * per field) same contract as the in-scan path.
2292
+ * per field), same contract as the in-scan path.
2234
2293
  */
2235
2294
  declare function runExtractorsForNode(opts: {
2236
2295
  extractors: IExtractor[];
@@ -2240,7 +2299,7 @@ declare function runExtractorsForNode(opts: {
2240
2299
  bodyHash: string;
2241
2300
  emitter: ProgressEmitterPort;
2242
2301
  /**
2243
- * Spec § A.12 per-plugin `ctx.store` wrappers keyed by `pluginId`.
2302
+ * Spec § A.12, per-plugin `ctx.store` wrappers keyed by `pluginId`.
2244
2303
  * The map's lookup is per-extractor inside the loop, so callers that
2245
2304
  * don't track plugin storage can omit it; the resulting `ctx.store`
2246
2305
  * stays `undefined` (the existing contract).
@@ -2274,7 +2333,7 @@ interface RenameOp {
2274
2333
  }
2275
2334
  /**
2276
2335
  * Pure rename / orphan classification per `spec/db-schema.md` §Rename
2277
- * detection. Mutates `issues` in place caller passes the in-progress
2336
+ * detection. Mutates `issues` in place, caller passes the in-progress
2278
2337
  * issue list; returns the `RenameOp[]` for the persistence layer to
2279
2338
  * apply inside its tx.
2280
2339
  *
@@ -2295,30 +2354,30 @@ interface RenameOp {
2295
2354
  * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
2296
2355
  *
2297
2356
  * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
2298
- * order so the same input always produces the same matches
2357
+ * order so the same input always produces the same matches,
2299
2358
  * required for reproducible tests and conformance fixtures (the spec
2300
2359
  * does not prescribe an order, but stability is the obvious contract).
2301
2360
  */
2302
2361
  declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
2303
2362
 
2304
2363
  /**
2305
- * Scan orchestrator runs the Provider → extractor → analyzer pipeline across
2364
+ * Scan orchestrator, runs the Provider → extractor → analyzer pipeline across
2306
2365
  * every registered extension and emits `ProgressEmitterPort` events in
2307
2366
  * canonical order. The callable extension set is injected via
2308
- * `RunScanOptions.extensions` the Registry holds manifest metadata, the
2367
+ * `RunScanOptions.extensions`, the Registry holds manifest metadata, the
2309
2368
  * callable set holds the runtime instances the orchestrator actually
2310
2369
  * invokes. Separating the two lets `sm plugins` and `sm help` introspect
2311
2370
  * the graph without loading code.
2312
2371
  *
2313
2372
  * With zero registered extensions (or a callable set that carries none)
2314
- * the pipeline still produces a valid zero-filled `ScanResult` the
2373
+ * the pipeline still produces a valid zero-filled `ScanResult`, the
2315
2374
  * kernel-empty-boot invariant.
2316
2375
  *
2317
2376
  * Roots are validated up front: each entry of `RunScanOptions.roots`
2318
2377
  * must exist on disk as a directory. The first failure throws a clear
2319
2378
  * `Error` naming the offending path. This guards every caller (CLI,
2320
2379
  * server, skill-agent) against silently producing a zero-filled
2321
- * `ScanResult` when a Provider walks a non-existent path the bug
2380
+ * `ScanResult` when a Provider walks a non-existent path, the bug
2322
2381
  * that wiped a populated DB via `sm scan -- --dry-run` (clipanion's
2323
2382
  * `--` made `--dry-run` a positional root that did not exist).
2324
2383
  *
@@ -2328,7 +2387,7 @@ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], iss
2328
2387
  * `bodyHash` and `frontmatterHash` match. New / modified files run
2329
2388
  * through the full extractor pipeline (including the external-url-counter
2330
2389
  * which produces ephemeral pseudo-links). Rules ALWAYS run over the
2331
- * 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
2332
2391
  * (e.g. a previously broken `references` link now resolves because a new
2333
2392
  * node was added). For unchanged nodes the prior `externalRefsCount` is
2334
2393
  * preserved as-is (the external pseudo-links were never persisted, so
@@ -2342,7 +2401,7 @@ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], iss
2342
2401
  * entry per `(node, extractor)` so attribution survives into the DB.
2343
2402
  * Persisted into `node_enrichments` (A.8). The author-supplied
2344
2403
  * frontmatter on `node.frontmatter` stays immutable from any Extractor
2345
- * the enrichment layer is the only writable surface, and rules /
2404
+ * , the enrichment layer is the only writable surface, and rules /
2346
2405
  * formatters consume it via `mergeNodeWithEnrichments`.
2347
2406
  * - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).
2348
2407
  * Wired by the driving adapter via `RunScanOptions.pluginStores`,
@@ -2377,13 +2436,13 @@ interface RunScanOptions {
2377
2436
  /** Runtime extension instances. Absent → empty pipeline. */
2378
2437
  extensions?: IScanExtensions;
2379
2438
  /**
2380
- * Step 9.6.6 runtime catalog of plugin-contributed annotation keys
2439
+ * Step 9.6.6, runtime catalog of plugin-contributed annotation keys
2381
2440
  * (the same shape `kernel.getRegisteredAnnotationKeys()` returns).
2382
2441
  * Threaded into the rule pass so `core/unknown-field` can
2383
2442
  * legitimise registered plugin namespaces / root keys without
2384
2443
  * re-walking the manifests. Absent → empty catalog (every plugin
2385
2444
  * key is treated as unknown). Built-in catalog from
2386
- * `annotations.schema.json` is NOT included that is hard-coded
2445
+ * `annotations.schema.json` is NOT included, that is hard-coded
2387
2446
  * inside the rule.
2388
2447
  */
2389
2448
  annotationContributions?: readonly IRegisteredAnnotationKey[];
@@ -2391,8 +2450,12 @@ interface RunScanOptions {
2391
2450
  * Runtime catalog of plugin-contributed view contributions (the same
2392
2451
  * shape `kernel.getRegisteredViewContributions()` returns). Threaded
2393
2452
  * into the rule pass so:
2394
- * - `core/unknown-slot` and `core/contribution-orphan` can
2395
- * 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).
2396
2459
  * - The orchestrator's per-rule emit closure can look up each
2397
2460
  * declared `(contributionId → slot)` pairing for AJV
2398
2461
  * payload validation.
@@ -2463,7 +2526,7 @@ interface RunScanOptions {
2463
2526
  */
2464
2527
  strict?: boolean;
2465
2528
  /**
2466
- * Spec § A.9 fine-grained Extractor cache breadcrumbs from the
2529
+ * Spec § A.9, fine-grained Extractor cache breadcrumbs from the
2467
2530
  * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
2468
2531
  * Loaded from the `scan_extractor_runs` table by the CLI before
2469
2532
  * invoking `runScan`; absent / empty for a fresh DB or an out-of-band
@@ -2483,11 +2546,11 @@ interface RunScanOptions {
2483
2546
  */
2484
2547
  priorExtractorRuns?: Map<string, Map<string, IPriorExtractorRun>>;
2485
2548
  /**
2486
- * Spec § A.12 per-plugin storage wrappers exposed to extractors via
2549
+ * Spec § A.12, per-plugin storage wrappers exposed to extractors via
2487
2550
  * `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves
2488
2551
  * `ctx.store` undefined for that extractor (the existing contract).
2489
2552
  *
2490
- * The kernel does not construct these the driving adapter (CLI,
2553
+ * The kernel does not construct these, the driving adapter (CLI,
2491
2554
  * future server) builds them with `makePluginStore` from
2492
2555
  * `kernel/adapters/plugin-store.js` and threads them through. This
2493
2556
  * keeps the orchestrator persistence-agnostic (the wrapper supplies
@@ -2504,7 +2567,7 @@ interface RunScanOptions {
2504
2567
  * its own FS walk. The driving adapter (CLI, BFF) computes this
2505
2568
  * inside its already-open storage transaction via
2506
2569
  * `findOrphanJobFiles(jobsDir, await port.jobs.listReferencedFilePaths())`
2507
- * mirrors the `orphanSidecars` model where detection lives
2570
+ * mirrors the `orphanSidecars` model where detection lives
2508
2571
  * outside the rule and the rule only projects. Absent / empty when
2509
2572
  * the caller has no jobs context (out-of-band tests, fresh DB,
2510
2573
  * `--no-built-ins`).
@@ -2516,7 +2579,7 @@ interface RunScanOptions {
2516
2579
  * through to `IAnalyzerContext.referenceablePaths` so the built-in
2517
2580
  * `core/broken-ref` rule can suppress its `warn` for path-style
2518
2581
  * links whose target lands in the set. Files are NOT walked by
2519
- * the kernel the driving adapter populates the set before
2582
+ * the kernel, the driving adapter populates the set before
2520
2583
  * calling `runScan`. Absent / empty when the operator left
2521
2584
  * `scan.referencePaths` unconfigured.
2522
2585
  */
@@ -2532,13 +2595,13 @@ interface RunScanOptions {
2532
2595
  }
2533
2596
  /**
2534
2597
  * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`
2535
- * the high- and medium-confidence renames the persistence layer must
2598
+ * the high- and medium-confidence renames the persistence layer must
2536
2599
  * apply to `state_*` rows inside the same tx as the scan zone replace-
2537
2600
  * all (per `spec/db-schema.md` §Rename detection). Most callers want
2538
2601
  * `runScan` (which returns just `ScanResult`); the CLI's `sm scan`
2539
2602
  * uses this variant so it can hand the ops off to `persistScanResult`.
2540
2603
  *
2541
- * Also returns `extractorRuns` the Spec § A.9 fine-grained cache
2604
+ * Also returns `extractorRuns`, the Spec § A.9 fine-grained cache
2542
2605
  * breadcrumbs the CLI persists into `scan_extractor_runs` so the next
2543
2606
  * incremental scan can decide per-(node, extractor) whether re-running
2544
2607
  * is required.
@@ -2558,15 +2621,15 @@ declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<Scan
2558
2621
  * sidecar annotations, resolve the sidecar overlay for a given relative
2559
2622
  * path, and produce a fresh `Node` (validating its frontmatter on the
2560
2623
  * way out). Also hosts `mergeNodeWithEnrichments` + `IPersistedEnrichment`
2561
- * the read-time merge of author frontmatter with the A.8 enrichment
2624
+ * the read-time merge of author frontmatter with the A.8 enrichment
2562
2625
  * layer.
2563
2626
  */
2564
2627
 
2565
2628
  /**
2566
- * 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.
2567
2630
  *
2568
2631
  * Rules / `sm check` / `sm export` consume `node.frontmatter` directly
2569
- * (deterministic CI-safe baseline author intent, byte-stable). UI / future
2632
+ * (deterministic CI-safe baseline, author intent, byte-stable). UI / future
2570
2633
  * rules that opt into enrichment context call this helper to merge the
2571
2634
  * author frontmatter with the live enrichment layer.
2572
2635
  *
@@ -2580,18 +2643,18 @@ declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<Scan
2580
2643
  * belongs to the UI layer next to the value.
2581
2644
  * 2. Sort the survivors by `enrichedAt` ASC so iteration order is
2582
2645
  * "oldest first". This makes the spread merge below
2583
- * last-write-wins per field the freshest Extractor's value
2646
+ * last-write-wins per field, the freshest Extractor's value
2584
2647
  * pisar the older one for any conflicting key.
2585
2648
  * 3. Spread-merge each row's `value` over `node.frontmatter`. The
2586
2649
  * author's keys are the base; enrichment keys overlay them.
2587
2650
  *
2588
- * 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
2589
2652
  * touch the caller's node. The original `node.frontmatter` reference
2590
2653
  * remains accessible via `node.frontmatter` for callers that want the
2591
2654
  * pristine author baseline.
2592
2655
  *
2593
2656
  * @param node Node to merge against; `node.frontmatter` is the base.
2594
- * @param enrichments Per-(node, extractor) enrichment records typically
2657
+ * @param enrichments Per-(node, extractor) enrichment records, typically
2595
2658
  * loaded via `loadNodeEnrichments(db, node.path)` or
2596
2659
  * pre-filtered to this node by the caller.
2597
2660
  * @param opts.includeStale When true, include rows flagged stale. Defaults
@@ -2621,7 +2684,7 @@ interface IPersistedEnrichment {
2621
2684
  }
2622
2685
 
2623
2686
  /**
2624
- * In-memory `ProgressEmitterPort` adapter. No network, no DB just a
2687
+ * In-memory `ProgressEmitterPort` adapter. No network, no DB, just a
2625
2688
  * synchronous fan-out to registered listeners. Used by the default scan
2626
2689
  * orchestrator; the WebSocket-backed emitter that streams to
2627
2690
  * the Web UI lands.
@@ -2638,7 +2701,7 @@ declare class InMemoryProgressEmitter implements ProgressEmitterPort {
2638
2701
  *
2639
2702
  * Wraps `chokidar` behind a small `IFsWatcher` interface so:
2640
2703
  *
2641
- * 1. The CLI command is impl-agnostic swapping chokidar for a
2704
+ * 1. The CLI command is impl-agnostic, swapping chokidar for a
2642
2705
  * different watcher later (Java? Rust port? a future `WatchPort`?)
2643
2706
  * doesn't ripple into the command.
2644
2707
  * 2. Debouncing, batching, and ignore-filter integration live in one
@@ -2687,28 +2750,37 @@ interface ICreateFsWatcherOptions {
2687
2750
  /** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */
2688
2751
  debounceMs: number;
2689
2752
  /**
2690
- * Optional ignore filter same instance the scan walker uses.
2753
+ * Optional ignore filter, same instance the scan walker uses.
2691
2754
  *
2692
2755
  * Two shapes are accepted:
2693
2756
  *
2694
- * - **`IIgnoreFilter`** (the static one) captured by reference at
2757
+ * - **`IIgnoreFilter`** (the static one), captured by reference at
2695
2758
  * construction. Use this when the filter never changes for the
2696
2759
  * lifetime of the watcher (the typical CLI `sm watch` flow).
2697
2760
  *
2698
- * - **`() => IIgnoreFilter | undefined`** (a getter) re-evaluated
2761
+ * - **`() => IIgnoreFilter | undefined`** (a getter), re-evaluated
2699
2762
  * on EVERY chokidar `ignored` predicate call. Use this when the
2700
- * 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
2701
2764
  * a `.skillmapignore` or `.skill-map/settings.json` edit and
2702
2765
  * wants chokidar to immediately respect the new patterns without
2703
2766
  * tearing down and rebuilding the watcher. A getter that returns
2704
2767
  * `undefined` disables ignore filtering for that call.
2705
2768
  */
2706
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;
2707
2779
  /** Called once per debounced batch. Awaited; concurrent batches are serialised. */
2708
2780
  onBatch: (batch: IWatchBatch) => void | Promise<void>;
2709
2781
  /**
2710
2782
  * Called when the underlying watcher surfaces an error. The watcher
2711
- * stays open callers decide whether to log, keep going, or close.
2783
+ * stays open, callers decide whether to log, keep going, or close.
2712
2784
  */
2713
2785
  onError?: (err: Error) => void;
2714
2786
  }
@@ -2717,7 +2789,7 @@ interface ICreateFsWatcherOptions {
2717
2789
  * returned `ready` promise resolves once chokidar's initial directory
2718
2790
  * walk completes, at which point only NEW events fire `onBatch`.
2719
2791
  *
2720
- * The initial directory walk is deliberately silent we set
2792
+ * The initial directory walk is deliberately silent, we set
2721
2793
  * `ignoreInitial: true`. The CLI runs a one-shot scan before flipping
2722
2794
  * the watcher on, so re-emitting an `add` for every existing file
2723
2795
  * would be redundant churn.
@@ -2725,20 +2797,20 @@ interface ICreateFsWatcherOptions {
2725
2797
  declare function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatcher;
2726
2798
 
2727
2799
  /**
2728
- * Scan delta pure comparison of two `ScanResult` snapshots. Drives
2800
+ * Scan delta, pure comparison of two `ScanResult` snapshots. Drives
2729
2801
  * `sm scan --compare-with <path>` and is the single place the kernel
2730
2802
  * knows how to identify "the same" entity across two scans.
2731
2803
  *
2732
2804
  * **Identity contract** (mirrors decisions made at earlier sub-steps):
2733
2805
  *
2734
2806
  * - **Node**: `node.path`. The path is the only field stable across
2735
- * edits every other Node field is content-derived (hashes, counts,
2807
+ * edits, every other Node field is content-derived (hashes, counts,
2736
2808
  * denormalised frontmatter). Two nodes with the same path are the
2737
2809
  * "same" node; differences are reported as a `changed` entry with
2738
2810
  * a reason narrowing what diverged.
2739
2811
  *
2740
2812
  * - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This
2741
- * mirrors the link-conflict rule and `sm show` aggregation
2813
+ * mirrors the link-conflict rule and `sm show` aggregation,
2742
2814
  * two links with identical endpoints, kind, and (optional) trigger
2743
2815
  * are the same link, even if emitted by different extractors. The
2744
2816
  * `sources[]` union and confidence are NOT part of identity; they
@@ -2746,13 +2818,13 @@ declare function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatche
2746
2818
  * "different" for delta purposes.
2747
2819
  *
2748
2820
  * - **Issue**: `(analyzerId, sorted nodeIds, message)`. Mirrors
2749
- * `spec/job-events.md` §issue.* same key → same issue, even when
2821
+ * `spec/job-events.md` §issue.*, same key → same issue, even when
2750
2822
  * `data` / `severity` / `linkIndices` shift. A meaningful change in
2751
2823
  * `message` (or a different set of node ids) is a different issue.
2752
2824
  * This is the same key future job events will use; keep it aligned
2753
2825
  * so consumers can reuse logic.
2754
2826
  *
2755
- * No "changed" bucket for links / issues identity already captures
2827
+ * No "changed" bucket for links / issues, identity already captures
2756
2828
  * everything that matters there. Nodes get a "changed" bucket because
2757
2829
  * the path stays stable while the body / frontmatter rewrite, and that
2758
2830
  * change is meaningful (formatters, summarisers, downstream consumers
@@ -2798,7 +2870,7 @@ declare function computeScanDelta(prior: ScanResult, current: ScanResult, compar
2798
2870
  declare function isEmptyDelta(delta: IScanDelta): boolean;
2799
2871
 
2800
2872
  /**
2801
- * 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).
2802
2874
  *
2803
2875
  * Spec contract: `spec/cli-contract.md` line 190 says "Query syntax is
2804
2876
  * implementation-defined pre-1.0". This module defines the v0.5.0 syntax.
@@ -2816,12 +2888,12 @@ declare function isEmptyDelta(delta: IScanDelta): boolean;
2816
2888
  *
2817
2889
  * **Filters**:
2818
2890
  *
2819
- * - `kind=skill` / `kind=skill,agent` node kind whitelist.
2820
- * - `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
2821
2893
  * expansion: `has=findings` / `has=summary` once Step 10 / 11 land.
2822
2894
  * Unknown values are a parse error today; we'll ratchet up the
2823
2895
  * accepted set additively.)
2824
- * - `path=foo/*` / `path=.claude/agents/**` POSIX glob over `node.path`.
2896
+ * - `path=foo/*` / `path=.claude/agents/**`, POSIX glob over `node.path`.
2825
2897
  * Supports `*` (any chars except `/`) and `**` (any chars including `/`).
2826
2898
  *
2827
2899
  * **Subset semantics** (`applyExportQuery`):
@@ -2830,7 +2902,7 @@ declare function isEmptyDelta(delta: IScanDelta): boolean;
2830
2902
  * OR within values).
2831
2903
  * - Links survive only when BOTH endpoints (`source` + `target`) belong
2832
2904
  * to the filtered node set. A subset that includes "edges out to
2833
- * unfiltered nodes" would be confusing the user asked for a focused
2905
+ * unfiltered nodes" would be confusing, the user asked for a focused
2834
2906
  * subgraph, not its boundary. External-URL pseudo-links are already
2835
2907
  * stripped by the orchestrator and never reach this layer.
2836
2908
  * - Issues survive when ANY of the issue's `nodeIds` is in the filtered
@@ -2845,7 +2917,7 @@ interface IExportQuery {
2845
2917
  /** Original query string echoed back so consumers can render the header. */
2846
2918
  raw: string;
2847
2919
  /**
2848
- * Whitelist of node kinds (`node.kind` is open string built-in
2920
+ * Whitelist of node kinds (`node.kind` is open string, built-in
2849
2921
  * Claude catalog `skill` / `agent` / `command` / `hook` / `note`,
2850
2922
  * plus whatever external Providers declare). The query parser does
2851
2923
  * not validate values against a closed enum; an unknown kind simply
@@ -2872,16 +2944,16 @@ declare function applyExportQuery(scan: {
2872
2944
  }, query: IExportQuery): IExportSubset;
2873
2945
 
2874
2946
  /**
2875
- * `scan_node_tags` adapter tags · dual-source persistence layer.
2947
+ * `scan_node_tags` adapter, tags · dual-source persistence layer.
2876
2948
  *
2877
2949
  * One row per `(node_path, tag, source)` triple. Projected at persist
2878
2950
  * time from BOTH `frontmatter.tags` (with `source='author'`) and
2879
2951
  * `sidecar.annotations.tags` (with `source='user'`). The same tag
2880
- * 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
2881
2953
  * accepts the pair; search returns the node once via DISTINCT, the
2882
2954
  * UI renders both chips with their attribution.
2883
2955
  *
2884
- * Belongs to the `scan_*` family replaced wholesale per scan.
2956
+ * Belongs to the `scan_*` family, replaced wholesale per scan.
2885
2957
  * Cached nodes' tag rows are projected from the cached
2886
2958
  * `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both
2887
2959
  * already in memory at persist time), so the rebuild is cheap
@@ -2906,17 +2978,17 @@ interface ITagRecord {
2906
2978
  * Pure helpers for the "update available" notification feature.
2907
2979
  *
2908
2980
  * Three responsibilities:
2909
- * - `fetchLatestVersion` query `https://registry.npmjs.org/<pkg>/latest`
2981
+ * - `fetchLatestVersion` , query `https://registry.npmjs.org/<pkg>/latest`
2910
2982
  * with `AbortController` + timeout. Throws on
2911
2983
  * non-200 / parse failure / abort.
2912
- * - `compareVersions` semver compare (-1 / 0 / 1). Pre-1.0 aware:
2984
+ * - `compareVersions` , semver compare (-1 / 0 / 1). Pre-1.0 aware:
2913
2985
  * treats prereleases via the standard rules
2914
2986
  * (release > prerelease at the same triple).
2915
- * - `isOutdated` sugar over `compareVersions` for the common
2987
+ * - `isOutdated` , sugar over `compareVersions` for the common
2916
2988
  * "is `latest` strictly greater than `current`"
2917
2989
  * check the banner runs against.
2918
2990
  *
2919
- * 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
2920
2992
  * built-in `fetch` / `AbortController` (Node 22+). Every env / settings
2921
2993
  * lookup happens in `src/cli/util/update-check-banner.ts`, the CLI-side
2922
2994
  * adapter that owns side effects.
@@ -2924,21 +2996,21 @@ interface ITagRecord {
2924
2996
  * The shared cache type (`IUpdateCheckCache`) is used by the storage
2925
2997
  * helpers under `kernel/storage/update-check.ts` and by the BFF's
2926
2998
  * `GET /api/update-status` projection. A second type
2927
- * (`IUpdateStatus`) shapes the BFF response it merges `current`
2999
+ * (`IUpdateStatus`) shapes the BFF response, it merges `current`
2928
3000
  * (from `VERSION`) into the cache so the UI can render without a
2929
- * second lookup. Both stay flat no nested objects so JSON
3001
+ * second lookup. Both stay flat, no nested objects, so JSON
2930
3002
  * serialization is trivial.
2931
3003
  */
2932
3004
  interface IUpdateCheckCache {
2933
3005
  latestVersion: string;
2934
- /** Epoch ms when the registry was last successfully probed. */
3006
+ /** Epoch ms, when the registry was last successfully probed. */
2935
3007
  checkedAt: number;
2936
- /** 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. */
2937
3009
  shownAt: number | null;
2938
3010
  }
2939
3011
 
2940
3012
  /**
2941
- * `PluginLoaderPort` discovers plugin directories and loads their
3013
+ * `PluginLoaderPort`, discovers plugin directories and loads their
2942
3014
  * extensions. The shape mirrors what the concrete loader actually
2943
3015
  * exposes (see `kernel/adapters/plugin-loader.ts`); the port exists so
2944
3016
  * the CLI consumes the abstract contract via `createPluginLoader(...)`
@@ -2961,12 +3033,12 @@ interface PluginLoaderPort {
2961
3033
  discoverPaths(): string[];
2962
3034
  /**
2963
3035
  * Discover every plugin, attempt to load each, then apply the
2964
- * cross-root id-collision pass. Never throws failures are reported
3036
+ * cross-root id-collision pass. Never throws, failures are reported
2965
3037
  * via `IDiscoveredPlugin.status`.
2966
3038
  */
2967
3039
  discoverAndLoadAll(): Promise<IDiscoveredPlugin[]>;
2968
3040
  /**
2969
- * Load a single plugin from its directory. Never throws failure is
3041
+ * Load a single plugin from its directory. Never throws, failure is
2970
3042
  * reported via the returned `status`.
2971
3043
  */
2972
3044
  loadOne(pluginPath: string): Promise<IDiscoveredPlugin>;
@@ -2974,7 +3046,7 @@ interface PluginLoaderPort {
2974
3046
 
2975
3047
  /**
2976
3048
  * Row-level filter for `port.scans.findNodes(...)` (driven by
2977
- * `sm list`'s flags). All fields are optional an empty filter
3049
+ * `sm list`'s flags). All fields are optional, an empty filter
2978
3050
  * returns every node sorted by `path` asc.
2979
3051
  */
2980
3052
  interface INodeFilter {
@@ -2998,7 +3070,7 @@ interface INodeFilter {
2998
3070
  limit?: number;
2999
3071
  }
3000
3072
  /**
3001
- * Bundled fetch for `port.scans.findNode(path)` one node and
3073
+ * Bundled fetch for `port.scans.findNode(path)`, one node and
3002
3074
  * everything `sm show <path>` displays alongside it. Every field is
3003
3075
  * computed from `scan_*` zone reads only; per-domain data (history,
3004
3076
  * jobs, plugin enrichments) ships through other namespaces.
@@ -3032,7 +3104,7 @@ interface IPersistOptions {
3032
3104
  enrichments?: IEnrichmentRecord[];
3033
3105
  contributions?: IContributionRecord[];
3034
3106
  /**
3035
- * Phase 3 / View contribution system active runtime catalog of
3107
+ * Phase 3 / View contribution system, active runtime catalog of
3036
3108
  * registered view contributions, keyed by qualified id
3037
3109
  * `<pluginId>/<extensionId>/<contributionId>`. Passed to the
3038
3110
  * `scan_contributions` upsert so the catalog sweep can drop rows
@@ -3044,10 +3116,10 @@ interface IPersistOptions {
3044
3116
  */
3045
3117
  registeredContributionKeys?: ReadonlySet<string>;
3046
3118
  /**
3047
- * Phase 3 / View contribution system set of `(plugin, extension,
3119
+ * Phase 3 / View contribution system, set of `(plugin, extension,
3048
3120
  * node)` tuples where the extension actually RAN against that node
3049
3121
  * in this scan. Format: `<pluginId>/<extensionId>/<nodePath>` (no
3050
- * contribution-id segment the sweep operates at the (plugin,
3122
+ * contribution-id segment, the sweep operates at the (plugin,
3051
3123
  * extension, node) level and inspects the buffer to decide which
3052
3124
  * contribution-ids survive).
3053
3125
  *
@@ -3070,7 +3142,7 @@ interface IPersistOptions {
3070
3142
  freshlyRunTuples?: ReadonlySet<string>;
3071
3143
  }
3072
3144
  /**
3073
- * 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
3074
3146
  * id so `port.issues.deleteById(id)` can target it inside a
3075
3147
  * transaction. The runtime `Issue` shape (per `issue.schema.json`) does
3076
3148
  * not carry `id` because the spec models issues as ephemeral findings
@@ -3081,6 +3153,54 @@ interface IIssueRow {
3081
3153
  id: number;
3082
3154
  issue: Issue;
3083
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
+ }
3084
3204
  /** Output of `port.jobs.pruneTerminal` / `listTerminalCandidates`. */
3085
3205
  interface IPruneResult {
3086
3206
  /** How many `state_jobs` rows were deleted (or would be, in dry-run). */
@@ -3127,7 +3247,7 @@ interface IMigrateNodeFksReport {
3127
3247
  /**
3128
3248
  * Collisions encountered when migrating any of the keyed-by-node
3129
3249
  * `state_*` tables because a row already existed at the destination
3130
- * PK. The pre-existing rows are preserved the migrating rows are
3250
+ * PK. The pre-existing rows are preserved, the migrating rows are
3131
3251
  * dropped (deleted from `fromPath` without a corresponding INSERT).
3132
3252
  * One entry per dropped row, with the affected PK fields included
3133
3253
  * for diagnostic output. `state_node_favorites` has no composite key
@@ -3210,7 +3330,7 @@ interface IPluginApplyResult {
3210
3330
 
3211
3331
  /**
3212
3332
  * Subset of `StoragePort` exposed inside a `transaction(fn)` callback.
3213
- * Lifecycle methods are intentionally omitted a transaction that
3333
+ * Lifecycle methods are intentionally omitted, a transaction that
3214
3334
  * tries to `init()` the adapter mid-flight is a category error.
3215
3335
  *
3216
3336
  * Every callable in the subset MUST run on the same underlying
@@ -3231,7 +3351,7 @@ interface ITransactionalStorage {
3231
3351
  * Upsert a batch of fresh enrichment records produced by an
3232
3352
  * extractor pass. Composite PK is `(nodePath, extractorId)`;
3233
3353
  * conflict → replace. Every row lands with `stale = 0` (the
3234
- * caller just refreshed it; ROADMAP §B.10 staleness is
3354
+ * caller just refreshed it; ROADMAP §B.10, staleness is
3235
3355
  * computed downstream when the body hash changes again).
3236
3356
  */
3237
3357
  upsertMany(records: IEnrichmentRecord[]): Promise<void>;
@@ -3255,23 +3375,23 @@ interface StoragePort {
3255
3375
  * Persist a fresh `ScanResult` (replace-all on the scan zone).
3256
3376
  * Called by `sm scan` after the orchestrator returns. The renames /
3257
3377
  * extractor-runs / enrichments side bags ride along inside the
3258
- * same transaction the call is atomic from the caller's view.
3378
+ * same transaction, the call is atomic from the caller's view.
3259
3379
  */
3260
3380
  persist(result: ScanResult, opts?: IPersistOptions): Promise<void>;
3261
3381
  /**
3262
3382
  * Hydrate the persisted `ScanResult`. Returns the snapshot the
3263
- * scan zone holds today (including external-Provider kinds
3383
+ * scan zone holds today (including external-Provider kinds,
3264
3384
  * `node.kind` is open string per `node.schema.json`).
3265
3385
  */
3266
3386
  load(): Promise<ScanResult>;
3267
3387
  /**
3268
- * Spec § A.9 fine-grained extractor-runs cache breadcrumbs.
3388
+ * Spec § A.9, fine-grained extractor-runs cache breadcrumbs.
3269
3389
  * Returns `Map<nodePath, Map<qualifiedExtractorId, IPriorExtractorRun>>`.
3270
3390
  * Inner value carries `bodyHash` AND `sidecarAnnotationsHash`; both
3271
3391
  * participate in the cache hit condition for every Extractor.
3272
3392
  */
3273
3393
  loadExtractorRuns(): Promise<Map<string, Map<string, IPriorExtractorRun>>>;
3274
- /** Universal enrichment layer every persisted `(node, extractor)` pair. */
3394
+ /** Universal enrichment layer, every persisted `(node, extractor)` pair. */
3275
3395
  loadNodeEnrichments(): Promise<IPersistedEnrichment[]>;
3276
3396
  /**
3277
3397
  * Row counts for `scan_nodes` / `scan_links` / `scan_issues`.
@@ -3287,7 +3407,7 @@ interface StoragePort {
3287
3407
  findNode(path: string): Promise<INodeBundle | null>;
3288
3408
  };
3289
3409
  /**
3290
- * Phase 3 / View contribution system read access to
3410
+ * Phase 3 / View contribution system, read access to
3291
3411
  * `scan_contributions`, plus the targeted purge used by
3292
3412
  * `sm plugins disable` to clear stale rows immediately at toggle time.
3293
3413
  * Bulk writes still happen exclusively via
@@ -3341,6 +3461,22 @@ interface StoragePort {
3341
3461
  issues: {
3342
3462
  /** Every issue from the latest scan, in insertion order. */
3343
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>;
3344
3480
  /**
3345
3481
  * Issue rows whose runtime `Issue` shape passes `predicate`.
3346
3482
  * `port.issues.findActive((i) => i.analyzerId === 'orphan')` is the
@@ -3390,19 +3526,19 @@ interface StoragePort {
3390
3526
  * `path.resolve()`. The CLI's `sm job prune --orphan-files` flow
3391
3527
  * pairs this set with `kernel/jobs/orphan-files.ts:findOrphanJobFiles`
3392
3528
  * (which walks the directory) to compute the MD files on disk that
3393
- * no row references keeps the storage layer FS-free.
3529
+ * no row references, keeps the storage layer FS-free.
3394
3530
  */
3395
3531
  listReferencedFilePaths(): Promise<Set<string>>;
3396
3532
  };
3397
3533
  /**
3398
3534
  * Generic key/value preferences keyed by a stable string. Backs the
3399
- * `config_preferences` table one row per `key`, `value_json` is a
3535
+ * `config_preferences` table, one row per `key`, `value_json` is a
3400
3536
  * single JSON blob the caller serialises. Keys with the `_kernel.`
3401
3537
  * prefix are reserved for kernel-managed entries (today: the
3402
3538
  * update-check cache); user-set preferences land under unprefixed
3403
3539
  * keys when those ship.
3404
3540
  *
3405
- * 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
3406
3542
  * CLI's post-run hook (`cli/util/update-check-banner.ts`), which
3407
3543
  * reaches the persistence helpers directly. The port surfaces the
3408
3544
  * read so the BFF's `GET /api/update-status` projection can stay
@@ -3412,31 +3548,31 @@ interface StoragePort {
3412
3548
  /**
3413
3549
  * Load the update-check cache row. Returns `null` when the row
3414
3550
  * is absent, malformed JSON, or fails the shape guard. Never
3415
- * throws read failures degrade silently because the banner is
3551
+ * throws, read failures degrade silently because the banner is
3416
3552
  * a non-essential surface.
3417
3553
  */
3418
3554
  loadUpdateCheckCache(): Promise<IUpdateCheckCache | null>;
3419
3555
  /**
3420
3556
  * Upsert the update-check cache row. Always overwrites the
3421
3557
  * existing JSON blob in place. `updated_at` tracks wall-clock
3422
- * now separate from the embedded `checkedAt` field, which
3558
+ * now, separate from the embedded `checkedAt` field, which
3423
3559
  * the caller controls.
3424
3560
  */
3425
3561
  saveUpdateCheckCache(cache: IUpdateCheckCache): Promise<void>;
3426
3562
  };
3427
3563
  favorites: {
3428
3564
  /**
3429
- * Mark `path` as favorited. Idempotent a second call refreshes
3565
+ * Mark `path` as favorited. Idempotent, a second call refreshes
3430
3566
  * `favoritedAt` but does not error. The path is FK-semantic to
3431
3567
  * `scan_nodes.path`; the route layer is responsible for confirming
3432
3568
  * the path exists in the live scan before calling.
3433
3569
  */
3434
3570
  set(path: string): Promise<void>;
3435
- /** Drop the favorite row for `path`. Idempotent no-op when absent. */
3571
+ /** Drop the favorite row for `path`. Idempotent, no-op when absent. */
3436
3572
  unset(path: string): Promise<void>;
3437
3573
  /**
3438
3574
  * Load every favorited path as a `Set<string>` ready for `O(1)`
3439
- * membership checks. Used by the BFF's `/api/nodes` decorator
3575
+ * membership checks. Used by the BFF's `/api/nodes` decorator,
3440
3576
  * one query per request, no SQL JOIN against `scan_nodes`.
3441
3577
  */
3442
3578
  listPaths(): Promise<Set<string>>;
@@ -3510,7 +3646,7 @@ interface StoragePort {
3510
3646
  }
3511
3647
 
3512
3648
  /**
3513
- * `FilesystemPort` walks roots, reads nodes, writes job files.
3649
+ * `FilesystemPort`, walks roots, reads nodes, writes job files.
3514
3650
  *
3515
3651
  * Shape-only. The real adapter ships with the scan end-to-end pipeline.
3516
3652
  */
@@ -3531,7 +3667,7 @@ interface FilesystemPort {
3531
3667
  }
3532
3668
 
3533
3669
  /**
3534
- * `RunnerPort` executes an action against a rendered job file.
3670
+ * `RunnerPort`, executes an action against a rendered job file.
3535
3671
  *
3536
3672
  * Shape-only. `ClaudeCliRunner` + `MockRunner` land with the job subsystem
3537
3673
  * (job subsystem + first summarizer).
@@ -3552,7 +3688,7 @@ interface RunnerPort {
3552
3688
  }
3553
3689
 
3554
3690
  /**
3555
- * `LoggerPort` structured logging port for the kernel.
3691
+ * `LoggerPort`, structured logging port for the kernel.
3556
3692
  *
3557
3693
  * The kernel must NOT write to stdout/stderr directly. Anything that
3558
3694
  * would historically have been a `console.log` / `console.error` goes
@@ -3563,7 +3699,7 @@ interface RunnerPort {
3563
3699
  *
3564
3700
  * trace < debug < info < warn < error < silent
3565
3701
  *
3566
- * `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
3567
3703
  * `LogRecord.level`. Setting an adapter to `silent` disables every
3568
3704
  * method.
3569
3705
  */
@@ -3600,7 +3736,7 @@ interface LoggerPort {
3600
3736
  * `InMemoryProgressEmitter`: callers that don't care get a working
3601
3737
  * implementation that does nothing.
3602
3738
  *
3603
- * Every method is intentionally empty that IS the contract of this
3739
+ * Every method is intentionally empty, that IS the contract of this
3604
3740
  * class. We disable `no-empty-function` for the whole file because
3605
3741
  * adding `// eslint-disable-next-line` to each method would be noise.
3606
3742
  */
@@ -3625,7 +3761,7 @@ declare class SilentLogger implements LoggerPort {
3625
3761
  * side-channel concern.
3626
3762
  * - The active impl is a pointer; the exported `log` is a stable
3627
3763
  * proxy. Imports made before `configureLogger` runs still see the
3628
- * new impl on every call no "captured stale logger" bugs.
3764
+ * new impl on every call, no "captured stale logger" bugs.
3629
3765
  *
3630
3766
  * Tradeoffs accepted:
3631
3767
  * - Tests must call `resetLogger()` (or replace the active impl) in
@@ -3640,7 +3776,7 @@ declare const log: LoggerPort;
3640
3776
  declare function configureLogger(impl: LoggerPort): void;
3641
3777
  /** Restore the default `SilentLogger`. Call from test teardown. */
3642
3778
  declare function resetLogger(): void;
3643
- /** Inspect the active logger. Test-only production code uses `log`. */
3779
+ /** Inspect the active logger. Test-only, production code uses `log`. */
3644
3780
  declare function getActiveLogger(): LoggerPort;
3645
3781
 
3646
3782
  /**
@@ -3659,7 +3795,7 @@ declare function getActiveLogger(): LoggerPort;
3659
3795
  * Error policy: a hook that throws is caught here, logged through a
3660
3796
  * synthetic `extension.error` event with kind `hook-error`, and the
3661
3797
  * caller continues. A buggy hook MUST NOT block the main pipeline (or
3662
- * the CLI exit path) that would invert the design intent (hooks
3798
+ * the CLI exit path), that would invert the design intent (hooks
3663
3799
  * REACT to events, they never steer them).
3664
3800
  *
3665
3801
  * The module lives under `kernel/extensions/` (alongside the `IHook`
@@ -3668,7 +3804,7 @@ declare function getActiveLogger(): LoggerPort;
3668
3804
  * `analyzer.completed`, `action.completed`, `job.*`) and the CLI entry
3669
3805
  * for the two CLI-process-driven triggers (`boot`, `shutdown`).
3670
3806
  * Pulling the dispatcher out of the orchestrator keeps both consumers
3671
- * symmetric same indexing, same filter semantics, same error
3807
+ * symmetric, same indexing, same filter semantics, same error
3672
3808
  * policy.
3673
3809
  */
3674
3810
 
@@ -3697,20 +3833,20 @@ declare function makeEvent(type: string, data: unknown): ProgressEvent;
3697
3833
  interface Kernel {
3698
3834
  registry: Registry;
3699
3835
  /**
3700
- * Step 9.6.6 read-only catalog of plugin-contributed annotation
3836
+ * Step 9.6.6, read-only catalog of plugin-contributed annotation
3701
3837
  * keys, keyed by `(pluginId, key)`. Populated at plugin-load time;
3702
3838
  * pure read with no side effects. Built-in catalog (from
3703
3839
  * `annotations.schema.json`) is NOT included here.
3704
3840
  */
3705
3841
  getRegisteredAnnotationKeys: () => readonly IRegisteredAnnotationKey[];
3706
3842
  /**
3707
- * Internal replace the frozen catalog. Called once by the
3843
+ * Internal, replace the frozen catalog. Called once by the
3708
3844
  * plugin runtime composer after every plugin has loaded; consumers
3709
3845
  * MUST treat the resulting array as immutable.
3710
3846
  */
3711
3847
  setRegisteredAnnotationKeys: (entries: readonly IRegisteredAnnotationKey[]) => void;
3712
3848
  /**
3713
- * Step 11.x read-only catalog of plugin-contributed view
3849
+ * Step 11.x, read-only catalog of plugin-contributed view
3714
3850
  * contributions, keyed by `(pluginId, extensionId, contributionId)`.
3715
3851
  * Populated at plugin-load time; pure read with no side effects.
3716
3852
  * Mirror of `getRegisteredAnnotationKeys` for the view contribution
@@ -3719,7 +3855,7 @@ interface Kernel {
3719
3855
  */
3720
3856
  getRegisteredViewContributions: () => readonly IRegisteredViewContribution[];
3721
3857
  /**
3722
- * Internal replace the frozen view-contribution catalog. Called
3858
+ * Internal, replace the frozen view-contribution catalog. Called
3723
3859
  * once by the plugin runtime composer after every plugin has loaded;
3724
3860
  * consumers MUST treat the resulting array as immutable.
3725
3861
  */