@skill-map/cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * --- Naming convention (kernel-wide) -------------------------------------
10
10
  *
11
- * Four categories with distinct prefix rules; the rules are deliberate
11
+ * Five categories with distinct prefix rules; the rules are deliberate
12
12
  * even though they look mixed at first read:
13
13
  *
14
14
  * 1. **Domain types** — every shape that mirrors a `spec/schemas/*.json`
@@ -31,13 +31,22 @@
31
31
  * reading as the rest of TypeScript's plugin ecosystems where a
32
32
  * shape is implementable.
33
33
  *
34
- * 4. **Internal shapes** — option bags, result records, config
35
- * slices, anything passed across function boundaries inside the
36
- * kernel / CLI but not part of the spec: `IRunScanOptions` (well,
37
- * `RunScanOptions` — see below), `IPluginRuntimeBundle`,
38
- * `IPruneResult`, `IMigrationFile`, `IDbLocationOptions`. **`I`
39
- * prefix.** The prefix matches category 3 because both are
40
- * "shapes that live in TypeScript only, never in JSON".
34
+ * 4. **Internal interfaces** — option bags, result records, config
35
+ * slices, anything declared as `interface` and passed across
36
+ * function boundaries inside the kernel / CLI but not part of the
37
+ * spec: `IPluginRuntimeBundle`, `IPruneResult`, `IMigrationFile`,
38
+ * `IDbLocationOptions`. **`I` prefix.** The prefix matches
39
+ * category 3 because both are "shapes that live in TypeScript
40
+ * only, never in JSON".
41
+ *
42
+ * 5. **Internal type aliases** — anything declared as `type` (string-
43
+ * literal unions, function types, mapped/derived types) that lives
44
+ * only in TS: `TLogLevel`, `TLogMethodLevel`, `TProgressListener`,
45
+ * `TLogFormatter`, `TActionWrite`, `TExecutionMode`, `TGranularity`,
46
+ * `THookFilter`, `THookTrigger`, `TNodeChangeReason`,
47
+ * `TPluginLoadStatus`, `TPluginStorage`, `TWatchEventKind`. **`T`
48
+ * prefix.** Use this bucket when `interface` is the wrong shape
49
+ * (a union, a callback signature, an `Exclude<…>` derivation).
41
50
  *
42
51
  * Edge cases worth knowing:
43
52
  * - The following category-4 names lack the `I` prefix because
@@ -46,15 +55,16 @@
46
55
  * option bags / records: `RunScanOptions`, `RenameOp`;
47
56
  * TS-only exports from `kernel/index.ts` / `kernel/ports/*`:
48
57
  * `Kernel`, `ProgressEvent`, `LogRecord`, `NodeStat`.
49
- * New public option bags and TS-only exports MUST still use
50
- * `I*`; removing a name from this list is a breaking change.
58
+ * New public option bags MUST still use `I*`; new public type
59
+ * aliases MUST still use `T*`. Removing a name from this list is a
60
+ * breaking change.
51
61
  * - `IDatabase` (SQLite schema) is category 4 but lives in
52
62
  * `adapters/sqlite/schema.ts`, not here. Same rule applies.
53
63
  *
54
64
  * If you find yourself wanting to add a new type and aren't sure which
55
65
  * bucket it falls in: ask "does this shape exist in the spec?". If
56
- * yes, no prefix and align the name with the schema. If no, `I`
57
- * prefix.
66
+ * yes, no prefix and align the name with the schema. If no, `I` prefix
67
+ * for `interface`, `T` prefix for `type` aliases.
58
68
  */
59
69
  /**
60
70
  * The four node kinds the **built-in Claude Provider** declares — `skill`,
@@ -70,8 +80,12 @@
70
80
  * Step 9.5 dropped `hook` from the catalog: `.claude/hooks/*.md` is NOT
71
81
  * an Anthropic-defined node type — hooks live in `settings.json` or as
72
82
  * sub-objects of agent / skill frontmatter (see
73
- * https://code.claude.com/docs/en/hooks.md). Files at the old path now
74
- * classify as `note` via the Provider's fallback.
83
+ * https://code.claude.com/docs/en/hooks.md). Files at the old path
84
+ * classify as `markdown` via the Provider's fallback. The fallback is
85
+ * named after the *format* because the file is generic markdown with
86
+ * no specific role; format-named kinds apply only as the generic
87
+ * fallback — a file that matches a specific role (agent / command /
88
+ * skill) classifies under that role, not under `markdown`.
75
89
  *
76
90
  * This alias survives because:
77
91
  * - claude-specific code legitimately wants to switch on the four
@@ -85,7 +99,7 @@
85
99
  * For "any kind a Provider could declare", use plain `string`. Only use
86
100
  * `NodeKind` when the code is intentionally claude-catalog-specific.
87
101
  */
88
- type NodeKind = 'skill' | 'agent' | 'command' | 'note';
102
+ type NodeKind = 'skill' | 'agent' | 'command' | 'markdown';
89
103
  type LinkKind = 'invokes' | 'references' | 'mentions' | 'supersedes';
90
104
  type Confidence = 'high' | 'medium' | 'low';
91
105
  type Severity = 'error' | 'warn' | 'info';
@@ -137,13 +151,58 @@ interface Node {
137
151
  linksOutCount: number;
138
152
  linksInCount: number;
139
153
  externalRefsCount: number;
140
- title?: string | null;
141
- description?: string | null;
142
- stability?: Stability | null;
143
- version?: string | null;
144
- author?: string | null;
145
154
  frontmatter?: Record<string, unknown>;
146
155
  tokens?: TripleSplit;
156
+ /**
157
+ * Step 9.6.2 — sidecar denormalisation surface. Populated by the
158
+ * orchestrator at scan time; absent when the orchestrator did not
159
+ * inspect sidecars (legacy code paths) or when no sidecar accompanies
160
+ * the node. Read by `annotation-stale` rule and the persistence layer.
161
+ */
162
+ sidecar?: ISidecarOverlay | null;
163
+ /**
164
+ * Per-user "favorite" flag, decorated by the BFF on `/api/nodes` and
165
+ * `/api/nodes/:pathB64` responses via in-memory `Set` lookup against
166
+ * `state_node_favorites`. Absent on emissions that don't carry per-user
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
169
+ * truthy `isFavorite` only ever lands when the BFF set it.
170
+ */
171
+ isFavorite?: boolean;
172
+ }
173
+ /**
174
+ * Drift status of a co-located `.sm` sidecar relative to the live
175
+ * node hashes. Mirrors `TSidecarStatus` on the SQLite schema.
176
+ */
177
+ type SidecarStatus = 'fresh' | 'stale-body' | 'stale-frontmatter' | 'stale-both';
178
+ /**
179
+ * Sidecar overlay attached to a `Node` after the orchestrator parses
180
+ * `<basename>.sm`. `present === false` is the empty overlay (no
181
+ * sidecar accompanies the node); the other fields are absent or null
182
+ * in that case. When `present === true` and parse + validation
183
+ * succeeded, `status` carries the drift state and `annotations` carries
184
+ * the parsed (typed) `annotations:` block.
185
+ */
186
+ interface ISidecarOverlay {
187
+ present: boolean;
188
+ status?: SidecarStatus | null;
189
+ /**
190
+ * Parsed `annotations:` block. Untyped object — schema lives in
191
+ * `spec/schemas/annotations.schema.json`. Null when no sidecar or
192
+ * the block is empty/absent.
193
+ */
194
+ annotations?: Record<string, unknown> | null;
195
+ /**
196
+ * R15 closure (2026-05-07) — full parsed YAML root of the sidecar
197
+ * (the entire `.sm` payload, mirroring `sidecar.schema.json`). Surfaced
198
+ * so the UI inspector can render `for:`, `audit:`, `settings:`, and
199
+ * `<plugin-id>:` namespace blocks without re-reading the file. NULL
200
+ * when no sidecar is present, or when the sidecar exists but failed
201
+ * to parse / validate. The `annotations` field above stays — it
202
+ * duplicates `root.annotations` intentionally so existing consumers
203
+ * keep working unchanged.
204
+ */
205
+ root?: Record<string, unknown> | null;
147
206
  }
148
207
  interface Link {
149
208
  /** The originating node — the path of the file the extractor was reading
@@ -186,8 +245,8 @@ interface ScanStats {
186
245
  /**
187
246
  * Files walked but not classified by any Provider. Today every walked
188
247
  * file is classified by its Provider (the `claude` Provider falls back to
189
- * `'note'`), so this is always 0; the field will matter once multiple
190
- * Providers can claim the same file.
248
+ * `'markdown'`), so this is always 0; the field will matter once
249
+ * multiple Providers can claim the same file.
191
250
  */
192
251
  filesSkipped: number;
193
252
  nodesCount: number;
@@ -316,7 +375,7 @@ interface ScanResult {
316
375
  * and each kind's code carries its own fuller type where needed.
317
376
  *
318
377
  * **Spec § A.6 — qualified ids.** Every extension is keyed in the registry
319
- * by `<pluginId>/<id>` (e.g. `core/frontmatter`, `claude/slash`,
378
+ * by `<pluginId>/<id>` (e.g. `core/annotations`, `core/slash`,
320
379
  * `hello-world/greet`). `Extension.id` carries the **short** id as authored;
321
380
  * `Extension.pluginId` carries the namespace; the registry composes the
322
381
  * qualifier internally and exposes lookup APIs that operate on either form
@@ -369,6 +428,333 @@ declare class Registry {
369
428
  totalCount(): number;
370
429
  }
371
430
 
431
+ /**
432
+ * Step 9.6.6 — runtime annotation-contribution catalog types.
433
+ *
434
+ * Lives in its own module (rather than `kernel/index.ts`) so consumers
435
+ * deep inside the kernel — `IRuleContext`, the BFF route factories,
436
+ * future Action contexts — can depend on the catalog shape without
437
+ * dragging the whole kernel barrel and risking a cycle.
438
+ */
439
+ /**
440
+ * Single row of the runtime annotation-contribution catalog surfaced by
441
+ * `kernel.getRegisteredAnnotationKeys()`. One row per (plugin × key)
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
444
+ * catalog via the schema bundle.
445
+ */
446
+ interface IRegisteredAnnotationKey {
447
+ pluginId: string;
448
+ key: string;
449
+ location: 'namespaced' | 'root';
450
+ ownership: 'exclusive' | 'shared';
451
+ /** Inline JSON Schema as declared in the manifest (not the AJV compiled validator). */
452
+ schema: Record<string, unknown>;
453
+ }
454
+
455
+ /**
456
+ * Step 11.x — runtime view-contribution catalog types.
457
+ *
458
+ * Lives in its own module (rather than `kernel/index.ts`) so consumers
459
+ * deep inside the kernel — `IRuleContext`, the BFF route factories,
460
+ * future Action contexts — can depend on the catalog shape without
461
+ * dragging the whole kernel barrel and risking a cycle.
462
+ *
463
+ * Mirrors `annotation-catalog.ts` for the annotation contribution side
464
+ * (Step 9.6.6). The two systems share the "plugin contributes data,
465
+ * kernel exposes catalog, UI renders" pattern but never overlap in
466
+ * storage or routing — see `architecture.md` §View contribution system
467
+ * for the comparison table.
468
+ *
469
+ * **Closed catalog by design.** Both `TContractName` and `TInputTypeName`
470
+ * mirror the closed enums in `spec/schemas/view-contracts.schema.json`
471
+ * and `spec/schemas/input-types.schema.json`. Adding a member is a
472
+ * coordinated kernel + spec + UI + scaffolder change. The closed-enum
473
+ * shape lets TypeScript surface unknown contracts at author time
474
+ * (in plugin authors' editors when their plugin imports `@skill-map/cli`)
475
+ * AND lets the runtime exhaustively dispatch contract → renderer in the
476
+ * UI without `default:` fallbacks.
477
+ */
478
+ /**
479
+ * Closed enum of view contract names. Mirror of
480
+ * `spec/schemas/view-contracts.schema.json#/$defs/ContractName`.
481
+ *
482
+ * Plugins pick one of these by name in their extension manifest's
483
+ * `viewContributions[<contributionId>].contract` field. The kernel
484
+ * validates each pick at load time (`invalid-manifest` on miss); the
485
+ * UI maps each contract to one or more slots and a renderer.
486
+ */
487
+ type TContractName = 'node-counter' | 'node-tag' | 'node-breakdown' | 'node-records' | 'node-tree' | 'node-key-values' | 'node-link-list' | 'node-markdown' | 'node-alert' | 'scope-stat';
488
+ /**
489
+ * Closed enum of input-type names for plugin settings. Mirror of
490
+ * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.
491
+ *
492
+ * Plugins pick one of these by name in their plugin manifest's
493
+ * `settings[<settingId>].type` field. The kernel exposes the resolved
494
+ * value via `ctx.settings.<settingId>` typed per the input-type's
495
+ * value-type promise.
496
+ */
497
+ type TInputTypeName = 'string-list' | 'single-string' | 'boolean-flag' | 'integer' | 'enum-pick' | 'enum-multipick' | 'path-glob' | 'regex' | 'secret' | 'key-value-list';
498
+ /** Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>`. */
499
+ type TSeverity = 'info' | 'warn' | 'success' | 'danger';
500
+ /**
501
+ * Manifest-side declaration of a single view contribution. The plugin
502
+ * author writes one of these per Record key in
503
+ * `IExtensionBase.viewContributions[<contributionId>]`.
504
+ *
505
+ * Mirror of `view-contracts.schema.json#/$defs/IViewContribution`.
506
+ */
507
+ interface IViewContribution {
508
+ /**
509
+ * Required. Closed-catalog contract name. Unknown name rejects the
510
+ * extension as `invalid-manifest` at load.
511
+ */
512
+ contract: TContractName;
513
+ /**
514
+ * Optional human-readable label. English-only per `AGENTS.md`
515
+ * (`Externalized texts, not internationalized`).
516
+ */
517
+ label?: string;
518
+ /** Optional hover tooltip. English-only. */
519
+ tooltip?: string;
520
+ /**
521
+ * Optional emoji codepoint OR PrimeIcons class id (without the
522
+ * `pi-` prefix). The UI discriminates: matches Unicode
523
+ * `\p{Extended_Pictographic}` → emoji text, otherwise → PrimeIcon.
524
+ */
525
+ icon?: string;
526
+ /**
527
+ * Optional empty placeholder text shown when the payload is empty
528
+ * AND `emitWhenEmpty` is true. Falls back to a UI-supplied generic
529
+ * 'No data.' string. English-only.
530
+ */
531
+ emptyText?: string;
532
+ /**
533
+ * When false (default), the kernel drops emissions whose payload is
534
+ * structurally empty so the slot stays silent. When true, the
535
+ * renderer surfaces an empty placeholder. Per-contract definition
536
+ * of "empty" lives in the contract's payload schema.
537
+ */
538
+ emitWhenEmpty?: boolean;
539
+ /**
540
+ * Optional ordering hint (default 100). Slots configured with
541
+ * `order: 'priority'` sort contributions ASC by this value, with
542
+ * alphabetical tie-break by qualified id. The plugin uses this to
543
+ * suggest where its contribution belongs relative to others sharing
544
+ * the same slot — the slot has the final say.
545
+ */
546
+ priority?: number;
547
+ }
548
+ /**
549
+ * Single row of the runtime view-contribution catalog surfaced by
550
+ * `kernel.getRegisteredViewContributions()`. One row per
551
+ * `(pluginId × extensionId × contributionId)` tuple. Composed at boot
552
+ * by `loadPluginRuntime` from every loaded extension's
553
+ * `viewContributions` map.
554
+ *
555
+ * The qualified id is `<pluginId>/<extensionId>/<contributionId>` —
556
+ * matches the qualified id pattern used elsewhere in the kernel
557
+ * (`<pluginId>/<extensionId>` for extensions; this adds the third
558
+ * segment for per-contribution identity).
559
+ */
560
+ interface IRegisteredViewContribution {
561
+ pluginId: string;
562
+ extensionId: string;
563
+ contributionId: string;
564
+ contract: TContractName;
565
+ /** Optional manifest-declared label (English-only). */
566
+ label?: string;
567
+ tooltip?: string;
568
+ icon?: string;
569
+ emptyText?: string;
570
+ emitWhenEmpty: boolean;
571
+ /** Manifest-declared ordering hint (default 100). See `IViewContribution.priority`. */
572
+ priority?: number;
573
+ }
574
+ /**
575
+ * Common fields on every setting declaration. The discriminated union
576
+ * `ISettingDeclaration` extends one of these per `type` value.
577
+ */
578
+ interface ISettingCommon {
579
+ /** Required. Short human-readable label. English-only. */
580
+ label: string;
581
+ /** Optional helper text shown below the control. English-only. */
582
+ description?: string;
583
+ }
584
+ interface ISetting_StringList extends ISettingCommon {
585
+ type: 'string-list';
586
+ default?: string[];
587
+ min?: number;
588
+ max?: number;
589
+ itemMaxLength?: number;
590
+ }
591
+ interface ISetting_SingleString extends ISettingCommon {
592
+ type: 'single-string';
593
+ default?: string;
594
+ minLength?: number;
595
+ maxLength?: number;
596
+ /** Optional ECMAScript regex pattern (no flags). */
597
+ pattern?: string;
598
+ }
599
+ interface ISetting_BooleanFlag extends ISettingCommon {
600
+ type: 'boolean-flag';
601
+ default?: boolean;
602
+ }
603
+ interface ISetting_Integer extends ISettingCommon {
604
+ type: 'integer';
605
+ default?: number;
606
+ min?: number;
607
+ max?: number;
608
+ step?: number;
609
+ }
610
+ interface ISetting_EnumOption {
611
+ value: string;
612
+ label: string;
613
+ }
614
+ interface ISetting_EnumPick extends ISettingCommon {
615
+ type: 'enum-pick';
616
+ options: ISetting_EnumOption[];
617
+ default?: string;
618
+ }
619
+ interface ISetting_EnumMultipick extends ISettingCommon {
620
+ type: 'enum-multipick';
621
+ options: ISetting_EnumOption[];
622
+ default?: string[];
623
+ min?: number;
624
+ max?: number;
625
+ }
626
+ interface ISetting_PathGlob extends ISettingCommon {
627
+ type: 'path-glob';
628
+ default?: string;
629
+ /** When true, accepts string[]; when false (default), single string. */
630
+ multiple?: boolean;
631
+ }
632
+ interface ISetting_Regex extends ISettingCommon {
633
+ type: 'regex';
634
+ default?: string;
635
+ /** Subset of `gimsuy`. Default `''`. */
636
+ flags?: string;
637
+ }
638
+ interface ISetting_Secret extends ISettingCommon {
639
+ type: 'secret';
640
+ /**
641
+ * Optional uppercase-ASCII identifier. When set in the process
642
+ * environment, that value wins over any stored value (lets CI
643
+ * inject without writing to disk).
644
+ */
645
+ envVar?: string;
646
+ }
647
+ interface ISetting_KeyValueListEntry {
648
+ key: string;
649
+ value: string;
650
+ }
651
+ interface ISetting_KeyValueList extends ISettingCommon {
652
+ type: 'key-value-list';
653
+ keyLabel?: string;
654
+ valueLabel?: string;
655
+ default?: ISetting_KeyValueListEntry[];
656
+ min?: number;
657
+ max?: number;
658
+ }
659
+ /**
660
+ * Discriminated union of every setting declaration shape. The plugin
661
+ * author NEVER writes JSON Schema for settings — they pick one of
662
+ * these `type` values and supply per-type parameters.
663
+ *
664
+ * Mirror of `input-types.schema.json#/$defs/ISettingDeclaration`.
665
+ */
666
+ type ISettingDeclaration = ISetting_StringList | ISetting_SingleString | ISetting_BooleanFlag | ISetting_Integer | ISetting_EnumPick | ISetting_EnumMultipick | ISetting_PathGlob | ISetting_Regex | ISetting_Secret | ISetting_KeyValueList;
667
+ /**
668
+ * Runtime value type for a setting, derived from its declaration. The
669
+ * kernel exposes settings to extractors as `Record<string, TSettingValue>`
670
+ * via `ctx.settings.<settingId>`; consumers that want narrow typing
671
+ * narrow at the call site by reading `manifest.settings[id].type`.
672
+ */
673
+ type TSettingValue = string | string[] | boolean | number | ISetting_KeyValueListEntry[];
674
+
675
+ /**
676
+ * Base manifest shape shared by every extension kind. Mirrors
677
+ * `spec/schemas/extensions/base.schema.json` at the TypeScript level.
678
+ *
679
+ * Spec § A.6 — every extension is identified in the registry by the
680
+ * qualified id `<pluginId>/<id>`. The `pluginId` field is required at the
681
+ * runtime / TS level: built-ins declare it directly in
682
+ * `src/extensions/built-ins.ts`; user plugins have it injected by the
683
+ * `PluginLoader` from `plugin.json#/id` before the extension reaches the
684
+ * registry. A plugin author who hand-codes a `pluginId` that disagrees
685
+ * with the manifest's `id` is rejected as `invalid-manifest`.
686
+ *
687
+ * The JSON Schema deliberately does NOT model `pluginId` — the qualifier
688
+ * is a runtime concern composed by the loader, not a manifest field
689
+ * authors are expected to set. Stripping it before AJV validation in
690
+ * the loader keeps the spec contract clean ("authors declare only the
691
+ * short id").
692
+ */
693
+
694
+ /**
695
+ * Step 9.6.6 — single entry of an extension's `annotationContributions`
696
+ * map. Mirrors `spec/schemas/extensions/base.schema.json#/properties/annotationContributions/additionalProperties`.
697
+ *
698
+ * `schema` is an INLINE JSON Schema (object literal in the manifest),
699
+ * not a `$ref` to a file. The kernel compiles it at load time; an
700
+ * invalid schema rejects the extension as `invalid-manifest`.
701
+ */
702
+ interface IAnnotationContribution {
703
+ /** Inline JSON Schema describing the value written under this key. */
704
+ schema: Record<string, unknown>;
705
+ /**
706
+ * Conflict policy. `shared` (default) — multiple plugins MAY write
707
+ * the key; `exclusive` — only this plugin may. REQUIRED to be
708
+ * `'exclusive'` when `location: 'root'`.
709
+ */
710
+ ownership?: 'exclusive' | 'shared';
711
+ /**
712
+ * Where the key lands. `namespaced` (default) — under the plugin's
713
+ * `<plugin-id>:` block; `root` — top-level, alongside `for` /
714
+ * `annotations` / `settings` / `audit`. Cross-plugin root-key
715
+ * collisions on `exclusive` are a fatal startup error.
716
+ */
717
+ location?: 'namespaced' | 'root';
718
+ }
719
+ interface IExtensionBase {
720
+ id: string;
721
+ /**
722
+ * Owning plugin namespace. Composed with `id` to produce the
723
+ * qualified registry key `<pluginId>/<id>`. Built-ins declare this
724
+ * directly; user plugins have it injected by the `PluginLoader`
725
+ * from `plugin.json#/id`.
726
+ */
727
+ pluginId: string;
728
+ version: string;
729
+ description?: string;
730
+ stability?: Stability;
731
+ preconditions?: string[];
732
+ entry?: string;
733
+ /**
734
+ * Step 9.6.6 — plugin-contributed annotation keys. Each entry maps a
735
+ * key name to an inline JSON Schema + ownership + location triple.
736
+ * The kernel surfaces the aggregate via `kernel.getRegisteredAnnotationKeys()`.
737
+ * See `IAnnotationContribution` for the field semantics and
738
+ * `plugin-author-guide.md` §Annotation contributions for examples.
739
+ */
740
+ annotationContributions?: Record<string, IAnnotationContribution>;
741
+ /**
742
+ * Plugin-contributed view contributions. Each entry maps a local
743
+ * contribution id (kebab-case, unique within the extension) to a
744
+ * `IViewContribution` declaration that picks a view contract by name
745
+ * from the closed kernel catalog (`view-catalog.ts#TContractName`).
746
+ * The kernel validates each `contract` pick at load time
747
+ * (`invalid-manifest` on miss); the plugin emits per-node payloads
748
+ * via `ctx.emitContribution(<contributionId>, payload)` during scan;
749
+ * the runtime validates payloads against the contract's payload
750
+ * schema. The aggregate runtime catalog is exposed via
751
+ * `kernel.getRegisteredViewContributions()`. The plugin author
752
+ * NEVER picks a UI slot — slot mapping is owned by the UI driving
753
+ * adapter. See `architecture.md` §View contribution system.
754
+ */
755
+ viewContributions?: Record<string, IViewContribution>;
756
+ }
757
+
372
758
  /**
373
759
  * `.skillmapignore` parser + filter facade. Wraps `ignore` (kaelzhang)
374
760
  * with the project-local layering: bundled defaults → `config.ignore`
@@ -412,10 +798,10 @@ interface ProgressEvent {
412
798
  jobId?: string;
413
799
  data?: unknown;
414
800
  }
415
- type ProgressListener = (event: ProgressEvent) => void;
801
+ type TProgressListener = (event: ProgressEvent) => void;
416
802
  interface ProgressEmitterPort {
417
803
  emit(event: ProgressEvent): void;
418
- subscribe(listener: ProgressListener): () => void;
804
+ subscribe(listener: TProgressListener): () => void;
419
805
  }
420
806
 
421
807
  /**
@@ -481,6 +867,16 @@ interface IPluginManifest {
481
867
  id: string;
482
868
  version: string;
483
869
  specCompat: string;
870
+ /**
871
+ * Optional semver range against the kernel's view-contracts +
872
+ * input-types catalog version. Independent from `specCompat` because
873
+ * the catalog evolves on its own cadence (see `architecture.md`
874
+ * §View contribution system → Catalog versioning). Mismatch surfaces
875
+ * as `incompatible-catalog`. Absent = the plugin opts out of catalog
876
+ * checking; `sm plugins doctor` warns if such a plugin actually
877
+ * declares `viewContributions` or `settings`.
878
+ */
879
+ catalogCompat?: string;
484
880
  extensions: string[];
485
881
  description?: string;
486
882
  storage?: TPluginStorage;
@@ -490,6 +886,18 @@ interface IPluginManifest {
490
886
  * the default.
491
887
  */
492
888
  granularity?: TGranularity;
889
+ /**
890
+ * Plugin user-configurable settings. Each entry picks an `input-type`
891
+ * from the closed catalog at
892
+ * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.
893
+ * The plugin author NEVER writes JSON Schema — they pick `type` by
894
+ * name and supply per-type parameters. The kernel exposes resolved
895
+ * settings to extractors via `ctx.settings.<settingId>`; settings
896
+ * are read once at extractor invocation; changing a setting requires
897
+ * `sm scan` to re-emit. See `architecture.md` §View contribution
898
+ * system → Settings.
899
+ */
900
+ settings?: Record<string, ISettingDeclaration>;
493
901
  author?: string;
494
902
  license?: string;
495
903
  homepage?: string;
@@ -530,7 +938,7 @@ interface IPluginManifest {
530
938
  * precedence rule applies. The user resolves
531
939
  * by renaming one of them and rerunning.
532
940
  */
533
- type TPluginLoadStatus = 'enabled' | 'disabled' | 'incompatible-spec' | 'invalid-manifest' | 'load-error' | 'id-collision';
941
+ type TPluginLoadStatus = 'enabled' | 'disabled' | 'incompatible-spec' | 'incompatible-catalog' | 'invalid-manifest' | 'load-error' | 'id-collision';
534
942
  interface ILoadedExtension {
535
943
  kind: ExtensionKind;
536
944
  id: string;
@@ -723,38 +1131,60 @@ declare function makePluginStore(opts: {
723
1131
  }): IPluginStore | undefined;
724
1132
 
725
1133
  /**
726
- * Base manifest shape shared by every extension kind. Mirrors
727
- * `spec/schemas/extensions/base.schema.json` at the TypeScript level.
1134
+ * `scan_contributions` adapter replace-all writer used by
1135
+ * `persistScanResult`, plus read helpers consumed by the BFF
1136
+ * (`/api/contributions/...`) and rules (`core/contribution-orphan`).
728
1137
  *
729
- * Spec § A.6 every extension is identified in the registry by the
730
- * qualified id `<pluginId>/<id>`. The `pluginId` field is required at the
731
- * runtime / TS level: built-ins declare it directly in
732
- * `src/extensions/built-ins.ts`; user plugins have it injected by the
733
- * `PluginLoader` from `plugin.json#/id` before the extension reaches the
734
- * registry. A plugin author who hand-codes a `pluginId` that disagrees
735
- * with the manifest's `id` is rejected as `invalid-manifest`.
1138
+ * One row per `(plugin_id, extension_id, node_path, contribution_id)`
1139
+ * tuple. See `spec/architecture.md` § View contribution system
1140
+ * Persistence and `migrations/001_initial.sql` § View contribution
1141
+ * layer for the normative shape.
736
1142
  *
737
- * The JSON Schema deliberately does NOT model `pluginId` the qualifier
738
- * is a runtime concern composed by the loader, not a manifest field
739
- * authors are expected to set. Stripping it before AJV validation in
740
- * the loader keeps the spec contract clean ("authors declare only the
741
- * short id").
1143
+ * Replace-all semantics mirror the rest of the `scan_*` zone: every
1144
+ * scan is a fresh snapshot, so prior rows are deleted before insert.
1145
+ * Wrapped in the same transaction `persistScanResult` opens.
1146
+ *
1147
+ * The rename heuristic does NOT need to migrate `node_path` here —
1148
+ * because of replace-all, every contribution is re-emitted on the new
1149
+ * path automatically. Keeping the rename path lighter than `state_*`
1150
+ * (which IS rename-migrated because state survives across scans).
742
1151
  */
743
1152
 
744
- interface IExtensionBase {
745
- id: string;
1153
+ /**
1154
+ * In-memory contribution record buffered during scan and flushed to
1155
+ * `scan_contributions` by `persistScanResult`. One entry per accepted
1156
+ * `ctx.emitContribution(id, payload)` call. Payload validation against
1157
+ * the contract's payload schema happens at emit time (orchestrator);
1158
+ * by the time records reach this adapter they are wire-shape clean.
1159
+ */
1160
+ interface IContributionRecord {
1161
+ pluginId: string;
1162
+ extensionId: string;
1163
+ nodePath: string;
1164
+ contributionId: string;
746
1165
  /**
747
- * Owning plugin namespace. Composed with `id` to produce the
748
- * qualified registry key `<pluginId>/<id>`. Built-ins declare this
749
- * directly; user plugins have it injected by the `PluginLoader`
750
- * from `plugin.json#/id`.
1166
+ * Closed enum value mirroring `view-contracts.schema.json#/$defs/ContractName`.
1167
+ * Persisted as TEXT (no SQL CHECK by design — see migration comment).
751
1168
  */
1169
+ contract: string;
1170
+ /** Already-validated payload. Serialised via `JSON.stringify` at write. */
1171
+ payload: unknown;
1172
+ emittedAt: number;
1173
+ }
1174
+ /**
1175
+ * Single contribution row as returned to callers. The payload is
1176
+ * `unknown` because the contract space is open at the type layer
1177
+ * (catalog evolution is a kernel + spec concern); narrow at the call
1178
+ * site by reading `contract`.
1179
+ */
1180
+ interface IPersistedContribution {
752
1181
  pluginId: string;
753
- version: string;
754
- description?: string;
755
- stability?: Stability;
756
- preconditions?: string[];
757
- entry?: string;
1182
+ extensionId: string;
1183
+ nodePath: string;
1184
+ contributionId: string;
1185
+ contract: string;
1186
+ payload: unknown;
1187
+ emittedAt: number;
758
1188
  }
759
1189
 
760
1190
  /**
@@ -882,7 +1312,7 @@ interface IProviderKindUi {
882
1312
  * `emoji`; when both are absent, the UI falls back to the first
883
1313
  * letter of `label` colored with `color`.
884
1314
  */
885
- icon?: IProviderKindIcon;
1315
+ icon?: TProviderKindIcon;
886
1316
  }
887
1317
  /**
888
1318
  * Discriminated icon contract. `pi` references a PrimeIcons identifier
@@ -891,7 +1321,7 @@ interface IProviderKindUi {
891
1321
  * `currentColor`. The discriminator (`kind`) keeps the UI dispatch
892
1322
  * exhaustive without string-sniffing the payload.
893
1323
  */
894
- type IProviderKindIcon = {
1324
+ type TProviderKindIcon = {
895
1325
  kind: 'pi';
896
1326
  id: string;
897
1327
  } | {
@@ -942,6 +1372,30 @@ interface IProvider extends IExtensionBase {
942
1372
  * concern of how the runtime composes those.
943
1373
  */
944
1374
  schemas?: unknown[];
1375
+ /**
1376
+ * Declarative file-discovery config consumed by the kernel walker.
1377
+ * When present, the kernel walks every root, includes files whose
1378
+ * extension matches `extensions`, parses each with the parser id
1379
+ * registered in the kernel-internal registry, and yields `IRawNode`
1380
+ * records the same shape `walk()` would.
1381
+ *
1382
+ * When neither `read` nor `walk` is declared, `resolveProviderWalk`
1383
+ * applies the default `{ extensions: ['.md'], parser: 'frontmatter-yaml' }`
1384
+ * so the most common Provider shape needs zero configuration.
1385
+ *
1386
+ * Precedence: when both `walk()` (runtime field) and `read` are
1387
+ * declared, `walk()` wins — `read` is ignored. The escape-hatch
1388
+ * relationship is intentional: most Providers should use `read`;
1389
+ * Providers with non-standard discovery requirements (custom file
1390
+ * naming, multi-pass walks, dynamic ignore logic) implement `walk()`
1391
+ * directly and accept the duplication of audit-cleared defences.
1392
+ *
1393
+ * Built-in parsers: `'frontmatter-yaml'` (markdown with `--- … ---`
1394
+ * YAML frontmatter; pollution-strip + JSON_SCHEMA-pinned), `'plain'`
1395
+ * (entire body, empty frontmatter). The set is closed; user plugins
1396
+ * cannot register their own.
1397
+ */
1398
+ read?: IProviderReadConfig;
945
1399
  /**
946
1400
  * Walk the given roots and yield every node the Provider recognises.
947
1401
  * Non-matching files are silently skipped. Unreadable files produce
@@ -952,22 +1406,52 @@ interface IProvider extends IExtensionBase {
952
1406
  * filter reports as ignored. Providers MAY also keep their own
953
1407
  * hard-coded skip list (e.g. `.git`) as a defensive measure, but the
954
1408
  * filter is the canonical source of user intent.
1409
+ *
1410
+ * Optional. When omitted, the Provider MUST declare `read` (or rely
1411
+ * on the default config). The orchestrator never calls `walk()`
1412
+ * directly — it goes through `resolveProviderWalk(provider)` which
1413
+ * picks `walk` over `read`.
955
1414
  */
956
- walk(roots: string[], options?: {
1415
+ walk?(roots: string[], options?: {
957
1416
  ignoreFilter?: IIgnoreFilter;
958
1417
  }): AsyncIterable<IRawNode>;
959
1418
  /**
960
- * Given a path and its parsed frontmatter, decide the node kind. The
961
- * classifier is called after walk() yields — Providers MAY embed the
962
- * logic inside walk itself, but exposing it lets the kernel rebuild
963
- * classification during partial scans without re-walking.
1419
+ * Given a path and its parsed frontmatter, decide the node kind — or
1420
+ * `null` to disclaim the file. The classifier is called after walk()
1421
+ * yields; with multiple Providers active, every Provider walks every
1422
+ * file matching its `read.extensions`, so each Provider MUST disclaim
1423
+ * paths it does not recognise. Returning the same path's kind from
1424
+ * two Providers fires the spec's `provider-ambiguous` issue and the
1425
+ * orchestrator drops the duplicate.
964
1426
  *
965
- * Returns an open `string`. The returned value MUST be a key of the
966
- * Provider's own `kinds` catalog; the orchestrator does not validate
967
- * the kind against `NodeKind`. External Providers (Cursor, Obsidian,
968
- * …) freely return their own kinds (e.g. `'cursorRule'`, `'daily'`).
1427
+ * Convention: a Provider's classify returns one of its own `kinds`
1428
+ * map keys for paths in its territory (`.claude/`, `.gemini/`,
1429
+ * `.agents/skills/`, etc.) and `null` elsewhere. External Providers
1430
+ * (Cursor, Obsidian, …) follow the same rule: claim what's yours,
1431
+ * disclaim everything else. The orchestrator does not validate the
1432
+ * kind against `NodeKind`.
1433
+ */
1434
+ classify(path: string, frontmatter: Record<string, unknown>): string | null;
1435
+ }
1436
+ /**
1437
+ * Declarative read config a Provider declares via `IProvider.read`.
1438
+ * Mirrors `extensions/provider.schema.json#/properties/read` at the
1439
+ * TypeScript level. Built-in parser ids: `'frontmatter-yaml'`, `'plain'`.
1440
+ */
1441
+ interface IProviderReadConfig {
1442
+ /**
1443
+ * File extensions the walker yields. Strings include the leading dot
1444
+ * (e.g. `'.md'`, `'.mdc'`, `'.toml'`). Match is suffix-based; the
1445
+ * comparison is case-sensitive.
1446
+ */
1447
+ extensions: string[];
1448
+ /**
1449
+ * Parser id from the kernel-internal registry. Built-ins:
1450
+ * `'frontmatter-yaml'`, `'plain'`. Unknown ids surface as
1451
+ * `UnknownParserError` from the walker; the orchestrator translates
1452
+ * the error into a Provider issue with status `invalid-manifest`.
969
1453
  */
970
- classify(path: string, frontmatter: Record<string, unknown>): string;
1454
+ parser: string;
971
1455
  }
972
1456
 
973
1457
  /**
@@ -976,6 +1460,11 @@ interface IProvider extends IExtensionBase {
976
1460
  * a return value. Extractors run in isolation: they MUST NOT read other
977
1461
  * nodes, the graph, or the DB. Cross-node reasoning lives in rules.
978
1462
  *
1463
+ * Extractors are deterministic-only. They run synchronously inside the
1464
+ * scan loop; LLM-driven enrichment of a node is an Action concern, not
1465
+ * an Extractor concern. The Extractor context therefore exposes no
1466
+ * `RunnerPort` — see spec `architecture.md` §Execution modes.
1467
+ *
979
1468
  * Output channels (all on the context):
980
1469
  *
981
1470
  * - `ctx.emitLink(link)` — persist a link in the kernel's `links` table.
@@ -983,14 +1472,12 @@ interface IProvider extends IExtensionBase {
983
1472
  * kind drops the link and surfaces an `extension.error` event.
984
1473
  * - `ctx.enrichNode(partial)` — merge canonical, kernel-curated properties
985
1474
  * onto the node. Strictly separate from the author-supplied frontmatter
986
- * (the latter remains immutable and survives verbatim). Persistence and
987
- * stale-tracking are spec'd in § A.8.
1475
+ * (the latter remains immutable and survives verbatim). Persistence
1476
+ * is spec'd in § A.8.
988
1477
  * - `ctx.store` — plugin-scoped persistence. Present only when the
989
1478
  * plugin declares `storage.mode` in `plugin.json`; shape depends on the
990
1479
  * mode (`KvStore` for mode A, scoped `Database` for mode B). See
991
1480
  * `plugin-kv-api.md` for the contract.
992
- * - `ctx.runner` — `RunnerPort` injection for `probabilistic` extractors.
993
- * `undefined` for the default `deterministic` mode.
994
1481
  *
995
1482
  * The manifest's `scope` field tells the orchestrator which parts to feed:
996
1483
  * `frontmatter` extractors receive an empty string for body and vice versa.
@@ -1021,6 +1508,21 @@ interface IExtractorCallbacks {
1021
1508
  * partials and `persistScanResult` upserts them.
1022
1509
  */
1023
1510
  enrichNode(partial: Partial<Node>): void;
1511
+ /**
1512
+ * Emit a per-node view contribution. The first argument is the
1513
+ * extension-local Record key declared under
1514
+ * `extension.viewContributions[<contributionId>]`; the second is a
1515
+ * payload that conforms to the contract's payload schema in
1516
+ * `spec/schemas/view-contracts.schema.json#/$defs/payloads/<contract>`.
1517
+ * The orchestrator validates the payload against the contract schema
1518
+ * before persisting to `scan_contributions`; off-contract payloads
1519
+ * are silently dropped with an `extension.error` event (mirror of
1520
+ * `emitLink` rejecting off-`emitsLinkKinds` links). Calling
1521
+ * `emitContribution` with a `contributionId` that is not declared in
1522
+ * the manifest is also dropped with an `extension.error`. See
1523
+ * `architecture.md` §View contribution system → Emit path.
1524
+ */
1525
+ emitContribution(contributionId: string, payload: unknown): void;
1024
1526
  }
1025
1527
  interface IExtractorContext extends IExtractorCallbacks {
1026
1528
  node: Node;
@@ -1041,33 +1543,17 @@ interface IExtractorContext extends IExtractorCallbacks {
1041
1543
  * it here.
1042
1544
  */
1043
1545
  store?: unknown;
1044
- /**
1045
- * `RunnerPort` injection for `probabilistic` extractors. `undefined`
1046
- * for `deterministic` mode (the default). The kernel rejects
1047
- * probabilistic extractors that try to register scan-time hooks at
1048
- * load time.
1049
- */
1050
- runner?: unknown;
1051
1546
  }
1052
1547
  interface IExtractor extends IExtensionBase {
1053
1548
  kind: 'extractor';
1054
- /**
1055
- * Execution mode. Optional in the manifest with a default of
1056
- * `deterministic` per `spec/schemas/extensions/extractor.schema.json`.
1057
- * `probabilistic` extractors invoke an LLM through the kernel's
1058
- * `RunnerPort` and never participate in scan-time pipelines —
1059
- * they dispatch only as queued jobs.
1060
- */
1061
- mode?: TExecutionMode;
1062
1549
  emitsLinkKinds: LinkKind[];
1063
1550
  defaultConfidence: Confidence;
1064
1551
  scope: 'frontmatter' | 'body' | 'both';
1065
1552
  /**
1066
1553
  * Optional opt-in filter on `node.kind`. When declared, the orchestrator
1067
1554
  * skips invocation of `extract()` for any node whose `kind` is NOT in
1068
- * this list — fail-fast, before context construction, so a
1069
- * probabilistic extractor wastes zero LLM cost on inapplicable nodes
1070
- * and a deterministic extractor wastes zero CPU.
1555
+ * this list — fail-fast, before context construction, so the extractor
1556
+ * wastes zero CPU on inapplicable nodes.
1071
1557
  *
1072
1558
  * Absent (`undefined`) is the default: the extractor applies to every
1073
1559
  * kind. There are no wildcards — the absence of the field already
@@ -1099,9 +1585,76 @@ interface IExtractor extends IExtensionBase {
1099
1585
  * `deterministic`).
1100
1586
  */
1101
1587
 
1588
+ /**
1589
+ * Step 9.6.2 — orphan sidecar entry surfaced to rules. A `.sm` file
1590
+ * whose sibling `.md` does not exist on disk; the `annotation-orphan`
1591
+ * built-in rule emits one warning per entry. Other rules that care
1592
+ * about orphan sidecars MAY consume the list too.
1593
+ */
1594
+ interface IRuleOrphanSidecar {
1595
+ /** Relative path (POSIX-separated) of the orphan `.sm`. */
1596
+ relativePath: string;
1597
+ /** Absolute path of the missing `.md` the sidecar was anchored to. */
1598
+ expectedMdPath: string;
1599
+ }
1102
1600
  interface IRuleContext {
1103
1601
  nodes: Node[];
1104
1602
  links: Link[];
1603
+ /**
1604
+ * Step 9.6.2 — orphaned sidecars discovered during the scan walk.
1605
+ * Empty when sidecar discovery did not run (legacy callers) or
1606
+ * when no orphans exist.
1607
+ */
1608
+ orphanSidecars?: IRuleOrphanSidecar[];
1609
+ /**
1610
+ * Step 9.6.6 — raw parsed sidecar root keyed by `node.path`. Populated
1611
+ * by the orchestrator alongside the public `Node.sidecar` overlay so
1612
+ * rules that inspect plugin namespaces (e.g. the built-in
1613
+ * `core/unknown-field` Rule) can walk the full tree without re-reading
1614
+ * the file from disk. Absent (or `undefined` per node) when no
1615
+ * sidecar accompanies the node, or when the sidecar failed to parse.
1616
+ * Treat as read-only.
1617
+ */
1618
+ sidecarRoots?: ReadonlyMap<string, Record<string, unknown>>;
1619
+ /**
1620
+ * Step 9.6.6 — runtime catalog of plugin-contributed annotation keys,
1621
+ * as exposed by `kernel.getRegisteredAnnotationKeys()`. Threaded
1622
+ * through so rules can reason about the registered-vs-unknown split
1623
+ * without reaching back into the kernel. Empty array when no plugin
1624
+ * declares contributions; absent for legacy callers (older runScan
1625
+ * sites that never wired the catalog through).
1626
+ */
1627
+ annotationContributions?: readonly IRegisteredAnnotationKey[];
1628
+ /**
1629
+ * Step 11.x — runtime catalog of plugin-contributed view contributions,
1630
+ * as exposed by `kernel.getRegisteredViewContributions()`. Threaded
1631
+ * through so rules can reason about emissions without reaching back
1632
+ * into the kernel: built-in `core/unknown-contract` walks this list to
1633
+ * detect deprecated contracts in use, and `core/contribution-orphan`
1634
+ * joins it with the live node set to flag dangling emissions. Empty
1635
+ * array when no extension declares view contributions; absent for
1636
+ * legacy callers (older runScan sites that never wired the catalog
1637
+ * through).
1638
+ */
1639
+ viewContributions?: readonly IRegisteredViewContribution[];
1640
+ /**
1641
+ * Emit a per-node view contribution declared in this rule's manifest
1642
+ * `viewContributions` map. Sync, void return; the orchestrator
1643
+ * validates the payload against the contract schema at call time and
1644
+ * silently drops invalid emissions with a logged `extension.error`
1645
+ * event (parallel to `IExtractorCallbacks.emitContribution`).
1646
+ *
1647
+ * Unlike Extractor's emit (which binds `nodePath` from `ctx.node.path`
1648
+ * implicitly because Extractors run per-node), Rule's `evaluate()`
1649
+ * sees the full graph at once. The rule walks `ctx.nodes` itself and
1650
+ * MUST supply the target node path explicitly per emission.
1651
+ *
1652
+ * Calling `emitContribution` with a `contributionId` that is not
1653
+ * declared in the manifest is dropped with an `extension.error`. The
1654
+ * kernel routes emitted contributions to the same persistence
1655
+ * pipeline as Extractor emissions (`scan_contributions`).
1656
+ */
1657
+ emitContribution(nodePath: string, contributionId: string, payload: unknown): void;
1105
1658
  }
1106
1659
  interface IRule extends IExtensionBase {
1107
1660
  kind: 'rule';
@@ -1156,6 +1709,58 @@ interface IRule extends IExtensionBase {
1156
1709
  * - `fanOutPolicy` — `'per-node'` (default) vs `'batch'`.
1157
1710
  */
1158
1711
 
1712
+ /**
1713
+ * Single sidecar write payload an Action can return. Discriminated union so
1714
+ * future write kinds (storage rows, plugin KV, etc.) can land additively
1715
+ * without breaking consumers that only handle `kind: 'sidecar'`.
1716
+ *
1717
+ * - `path` — absolute path to the `.sm` file the kernel must materialise
1718
+ * the change into. Resolved by the Action from the node's absolute
1719
+ * path via `sidecarPathFor()`.
1720
+ * - `changes` — partial sidecar root used as a deep-merge patch (NOT a
1721
+ * full replacement). Arrays REPLACE; objects RECURSE. Reason:
1722
+ * sidecars are shared-write between skill-map core and plugins;
1723
+ * a full replace would clobber `<plugin-id>:` namespaced blocks.
1724
+ */
1725
+ type TActionWrite = {
1726
+ kind: 'sidecar';
1727
+ path: string;
1728
+ changes: Record<string, unknown>;
1729
+ };
1730
+ /**
1731
+ * Result envelope returned by deterministic Actions. The `report` field
1732
+ * carries the typed report payload (each Action declares its shape via
1733
+ * `reportSchemaRef`); `writes` is opt-in — Actions that do not mutate
1734
+ * persistent state simply omit it.
1735
+ */
1736
+ interface IActionResult<TReport = unknown> {
1737
+ report: TReport;
1738
+ writes?: TActionWrite[];
1739
+ }
1740
+ /**
1741
+ * Runtime context passed to a deterministic Action's `invoke()` method.
1742
+ * Minimal surface — Actions stay pure (no IO inside `invoke`); the kernel
1743
+ * materialises any returned `writes` after the call.
1744
+ *
1745
+ * - `node` — the target `Node` the Action operates on. Open-by-design;
1746
+ * batch / fan-out flows pick the matching nodes upstream.
1747
+ * - `nodeAbsolutePath` — absolute path to the node's `.md` file on
1748
+ * disk. The Action uses this to compute the sidecar path it returns
1749
+ * in a `TActionWrite`. Surfaced separately from `node.path` (which is
1750
+ * the relative scope-root form) so Actions never compose absolute
1751
+ * paths from `node.path` themselves.
1752
+ * - `invoker` — identity of the caller; written into the sidecar's
1753
+ * `audit.lastBumpedBy` when the Action chooses to. CLI invocations
1754
+ * pass `'cli'`; plugin-driven invocations pass `'plugin:<plugin-id>'`.
1755
+ * - `now` — clock function; tests inject a deterministic source.
1756
+ * Defaults to `() => new Date()` at the composition root.
1757
+ */
1758
+ interface IActionContext {
1759
+ node: Node;
1760
+ nodeAbsolutePath: string;
1761
+ invoker: string;
1762
+ now: () => Date;
1763
+ }
1159
1764
  /**
1160
1765
  * Declarative filter applied by `--all` fan-out, UI button gating, and
1161
1766
  * `sm actions show`. All fields optional — an empty precondition matches
@@ -1225,6 +1830,26 @@ interface IAction extends IExtensionBase {
1225
1830
  * full list. Batch actions tend to hit context limits; use sparingly.
1226
1831
  */
1227
1832
  fanOutPolicy?: 'per-node' | 'batch';
1833
+ /**
1834
+ * Deterministic invocation entry point. OPTIONAL on the runtime
1835
+ * contract for backward compatibility with the manifest-only era
1836
+ * (Decision #114) — actions that ship for the future probabilistic
1837
+ * runner / record path leave it absent and the kernel never calls it.
1838
+ * Step 9.6.3 (Decision #125) introduces the first concrete consumer:
1839
+ * the built-in `bump` Action implements `invoke()` and returns a
1840
+ * `writes: [{ kind: 'sidecar', ... }]` payload that the kernel
1841
+ * materialises through `ISidecarStore`.
1842
+ *
1843
+ * Implementations MUST stay pure — no IO inside `invoke()`. The Action
1844
+ * computes the patch and returns it; the kernel reads the on-disk
1845
+ * sidecar, deep-merges, validates, and writes back inside its critical
1846
+ * section.
1847
+ *
1848
+ * `TInput` is action-specific; the built-in `bump` Action declares
1849
+ * `{ force?: boolean; reason?: string }`. The signature stays generic
1850
+ * so each Action narrows it locally without forcing a common base.
1851
+ */
1852
+ invoke?: <TInput, TReport>(input: TInput, ctx: IActionContext) => IActionResult<TReport>;
1228
1853
  }
1229
1854
 
1230
1855
  /**
@@ -1499,6 +2124,31 @@ interface RunScanOptions {
1499
2124
  emitter?: ProgressEmitterPort;
1500
2125
  /** Runtime extension instances. Absent → empty pipeline. */
1501
2126
  extensions?: IScanExtensions;
2127
+ /**
2128
+ * Step 9.6.6 — runtime catalog of plugin-contributed annotation keys
2129
+ * (the same shape `kernel.getRegisteredAnnotationKeys()` returns).
2130
+ * Threaded into the rule pass so `core/unknown-field` can
2131
+ * legitimise registered plugin namespaces / root keys without
2132
+ * re-walking the manifests. Absent → empty catalog (every plugin
2133
+ * key is treated as unknown). Built-in catalog from
2134
+ * `annotations.schema.json` is NOT included — that is hard-coded
2135
+ * inside the rule.
2136
+ */
2137
+ annotationContributions?: readonly IRegisteredAnnotationKey[];
2138
+ /**
2139
+ * Runtime catalog of plugin-contributed view contributions (the same
2140
+ * shape `kernel.getRegisteredViewContributions()` returns). Threaded
2141
+ * into the rule pass so:
2142
+ * - `core/unknown-contract` and `core/contribution-orphan` can
2143
+ * introspect the catalog (read-only).
2144
+ * - The orchestrator's per-rule emit closure can look up each
2145
+ * declared `(contributionId → contract)` pairing for AJV
2146
+ * payload validation.
2147
+ * Absent → empty catalog. Rules that emit contributions silently
2148
+ * drop emissions when the catalog has no entry for the rule's
2149
+ * declared contributionId.
2150
+ */
2151
+ viewContributions?: readonly IRegisteredViewContribution[];
1502
2152
  /**
1503
2153
  * Scan scope. Defaults to `'project'`. The CLI flag wiring lands in
1504
2154
  * the config layer wiring; `runScan` already accepts the override
@@ -1618,8 +2268,6 @@ interface IExtractorRunRecord {
1618
2268
  *
1619
2269
  * - upsert a single row per pair (stable PRIMARY KEY conflict on
1620
2270
  * re-extract);
1621
- * - flag probabilistic rows `stale = 1` when the body changes between
1622
- * scans (preserving the prior LLM cost);
1623
2271
  * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
1624
2272
  * for last-write-wins per field at read time.
1625
2273
  *
@@ -1629,10 +2277,12 @@ interface IExtractorRunRecord {
1629
2277
  * fold into a single row, but two different Extractors hitting the
1630
2278
  * same node yield two distinct rows.
1631
2279
  *
1632
- * `isProbabilistic` is denormalised so the persistence layer's stale
1633
- * flag query stays a single-table read; recomputing from the live
1634
- * registry would force every read-path to thread the runtime extension
1635
- * set through.
2280
+ * `isProbabilistic` is reserved: Extractors are deterministic-only, so
2281
+ * every record produced by the orchestrator sets it to `false`. The
2282
+ * field is kept on the record (and the row in `node_enrichments`) so a
2283
+ * future Action-issued enrichment can populate it without reshaping
2284
+ * the persistence contract — see spec `architecture.md`
2285
+ * §Extractor · enrichment layer.
1636
2286
  */
1637
2287
  interface IEnrichmentRecord {
1638
2288
  nodePath: string;
@@ -1660,6 +2310,7 @@ declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): P
1660
2310
  renameOps: RenameOp[];
1661
2311
  extractorRuns: IExtractorRunRecord[];
1662
2312
  enrichments: IEnrichmentRecord[];
2313
+ contributions: IContributionRecord[];
1663
2314
  }>;
1664
2315
  declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
1665
2316
  /**
@@ -1696,6 +2347,7 @@ declare function runExtractorsForNode(opts: {
1696
2347
  internalLinks: Link[];
1697
2348
  externalLinks: Link[];
1698
2349
  enrichments: IEnrichmentRecord[];
2350
+ contributions: IContributionRecord[];
1699
2351
  }>;
1700
2352
  /**
1701
2353
  * Pure rename / orphan classification per `spec/db-schema.md` §Rename
@@ -1736,10 +2388,11 @@ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], iss
1736
2388
  * Algorithm:
1737
2389
  *
1738
2390
  * 1. Filter `enrichments` down to rows targeting this node AND not
1739
- * flagged `stale`. Stale rows (probabilistic enrichments whose
1740
- * body changed since their last run) are excluded by default —
1741
- * stale visibility belongs to the UI layer where the marker is
1742
- * shown next to the value.
2391
+ * flagged `stale`. With Extractors deterministic-only no row is
2392
+ * stale-flagged in this revision; the filter is preserved for the
2393
+ * future Action-issued enrichment revision (queued LLM jobs whose
2394
+ * output must survive body changes), where stale visibility
2395
+ * belongs to the UI layer next to the value.
1743
2396
  * 2. Sort the survivors by `enrichedAt` ASC so iteration order is
1744
2397
  * "oldest first". This makes the spread merge below
1745
2398
  * last-write-wins per field — the freshest Extractor's value
@@ -1792,7 +2445,7 @@ interface IPersistedEnrichment {
1792
2445
  declare class InMemoryProgressEmitter implements ProgressEmitterPort {
1793
2446
  #private;
1794
2447
  emit(event: ProgressEvent): void;
1795
- subscribe(listener: ProgressListener): () => void;
2448
+ subscribe(listener: TProgressListener): () => void;
1796
2449
  }
1797
2450
 
1798
2451
  /**
@@ -2033,6 +2686,72 @@ declare function applyExportQuery(scan: {
2033
2686
  issues: Issue[];
2034
2687
  }, query: IExportQuery): IExportSubset;
2035
2688
 
2689
+ /**
2690
+ * `scan_node_tags` adapter — tags · dual-source persistence layer.
2691
+ *
2692
+ * One row per `(node_path, tag, source)` triple. Projected at persist
2693
+ * time from BOTH `frontmatter.tags` (with `source='author'`) and
2694
+ * `sidecar.annotations.tags` (with `source='user'`). The same tag
2695
+ * string MAY appear under both sources for the same node — the PK
2696
+ * accepts the pair; search returns the node once via DISTINCT, the
2697
+ * UI renders both chips with their attribution.
2698
+ *
2699
+ * Belongs to the `scan_*` family — replaced wholesale per scan.
2700
+ * Cached nodes' tag rows are projected from the cached
2701
+ * `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both
2702
+ * already in memory at persist time), so the rebuild is cheap
2703
+ * regardless of cache hit / miss. See `spec/db-schema.md`
2704
+ * § scan_node_tags for the normative shape and replace-all semantics.
2705
+ */
2706
+
2707
+ /**
2708
+ * In-memory tag record buffered during scan and flushed to
2709
+ * `scan_node_tags` by `persistScanResult`. One entry per
2710
+ * `(node_path, tag, source)` projected from a node's frontmatter tags
2711
+ * (`source: 'author'`) or sidecar annotations tags
2712
+ * (`source: 'user'`).
2713
+ */
2714
+ interface ITagRecord {
2715
+ nodePath: string;
2716
+ tag: string;
2717
+ source: 'author' | 'user';
2718
+ }
2719
+
2720
+ /**
2721
+ * Pure helpers for the "update available" notification feature.
2722
+ *
2723
+ * Three responsibilities:
2724
+ * - `fetchLatestVersion` — query `https://registry.npmjs.org/<pkg>/latest`
2725
+ * with `AbortController` + timeout. Throws on
2726
+ * non-200 / parse failure / abort.
2727
+ * - `compareVersions` — semver compare (-1 / 0 / 1). Pre-1.0 aware:
2728
+ * treats prereleases via the standard rules
2729
+ * (release > prerelease at the same triple).
2730
+ * - `isOutdated` — sugar over `compareVersions` for the common
2731
+ * "is `latest` strictly greater than `current`"
2732
+ * check the banner runs against.
2733
+ *
2734
+ * Pure kernel module — NO `process.env` reads, NO Node globals beyond the
2735
+ * built-in `fetch` / `AbortController` (Node 22+). Every env / settings
2736
+ * lookup happens in `src/cli/util/update-check-banner.ts`, the CLI-side
2737
+ * adapter that owns side effects.
2738
+ *
2739
+ * The shared cache type (`IUpdateCheckCache`) is used by the storage
2740
+ * helpers under `kernel/storage/update-check.ts` and by the BFF's
2741
+ * `GET /api/update-status` projection. A second type
2742
+ * (`IUpdateStatus`) shapes the BFF response — it merges `current`
2743
+ * (from `VERSION`) into the cache so the UI can render without a
2744
+ * second lookup. Both stay flat — no nested objects — so JSON
2745
+ * serialization is trivial.
2746
+ */
2747
+ interface IUpdateCheckCache {
2748
+ latestVersion: string;
2749
+ /** Epoch ms — when the registry was last successfully probed. */
2750
+ checkedAt: number;
2751
+ /** Epoch ms — when the banner was last printed; null = never shown yet. */
2752
+ shownAt: number | null;
2753
+ }
2754
+
2036
2755
  /**
2037
2756
  * `PluginLoaderPort` — discovers plugin directories and loads their
2038
2757
  * extensions. The shape mirrors what the concrete loader actually
@@ -2126,6 +2845,19 @@ interface IPersistOptions {
2126
2845
  renameOps?: RenameOp[];
2127
2846
  extractorRuns?: IExtractorRunRecord[];
2128
2847
  enrichments?: IEnrichmentRecord[];
2848
+ contributions?: IContributionRecord[];
2849
+ /**
2850
+ * Phase 3 / View contribution system — active runtime catalog of
2851
+ * registered view contributions, keyed by qualified id
2852
+ * `<pluginId>/<extensionId>/<contributionId>`. Passed to the
2853
+ * `scan_contributions` upsert so the catalog sweep can drop rows
2854
+ * belonging to plugins / extensions that are no longer in the
2855
+ * catalog (uninstalled plugins, disabled bundles, removed
2856
+ * contributions). Empty / absent set = no catalog sweep (legacy
2857
+ * behaviour, leaves disabled-plugin rows stale per design F24
2858
+ * pre-fix).
2859
+ */
2860
+ registeredContributionKeys?: ReadonlySet<string>;
2129
2861
  }
2130
2862
  /**
2131
2863
  * Issue row as the storage layer sees it — paired with its DB-assigned
@@ -2181,16 +2913,18 @@ interface IMigrateNodeFksReport {
2181
2913
  summaries: number;
2182
2914
  enrichments: number;
2183
2915
  pluginKvs: number;
2916
+ nodeFavorites: number;
2184
2917
  /**
2185
- * Composite-PK collisions encountered when migrating
2186
- * `state_summaries` / `state_enrichments` / `state_plugin_kvs` because
2187
- * a row already existed at the destination PK. The pre-existing rows
2188
- * are preserved the migrating rows are dropped (deleted from
2189
- * `fromPath` without a corresponding INSERT). One entry per dropped
2190
- * row, with the affected PK fields included for diagnostic output.
2918
+ * Collisions encountered when migrating any of the keyed-by-node
2919
+ * `state_*` tables because a row already existed at the destination
2920
+ * PK. The pre-existing rows are preserved the migrating rows are
2921
+ * dropped (deleted from `fromPath` without a corresponding INSERT).
2922
+ * One entry per dropped row, with the affected PK fields included
2923
+ * for diagnostic output. `state_node_favorites` has no composite key
2924
+ * so its `keys` is the empty object.
2191
2925
  */
2192
2926
  collisions: Array<{
2193
- table: 'state_summaries' | 'state_enrichments' | 'state_plugin_kvs';
2927
+ table: 'state_summaries' | 'state_enrichments' | 'state_plugin_kvs' | 'state_node_favorites';
2194
2928
  fromPath: string;
2195
2929
  toPath: string;
2196
2930
  keys: Record<string, string>;
@@ -2264,28 +2998,6 @@ interface IPluginApplyResult {
2264
2998
  intrusions: string[];
2265
2999
  }
2266
3000
 
2267
- /**
2268
- * `StoragePort` — the kernel's persistence boundary. Driving adapters
2269
- * (CLI, future server, in-memory test harness) consume this surface
2270
- * exclusively; nothing in `cli/**` should reach into the SQLite
2271
- * adapter's internal helpers (free functions on
2272
- * `kernel/adapters/sqlite/*`) directly. Phase F of the
2273
- * storage-port-promotion refactor finishes that hardening; A-E grow
2274
- * the port enough that the CLI has somewhere to land.
2275
- *
2276
- * The port is namespaced by domain (`scans`, `issues`, `enrichments`,
2277
- * etc.) — explicitly NOT a generic `port.query<T>(sql)`. Each
2278
- * namespace's methods name an operation the kernel cares about; the
2279
- * adapter translates to its persistence engine's idioms.
2280
- *
2281
- * Phase A lands the **scans / issues / enrichments / transaction**
2282
- * namespaces — the core scan pipeline. The remaining namespaces
2283
- * (history / jobs / pluginConfig / migrations / pluginMigrations)
2284
- * arrive in subsequent phases. The port shape declared here is the
2285
- * Phase A subset; later phases extend it without reshaping what
2286
- * lands today.
2287
- */
2288
-
2289
3001
  /**
2290
3002
  * Subset of `StoragePort` exposed inside a `transaction(fn)` callback.
2291
3003
  * Lifecycle methods are intentionally omitted — a transaction that
@@ -2362,6 +3074,50 @@ interface StoragePort {
2362
3074
  */
2363
3075
  findNode(path: string): Promise<INodeBundle | null>;
2364
3076
  };
3077
+ /**
3078
+ * Phase 3 / View contribution system — read access to
3079
+ * `scan_contributions`. Writes happen exclusively via
3080
+ * `scans.persist({ contributions })` to keep the replace-all
3081
+ * semantics intact; this namespace is read-only.
3082
+ */
3083
+ contributions: {
3084
+ /** Every contribution row for a single node. Stable order. */
3085
+ listForNode(nodePath: string): Promise<IPersistedContribution[]>;
3086
+ /**
3087
+ * Bulk variant for the BFF nodes-list route. Returns rows for
3088
+ * every path in `paths`, sorted `nodePath` ASC, then qualified-id
3089
+ * ASC. Empty `paths` returns `[]` without a query.
3090
+ */
3091
+ listForPaths(paths: readonly string[]): Promise<IPersistedContribution[]>;
3092
+ /**
3093
+ * Lookup by qualified id + path. Used by
3094
+ * `GET /api/contributions/:pluginId/:contributionId?path=...`.
3095
+ */
3096
+ lookup(pluginId: string, contributionId: string, nodePath: string, extensionId?: string): Promise<IPersistedContribution[]>;
3097
+ };
3098
+ /**
3099
+ * Read-only access to `scan_node_tags`. Writes happen exclusively
3100
+ * via `scans.persist({...})` (the persistence layer projects from
3101
+ * `node.frontmatter.tags` and `node.sidecar.annotations.tags`); this
3102
+ * namespace is read-only.
3103
+ */
3104
+ tags: {
3105
+ /** Every tag row for a single node. Author entries first, then user. */
3106
+ listForNode(nodePath: string): Promise<ITagRecord[]>;
3107
+ /**
3108
+ * Bulk variant for the BFF nodes-list route. Returns rows for every
3109
+ * path in `paths`, sorted `nodePath` ASC, then `source` ASC, then
3110
+ * `tag` ASC. Empty `paths` returns `[]` without a query.
3111
+ */
3112
+ listForPaths(paths: readonly string[]): Promise<ITagRecord[]>;
3113
+ /**
3114
+ * Find every node carrying `tag`. Optional `source` narrows to one
3115
+ * side of the dual surface (matches `sm list --tag <name>
3116
+ * --tag-source author|user`); absent matches the union (default
3117
+ * `sm list --tag`).
3118
+ */
3119
+ findNodes(tag: string, source?: 'author' | 'user'): Promise<string[]>;
3120
+ };
2365
3121
  issues: {
2366
3122
  /** Every issue from the latest scan, in insertion order. */
2367
3123
  listAll(): Promise<Issue[]>;
@@ -2418,6 +3174,53 @@ interface StoragePort {
2418
3174
  */
2419
3175
  listReferencedFilePaths(): Promise<Set<string>>;
2420
3176
  };
3177
+ /**
3178
+ * Generic key/value preferences keyed by a stable string. Backs the
3179
+ * `config_preferences` table — one row per `key`, `value_json` is a
3180
+ * single JSON blob the caller serialises. Keys with the `_kernel.`
3181
+ * prefix are reserved for kernel-managed entries (today: the
3182
+ * update-check cache); user-set preferences land under unprefixed
3183
+ * keys when those ship.
3184
+ *
3185
+ * Read-only by design at the port level — the only writer is the
3186
+ * CLI's post-run hook (`cli/util/update-check-banner.ts`), which
3187
+ * reaches the persistence helpers directly. The port surfaces the
3188
+ * read so the BFF's `GET /api/update-status` projection can stay
3189
+ * inside the abstract contract.
3190
+ */
3191
+ preferences: {
3192
+ /**
3193
+ * Load the update-check cache row. Returns `null` when the row
3194
+ * is absent, malformed JSON, or fails the shape guard. Never
3195
+ * throws — read failures degrade silently because the banner is
3196
+ * a non-essential surface.
3197
+ */
3198
+ loadUpdateCheckCache(): Promise<IUpdateCheckCache | null>;
3199
+ /**
3200
+ * Upsert the update-check cache row. Always overwrites the
3201
+ * existing JSON blob in place. `updated_at` tracks wall-clock
3202
+ * now — separate from the embedded `checkedAt` field, which
3203
+ * the caller controls.
3204
+ */
3205
+ saveUpdateCheckCache(cache: IUpdateCheckCache): Promise<void>;
3206
+ };
3207
+ favorites: {
3208
+ /**
3209
+ * Mark `path` as favorited. Idempotent — a second call refreshes
3210
+ * `favoritedAt` but does not error. The path is FK-semantic to
3211
+ * `scan_nodes.path`; the route layer is responsible for confirming
3212
+ * the path exists in the live scan before calling.
3213
+ */
3214
+ set(path: string): Promise<void>;
3215
+ /** Drop the favorite row for `path`. Idempotent — no-op when absent. */
3216
+ unset(path: string): Promise<void>;
3217
+ /**
3218
+ * Load every favorited path as a `Set<string>` ready for `O(1)`
3219
+ * membership checks. Used by the BFF's `/api/nodes` decorator —
3220
+ * one query per request, no SQL JOIN against `scan_nodes`.
3221
+ */
3222
+ listPaths(): Promise<Set<string>>;
3223
+ };
2421
3224
  history: {
2422
3225
  /** List `state_executions` rows (paginated by filter). */
2423
3226
  list(filter: IListExecutionsFilter): Promise<ExecutionRecord[]>;
@@ -2544,19 +3347,19 @@ interface RunnerPort {
2544
3347
  * `LogRecord.level`. Setting an adapter to `silent` disables every
2545
3348
  * method.
2546
3349
  */
2547
- type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
2548
- type LogMethodLevel = Exclude<LogLevel, 'silent'>;
2549
- declare const LOG_LEVELS: readonly LogLevel[];
2550
- declare function logLevelRank(level: LogLevel): number;
2551
- declare function isLogLevel(value: unknown): value is LogLevel;
3350
+ type TLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
3351
+ type TLogMethodLevel = Exclude<TLogLevel, 'silent'>;
3352
+ declare const LOG_LEVELS: readonly TLogLevel[];
3353
+ declare function logLevelRank(level: TLogLevel): number;
3354
+ declare function isLogLevel(value: unknown): value is TLogLevel;
2552
3355
  /**
2553
- * Parse a string into a `LogLevel`. Returns `null` for invalid input
3356
+ * Parse a string into a `TLogLevel`. Returns `null` for invalid input
2554
3357
  * (incl. `undefined` / `null` / empty). Case-insensitive; trims
2555
3358
  * whitespace.
2556
3359
  */
2557
- declare function parseLogLevel(value: string | undefined | null): LogLevel | null;
3360
+ declare function parseLogLevel(value: string | undefined | null): TLogLevel | null;
2558
3361
  interface LogRecord {
2559
- level: LogMethodLevel;
3362
+ level: TLogMethodLevel;
2560
3363
  /** ISO 8601 timestamp produced at the moment the log call was made. */
2561
3364
  timestamp: string;
2562
3365
  message: string;
@@ -2628,7 +3431,35 @@ declare function getActiveLogger(): LoggerPort;
2628
3431
 
2629
3432
  interface Kernel {
2630
3433
  registry: Registry;
3434
+ /**
3435
+ * Step 9.6.6 — read-only catalog of plugin-contributed annotation
3436
+ * keys, keyed by `(pluginId, key)`. Populated at plugin-load time;
3437
+ * pure read with no side effects. Built-in catalog (from
3438
+ * `annotations.schema.json`) is NOT included here.
3439
+ */
3440
+ getRegisteredAnnotationKeys: () => readonly IRegisteredAnnotationKey[];
3441
+ /**
3442
+ * Internal — replace the frozen catalog. Called once by the
3443
+ * plugin runtime composer after every plugin has loaded; consumers
3444
+ * MUST treat the resulting array as immutable.
3445
+ */
3446
+ setRegisteredAnnotationKeys: (entries: readonly IRegisteredAnnotationKey[]) => void;
3447
+ /**
3448
+ * Step 11.x — read-only catalog of plugin-contributed view
3449
+ * contributions, keyed by `(pluginId, extensionId, contributionId)`.
3450
+ * Populated at plugin-load time; pure read with no side effects.
3451
+ * Mirror of `getRegisteredAnnotationKeys` for the view contribution
3452
+ * surface (see `architecture.md` §View contribution system →
3453
+ * Runtime catalog).
3454
+ */
3455
+ getRegisteredViewContributions: () => readonly IRegisteredViewContribution[];
3456
+ /**
3457
+ * Internal — replace the frozen view-contribution catalog. Called
3458
+ * once by the plugin runtime composer after every plugin has loaded;
3459
+ * consumers MUST treat the resulting array as immutable.
3460
+ */
3461
+ setRegisteredViewContributions: (entries: readonly IRegisteredViewContribution[]) => void;
2631
3462
  }
2632
3463
  declare function createKernel(): Kernel;
2633
3464
 
2634
- export { type Confidence, DuplicateExtensionError, EXTENSION_KINDS, type ExecutionFailureReason, type ExecutionKind, type ExecutionRecord, type ExecutionRunner, type ExecutionStatus, ExportQueryError, type Extension, type ExtensionKind, type FilesystemPort, HOOK_TRIGGERS, type HistoryStats, type HistoryStatsErrorRates, type HistoryStatsExecutionsPerPeriod, type HistoryStatsPerActionRate, type HistoryStatsTokensPerAction, type HistoryStatsTopNode, type HistoryStatsTotals, type IAction, type IActionPrecondition, type ICreateFsWatcherOptions, type IDedicatedStorePersist, type IDedicatedStoreWrapper, type IDiscoveredPlugin, type IEnrichmentRecord, type IExportQuery, type IExportSubset, type IExtensionBase, type IExtractor, type IExtractorCallbacks, type IExtractorContext, type IExtractorRunRecord, type IFormatter, type IFormatterContext, type IFsWatcher, type IHook, type IHookContext, type IIssueRow, type IKvStorePersist, type IKvStoreWrapper, type ILoadedExtension, type INodeBundle, type INodeChange, type INodeCounts, type INodeFilter, type IPersistOptions, type IPersistedEnrichment, type IPluginManifest, type IPluginStorageSchema, type IPluginStore, type IProvider, type IRawNode, type IRule, type IRuleContext, type IRunOptions, type IRunResult, type IScanDelta, type ITransactionalStorage, type IWalkOptions, type IWatchBatch, type IWatchEvent, InMemoryProgressEmitter, type Issue, type IssueFix, KV_SCHEMA_KEY, type Kernel, LOG_LEVELS, type Link, type LinkKind, type LinkLocation, type LinkTrigger, type LogLevel, type LogMethodLevel, type LogRecord, type LoggerPort, type Node, type NodeKind, type NodeStat, type PluginLoaderPort, type ProgressEmitterPort, type ProgressEvent, type ProgressListener, Registry, type RenameOp, type RunScanOptions, type RunnerPort, type ScanResult, type ScanScannedBy, type ScanStats, type Severity, SilentLogger, type Stability, type StoragePort, type TExecutionMode, type TGranularity, type THookFilter, type THookTrigger, type TNodeChangeReason, type TPluginLoadStatus, type TPluginStorage, type TWatchEventKind, type TripleSplit, applyExportQuery, computeScanDelta, configureLogger, createChokidarWatcher, createKernel, detectRenamesAndOrphans, getActiveLogger, isEmptyDelta, isLogLevel, log, logLevelRank, makeDedicatedStoreWrapper, makeKvStoreWrapper, makePluginStore, mergeNodeWithEnrichments, parseExportQuery, parseLogLevel, qualifiedExtensionId, resetLogger, runExtractorsForNode, runScan, runScanWithRenames };
3465
+ export { type Confidence, DuplicateExtensionError, EXTENSION_KINDS, type ExecutionFailureReason, type ExecutionKind, type ExecutionRecord, type ExecutionRunner, type ExecutionStatus, ExportQueryError, type Extension, type ExtensionKind, type FilesystemPort, HOOK_TRIGGERS, type HistoryStats, type HistoryStatsErrorRates, type HistoryStatsExecutionsPerPeriod, type HistoryStatsPerActionRate, type HistoryStatsTokensPerAction, type HistoryStatsTopNode, type HistoryStatsTotals, type IAction, type IActionContext, type IActionPrecondition, type IActionResult, type IAnnotationContribution, type ICreateFsWatcherOptions, type IDedicatedStorePersist, type IDedicatedStoreWrapper, type IDiscoveredPlugin, type IEnrichmentRecord, type IExportQuery, type IExportSubset, type IExtensionBase, type IExtractor, type IExtractorCallbacks, type IExtractorContext, type IExtractorRunRecord, type IFormatter, type IFormatterContext, type IFsWatcher, type IHook, type IHookContext, type IIssueRow, type IKvStorePersist, type IKvStoreWrapper, type ILoadedExtension, type INodeBundle, type INodeChange, type INodeCounts, type INodeFilter, type IPersistOptions, type IPersistedEnrichment, type IPluginManifest, type IPluginStorageSchema, type IPluginStore, type IProvider, type IRawNode, type IRegisteredAnnotationKey, type IRegisteredViewContribution, type IRule, type IRuleContext, type IRunOptions, type IRunResult, type IScanDelta, type ISettingDeclaration, type ITransactionalStorage, type IViewContribution, type IWalkOptions, type IWatchBatch, type IWatchEvent, InMemoryProgressEmitter, type Issue, type IssueFix, KV_SCHEMA_KEY, type Kernel, LOG_LEVELS, type Link, type LinkKind, type LinkLocation, type LinkTrigger, type LogRecord, type LoggerPort, type Node, type NodeKind, type NodeStat, type PluginLoaderPort, type ProgressEmitterPort, type ProgressEvent, Registry, type RenameOp, type RunScanOptions, type RunnerPort, type ScanResult, type ScanScannedBy, type ScanStats, type Severity, SilentLogger, type Stability, type StoragePort, type TActionWrite, type TContractName, type TExecutionMode, type TGranularity, type THookFilter, type THookTrigger, type TInputTypeName, type TLogLevel, type TLogMethodLevel, type TNodeChangeReason, type TPluginLoadStatus, type TPluginStorage, type TProgressListener, type TSettingValue, type TSeverity, type TWatchEventKind, type TripleSplit, applyExportQuery, computeScanDelta, configureLogger, createChokidarWatcher, createKernel, detectRenamesAndOrphans, getActiveLogger, isEmptyDelta, isLogLevel, log, logLevelRank, makeDedicatedStoreWrapper, makeKvStoreWrapper, makePluginStore, mergeNodeWithEnrichments, parseExportQuery, parseLogLevel, qualifiedExtensionId, resetLogger, runExtractorsForNode, runScan, runScanWithRenames };