@skill-map/cli 0.7.0 → 0.9.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.
@@ -0,0 +1,2531 @@
1
+ /**
2
+ * Domain types — byte-aligned with `spec/schemas/{node,link,issue,scan-result}.schema.json`.
3
+ *
4
+ * The kernel is the reference consumer of the spec; these types are therefore
5
+ * derived from the schemas, not invented. When a schema changes, this file
6
+ * follows. Until automatic AJV-driven derivation lands, the mapping is
7
+ * hand-maintained and the release gate is the conformance suite.
8
+ *
9
+ * --- Naming convention (kernel-wide) -------------------------------------
10
+ *
11
+ * Four categories with distinct prefix rules; the rules are deliberate
12
+ * even though they look mixed at first read:
13
+ *
14
+ * 1. **Domain types** — every shape that mirrors a `spec/schemas/*.json`
15
+ * file: `Node`, `Link`, `Issue`, `ScanResult`, `ScanStats`,
16
+ * `ExecutionRecord`, `HistoryStats`, …. **No prefix.** Names track
17
+ * the spec verbatim because the spec is the source of truth.
18
+ * Renaming any of these is a spec change.
19
+ *
20
+ * 2. **Hexagonal ports** — the abstract boundaries the kernel calls
21
+ * out to (`StoragePort`, `RunnerPort`, `ProgressEmitterPort`,
22
+ * `FilesystemPort`, `PluginLoaderPort`). **`Port` suffix.** The
23
+ * suffix calls out the architectural role and avoids name clashes
24
+ * with the concrete adapter classes (`SqliteStorageAdapter`
25
+ * implements `StoragePort`).
26
+ *
27
+ * 3. **Runtime extension contracts** — what a plugin author
28
+ * implements: `IProvider`, `IExtractor`, `IRule`, `IFormatter`,
29
+ * `IExtensionBase`. **`I` prefix.** The prefix flags "this is a
30
+ * contract you supply, not a value the kernel hands you" — same
31
+ * reading as the rest of TypeScript's plugin ecosystems where a
32
+ * shape is implementable.
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".
41
+ *
42
+ * Edge cases worth knowing:
43
+ * - The following category-4 names lack the `I` prefix because
44
+ * they are part of the public kernel surface and renaming is a
45
+ * breaking change for downstream consumers. The list is closed:
46
+ * option bags / records: `RunScanOptions`, `RenameOp`;
47
+ * TS-only exports from `kernel/index.ts` / `kernel/ports/*`:
48
+ * `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.
51
+ * - `IDatabase` (SQLite schema) is category 4 but lives in
52
+ * `adapters/sqlite/schema.ts`, not here. Same rule applies.
53
+ *
54
+ * If you find yourself wanting to add a new type and aren't sure which
55
+ * 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.
58
+ */
59
+ /**
60
+ * The five node kinds the **built-in Claude Provider** declares — `skill`,
61
+ * `agent`, `command`, `hook`, `note`. **NOT** the kernel-wide kind type.
62
+ *
63
+ * `Node.kind` is `string`. An external Provider (Cursor, Obsidian, …)
64
+ * MAY classify into its own kinds (e.g. `'cursorRule'`, `'daily'`); the
65
+ * orchestrator, persistence layer, and AJV `node.schema.json` accept any
66
+ * non-empty string. Per `spec/db-schema.md` § scan_nodes and
67
+ * `node.schema.json#/properties/kind`, the contract is open-by-design
68
+ * (matches `IProvider.kinds` "open by design" docstring).
69
+ *
70
+ * This alias survives because:
71
+ * - claude-specific code legitimately wants to switch on the five
72
+ * hard-coded values (filter widgets, kind-aware UI cards, the
73
+ * `validate-all` built-in rule that maps each kind to its
74
+ * frontmatter schema);
75
+ * - sorting helpers want a stable `KIND_ORDER` for the canonical
76
+ * catalog;
77
+ * - tests expect to enumerate the five kinds when seeding fixtures.
78
+ *
79
+ * For "any kind a Provider could declare", use plain `string`. Only use
80
+ * `NodeKind` when the code is intentionally claude-catalog-specific.
81
+ */
82
+ type NodeKind = 'skill' | 'agent' | 'command' | 'hook' | 'note';
83
+ type LinkKind = 'invokes' | 'references' | 'mentions' | 'supersedes';
84
+ type Confidence = 'high' | 'medium' | 'low';
85
+ type Severity = 'error' | 'warn' | 'info';
86
+ type Stability = 'experimental' | 'stable' | 'deprecated';
87
+ /**
88
+ * Execution mode of an analytical extension. Mirrors the per-kind capability
89
+ * matrix in `spec/architecture.md` §Execution modes:
90
+ *
91
+ * - `deterministic` — pure code, runs synchronously inside `sm scan` /
92
+ * `sm check`. Same input → same output, every run.
93
+ * - `probabilistic` — calls an LLM through `RunnerPort`, dispatches only
94
+ * as a queued job (`sm job submit <kind>:<id>`); never participates in
95
+ * scan-time pipelines.
96
+ *
97
+ * Extractor / Rule / Action declare it directly (default `deterministic` when
98
+ * omitted in the manifest). Provider / Formatter are deterministic-only and
99
+ * MUST NOT carry the field.
100
+ */
101
+ type TExecutionMode = 'deterministic' | 'probabilistic';
102
+ interface TripleSplit {
103
+ frontmatter: number;
104
+ body: number;
105
+ total: number;
106
+ }
107
+ interface LinkTrigger {
108
+ originalTrigger: string;
109
+ normalizedTrigger: string;
110
+ }
111
+ interface LinkLocation {
112
+ line: number;
113
+ column?: number;
114
+ offset?: number;
115
+ }
116
+ interface Node {
117
+ path: string;
118
+ /**
119
+ * Provider-declared category. Open string (matches
120
+ * `node.schema.json#/properties/kind`): the built-in Claude Provider
121
+ * emits one of `NodeKind`'s values, but external Providers MAY emit
122
+ * their own. Code that intentionally switches on the claude catalog
123
+ * narrows via `if (kind === 'skill' \| ... )`; everything else
124
+ * accepts the open string and treats unknown values as opaque labels.
125
+ */
126
+ kind: string;
127
+ provider: string;
128
+ bodyHash: string;
129
+ frontmatterHash: string;
130
+ bytes: TripleSplit;
131
+ linksOutCount: number;
132
+ linksInCount: number;
133
+ externalRefsCount: number;
134
+ title?: string | null;
135
+ description?: string | null;
136
+ stability?: Stability | null;
137
+ version?: string | null;
138
+ author?: string | null;
139
+ frontmatter?: Record<string, unknown>;
140
+ tokens?: TripleSplit;
141
+ }
142
+ interface Link {
143
+ /** The originating node — the path of the file the extractor was reading
144
+ * when it emitted this link. Singular, NOT to be confused with
145
+ * `sources` (plural) below. */
146
+ source: string;
147
+ target: string;
148
+ kind: LinkKind;
149
+ confidence: Confidence;
150
+ /** Identifiers of the extractors / extensions that contributed evidence
151
+ * for this link (one link can be confirmed by multiple extractors).
152
+ * Plural; NOT the same as `source` (singular) above, which is the
153
+ * originating node path. Naming is unfortunate but spec-frozen. */
154
+ sources: string[];
155
+ trigger?: LinkTrigger | null;
156
+ location?: LinkLocation | null;
157
+ raw?: string | null;
158
+ }
159
+ interface IssueFix {
160
+ summary?: string;
161
+ autofixable?: boolean;
162
+ }
163
+ interface Issue {
164
+ ruleId: string;
165
+ severity: Severity;
166
+ nodeIds: string[];
167
+ message: string;
168
+ linkIndices?: number[];
169
+ detail?: string | null;
170
+ fix?: IssueFix | null;
171
+ data?: Record<string, unknown>;
172
+ }
173
+ interface ScanStats {
174
+ /**
175
+ * Files visited by the Provider walkers. With a single Provider this
176
+ * matches `nodesCount`; with multiple Providers running on overlapping
177
+ * roots it can diverge (each yielded `IRawNode` is one walked file).
178
+ */
179
+ filesWalked: number;
180
+ /**
181
+ * Files walked but not classified by any Provider. Today every walked
182
+ * file is classified by its Provider (the `claude` Provider falls back to
183
+ * `'note'`), so this is always 0; the field will matter once multiple
184
+ * Providers can claim the same file.
185
+ */
186
+ filesSkipped: number;
187
+ nodesCount: number;
188
+ linksCount: number;
189
+ issuesCount: number;
190
+ durationMs: number;
191
+ }
192
+ interface ScanScannedBy {
193
+ name: string;
194
+ version: string;
195
+ specVersion: string;
196
+ }
197
+ type ExecutionKind = 'action';
198
+ type ExecutionStatus = 'completed' | 'failed' | 'cancelled';
199
+ type ExecutionFailureReason = 'runner-error' | 'report-invalid' | 'timeout' | 'abandoned' | 'job-file-missing' | 'user-cancelled';
200
+ type ExecutionRunner = 'cli' | 'skill' | 'in-process';
201
+ /**
202
+ * One row of execution history (`state_executions`). Matches
203
+ * `spec/schemas/execution-record.schema.json`. `nodeIds` is the camelCased
204
+ * domain field name; storage flattens it to `node_ids_json`.
205
+ */
206
+ interface ExecutionRecord {
207
+ id: string;
208
+ kind: ExecutionKind;
209
+ extensionId: string;
210
+ extensionVersion: string;
211
+ nodeIds?: string[];
212
+ contentHash?: string | null;
213
+ status: ExecutionStatus;
214
+ failureReason?: ExecutionFailureReason | null;
215
+ exitCode?: number | null;
216
+ runner?: ExecutionRunner | null;
217
+ startedAt: number;
218
+ finishedAt: number;
219
+ durationMs?: number | null;
220
+ tokensIn?: number | null;
221
+ tokensOut?: number | null;
222
+ reportPath?: string | null;
223
+ jobId?: string | null;
224
+ }
225
+ interface HistoryStatsTotals {
226
+ executionsCount: number;
227
+ completedCount: number;
228
+ failedCount: number;
229
+ tokensIn: number;
230
+ tokensOut: number;
231
+ durationMsTotal: number;
232
+ }
233
+ interface HistoryStatsTokensPerAction {
234
+ actionId: string;
235
+ actionVersion: string;
236
+ executionsCount: number;
237
+ tokensIn: number;
238
+ tokensOut: number;
239
+ durationMsMean: number | null;
240
+ durationMsMedian: number | null;
241
+ }
242
+ interface HistoryStatsExecutionsPerPeriod {
243
+ periodStart: string;
244
+ periodUnit: 'day' | 'week' | 'month';
245
+ executionsCount: number;
246
+ tokensIn: number;
247
+ tokensOut: number;
248
+ }
249
+ interface HistoryStatsTopNode {
250
+ nodePath: string;
251
+ executionsCount: number;
252
+ lastExecutedAt: number;
253
+ }
254
+ interface HistoryStatsPerActionRate {
255
+ actionId: string;
256
+ rate: number;
257
+ executionsCount: number;
258
+ failedCount: number;
259
+ }
260
+ interface HistoryStatsErrorRates {
261
+ global: number;
262
+ perAction: HistoryStatsPerActionRate[];
263
+ perFailureReason: Record<ExecutionFailureReason, number>;
264
+ }
265
+ /**
266
+ * `sm history stats --json` payload, conforming to
267
+ * `spec/schemas/history-stats.schema.json`. `elapsedMs` is the command's
268
+ * own wall-clock per `cli-contract.md` §Elapsed time.
269
+ */
270
+ interface HistoryStats {
271
+ schemaVersion: 1;
272
+ range: {
273
+ since: string | null;
274
+ until: string;
275
+ };
276
+ totals: HistoryStatsTotals;
277
+ tokensPerAction: HistoryStatsTokensPerAction[];
278
+ executionsPerPeriod: HistoryStatsExecutionsPerPeriod[];
279
+ topNodes: HistoryStatsTopNode[];
280
+ errorRates: HistoryStatsErrorRates;
281
+ elapsedMs: number;
282
+ }
283
+ interface ScanResult {
284
+ schemaVersion: 1;
285
+ /** Unix milliseconds when the scan started. */
286
+ scannedAt: number;
287
+ /** Scan scope. `project` walks the cwd repo; `global` walks user-level skill dirs. */
288
+ scope: 'project' | 'global';
289
+ /**
290
+ * Filesystem roots that were walked during this scan. Spec requires
291
+ * `minItems: 1` — `runScan` throws if `roots: []` is supplied.
292
+ */
293
+ roots: string[];
294
+ /** Provider ids that participated in classification. Empty if no Provider matched. */
295
+ providers: string[];
296
+ /** Implementation metadata. Populated by `runScan` for self-describing output. */
297
+ scannedBy?: ScanScannedBy;
298
+ nodes: Node[];
299
+ links: Link[];
300
+ issues: Issue[];
301
+ stats: ScanStats;
302
+ }
303
+
304
+ /**
305
+ * Extension registry — six kinds, first-class, loaded through a single API.
306
+ *
307
+ * The `Extension` shape is aligned with `spec/schemas/extensions/base.schema.json`.
308
+ * Kind-specific manifests (provider / extractor / rule / action / formatter /
309
+ * hook) extend this base structurally; the registry stores the base view
310
+ * and each kind's code carries its own fuller type where needed.
311
+ *
312
+ * **Spec § A.6 — qualified ids.** Every extension is keyed in the registry
313
+ * by `<pluginId>/<id>` (e.g. `core/frontmatter`, `claude/slash`,
314
+ * `hello-world/greet`). `Extension.id` carries the **short** id as authored;
315
+ * `Extension.pluginId` carries the namespace; the registry composes the
316
+ * qualifier internally and exposes lookup APIs that operate on either form
317
+ * (qualified for direct lookup, kind-scoped listing for enumeration).
318
+ *
319
+ * Boot invariant: `new Registry()` is empty. `registry.totalCount() === 0`
320
+ * when the kernel boots with zero extensions. This is the data side of the
321
+ * `kernel-empty-boot` conformance contract.
322
+ */
323
+
324
+ type ExtensionKind = 'provider' | 'extractor' | 'rule' | 'action' | 'formatter' | 'hook';
325
+ declare const EXTENSION_KINDS: readonly ExtensionKind[];
326
+ interface Extension {
327
+ /** Short (unqualified) extension id as declared in the manifest. */
328
+ id: string;
329
+ /** Owning plugin namespace. Composed with `id` to form the qualified key. */
330
+ pluginId: string;
331
+ kind: ExtensionKind;
332
+ version: string;
333
+ description?: string;
334
+ stability?: Stability;
335
+ preconditions?: string[];
336
+ entry?: string;
337
+ }
338
+ /**
339
+ * Compose the qualified registry key for an extension. Single source of
340
+ * truth so callers don't reinvent the format and a future change (e.g. a
341
+ * different separator) lands in one place.
342
+ */
343
+ declare function qualifiedExtensionId(pluginId: string, id: string): string;
344
+ declare class DuplicateExtensionError extends Error {
345
+ constructor(kind: ExtensionKind, qualifiedId: string);
346
+ }
347
+ declare class Registry {
348
+ #private;
349
+ constructor();
350
+ register(ext: Extension): void;
351
+ /**
352
+ * Lookup by qualified id (`<pluginId>/<id>`). Returns `undefined` when
353
+ * no extension of that kind is registered under the qualifier.
354
+ */
355
+ get(kind: ExtensionKind, qualifiedId: string): Extension | undefined;
356
+ /**
357
+ * Convenience wrapper that composes the qualified id for the caller.
358
+ * Equivalent to `get(kind, qualifiedExtensionId(pluginId, id))`.
359
+ */
360
+ find(kind: ExtensionKind, pluginId: string, id: string): Extension | undefined;
361
+ all(kind: ExtensionKind): Extension[];
362
+ count(kind: ExtensionKind): number;
363
+ totalCount(): number;
364
+ }
365
+
366
+ /**
367
+ * `.skill-mapignore` parser + filter facade. Wraps `ignore` (kaelzhang)
368
+ * with the project-local layering: bundled defaults → `config.ignore`
369
+ * (from `.skill-map/settings.json`) → `.skill-mapignore` file content.
370
+ *
371
+ * Why a wrapper instead of exposing `ignore` directly:
372
+ *
373
+ * 1. Single-source defaults — `src/config/defaults/skill-mapignore` is
374
+ * the canonical default list, loaded once at module init (or at
375
+ * explicit build time, depending on bundling). The runtime never
376
+ * re-reads it per scan.
377
+ * 2. Stable interface — Providers and the orchestrator depend on a
378
+ * minimal `IIgnoreFilter` shape, so the underlying library can be
379
+ * swapped without touching every consumer.
380
+ * 3. Path normalization — every consumer passes the path RELATIVE to
381
+ * the scan root (POSIX separators); the wrapper guarantees that
382
+ * contract before delegating to `ignore`.
383
+ */
384
+ interface IIgnoreFilter {
385
+ /**
386
+ * Returns `true` when `relativePath` should be skipped. The caller
387
+ * MUST pass paths relative to the scan root, with POSIX separators
388
+ * (forward slashes), no leading `/`. Directories MAY be passed with
389
+ * or without trailing `/`; the wrapper does not require it.
390
+ */
391
+ ignores(relativePath: string): boolean;
392
+ }
393
+
394
+ /**
395
+ * `ProgressEmitterPort` — emits progress events during long operations.
396
+ *
397
+ * Shape-only today. The full event catalog (`run.started`,
398
+ * `job.claimed`, `model.delta`, etc.) is normative in
399
+ * `spec/job-events.md`; this port carries an open `data` payload so
400
+ * adapters can emit any documented event without type churn.
401
+ */
402
+ interface ProgressEvent {
403
+ type: string;
404
+ timestamp: string;
405
+ runId?: string;
406
+ jobId?: string;
407
+ data?: unknown;
408
+ }
409
+ type ProgressListener = (event: ProgressEvent) => void;
410
+ interface ProgressEmitterPort {
411
+ emit(event: ProgressEvent): void;
412
+ subscribe(listener: ProgressListener): () => void;
413
+ }
414
+
415
+ /**
416
+ * Plugin-surface types, hand-written to mirror
417
+ * `spec/schemas/plugins-registry.schema.json#/$defs/PluginManifest` and the
418
+ * extension-kind manifests under `spec/schemas/extensions/`.
419
+ *
420
+ * Per ROADMAP §DTO gap (review-pass decision): the proper emission of
421
+ * typed DTOs from `@skill-map/spec` is deferred to a future iteration when a
422
+ * third consumer (real providers / extractors / rules) forces a single
423
+ * source of truth. Until then, both `ui/src/models/` and `src/kernel/types/`
424
+ * hand-curate their own local mirror — the risk of drift is accepted at
425
+ * this scale (17 schemas) and flagged in the roadmap.
426
+ */
427
+
428
+ /**
429
+ * Plugin storage mode. Matches the `oneOf` in the plugin manifest schema:
430
+ * either shared `state_plugin_kvs` (mode `kv`) or dedicated plugin-owned
431
+ * tables with explicit migrations (mode `dedicated`). Absent = the plugin
432
+ * does not persist state at all.
433
+ *
434
+ * Optional output-schema declarations (spec § A.12 — opt-in correctness
435
+ * for plugin custom storage):
436
+ * - Mode `kv` → `schema` (single relative path). Validates the value
437
+ * written by `ctx.store.set(key, value)`.
438
+ * - Mode `dedicated` → `schemas` (per-table relative paths). Validates
439
+ * each row written by `ctx.store.write(table, row)` whose table has
440
+ * a declared schema; tables absent from the map accept any shape.
441
+ *
442
+ * Absent in both cases = permissive (status quo, no validation). Schema
443
+ * load failures surface as `load-error`. `emitLink` and `enrichNode`
444
+ * keep their universal kernel validation regardless of these fields.
445
+ */
446
+ type TPluginStorage = {
447
+ mode: 'kv';
448
+ schema?: string;
449
+ } | {
450
+ mode: 'dedicated';
451
+ tables: string[];
452
+ migrations: string[];
453
+ schemas?: Record<string, string>;
454
+ };
455
+ /**
456
+ * Toggle granularity for a plugin / built-in bundle.
457
+ *
458
+ * - `'bundle'` — the plugin id is the only enable/disable key. The whole
459
+ * bundle of extensions follows the toggle; the user cannot
460
+ * enable some extensions of the bundle and disable others.
461
+ * Default for plugins (and for the built-in `claude`
462
+ * bundle, where the provider and its kind-aware extractors
463
+ * form a coherent provider).
464
+ * - `'extension'` — each extension is independently toggle-able under its
465
+ * qualified id `<plugin-id>/<extension-id>`. Used for
466
+ * the built-in `core` bundle (every kernel built-in
467
+ * rule / formatter is removable per spec
468
+ * "no extension is privileged"). Plugin authors opt in
469
+ * only when the plugin ships several orthogonal
470
+ * capabilities a user might reasonably want piecemeal.
471
+ */
472
+ type TGranularity = 'bundle' | 'extension';
473
+ /** Raw `plugin.json` shape after successful AJV validation. */
474
+ interface IPluginManifest {
475
+ id: string;
476
+ version: string;
477
+ specCompat: string;
478
+ extensions: string[];
479
+ description?: string;
480
+ storage?: TPluginStorage;
481
+ /**
482
+ * Toggle granularity for this plugin. Default `'bundle'`. See
483
+ * `TGranularity` for the trade-off; in practice 95% of plugins want
484
+ * the default.
485
+ */
486
+ granularity?: TGranularity;
487
+ author?: string;
488
+ license?: string;
489
+ homepage?: string;
490
+ repository?: string;
491
+ }
492
+ /**
493
+ * Failure mode produced by the loader when a plugin cannot be loaded.
494
+ * Matches the three states named in spec §Plugin discovery / load.
495
+ *
496
+ * - `incompatible-spec`: manifest parsed fine but `semver.satisfies` failed
497
+ * against the installed `@skill-map/spec` version.
498
+ * - `invalid-manifest`: `plugin.json` missing, unparseable, or failing AJV.
499
+ * - `load-error`: manifest passed but an extension module failed to import
500
+ * or the imported manifest failed its extension-kind schema.
501
+ */
502
+ /**
503
+ * Possible outcomes after the loader sees a plugin.json. Mirrors the
504
+ * `status` enum in `spec/schemas/plugins-registry.schema.json`.
505
+ *
506
+ * - `enabled` — manifest valid, specCompat satisfied, every
507
+ * extension imported and validated.
508
+ * - `disabled` — user-toggled off via `sm plugins disable` or
509
+ * `settings.json#/plugins/<id>/enabled`. Manifest
510
+ * is parsed and surfaced (so `sm plugins list`
511
+ * shows it), but extensions are not imported.
512
+ * - `incompatible-spec` — manifest parsed but `semver.satisfies` failed.
513
+ * - `invalid-manifest` — `plugin.json` missing, unparseable, AJV-fails,
514
+ * OR the directory name does not equal the
515
+ * manifest id (a cheap structural rule that
516
+ * rules out same-root collisions by construction:
517
+ * a filesystem cannot contain two siblings with
518
+ * the same name).
519
+ * - `load-error` — manifest passed, an extension module failed.
520
+ * - `id-collision` — two plugins reachable from different roots
521
+ * (project + global, or any `--plugin-dir`
522
+ * combination) declared the same `id`. Both
523
+ * collided plugins receive this status; no
524
+ * precedence rule applies. The user resolves
525
+ * by renaming one of them and rerunning.
526
+ */
527
+ type TPluginLoadStatus = 'enabled' | 'disabled' | 'incompatible-spec' | 'invalid-manifest' | 'load-error' | 'id-collision';
528
+ interface ILoadedExtension {
529
+ kind: ExtensionKind;
530
+ id: string;
531
+ /**
532
+ * Owning plugin namespace — `manifest.id` of the `plugin.json` that
533
+ * declared this extension. Composed with `id` to form the qualified
534
+ * registry key `<pluginId>/<id>`. Per spec § A.6 the loader injects
535
+ * this from the manifest; an extension that hand-declares a
536
+ * mismatching `pluginId` is rejected as `invalid-manifest`.
537
+ */
538
+ pluginId: string;
539
+ version: string;
540
+ entryPath: string;
541
+ /** Raw module namespace as returned by the dynamic `import()`. */
542
+ module: unknown;
543
+ /**
544
+ * Runtime extension instance ready for the registry / orchestrator —
545
+ * the `default` export of `module` (or the module itself when no
546
+ * default), shallow-cloned with `pluginId` injected per spec § A.6.
547
+ *
548
+ * The clone is essential: ESM caches the imported module, so two
549
+ * plugins importing the same file would otherwise share a single
550
+ * mutable instance and overwrite each other's `pluginId`. The loader
551
+ * owns the clone so consumers (CLI, tests) never need to mutate.
552
+ */
553
+ instance: unknown;
554
+ }
555
+ interface IDiscoveredPlugin {
556
+ /** Absolute path to the plugin directory. */
557
+ path: string;
558
+ /** Plugin id — populated from the manifest if it parsed, else a path hint. */
559
+ id: string;
560
+ status: TPluginLoadStatus;
561
+ /** Only present when status === 'enabled' or 'incompatible-spec'. */
562
+ manifest?: IPluginManifest;
563
+ /** Only present when status === 'enabled'. */
564
+ extensions?: ILoadedExtension[];
565
+ /**
566
+ * Resolved granularity for this plugin. Always populated from
567
+ * `manifest.granularity` (default `'bundle'`) when the manifest parsed;
568
+ * absent for `invalid-manifest` paths where the manifest never validated.
569
+ */
570
+ granularity?: TGranularity;
571
+ /**
572
+ * Runtime-only — never persisted, never spec-modeled.
573
+ *
574
+ * Spec § A.12 — opt-in JSON Schema validation for plugin custom storage.
575
+ * Populated by the loader when `manifest.storage.schemas` (Mode B) or
576
+ * `manifest.storage.schema` (Mode A) declares schema paths the loader
577
+ * successfully read and AJV-compiled. Consumed by the runtime store
578
+ * wrapper to validate `ctx.store.write(table, row)` (Mode B) and
579
+ * `ctx.store.set(key, value)` (Mode A) before persisting.
580
+ *
581
+ * Mode B layout — keyed by logical table name (without the
582
+ * `plugin_<normalizedId>_` prefix), matching the manifest's `schemas`
583
+ * map. Tables not present in the map accept any shape (permissive).
584
+ *
585
+ * Mode A layout — uses the sentinel key `__kv__` for the single
586
+ * value-shape schema. The sentinel survives the runtime contract change
587
+ * if Mode A ever grows multiple namespaces.
588
+ *
589
+ * Absent (`undefined`) when no schemas were declared OR when the load
590
+ * surfaced a `load-error` (the discovered plugin keeps its failure
591
+ * status; consumers must check `status === 'enabled'`).
592
+ */
593
+ storageSchemas?: Record<string, IPluginStorageSchema>;
594
+ /** Human-readable diagnostic shown by `sm plugins list/show`. */
595
+ reason?: string;
596
+ }
597
+ /**
598
+ * Runtime-only — a single AJV-compiled storage schema attached to a
599
+ * loaded plugin. The schema path (relative to the plugin directory) is
600
+ * preserved so error messages can name the offending file. `validate`
601
+ * is the AJV `ValidateFunction` itself: it returns `true` on shape
602
+ * match, otherwise `false` with `validate.errors` populated. Typed
603
+ * loosely here (no `ajv/dist/2020.js` import) to keep the shared type
604
+ * module free of Ajv at compile time; the runtime adapter narrows.
605
+ */
606
+ interface IPluginStorageSchema {
607
+ /** Plugin-relative path to the schema file (`storage.schemas[<table>]` or `storage.schema`). */
608
+ schemaPath: string;
609
+ /** AJV-compiled validator. `errors` is populated after a failed call. */
610
+ validate: ((row: unknown) => boolean) & {
611
+ errors?: {
612
+ instancePath: string;
613
+ message?: string;
614
+ keyword: string;
615
+ }[] | null;
616
+ };
617
+ }
618
+
619
+ /**
620
+ * Plugin store wrappers — runtime injection for `ctx.store` per spec
621
+ * § A.12 (opt-in `outputSchema` for plugin custom storage).
622
+ *
623
+ * Two shapes, mirroring the manifest's storage modes documented in
624
+ * `spec/plugin-kv-api.md`:
625
+ *
626
+ * - Mode A — `KvStore.set(key, value)`. AJV-validates `value` against
627
+ * the schema declared by `manifest.storage.schema` (single
628
+ * value-shape) when present. Absent = permissive.
629
+ * - Mode B — `DedicatedStore.write(table, row)`. AJV-validates `row`
630
+ * against the per-table schema declared in `manifest.storage.schemas`
631
+ * when present. Tables absent from the map accept any shape.
632
+ *
633
+ * Both wrappers are storage-engine agnostic — they accept a `persist`
634
+ * callback the caller supplies. The persistence side (SQLite, in-memory,
635
+ * mock) is the caller's concern; this wrapper's only job is the
636
+ * AJV gate. That separation lets the test suite exercise the validator
637
+ * without spinning up a real DB and lets the kernel adapter (future
638
+ * `state_plugin_kvs` writer / dedicated-table writer) plug in
639
+ * unchanged.
640
+ *
641
+ * Universal validation (`emitLink` against `link.schema.json`,
642
+ * `enrichNode` against `node.schema.json`) is unaffected — it lives on
643
+ * the orchestrator side and runs regardless of the plugin's
644
+ * `outputSchema` opt-in.
645
+ */
646
+
647
+ /**
648
+ * Sentinel key under which Mode A stores its single value-shape schema
649
+ * inside `IDiscoveredPlugin.storageSchemas`. The sentinel keeps the
650
+ * shared `Record<string, IPluginStorageSchema>` map a single-typed
651
+ * surface across both modes; consumers look up by sentinel for KV and
652
+ * by table name for dedicated.
653
+ */
654
+ declare const KV_SCHEMA_KEY = "__kv__";
655
+ interface IKvStorePersist {
656
+ (key: string, value: unknown): void | Promise<void>;
657
+ }
658
+ interface IDedicatedStorePersist {
659
+ (table: string, row: unknown): void | Promise<void>;
660
+ }
661
+ /**
662
+ * Mode A wrapper. `set(key, value)` AJV-validates `value` against the
663
+ * Mode A schema (sentinel key `__kv__`) when declared, then forwards
664
+ * to `persist`. Validation failure throws with a message naming the
665
+ * schema path and AJV errors; persistence is skipped on failure.
666
+ *
667
+ * `pluginId` is captured for diagnostics (the throw message names the
668
+ * plugin). The wrapper does NOT itself scope by plugin id — that is
669
+ * the persistence layer's job (the spec's `state_plugin_kvs` PK includes
670
+ * `pluginId` and the kernel-side adapter prepends it before write).
671
+ */
672
+ interface IKvStoreWrapper {
673
+ set(key: string, value: unknown): Promise<void>;
674
+ }
675
+ /**
676
+ * Union shape exposed to extractors via `ctx.store`. Spec § A.12 — Mode A
677
+ * (`kv`) returns a `set(key, value)` surface; Mode B (`dedicated`) returns
678
+ * `write(table, row)`. Plugin authors narrow at the call site based on
679
+ * the storage mode declared in their `plugin.json`.
680
+ */
681
+ type IPluginStore = IKvStoreWrapper | IDedicatedStoreWrapper;
682
+ declare function makeKvStoreWrapper(opts: {
683
+ pluginId: string;
684
+ schema: IPluginStorageSchema | undefined;
685
+ persist: IKvStorePersist;
686
+ }): IKvStoreWrapper;
687
+ /**
688
+ * Mode B wrapper. `write(table, row)` AJV-validates `row` against
689
+ * `storageSchemas[table]` when declared, then forwards to `persist`.
690
+ * Tables absent from the map are permissive — the wrapper forwards
691
+ * straight to `persist` without validation.
692
+ *
693
+ * The wrapper accepts the full `storageSchemas` map (rather than a
694
+ * single schema) so a plugin author can declare schemas for some
695
+ * tables and leave others permissive in the same map without the
696
+ * caller having to lookup-then-narrow.
697
+ */
698
+ interface IDedicatedStoreWrapper {
699
+ write(table: string, row: unknown): Promise<void>;
700
+ }
701
+ declare function makeDedicatedStoreWrapper(opts: {
702
+ pluginId: string;
703
+ schemas: Record<string, IPluginStorageSchema> | undefined;
704
+ persist: IDedicatedStorePersist;
705
+ }): IDedicatedStoreWrapper;
706
+ /**
707
+ * Convenience entry point: build whichever wrapper matches the
708
+ * discovered plugin's storage mode. Returns `undefined` when the
709
+ * plugin declared no storage at all (the orchestrator omits
710
+ * `ctx.store` in that case, per the existing contract). Mode A
711
+ * extracts the sentinel-keyed schema; Mode B forwards the full map.
712
+ */
713
+ declare function makePluginStore(opts: {
714
+ plugin: IDiscoveredPlugin;
715
+ persistKv?: IKvStorePersist;
716
+ persistDedicated?: IDedicatedStorePersist;
717
+ }): IPluginStore | undefined;
718
+
719
+ /**
720
+ * Base manifest shape shared by every extension kind. Mirrors
721
+ * `spec/schemas/extensions/base.schema.json` at the TypeScript level.
722
+ *
723
+ * Spec § A.6 — every extension is identified in the registry by the
724
+ * qualified id `<pluginId>/<id>`. The `pluginId` field is required at the
725
+ * runtime / TS level: built-ins declare it directly in
726
+ * `src/extensions/built-ins.ts`; user plugins have it injected by the
727
+ * `PluginLoader` from `plugin.json#/id` before the extension reaches the
728
+ * registry. A plugin author who hand-codes a `pluginId` that disagrees
729
+ * with the manifest's `id` is rejected as `invalid-manifest`.
730
+ *
731
+ * The JSON Schema deliberately does NOT model `pluginId` — the qualifier
732
+ * is a runtime concern composed by the loader, not a manifest field
733
+ * authors are expected to set. Stripping it before AJV validation in
734
+ * the loader keeps the spec contract clean ("authors declare only the
735
+ * short id").
736
+ */
737
+
738
+ interface IExtensionBase {
739
+ id: string;
740
+ /**
741
+ * Owning plugin namespace. Composed with `id` to produce the
742
+ * qualified registry key `<pluginId>/<id>`. Built-ins declare this
743
+ * directly; user plugins have it injected by the `PluginLoader`
744
+ * from `plugin.json#/id`.
745
+ */
746
+ pluginId: string;
747
+ version: string;
748
+ description?: string;
749
+ stability?: Stability;
750
+ preconditions?: string[];
751
+ entry?: string;
752
+ }
753
+
754
+ /**
755
+ * Provider runtime contract. Walks filesystem roots and emits raw node
756
+ * records; classification maps path conventions to a node kind.
757
+ *
758
+ * Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`,
759
+ * `StoragePort.adapter`, etc.). A `Provider` is an extension kind authored
760
+ * by plugins to declare a platform's universe (the catalog of kinds it
761
+ * emits, the per-kind frontmatter schema, the filesystem directory it
762
+ * owns); a hexagonal adapter is an internal implementation of a port.
763
+ * Both can coexist without confusion because they live in different
764
+ * namespaces.
765
+ *
766
+ * `walk()` is an async iterator so large scopes don't buffer in memory.
767
+ * Each yielded `IRawNode` carries the full parsed frontmatter + body plus
768
+ * the path relative to the scan root; the kernel computes hashes, bytes,
769
+ * and tokens on top.
770
+ *
771
+ * **Spec 0.8.0**. Per-kind frontmatter schemas relocated from the spec
772
+ * to the Provider that owns them. The flat
773
+ * `defaultRefreshAction` map collapsed into the new `kinds` map: every
774
+ * kind the Provider emits gets one entry that declares both its schema
775
+ * and its refresh action. Spec keeps only `frontmatter/base.schema.json`
776
+ * (universal); per-kind schemas live with the Provider.
777
+ */
778
+
779
+ interface IRawNode {
780
+ /** Path relative to the scan root that produced this node. */
781
+ path: string;
782
+ /** Raw markdown body (everything after the frontmatter fence). */
783
+ body: string;
784
+ /** Raw frontmatter text (between `---` fences). Empty string when absent. */
785
+ frontmatterRaw: string;
786
+ /** Parsed frontmatter, or `{}` when absent / unparseable. */
787
+ frontmatter: Record<string, unknown>;
788
+ }
789
+ /**
790
+ * One entry in a Provider's `kinds` map. Declares both the per-kind
791
+ * frontmatter schema (path relative to the Provider's package dir, plus
792
+ * the loaded JSON object the kernel passes to AJV) and the qualified
793
+ * default refresh action id the UI dispatches for nodes of this kind.
794
+ *
795
+ * The split between `schema` (manifest-level path) and `schemaJson`
796
+ * (runtime-loaded JSON) keeps the manifest shape spec-conformant while
797
+ * letting the runtime instance carry the parsed schema without a second
798
+ * filesystem read at scan time. Built-in Providers populate `schemaJson`
799
+ * via `import schema from './schemas/skill.schema.json' with { type: 'json' }`;
800
+ * user-plugin Providers loaded by `PluginLoader` will have it filled in
801
+ * by the loader after manifest validation.
802
+ */
803
+ interface IProviderKind {
804
+ /**
805
+ * Path to the kind's frontmatter JSON Schema, relative to the
806
+ * Provider's package directory. Mirrors the spec field of the same
807
+ * name in `extensions/provider.schema.json#/properties/kinds/.../schema`.
808
+ */
809
+ schema: string;
810
+ /**
811
+ * Loaded JSON Schema document for the kind. The kernel registers this
812
+ * with AJV at scan boot and validates each node's frontmatter against
813
+ * it. The schema MUST extend the spec's
814
+ * `frontmatter/base.schema.json` via `allOf` + `$ref` to base's
815
+ * `$id`; the loader registers base into the same AJV instance so
816
+ * cross-package `$ref`-by-`$id` resolves transparently.
817
+ *
818
+ * `unknown` rather than a stronger type because AJV consumes any JSON
819
+ * Schema object; tightening to a concrete shape would require mirroring
820
+ * the JSON Schema vocabulary in TypeScript.
821
+ */
822
+ schemaJson: unknown;
823
+ /**
824
+ * Qualified action id (`<plugin-id>/<action-id>`) the probabilistic-
825
+ * refresh UI dispatches for nodes of this kind. The kernel resolves
826
+ * the id against its qualified action registry; a dangling reference
827
+ * disables the Provider with status `invalid-manifest`.
828
+ */
829
+ defaultRefreshAction: string;
830
+ }
831
+ interface IProvider extends IExtensionBase {
832
+ kind: 'provider';
833
+ /**
834
+ * Filesystem directory (relative to user home or project root) where this
835
+ * Provider's content lives. Required. Examples: `'~/.claude'` for the
836
+ * Claude Provider, `'~/.cursor'` for a hypothetical Cursor Provider.
837
+ * The kernel walks this directory during boot/scan to discover nodes;
838
+ * `sm doctor` validates the directory exists and emits a non-blocking
839
+ * warning when it does not.
840
+ */
841
+ explorationDir: string;
842
+ /**
843
+ * Catalog of node kinds this Provider emits. Keyed by kind name. Every
844
+ * kind the Provider can `classify()` MUST have an entry; an entry is
845
+ * the union of the kind's frontmatter schema and its default refresh
846
+ * action.
847
+ *
848
+ * The string keys are typed loosely (`string`) rather than `NodeKind`
849
+ * because the value space is open by design: a future Cursor Provider
850
+ * could declare `rule`, an Obsidian Provider could declare `daily`.
851
+ * The kernel's hard-coded `NodeKind` union represents the kinds the
852
+ * built-in Claude Provider emits; it is NOT the kernel-wide kind type
853
+ * (see `kernel/types.ts:NodeKind` docstring). `Node.kind`, the AJV
854
+ * `node.schema.json` validator, and the SQLite `scan_nodes.kind`
855
+ * column all accept any non-empty string an enabled Provider returns.
856
+ */
857
+ kinds: Record<string, IProviderKind>;
858
+ /**
859
+ * Walk the given roots and yield every node the Provider recognises.
860
+ * Non-matching files are silently skipped. Unreadable files produce
861
+ * a diagnostic via the emitter but do not abort the walk.
862
+ *
863
+ * `options.ignoreFilter` — when supplied, the Provider MUST
864
+ * skip every directory and file whose path-relative-to-root the
865
+ * filter reports as ignored. Providers MAY also keep their own
866
+ * hard-coded skip list (e.g. `.git`) as a defensive measure, but the
867
+ * filter is the canonical source of user intent.
868
+ */
869
+ walk(roots: string[], options?: {
870
+ ignoreFilter?: IIgnoreFilter;
871
+ }): AsyncIterable<IRawNode>;
872
+ /**
873
+ * Given a path and its parsed frontmatter, decide the node kind. The
874
+ * classifier is called after walk() yields — Providers MAY embed the
875
+ * logic inside walk itself, but exposing it lets the kernel rebuild
876
+ * classification during partial scans without re-walking.
877
+ *
878
+ * Returns an open `string`. The returned value MUST be a key of the
879
+ * Provider's own `kinds` catalog; the orchestrator does not validate
880
+ * the kind against `NodeKind`. External Providers (Cursor, Obsidian,
881
+ * …) freely return their own kinds (e.g. `'cursorRule'`, `'daily'`).
882
+ */
883
+ classify(path: string, frontmatter: Record<string, unknown>): string;
884
+ }
885
+
886
+ /**
887
+ * Extractor runtime contract. Consumes a single node (frontmatter + body)
888
+ * and emits its output through three context-supplied callbacks rather than
889
+ * a return value. Extractors run in isolation: they MUST NOT read other
890
+ * nodes, the graph, or the DB. Cross-node reasoning lives in rules.
891
+ *
892
+ * Output channels (all on the context):
893
+ *
894
+ * - `ctx.emitLink(link)` — persist a link in the kernel's `links` table.
895
+ * Validated against `emitsLinkKinds` before insertion; an off-contract
896
+ * kind drops the link and surfaces an `extension.error` event.
897
+ * - `ctx.enrichNode(partial)` — merge canonical, kernel-curated properties
898
+ * onto the node. Strictly separate from the author-supplied frontmatter
899
+ * (the latter remains immutable and survives verbatim). Persistence and
900
+ * stale-tracking are spec'd in § A.8.
901
+ * - `ctx.store` — plugin-scoped persistence. Present only when the
902
+ * plugin declares `storage.mode` in `plugin.json`; shape depends on the
903
+ * mode (`KvStore` for mode A, scoped `Database` for mode B). See
904
+ * `plugin-kv-api.md` for the contract.
905
+ * - `ctx.runner` — `RunnerPort` injection for `probabilistic` extractors.
906
+ * `undefined` for the default `deterministic` mode.
907
+ *
908
+ * The manifest's `scope` field tells the orchestrator which parts to feed:
909
+ * `frontmatter` extractors receive an empty string for body and vice versa.
910
+ *
911
+ * Renamed from `Detector` in spec 0.8.x. The previous `detect(ctx) → Link[]`
912
+ * signature is gone; everything now flows through `extract(ctx) → void`
913
+ * and the callbacks above.
914
+ */
915
+
916
+ /**
917
+ * Output callbacks supplied by the kernel on the extractor context.
918
+ * Split out so plugin authors can name the callback shape if they
919
+ * want to mock it in unit tests without depending on the wider
920
+ * `IExtractorContext`.
921
+ */
922
+ interface IExtractorCallbacks {
923
+ /**
924
+ * Emit a single Link. The orchestrator validates the link against the
925
+ * extractor's declared `emitsLinkKinds` before inserting it; off-contract
926
+ * links are silently dropped with an `extension.error` event.
927
+ */
928
+ emitLink(link: Link): void;
929
+ /**
930
+ * Merge canonical, kernel-curated properties onto the current node's
931
+ * enrichment layer. The author-supplied frontmatter stays untouched
932
+ * (Decision #109 in `ROADMAP.md`). Persistence and stale-tracking
933
+ * semantics live in spec § A.8; the orchestrator already buffers the
934
+ * partials and `persistScanResult` upserts them.
935
+ */
936
+ enrichNode(partial: Partial<Node>): void;
937
+ }
938
+ interface IExtractorContext extends IExtractorCallbacks {
939
+ node: Node;
940
+ body: string;
941
+ frontmatter: Record<string, unknown>;
942
+ /**
943
+ * Plugin-scoped persistence. Optional because not every plugin declares
944
+ * a `storage.mode` in `plugin.json`. Shape: `KvStoreWrapper` for mode A
945
+ * (`set(key, value)`), `DedicatedStoreWrapper` for mode B
946
+ * (`write(table, row)`). See `spec/plugin-kv-api.md`.
947
+ *
948
+ * Typed as `unknown` so this contract module stays free of any
949
+ * adapter-side imports — the concrete `IPluginStore` lives in
950
+ * `kernel/adapters/plugin-store.js`. Plugin authors narrow at the
951
+ * call site based on the storage mode declared in their manifest.
952
+ * The orchestrator looks up the wrapper per-extractor in
953
+ * `RunScanOptions.pluginStores` (keyed by `pluginId`) and attaches
954
+ * it here.
955
+ */
956
+ store?: unknown;
957
+ /**
958
+ * `RunnerPort` injection for `probabilistic` extractors. `undefined`
959
+ * for `deterministic` mode (the default). The kernel rejects
960
+ * probabilistic extractors that try to register scan-time hooks at
961
+ * load time.
962
+ */
963
+ runner?: unknown;
964
+ }
965
+ interface IExtractor extends IExtensionBase {
966
+ kind: 'extractor';
967
+ /**
968
+ * Execution mode. Optional in the manifest with a default of
969
+ * `deterministic` per `spec/schemas/extensions/extractor.schema.json`.
970
+ * `probabilistic` extractors invoke an LLM through the kernel's
971
+ * `RunnerPort` and never participate in scan-time pipelines —
972
+ * they dispatch only as queued jobs.
973
+ */
974
+ mode?: TExecutionMode;
975
+ emitsLinkKinds: LinkKind[];
976
+ defaultConfidence: Confidence;
977
+ scope: 'frontmatter' | 'body' | 'both';
978
+ /**
979
+ * Optional opt-in filter on `node.kind`. When declared, the orchestrator
980
+ * skips invocation of `extract()` for any node whose `kind` is NOT in
981
+ * this list — fail-fast, before context construction, so a
982
+ * probabilistic extractor wastes zero LLM cost on inapplicable nodes
983
+ * and a deterministic extractor wastes zero CPU.
984
+ *
985
+ * Absent (`undefined`) is the default: the extractor applies to every
986
+ * kind. There are no wildcards — the absence of the field already
987
+ * encodes "every kind". An empty array (`[]`) is rejected at load
988
+ * time by AJV (`minItems: 1` in the schema).
989
+ *
990
+ * Unknown kinds (no installed Provider declares them) do NOT block
991
+ * the load: the extractor keeps `loaded` status and `sm plugins doctor`
992
+ * surfaces a warning. The Provider that declares the kind may arrive
993
+ * later (e.g. a user installs the corresponding plugin).
994
+ *
995
+ * Spec: `spec/schemas/extensions/extractor.schema.json#/properties/applicableKinds`.
996
+ */
997
+ applicableKinds?: string[];
998
+ /**
999
+ * Extractor entry point. Returns nothing; output flows through
1000
+ * `ctx.emitLink`, `ctx.enrichNode`, and `ctx.store`.
1001
+ */
1002
+ extract(ctx: IExtractorContext): void | Promise<void>;
1003
+ }
1004
+
1005
+ /**
1006
+ * Rule runtime contract. Runs against the whole graph after every Provider
1007
+ * and extractor has completed; emits issues. Deterministic rules are pure
1008
+ * (same graph in → same issues out) and run synchronously inside `sm scan`
1009
+ * / `sm check`. Probabilistic rules invoke an LLM through the kernel's
1010
+ * `RunnerPort` and dispatch only as queued jobs — they never participate
1011
+ * in scan-time pipelines. Mode is declared in the manifest (default
1012
+ * `deterministic`).
1013
+ */
1014
+
1015
+ interface IRuleContext {
1016
+ nodes: Node[];
1017
+ links: Link[];
1018
+ }
1019
+ interface IRule extends IExtensionBase {
1020
+ kind: 'rule';
1021
+ /**
1022
+ * Execution mode. Optional in the manifest with a default of
1023
+ * `deterministic` per `spec/schemas/extensions/rule.schema.json`.
1024
+ */
1025
+ mode?: TExecutionMode;
1026
+ evaluate(ctx: IRuleContext): Issue[] | Promise<Issue[]>;
1027
+ }
1028
+
1029
+ /**
1030
+ * Action runtime contract. The fourth plugin kind (spec § A.4 +
1031
+ * `spec/schemas/extensions/action.schema.json`).
1032
+ *
1033
+ * Actions operate on one or more nodes in one of two execution modes:
1034
+ *
1035
+ * - `deterministic` — code runs in-process; the action computes the
1036
+ * report synchronously and returns it. No job file, no runner.
1037
+ * - `probabilistic` — the kernel renders a prompt + preamble into a
1038
+ * job file; a runner executes it via `RunnerPort` against an LLM;
1039
+ * `sm record` closes the job and validates the report against
1040
+ * `reportSchemaRef`.
1041
+ *
1042
+ * **Deferred runtime invocation.** The dispatcher (`Action.run(ctx)` for
1043
+ * deterministic; the `RunnerPort` + `sm record` round-trip for
1044
+ * probabilistic) lands with the job subsystem (Decision #114 in
1045
+ * `ROADMAP.md`). Today the loader still validates `kind: 'action'`
1046
+ * manifests against `extension-action.schema.json` and the registry
1047
+ * holds them — `sm actions show` and the precondition gating UI consume
1048
+ * the manifest data. The runtime entry point is intentionally absent
1049
+ * from `IAction` so plugin authors don't ship a method the kernel will
1050
+ * not call until the job subsystem is in place; when it ships, the
1051
+ * method shape will land here without breaking the manifest contract.
1052
+ *
1053
+ * Mirrors `extensions/action.schema.json`:
1054
+ *
1055
+ * - `mode` (required) — discriminator between the two modes.
1056
+ * - `reportSchemaRef` (required) — JSON Schema reference the report
1057
+ * MUST validate against. MUST extend `report-base.schema.json`.
1058
+ * - `promptTemplateRef` — REQUIRED when `mode: 'probabilistic'`,
1059
+ * FORBIDDEN when `mode: 'deterministic'`. The schema's conditional
1060
+ * `allOf` enforces both directions; the runtime contract simply
1061
+ * surfaces the field as optional and lets the loader catch shape
1062
+ * violations at AJV time.
1063
+ * - `expectedDurationSeconds` — REQUIRED for probabilistic (drives
1064
+ * TTL); advisory for deterministic.
1065
+ * - `precondition` — declarative filter consumed by `--all` fan-out,
1066
+ * UI button gating, `sm actions show`.
1067
+ * - `expectedTools` — hint to Skill / CLI runners about expected
1068
+ * tools (no normative enforcement in v0).
1069
+ * - `fanOutPolicy` — `'per-node'` (default) vs `'batch'`.
1070
+ */
1071
+
1072
+ /**
1073
+ * Declarative filter applied by `--all` fan-out, UI button gating, and
1074
+ * `sm actions show`. All fields optional — an empty precondition matches
1075
+ * every node.
1076
+ */
1077
+ interface IActionPrecondition {
1078
+ /**
1079
+ * Node kinds this action accepts. Open-by-design (matches
1080
+ * `node.schema.json#/properties/kind`): an action declared with
1081
+ * `kind: ['cursorRule']` is valid as long as some Provider classifies
1082
+ * into `cursorRule`. Omitted → any kind.
1083
+ */
1084
+ kind?: string[];
1085
+ /** Provider ids whose nodes this action accepts. Omitted → any Provider. */
1086
+ provider?: string[];
1087
+ /** Node stability filter. */
1088
+ stability?: Array<'experimental' | 'stable' | 'deprecated'>;
1089
+ /**
1090
+ * Free-form precondition strings the kernel forwards to the action for
1091
+ * runtime evaluation (example: `frontmatter.metadata.source != null`).
1092
+ */
1093
+ custom?: string[];
1094
+ }
1095
+ interface IAction extends IExtensionBase {
1096
+ kind: 'action';
1097
+ /**
1098
+ * Execution mode discriminator. Required per
1099
+ * `extensions/action.schema.json`.
1100
+ */
1101
+ mode: TExecutionMode;
1102
+ /**
1103
+ * Reference to the JSON Schema the report MUST validate against. MUST
1104
+ * extend `report-base.schema.json` (directly or transitively).
1105
+ * Validation failure → job transitions to `failed` with reason
1106
+ * `report-invalid`.
1107
+ */
1108
+ reportSchemaRef: string;
1109
+ /**
1110
+ * Best-effort estimate of wall-clock duration in seconds. Drives TTL
1111
+ * (`ttl = max(expectedDurationSeconds × graceMultiplier,
1112
+ * minimumTtlSeconds)`). Required for `probabilistic`; advisory for
1113
+ * `deterministic`.
1114
+ */
1115
+ expectedDurationSeconds?: number;
1116
+ /**
1117
+ * Path (relative to the extension file) to the prompt template the
1118
+ * kernel renders at `sm job submit`. REQUIRED when `mode:
1119
+ * 'probabilistic'`; FORBIDDEN when `mode: 'deterministic'`. The
1120
+ * conditional shape is enforced by AJV at load time; the runtime
1121
+ * contract carries the field as optional so both modes share one
1122
+ * interface.
1123
+ */
1124
+ promptTemplateRef?: string;
1125
+ /**
1126
+ * Optional declarative filter; absent → applies to every node.
1127
+ */
1128
+ precondition?: IActionPrecondition;
1129
+ /**
1130
+ * Hint to Skill / CLI runners about what tools the rendered prompt
1131
+ * expects access to (`Bash`, `Read`, `WebSearch`, …). No normative
1132
+ * enforcement in v0.
1133
+ */
1134
+ expectedTools?: string[];
1135
+ /**
1136
+ * `'per-node'` (default): `sm job submit --all` produces one job per
1137
+ * matching node. `'batch'`: one job whose prompt template receives the
1138
+ * full list. Batch actions tend to hit context limits; use sparingly.
1139
+ */
1140
+ fanOutPolicy?: 'per-node' | 'batch';
1141
+ }
1142
+
1143
+ /**
1144
+ * Formatter runtime contract. Turns the (nodes, links, issues) graph into
1145
+ * a textual representation for `sm graph --format <name>`.
1146
+ *
1147
+ * Two adjacent names live on the same instance:
1148
+ *
1149
+ * - `formatId: string` — the manifest field consumed by the
1150
+ * `--format <name>` CLI flag. The kernel's lookup is
1151
+ * `formatters.find((f) => f.formatId === flag)`.
1152
+ * - `format(ctx) → string` — the runtime method. Receives the full
1153
+ * graph and returns the serialized output. Output MUST be
1154
+ * byte-deterministic for the same input (the snapshot-test suite
1155
+ * relies on this).
1156
+ *
1157
+ * The split (`formatId` vs `format`) is deliberate: it keeps the method
1158
+ * named after the kind (`Formatter.format()` reads naturally) while the
1159
+ * field carries the identifier the user types on the command line.
1160
+ */
1161
+
1162
+ interface IFormatterContext {
1163
+ nodes: Node[];
1164
+ links: Link[];
1165
+ issues: Issue[];
1166
+ }
1167
+ interface IFormatter extends IExtensionBase {
1168
+ kind: 'formatter';
1169
+ /** Format identifier consumed by `sm graph --format <name>`. */
1170
+ formatId: string;
1171
+ /** Serialize the graph into a string. Deterministic-only. */
1172
+ format(ctx: IFormatterContext): string;
1173
+ }
1174
+
1175
+ /**
1176
+ * Hook runtime contract. The sixth plugin kind (spec § A.11).
1177
+ *
1178
+ * Hooks subscribe declaratively to a curated set of kernel lifecycle
1179
+ * events and react to them. Reaction-only by design: a hook cannot
1180
+ * mutate the pipeline, block emission, or alter outputs. Use cases
1181
+ * are notification (Slack on `job.completed`), integration glue (CI
1182
+ * webhook on `job.failed`), and bookkeeping (per-extractor metrics).
1183
+ *
1184
+ * The hookable trigger set is INTENTIONALLY SMALL — eight events. The
1185
+ * full `ProgressEmitterPort` catalog (per-node `scan.progress`,
1186
+ * `model.delta`, `run.*`, internal job lifecycle) is deliberately not
1187
+ * hookable: too verbose for a reactive surface, internal to the runner,
1188
+ * or covered elsewhere. Declaring a trigger outside the curated set
1189
+ * yields `invalid-manifest` at load time.
1190
+ *
1191
+ * Dual-mode (declared in manifest):
1192
+ *
1193
+ * - `deterministic` (default): `on(ctx)` runs in-process during the
1194
+ * dispatch of the matching event, synchronously between the
1195
+ * event's emission and the next pipeline step. Errors are caught
1196
+ * by the dispatcher, logged via `extension.error`, and never
1197
+ * block the main flow.
1198
+ * - `probabilistic`: the hook is enqueued as a job. Until the job
1199
+ * subsystem ships, probabilistic hooks load but skip dispatch
1200
+ * with a stderr advisory (Decision #114 in `ROADMAP.md`).
1201
+ *
1202
+ * Curated trigger set (per spec § A.11):
1203
+ *
1204
+ * 1. `scan.started` — pre-scan setup (one per scan).
1205
+ * 2. `scan.completed` — post-scan reaction (one per scan).
1206
+ * 3. `extractor.completed` — aggregated per-Extractor outputs.
1207
+ * 4. `rule.completed` — aggregated per-Rule outputs.
1208
+ * 5. `action.completed` — Action executed on a node.
1209
+ * 6. `job.spawning` — pre-spawn of runner subprocess.
1210
+ * 7. `job.completed` — most common trigger.
1211
+ * 8. `job.failed` — alerts, retry triggers.
1212
+ */
1213
+
1214
+ /**
1215
+ * The eight hookable lifecycle events. Mirrors the `triggers[]` enum in
1216
+ * `spec/schemas/extensions/hook.schema.json`. Anything outside this set
1217
+ * is rejected at load time as `invalid-manifest`.
1218
+ */
1219
+ type THookTrigger = 'scan.started' | 'scan.completed' | 'extractor.completed' | 'rule.completed' | 'action.completed' | 'job.spawning' | 'job.completed' | 'job.failed';
1220
+ /**
1221
+ * Frozen list mirror of `THookTrigger` for runtime introspection. The
1222
+ * loader validates `manifest.triggers[]` against this set; the
1223
+ * orchestrator's dispatcher iterates it in order when fanning an event
1224
+ * out to subscribed hooks.
1225
+ */
1226
+ declare const HOOK_TRIGGERS: readonly THookTrigger[];
1227
+ /**
1228
+ * Context the dispatcher hands to `Hook.on()`. The shape is intentionally
1229
+ * narrow: a hook reacts to an event, it does not steer the pipeline.
1230
+ *
1231
+ * The `event` carries the raw `ProgressEvent` envelope (type, timestamp,
1232
+ * runId/jobId when applicable, data). Optional `node` / `extractorId`
1233
+ * / `ruleId` / `actionId` are extracted from the event payload by the
1234
+ * dispatcher when present so authors don't have to walk `event.data`.
1235
+ *
1236
+ * Probabilistic hooks additionally receive `runner` for LLM dispatch.
1237
+ * Deterministic hooks SHOULD ignore the field.
1238
+ */
1239
+ interface IHookContext {
1240
+ /** The raw event the dispatcher matched. */
1241
+ event: {
1242
+ type: THookTrigger;
1243
+ timestamp: string;
1244
+ runId?: string;
1245
+ jobId?: string;
1246
+ data?: unknown;
1247
+ };
1248
+ /**
1249
+ * Convenience extraction of the node payload when the event is
1250
+ * node-scoped (`action.completed`). Undefined for run-scoped or
1251
+ * scan-scoped events.
1252
+ */
1253
+ node?: Node;
1254
+ /**
1255
+ * Set on `extractor.completed` events. Qualified extension id of the
1256
+ * Extractor whose work the event aggregates.
1257
+ */
1258
+ extractorId?: string;
1259
+ /**
1260
+ * Set on `rule.completed` events. Qualified extension id of the Rule.
1261
+ */
1262
+ ruleId?: string;
1263
+ /**
1264
+ * Set on `action.completed` events. Qualified extension id of the
1265
+ * Action that just ran.
1266
+ */
1267
+ actionId?: string;
1268
+ /**
1269
+ * Set on `job.*` events once the job subsystem lands. Carries the
1270
+ * report payload for `job.completed`, the failure record for
1271
+ * `job.failed`, and the spawn metadata for `job.spawning`.
1272
+ */
1273
+ jobResult?: unknown;
1274
+ /**
1275
+ * `RunnerPort` injection for `probabilistic` hooks. `undefined` for
1276
+ * `deterministic` mode (the default). Probabilistic hooks land with
1277
+ * the job subsystem; the field is reserved here so the runtime
1278
+ * contract is forward-compatible without a major bump.
1279
+ */
1280
+ runner?: unknown;
1281
+ }
1282
+ /**
1283
+ * Optional declarative filter applied by the dispatcher BEFORE
1284
+ * invoking `on(ctx)`. Keys are payload field paths (top-level only in
1285
+ * v0.x); values are the literal expected match. The dispatcher walks
1286
+ * `event.data` for the field and short-circuits the invocation if the
1287
+ * value disagrees.
1288
+ *
1289
+ * Cross-field validation against declared `triggers` is best-effort
1290
+ * at load time: when none of the declared triggers carries a given
1291
+ * filter field, the loader surfaces `invalid-manifest`. The current
1292
+ * impl performs the basic enum check but defers full payload-shape
1293
+ * cross-validation to a follow-up — the dispatcher is permissive at
1294
+ * runtime (an unknown field never matches → the hook simply never
1295
+ * fires for that event, which is a correct interpretation of "filter
1296
+ * by a field that doesn't exist").
1297
+ */
1298
+ type THookFilter = Record<string, string | number | boolean>;
1299
+ interface IHook extends IExtensionBase {
1300
+ kind: 'hook';
1301
+ /**
1302
+ * Execution mode. Optional in the manifest with a default of
1303
+ * `deterministic` per `spec/schemas/extensions/hook.schema.json`.
1304
+ * Probabilistic hooks load but skip dispatch with a stderr advisory
1305
+ * until the job subsystem ships (Decision #114).
1306
+ */
1307
+ mode?: TExecutionMode;
1308
+ /**
1309
+ * Subset of the curated lifecycle trigger set this hook subscribes
1310
+ * to. MUST be non-empty; every entry MUST be a member of
1311
+ * `HOOK_TRIGGERS`. The loader validates both invariants and surfaces
1312
+ * `invalid-manifest` on violation.
1313
+ */
1314
+ triggers: THookTrigger[];
1315
+ /**
1316
+ * Optional declarative filter. Absent → invoke on every dispatched
1317
+ * event of every declared trigger.
1318
+ */
1319
+ filter?: THookFilter;
1320
+ /**
1321
+ * Hook entry point. Returns nothing; reactions are side effects.
1322
+ * Errors are caught by the dispatcher (logged as `extension.error`,
1323
+ * surfaced via `hook.failed` meta-event) and NEVER block the main
1324
+ * pipeline — a buggy hook degrades gracefully.
1325
+ */
1326
+ on(ctx: IHookContext): void | Promise<void>;
1327
+ }
1328
+
1329
+ /**
1330
+ * Scan orchestrator — runs the Provider → extractor → rule pipeline across
1331
+ * every registered extension and emits `ProgressEmitterPort` events in
1332
+ * canonical order. The callable extension set is injected via
1333
+ * `RunScanOptions.extensions` — the Registry holds manifest metadata, the
1334
+ * callable set holds the runtime instances the orchestrator actually
1335
+ * invokes. Separating the two lets `sm plugins` and `sm help` introspect
1336
+ * the graph without loading code.
1337
+ *
1338
+ * With zero registered extensions (or a callable set that carries none)
1339
+ * the pipeline still produces a valid zero-filled `ScanResult` — the
1340
+ * kernel-empty-boot invariant.
1341
+ *
1342
+ * Roots are validated up front: each entry of `RunScanOptions.roots`
1343
+ * must exist on disk as a directory. The first failure throws a clear
1344
+ * `Error` naming the offending path. This guards every caller (CLI,
1345
+ * server, skill-agent) against silently producing a zero-filled
1346
+ * `ScanResult` when a Provider walks a non-existent path — the bug
1347
+ * that wiped a populated DB via `sm scan -- --dry-run` (clipanion's
1348
+ * `--` made `--dry-run` a positional root that did not exist).
1349
+ *
1350
+ * Incremental scans: when `priorSnapshot` is supplied, the
1351
+ * orchestrator walks the filesystem, hashes each file, and reuses the
1352
+ * prior node + its prior-extracted internal links whenever both
1353
+ * `bodyHash` and `frontmatterHash` match. New / modified files run
1354
+ * through the full extractor pipeline (including the external-url-counter
1355
+ * which produces ephemeral pseudo-links). Rules ALWAYS run over the
1356
+ * fully merged graph — issue state can change even for an unchanged node
1357
+ * (e.g. a previously broken `references` link now resolves because a new
1358
+ * node was added). For unchanged nodes the prior `externalRefsCount` is
1359
+ * preserved as-is (the external pseudo-links were never persisted, so
1360
+ * they cannot be reconstructed; the count survived in the node row).
1361
+ *
1362
+ * Extractor output model (B.1, post-rename from Detector): extractors
1363
+ * return `void` and emit through three callbacks injected on the context:
1364
+ * - `ctx.emitLink(link)` → orchestrator validates against
1365
+ * `emitsLinkKinds` then partitions into internal / external buckets.
1366
+ * - `ctx.enrichNode(partial)` → orchestrator records ONE enrichment
1367
+ * entry per `(node, extractor)` so attribution survives into the DB.
1368
+ * Persisted into `node_enrichments` (A.8). The author-supplied
1369
+ * frontmatter on `node.frontmatter` stays immutable from any Extractor
1370
+ * — the enrichment layer is the only writable surface, and rules /
1371
+ * formatters consume it via `mergeNodeWithEnrichments`.
1372
+ * - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).
1373
+ * Wired by the driving adapter via `RunScanOptions.pluginStores`,
1374
+ * which the orchestrator looks up per-extractor by `pluginId` and
1375
+ * attaches to the context. The orchestrator never inspects what
1376
+ * plugins write through it; the wrapper handles AJV validation
1377
+ * when the manifest declared an output schema.
1378
+ */
1379
+
1380
+ interface IScanExtensions {
1381
+ providers: IProvider[];
1382
+ extractors: IExtractor[];
1383
+ rules: IRule[];
1384
+ /**
1385
+ * Optional hooks (spec § A.11). When supplied, the orchestrator's
1386
+ * lifecycle dispatcher invokes deterministic hooks subscribed to one
1387
+ * of the eight hookable triggers in canonical order with the matching
1388
+ * event payload. Absent → no hooks fire (the scan still emits its
1389
+ * lifecycle events to `ProgressEmitterPort` for observability).
1390
+ * Probabilistic hooks are loaded but skipped here with a stderr
1391
+ * advisory until the job subsystem ships once the job subsystem ships.
1392
+ */
1393
+ hooks?: IHook[];
1394
+ }
1395
+ /**
1396
+ * Confidence-tagged plan to repoint `state_*` references from one node
1397
+ * path to another. Emitted by the rename heuristic during `runScan` and
1398
+ * consumed by `persistScanResult` so the FK migration runs inside the
1399
+ * same transaction as the scan zone replace-all.
1400
+ */
1401
+ interface RenameOp {
1402
+ from: string;
1403
+ to: string;
1404
+ confidence: 'high' | 'medium';
1405
+ }
1406
+ interface RunScanOptions {
1407
+ /**
1408
+ * Filesystem roots to walk. Spec requires `minItems: 1`; passing an
1409
+ * empty array makes `runScan` throw before any work happens.
1410
+ */
1411
+ roots: string[];
1412
+ emitter?: ProgressEmitterPort;
1413
+ /** Runtime extension instances. Absent → empty pipeline. */
1414
+ extensions?: IScanExtensions;
1415
+ /**
1416
+ * Scan scope. Defaults to `'project'`. The CLI flag wiring lands in
1417
+ * the config layer wiring; `runScan` already accepts the override
1418
+ * so plugins / tests can opt into `'global'` today.
1419
+ */
1420
+ scope?: 'project' | 'global';
1421
+ /**
1422
+ * Compute per-node token counts (frontmatter / body / total) using the
1423
+ * cl100k_base BPE (the modern OpenAI tokenizer used by GPT-4 / GPT-3.5).
1424
+ * Defaults to true. Set false to skip tokenization; `node.tokens` is
1425
+ * left undefined (spec-valid: the field is optional).
1426
+ */
1427
+ tokenize?: boolean;
1428
+ /**
1429
+ * Prior snapshot for two purposes (decoupled by design):
1430
+ *
1431
+ * 1. **Rename heuristic** (`spec/db-schema.md` §Rename detection):
1432
+ * always evaluated when `priorSnapshot` is supplied. The
1433
+ * heuristic compares prior vs current node paths and emits
1434
+ * high / medium / ambiguous / orphan classifications. This
1435
+ * runs on EVERY `sm scan` (with or without `--changed`) so
1436
+ * reorganising files always preserves history, never silently.
1437
+ *
1438
+ * 2. **Cache reuse** (`sm scan --changed`): only kicks in when
1439
+ * `enableCache: true` is also passed. With the flag set, nodes
1440
+ * whose `path` exists in the prior with both `bodyHash` and
1441
+ * `frontmatterHash` matching the freshly-computed hashes are
1442
+ * reused as-is (their internal links and `externalRefsCount`
1443
+ * survive); only new / modified nodes run through extractors.
1444
+ * Rules always re-run over the merged graph.
1445
+ *
1446
+ * Pass `null` (or omit) for a fresh scan with no rename detection.
1447
+ */
1448
+ priorSnapshot?: ScanResult | null;
1449
+ /**
1450
+ * Reuse unchanged nodes from `priorSnapshot` instead of re-running
1451
+ * extractors over them. Defaults to `false` so a plain `sm scan`
1452
+ * always re-walks deterministically. `sm scan --changed` flips this
1453
+ * to `true` for the perf win on unchanged files.
1454
+ *
1455
+ * Has no effect without `priorSnapshot`; setting it to `true` with
1456
+ * a null prior is a no-op (every file is "new").
1457
+ */
1458
+ enableCache?: boolean;
1459
+ /**
1460
+ * Filter that decides which paths the Providers skip. Composed by the
1461
+ * caller (typically the CLI) from bundled defaults + `config.ignore`
1462
+ * + `.skill-mapignore`. Providers that omit this option fall back to
1463
+ * their own defensive defaults (just enough to keep `.git` /
1464
+ * `node_modules` out).
1465
+ */
1466
+ ignoreFilter?: IIgnoreFilter;
1467
+ /**
1468
+ * Promote frontmatter-validation findings from `warn` to `error`.
1469
+ * Defaults to false. The CLI surfaces this via `--strict` on `sm scan`
1470
+ * and the `scan.strict` config key. When false, the orchestrator
1471
+ * still emits a `frontmatter-invalid` issue per malformed file but
1472
+ * leaves the severity at `warn` so a clean scan exits 0; when true,
1473
+ * the same finding becomes `error` and the scan exits 1.
1474
+ */
1475
+ strict?: boolean;
1476
+ /**
1477
+ * Spec § A.9 — fine-grained Extractor cache breadcrumbs from the
1478
+ * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, bodyHashAtRun>>`.
1479
+ * Loaded from the `scan_extractor_runs` table by the CLI before
1480
+ * invoking `runScan`; absent / empty for a fresh DB or an out-of-band
1481
+ * caller that does not maintain a cache. Decoupled from `priorSnapshot`
1482
+ * because the runs live in a sibling table and are useful only when
1483
+ * `enableCache` is also set.
1484
+ *
1485
+ * Cache decision per `(node, extractor)`:
1486
+ * - body+frontmatter hashes match the prior node AND every currently-
1487
+ * registered extractor that applies to this kind has a matching
1488
+ * row → full skip, all prior outbound links reused.
1489
+ * - some applicable extractor lacks a matching row (newly registered,
1490
+ * or its prior run targeted a different body hash) → run only the
1491
+ * missing extractors, drop prior links whose `sources` map to any
1492
+ * missing extractor or to an extractor that is no longer registered.
1493
+ */
1494
+ priorExtractorRuns?: Map<string, Map<string, string>>;
1495
+ /**
1496
+ * Spec § A.12 — per-plugin storage wrappers exposed to extractors via
1497
+ * `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves
1498
+ * `ctx.store` undefined for that extractor (the existing contract).
1499
+ *
1500
+ * The kernel does not construct these — the driving adapter (CLI,
1501
+ * future server) builds them with `makePluginStore` from
1502
+ * `kernel/adapters/plugin-store.js` and threads them through. This
1503
+ * keeps the orchestrator persistence-agnostic (the wrapper supplies
1504
+ * its own persist callback) and lets tests inject a captured-call
1505
+ * mock without spinning up a DB.
1506
+ */
1507
+ pluginStores?: ReadonlyMap<string, IPluginStore>;
1508
+ }
1509
+ /**
1510
+ * Spec § A.9 — runs to persist into `scan_extractor_runs`. One entry
1511
+ * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided
1512
+ * "this extractor is current for this body". Includes both freshly-run
1513
+ * pairs (extractor invoked this scan) and reused pairs (cached node, the
1514
+ * extractor's prior run still applies to the same body hash). Excludes
1515
+ * obsolete pairs — extractors that ran in the prior but are no longer
1516
+ * registered — so a replace-all persist drops them automatically.
1517
+ */
1518
+ interface IExtractorRunRecord {
1519
+ nodePath: string;
1520
+ extractorId: string;
1521
+ bodyHashAtRun: string;
1522
+ ranAt: number;
1523
+ }
1524
+ /**
1525
+ * Spec § A.8 — universal enrichment layer.
1526
+ *
1527
+ * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor
1528
+ * produced via `ctx.enrichNode(...)` during the walk. Attribution is
1529
+ * preserved per-Extractor (rather than merged client-side as B.1 did)
1530
+ * so the persistence layer can:
1531
+ *
1532
+ * - upsert a single row per pair (stable PRIMARY KEY conflict on
1533
+ * re-extract);
1534
+ * - flag probabilistic rows `stale = 1` when the body changes between
1535
+ * scans (preserving the prior LLM cost);
1536
+ * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials
1537
+ * for last-write-wins per field at read time.
1538
+ *
1539
+ * `value` is the cumulative merge across every `enrichNode` call that
1540
+ * Extractor made for this node within this scan — multiple
1541
+ * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation
1542
+ * fold into a single row, but two different Extractors hitting the
1543
+ * same node yield two distinct rows.
1544
+ *
1545
+ * `isProbabilistic` is denormalised so the persistence layer's stale
1546
+ * flag query stays a single-table read; recomputing from the live
1547
+ * registry would force every read-path to thread the runtime extension
1548
+ * set through.
1549
+ */
1550
+ interface IEnrichmentRecord {
1551
+ nodePath: string;
1552
+ extractorId: string;
1553
+ bodyHashAtEnrichment: string;
1554
+ value: Partial<Node>;
1555
+ enrichedAt: number;
1556
+ isProbabilistic: boolean;
1557
+ }
1558
+ /**
1559
+ * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`
1560
+ * — the high- and medium-confidence renames the persistence layer must
1561
+ * apply to `state_*` rows inside the same tx as the scan zone replace-
1562
+ * all (per `spec/db-schema.md` §Rename detection). Most callers want
1563
+ * `runScan` (which returns just `ScanResult`); the CLI's `sm scan`
1564
+ * uses this variant so it can hand the ops off to `persistScanResult`.
1565
+ *
1566
+ * Also returns `extractorRuns` — the Spec § A.9 fine-grained cache
1567
+ * breadcrumbs the CLI persists into `scan_extractor_runs` so the next
1568
+ * incremental scan can decide per-(node, extractor) whether re-running
1569
+ * is required.
1570
+ */
1571
+ declare function runScanWithRenames(_kernel: Kernel, options: RunScanOptions): Promise<{
1572
+ result: ScanResult;
1573
+ renameOps: RenameOp[];
1574
+ extractorRuns: IExtractorRunRecord[];
1575
+ enrichments: IEnrichmentRecord[];
1576
+ }>;
1577
+ declare function runScan(_kernel: Kernel, options: RunScanOptions): Promise<ScanResult>;
1578
+ /**
1579
+ * Run a set of extractors against a single node, collecting their link
1580
+ * emissions and node-enrichment partials. Each extractor is invoked
1581
+ * exactly once with a fresh `IExtractorContext`. Caller decides what
1582
+ * to do with the returned arrays (push into per-scan buffers, write to
1583
+ * a focused refresh result, etc.).
1584
+ *
1585
+ * Exported so `cli/commands/refresh.ts` can reuse the same wiring it
1586
+ * needs for re-running a single extractor against a single node — the
1587
+ * pre-extraction code in `refresh.ts` was hand-duplicating this loop
1588
+ * (audit item V4).
1589
+ *
1590
+ * Within this call, multiple `enrichNode(partial)` calls from the same
1591
+ * extractor against the same node fold into one record (last-write-wins
1592
+ * per field) — same contract as the in-scan path.
1593
+ */
1594
+ declare function runExtractorsForNode(opts: {
1595
+ extractors: IExtractor[];
1596
+ node: Node;
1597
+ body: string;
1598
+ frontmatter: Record<string, unknown>;
1599
+ bodyHash: string;
1600
+ emitter: ProgressEmitterPort;
1601
+ /**
1602
+ * Spec § A.12 — per-plugin `ctx.store` wrappers keyed by `pluginId`.
1603
+ * The map's lookup is per-extractor inside the loop, so callers that
1604
+ * don't track plugin storage can omit it; the resulting `ctx.store`
1605
+ * stays `undefined` (the existing contract).
1606
+ */
1607
+ pluginStores?: ReadonlyMap<string, IPluginStore>;
1608
+ }): Promise<{
1609
+ internalLinks: Link[];
1610
+ externalLinks: Link[];
1611
+ enrichments: IEnrichmentRecord[];
1612
+ }>;
1613
+ /**
1614
+ * Pure rename / orphan classification per `spec/db-schema.md` §Rename
1615
+ * detection. Mutates `issues` in place — caller passes the in-progress
1616
+ * issue list; returns the `RenameOp[]` for the persistence layer to
1617
+ * apply inside its tx.
1618
+ *
1619
+ * Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused
1620
+ * by another):
1621
+ *
1622
+ * 1. **High-confidence**: pair each `deletedPath` with a `newPath`
1623
+ * that has the same `bodyHash`. No issue, no prompt.
1624
+ * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
1625
+ * each with the *unique* unclaimed `newPath` that shares its
1626
+ * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
1627
+ * with `data: { from, to, confidence: 'medium' }`.
1628
+ * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
1629
+ * remaining frontmatter-matching candidate, emit ONE
1630
+ * `auto-rename-ambiguous` issue per `newPath`, listing all
1631
+ * candidates in `data.candidates`. NO migration.
1632
+ * 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one
1633
+ * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.
1634
+ *
1635
+ * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc
1636
+ * order so the same input always produces the same matches —
1637
+ * required for reproducible tests and conformance fixtures (the spec
1638
+ * does not prescribe an order, but stability is the obvious contract).
1639
+ */
1640
+ declare function detectRenamesAndOrphans(prior: ScanResult, current: Node[], issues: Issue[]): RenameOp[];
1641
+ /**
1642
+ * Spec § A.8 — produce the merged read-time view of a Node.
1643
+ *
1644
+ * Rules / `sm check` / `sm export` consume `node.frontmatter` directly
1645
+ * (deterministic CI-safe baseline — author intent, byte-stable). UI / future
1646
+ * rules that opt into enrichment context call this helper to merge the
1647
+ * author frontmatter with the live enrichment layer.
1648
+ *
1649
+ * Algorithm:
1650
+ *
1651
+ * 1. Filter `enrichments` down to rows targeting this node AND not
1652
+ * flagged `stale`. Stale rows (probabilistic enrichments whose
1653
+ * body changed since their last run) are excluded by default —
1654
+ * stale visibility belongs to the UI layer where the marker is
1655
+ * shown next to the value.
1656
+ * 2. Sort the survivors by `enrichedAt` ASC so iteration order is
1657
+ * "oldest first". This makes the spread merge below
1658
+ * last-write-wins per field — the freshest Extractor's value
1659
+ * pisar the older one for any conflicting key.
1660
+ * 3. Spread-merge each row's `value` over `node.frontmatter`. The
1661
+ * author's keys are the base; enrichment keys overlay them.
1662
+ *
1663
+ * The returned object is a fresh shallow copy — mutating it does not
1664
+ * touch the caller's node. The original `node.frontmatter` reference
1665
+ * remains accessible via `node.frontmatter` for callers that want the
1666
+ * pristine author baseline.
1667
+ *
1668
+ * @param node Node to merge against; `node.frontmatter` is the base.
1669
+ * @param enrichments Per-(node, extractor) enrichment records — typically
1670
+ * loaded via `loadNodeEnrichments(db, node.path)` or
1671
+ * pre-filtered to this node by the caller.
1672
+ * @param opts.includeStale When true, include rows flagged stale. Defaults
1673
+ * to false (the safe, CI-deterministic default).
1674
+ * UIs that want to display "stale (last value: …)"
1675
+ * pass `true` and consult `enrichment.stale`
1676
+ * on the source rows.
1677
+ */
1678
+ declare function mergeNodeWithEnrichments(node: Node, enrichments: IPersistedEnrichment[], opts?: {
1679
+ includeStale?: boolean;
1680
+ }): Record<string, unknown>;
1681
+ /**
1682
+ * A persisted enrichment row, post-load. Mirrors the DB row shape
1683
+ * but with `value` already deserialised from JSON and `stale` /
1684
+ * `isProbabilistic` already decoded from `0 | 1`. Surfaced via
1685
+ * `loadNodeEnrichments` (driven adapter) and consumed by
1686
+ * `mergeNodeWithEnrichments` and the `sm refresh` command.
1687
+ */
1688
+ interface IPersistedEnrichment {
1689
+ nodePath: string;
1690
+ extractorId: string;
1691
+ bodyHashAtEnrichment: string;
1692
+ value: Partial<Node>;
1693
+ stale: boolean;
1694
+ enrichedAt: number;
1695
+ isProbabilistic: boolean;
1696
+ }
1697
+
1698
+ /**
1699
+ * In-memory `ProgressEmitterPort` adapter. No network, no DB — just a
1700
+ * synchronous fan-out to registered listeners. Used by the default scan
1701
+ * orchestrator; the WebSocket-backed emitter that streams to
1702
+ * the Web UI lands.
1703
+ */
1704
+
1705
+ declare class InMemoryProgressEmitter implements ProgressEmitterPort {
1706
+ #private;
1707
+ emit(event: ProgressEvent): void;
1708
+ subscribe(listener: ProgressListener): () => void;
1709
+ }
1710
+
1711
+ /**
1712
+ * File watcher for `sm watch` / `sm scan --watch`.
1713
+ *
1714
+ * Wraps `chokidar` behind a small `IFsWatcher` interface so:
1715
+ *
1716
+ * 1. The CLI command is impl-agnostic — swapping chokidar for a
1717
+ * different watcher later (Java? Rust port? a future `WatchPort`?)
1718
+ * doesn't ripple into the command.
1719
+ * 2. Debouncing, batching, and ignore-filter integration live in one
1720
+ * place. The CLI just gets `onBatch(paths)` callbacks and decides
1721
+ * whether to re-scan.
1722
+ *
1723
+ * The watcher does NOT call into the orchestrator itself. That decision
1724
+ * is deliberate: the CLI owns the scan-and-persist pipeline (`runScan`,
1725
+ * `persistScanResult`, optional rebuild of the ignore filter when
1726
+ * `.skill-mapignore` itself changes). Pulling that into the watcher
1727
+ * would couple the kernel module to `SqliteStorageAdapter`, which the
1728
+ * Server wouldn't want. Keep this module side-effect free
1729
+ * apart from filesystem subscription.
1730
+ *
1731
+ * Ignore filter integration: the supplied `IIgnoreFilter` is consulted
1732
+ * via chokidar's `ignored` predicate, which receives an absolute path.
1733
+ * We re-derive the path RELATIVE to the closest matching root before
1734
+ * passing it through `IIgnoreFilter.ignores`. This mirrors what the
1735
+ * scan walker does (`extensions/providers/claude/index.ts`) so both code
1736
+ * paths agree on what "ignored" means.
1737
+ */
1738
+
1739
+ type TWatchEventKind = 'add' | 'change' | 'unlink';
1740
+ interface IWatchEvent {
1741
+ kind: TWatchEventKind;
1742
+ /** Absolute path. */
1743
+ absolutePath: string;
1744
+ }
1745
+ interface IWatchBatch {
1746
+ /** Events that arrived inside the debounce window, in arrival order. */
1747
+ events: IWatchEvent[];
1748
+ /** Convenience: deduplicated absolute paths across the batch. */
1749
+ paths: string[];
1750
+ }
1751
+ interface IFsWatcher {
1752
+ /** Resolves once chokidar has finished its initial directory scan and is ready to emit. */
1753
+ ready: Promise<void>;
1754
+ /** Tear down the watcher. Resolves after chokidar releases handles. */
1755
+ close: () => Promise<void>;
1756
+ }
1757
+ interface ICreateFsWatcherOptions {
1758
+ /** Roots to watch. Resolved relative to `cwd` if relative paths are passed. */
1759
+ roots: string[];
1760
+ /** Working directory used to resolve relative roots and the ignore-filter root. */
1761
+ cwd: string;
1762
+ /** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */
1763
+ debounceMs: number;
1764
+ /** Optional ignore filter — same instance the scan walker uses. */
1765
+ ignoreFilter?: IIgnoreFilter | undefined;
1766
+ /** Called once per debounced batch. Awaited; concurrent batches are serialised. */
1767
+ onBatch: (batch: IWatchBatch) => void | Promise<void>;
1768
+ /**
1769
+ * Called when the underlying watcher surfaces an error. The watcher
1770
+ * stays open — callers decide whether to log, keep going, or close.
1771
+ */
1772
+ onError?: (err: Error) => void;
1773
+ }
1774
+ /**
1775
+ * Construct a chokidar-backed watcher. Subscribes immediately; the
1776
+ * returned `ready` promise resolves once chokidar's initial directory
1777
+ * walk completes, at which point only NEW events fire `onBatch`.
1778
+ *
1779
+ * The initial directory walk is deliberately silent — we set
1780
+ * `ignoreInitial: true`. The CLI runs a one-shot scan before flipping
1781
+ * the watcher on, so re-emitting an `add` for every existing file
1782
+ * would be redundant churn.
1783
+ */
1784
+ declare function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatcher;
1785
+
1786
+ /**
1787
+ * Scan delta — pure comparison of two `ScanResult` snapshots. Drives
1788
+ * `sm scan --compare-with <path>` and is the single place the kernel
1789
+ * knows how to identify "the same" entity across two scans.
1790
+ *
1791
+ * **Identity contract** (mirrors decisions made at earlier sub-steps):
1792
+ *
1793
+ * - **Node**: `node.path`. The path is the only field stable across
1794
+ * edits — every other Node field is content-derived (hashes, counts,
1795
+ * denormalised frontmatter). Two nodes with the same path are the
1796
+ * "same" node; differences are reported as a `changed` entry with
1797
+ * a reason narrowing what diverged.
1798
+ *
1799
+ * - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This
1800
+ * mirrors the link-conflict rule and `sm show` aggregation —
1801
+ * two links with identical endpoints, kind, and (optional) trigger
1802
+ * are the same link, even if emitted by different extractors. The
1803
+ * `sources[]` union and confidence are NOT part of identity; they
1804
+ * are presentation facets that can churn without making the link
1805
+ * "different" for delta purposes.
1806
+ *
1807
+ * - **Issue**: `(ruleId, sorted nodeIds, message)`. Mirrors
1808
+ * `spec/job-events.md` §issue.* — same key → same issue, even when
1809
+ * `data` / `severity` / `linkIndices` shift. A meaningful change in
1810
+ * `message` (or a different set of node ids) is a different issue.
1811
+ * This is the same key future job events will use; keep it aligned
1812
+ * so consumers can reuse logic.
1813
+ *
1814
+ * No "changed" bucket for links / issues — identity already captures
1815
+ * everything that matters there. Nodes get a "changed" bucket because
1816
+ * the path stays stable while the body / frontmatter rewrite, and that
1817
+ * change is meaningful (formatters, summarisers, downstream consumers
1818
+ * all care about it).
1819
+ *
1820
+ * Pure: no IO, no DB, no FS. Safe to run in-memory inside `sm scan`
1821
+ * without polluting the persisted snapshot.
1822
+ */
1823
+
1824
+ type TNodeChangeReason = 'body' | 'frontmatter' | 'both';
1825
+ interface INodeChange {
1826
+ before: Node;
1827
+ after: Node;
1828
+ /**
1829
+ * Which hash diverged. `'body'` means body rewritten, frontmatter
1830
+ * untouched; `'frontmatter'` means metadata rewritten, body
1831
+ * untouched; `'both'` means both rewritten in the same edit.
1832
+ */
1833
+ reason: TNodeChangeReason;
1834
+ }
1835
+ interface IScanDelta {
1836
+ /** Path the current scan was compared against (echoed for the report header). */
1837
+ comparedWith: string;
1838
+ nodes: {
1839
+ added: Node[];
1840
+ removed: Node[];
1841
+ changed: INodeChange[];
1842
+ };
1843
+ links: {
1844
+ added: Link[];
1845
+ removed: Link[];
1846
+ };
1847
+ issues: {
1848
+ added: Issue[];
1849
+ removed: Issue[];
1850
+ };
1851
+ }
1852
+ declare function computeScanDelta(prior: ScanResult, current: ScanResult, comparedWith: string): IScanDelta;
1853
+ /**
1854
+ * `true` iff every bucket is empty. Callers use this to decide the
1855
+ * exit code (`0` clean, `1` non-empty delta).
1856
+ */
1857
+ declare function isEmptyDelta(delta: IScanDelta): boolean;
1858
+
1859
+ /**
1860
+ * Export query — minimal filter language for `sm export <query>` (Step 8.3).
1861
+ *
1862
+ * Spec contract: `spec/cli-contract.md` line 190 says "Query syntax is
1863
+ * implementation-defined pre-1.0". This module defines the v0.5.0 syntax.
1864
+ *
1865
+ * **Grammar** (BNF-ish, intentionally tiny):
1866
+ *
1867
+ * query := token (WS+ token)*
1868
+ * token := key "=" value-list
1869
+ * key := "kind" | "has" | "path"
1870
+ * value-list := value ("," value)*
1871
+ * value := non-comma, non-whitespace string
1872
+ *
1873
+ * Tokens AND together; values within one token OR. An empty / whitespace-only
1874
+ * query is valid and matches every node ("export everything").
1875
+ *
1876
+ * **Filters**:
1877
+ *
1878
+ * - `kind=skill` / `kind=skill,agent` — node kind whitelist.
1879
+ * - `has=issues` — node must appear in some issue's `nodeIds`. (Future
1880
+ * expansion: `has=findings` / `has=summary` once Step 10 / 11 land.
1881
+ * Unknown values are a parse error today; we'll ratchet up the
1882
+ * accepted set additively.)
1883
+ * - `path=foo/*` / `path=.claude/agents/**` — POSIX glob over `node.path`.
1884
+ * Supports `*` (any chars except `/`) and `**` (any chars including `/`).
1885
+ *
1886
+ * **Subset semantics** (`applyExportQuery`):
1887
+ *
1888
+ * - Nodes pass when every specified filter matches (AND across keys,
1889
+ * OR within values).
1890
+ * - Links survive only when BOTH endpoints (`source` + `target`) belong
1891
+ * to the filtered node set. A subset that includes "edges out to
1892
+ * unfiltered nodes" would be confusing — the user asked for a focused
1893
+ * subgraph, not its boundary. External-URL pseudo-links are already
1894
+ * stripped by the orchestrator and never reach this layer.
1895
+ * - Issues survive when ANY of the issue's `nodeIds` is in the filtered
1896
+ * set. Issues span multiple nodes (e.g. `trigger-collision` over two
1897
+ * advertisers); dropping an issue when one of its nodes is outside
1898
+ * would hide cross-cutting problems the user is investigating.
1899
+ *
1900
+ * Pure: no IO, no DB, no FS.
1901
+ */
1902
+
1903
+ interface IExportQuery {
1904
+ /** Original query string echoed back so consumers can render the header. */
1905
+ raw: string;
1906
+ /**
1907
+ * Whitelist of node kinds (`node.kind` is open string — built-in
1908
+ * Claude catalog `skill` / `agent` / `command` / `hook` / `note`,
1909
+ * plus whatever external Providers declare). The query parser does
1910
+ * not validate values against a closed enum; an unknown kind simply
1911
+ * yields zero matches at filter time.
1912
+ */
1913
+ kinds?: string[];
1914
+ hasIssues?: boolean;
1915
+ pathGlobs?: string[];
1916
+ }
1917
+ interface IExportSubset {
1918
+ query: IExportQuery;
1919
+ nodes: Node[];
1920
+ links: Link[];
1921
+ issues: Issue[];
1922
+ }
1923
+ declare class ExportQueryError extends Error {
1924
+ constructor(message: string);
1925
+ }
1926
+ declare function parseExportQuery(raw: string): IExportQuery;
1927
+ declare function applyExportQuery(scan: {
1928
+ nodes: Node[];
1929
+ links: Link[];
1930
+ issues: Issue[];
1931
+ }, query: IExportQuery): IExportSubset;
1932
+
1933
+ /**
1934
+ * `PluginLoaderPort` — discovers plugin directories and loads their
1935
+ * extensions. The shape mirrors what the concrete loader actually
1936
+ * exposes (see `kernel/adapters/plugin-loader.ts`); the port exists so
1937
+ * the CLI consumes the abstract contract via `createPluginLoader(...)`
1938
+ * instead of `new PluginLoader(...)` and so the concrete adapter is
1939
+ * structurally pinned to the port (`implements PluginLoaderPort` makes
1940
+ * any drift a compile error).
1941
+ *
1942
+ * Domain types (`IPluginManifest`, `ILoadedExtension`, `IDiscoveredPlugin`,
1943
+ * `TPluginStorage`, `TPluginLoadStatus`, `TGranularity`) live in
1944
+ * `kernel/types/plugin.ts` because they are spec-mirroring DTOs, not
1945
+ * port-shape types. The port re-exports them for callers that import
1946
+ * from the ports barrel.
1947
+ */
1948
+
1949
+ interface PluginLoaderPort {
1950
+ /**
1951
+ * Synchronously enumerate every directory containing a `plugin.json`
1952
+ * across the configured search paths. Non-existent paths are skipped.
1953
+ */
1954
+ discoverPaths(): string[];
1955
+ /**
1956
+ * Discover every plugin, attempt to load each, then apply the
1957
+ * cross-root id-collision pass. Never throws — failures are reported
1958
+ * via `IDiscoveredPlugin.status`.
1959
+ */
1960
+ discoverAndLoadAll(): Promise<IDiscoveredPlugin[]>;
1961
+ /**
1962
+ * Load a single plugin from its directory. Never throws — failure is
1963
+ * reported via the returned `status`.
1964
+ */
1965
+ loadOne(pluginPath: string): Promise<IDiscoveredPlugin>;
1966
+ }
1967
+
1968
+ /**
1969
+ * Row-level filter for `port.scans.findNodes(...)` (driven by
1970
+ * `sm list`'s flags). All fields are optional — an empty filter
1971
+ * returns every node sorted by `path` asc.
1972
+ */
1973
+ interface INodeFilter {
1974
+ /** Restrict to a single node kind. Open string (matches `Node.kind`). */
1975
+ kind?: string;
1976
+ /**
1977
+ * When `true`, keep only nodes whose path is referenced by at least
1978
+ * one `scan_issues.nodeIds` array.
1979
+ */
1980
+ hasIssues?: boolean;
1981
+ /**
1982
+ * Sort column. The adapter validates against its own whitelist and
1983
+ * rejects anything else with an Error (the CLI's own usage-error
1984
+ * exit is the right place to surface a bad `--sort-by`; the port
1985
+ * defends in depth).
1986
+ */
1987
+ sortBy?: string;
1988
+ /** `'asc'` or `'desc'`. Defaults to the adapter's per-column convention. */
1989
+ sortDirection?: 'asc' | 'desc';
1990
+ /** Cap the result. Positive integer; absent → no limit. */
1991
+ limit?: number;
1992
+ }
1993
+ /**
1994
+ * Bundled fetch for `port.scans.findNode(path)` — one node and
1995
+ * everything `sm show <path>` displays alongside it. Every field is
1996
+ * computed from `scan_*` zone reads only; per-domain data (history,
1997
+ * jobs, plugin enrichments) ships through other namespaces.
1998
+ */
1999
+ interface INodeBundle {
2000
+ node: Node;
2001
+ linksOut: Link[];
2002
+ linksIn: Link[];
2003
+ issues: Issue[];
2004
+ }
2005
+ /**
2006
+ * Output of `port.scans.countRows()`. Used by `sm scan` to decide
2007
+ * whether the persist would wipe a populated DB (the "refusing to
2008
+ * wipe" guard) and by `sm db status` for the human summary.
2009
+ */
2010
+ interface INodeCounts {
2011
+ nodes: number;
2012
+ links: number;
2013
+ issues: number;
2014
+ }
2015
+ /**
2016
+ * Lightweight option bag for `port.scans.persist`. Mirrors the trailing
2017
+ * arguments of the legacy `persistScanResult(db, result, renameOps,
2018
+ * extractorRuns, enrichments)` free function so the adapter
2019
+ * implementation is a one-line delegation today; the named-bag shape
2020
+ * tomorrow lets new optional inputs land without breaking callers.
2021
+ */
2022
+ interface IPersistOptions {
2023
+ renameOps?: RenameOp[];
2024
+ extractorRuns?: IExtractorRunRecord[];
2025
+ enrichments?: IEnrichmentRecord[];
2026
+ }
2027
+ /**
2028
+ * Issue row as the storage layer sees it — paired with its DB-assigned
2029
+ * id so `port.issues.deleteById(id)` can target it inside a
2030
+ * transaction. The runtime `Issue` shape (per `issue.schema.json`) does
2031
+ * not carry `id` because the spec models issues as ephemeral findings
2032
+ * scoped to a scan; the DB does need the synthetic id to update / delete
2033
+ * a single row.
2034
+ */
2035
+ interface IIssueRow {
2036
+ id: number;
2037
+ issue: Issue;
2038
+ }
2039
+ /** Output of `port.jobs.pruneTerminal` / `listTerminalCandidates`. */
2040
+ interface IPruneResult {
2041
+ /** How many `state_jobs` rows were deleted (or would be, in dry-run). */
2042
+ deletedCount: number;
2043
+ /** Job-file paths from the affected rows; the CLI unlinks these from disk. `null` `filePath` rows contribute nothing here. */
2044
+ filePaths: string[];
2045
+ }
2046
+ /** Filter shape for `port.history.list`. All fields optional. */
2047
+ interface IListExecutionsFilter {
2048
+ /** Restrict to executions whose `nodeIds` array contains this path. */
2049
+ nodePath?: string;
2050
+ /** Exact match on `extension_id`. */
2051
+ actionId?: string;
2052
+ /** Subset of {`completed`,`failed`,`cancelled`}. */
2053
+ statuses?: ExecutionStatus[];
2054
+ /** Lower bound (inclusive) on `started_at`. Unix ms. */
2055
+ sinceMs?: number;
2056
+ /** Upper bound (exclusive) on `started_at`. Unix ms. */
2057
+ untilMs?: number;
2058
+ /** Cap result count. No default. */
2059
+ limit?: number;
2060
+ }
2061
+ /** Window shape for `port.history.aggregateStats`. */
2062
+ interface IHistoryStatsRange {
2063
+ /** Inclusive lower bound. `null` = all-time. */
2064
+ sinceMs: number | null;
2065
+ /** Exclusive upper bound. */
2066
+ untilMs: number;
2067
+ }
2068
+ /** Period bucket granularity for `port.history.aggregateStats`. */
2069
+ type THistoryStatsPeriod = 'day' | 'week' | 'month';
2070
+ /**
2071
+ * Output of `port.transaction(tx => tx.history.migrateNodeFks(from, to))`.
2072
+ * Lists how many rows in each `state_*` table were repointed plus any
2073
+ * composite-PK collisions that forced a drop instead of an update.
2074
+ */
2075
+ interface IMigrateNodeFksReport {
2076
+ jobs: number;
2077
+ executions: number;
2078
+ summaries: number;
2079
+ enrichments: number;
2080
+ pluginKvs: number;
2081
+ /**
2082
+ * Composite-PK collisions encountered when migrating
2083
+ * `state_summaries` / `state_enrichments` / `state_plugin_kvs` because
2084
+ * a row already existed at the destination PK. The pre-existing rows
2085
+ * are preserved — the migrating rows are dropped (deleted from
2086
+ * `fromPath` without a corresponding INSERT). One entry per dropped
2087
+ * row, with the affected PK fields included for diagnostic output.
2088
+ */
2089
+ collisions: Array<{
2090
+ table: 'state_summaries' | 'state_enrichments' | 'state_plugin_kvs';
2091
+ fromPath: string;
2092
+ toPath: string;
2093
+ keys: Record<string, string>;
2094
+ }>;
2095
+ }
2096
+ /** A single `config_plugins` override row as the kernel sees it. */
2097
+ interface IPluginConfigRow {
2098
+ pluginId: string;
2099
+ enabled: boolean;
2100
+ configJson: string | null;
2101
+ updatedAt: number;
2102
+ }
2103
+ /** Discovered kernel migration file (one of `NNN_snake_case.sql`). */
2104
+ interface IMigrationFile {
2105
+ version: number;
2106
+ description: string;
2107
+ filePath: string;
2108
+ }
2109
+ /** A row from the `config_schema_versions` ledger for the kernel scope. */
2110
+ interface IMigrationRecord {
2111
+ scope: string;
2112
+ ownerId: string;
2113
+ version: number;
2114
+ description: string;
2115
+ appliedAt: number;
2116
+ }
2117
+ /** `port.migrations.plan` output: applied vs pending. */
2118
+ interface IMigrationPlan {
2119
+ applied: IMigrationRecord[];
2120
+ pending: IMigrationFile[];
2121
+ }
2122
+ /** Apply-time options for `port.migrations.apply`. */
2123
+ interface IApplyOptions {
2124
+ backup?: boolean;
2125
+ dryRun?: boolean;
2126
+ to?: number;
2127
+ }
2128
+ /** Result of `port.migrations.apply`. */
2129
+ interface IApplyResult {
2130
+ applied: IMigrationFile[];
2131
+ backupPath: string | null;
2132
+ }
2133
+ /** Discovered plugin migration file. Same `NNN_snake_case.sql` convention. */
2134
+ interface IPluginMigrationFile {
2135
+ version: number;
2136
+ description: string;
2137
+ filePath: string;
2138
+ }
2139
+ /** A row from the `config_schema_versions` ledger for a single plugin. */
2140
+ interface IPluginMigrationRecord {
2141
+ version: number;
2142
+ description: string;
2143
+ appliedAt: number;
2144
+ }
2145
+ /** `port.pluginMigrations.plan` output for a single plugin. */
2146
+ interface IPluginMigrationPlan {
2147
+ pluginId: string;
2148
+ applied: IPluginMigrationRecord[];
2149
+ pending: IPluginMigrationFile[];
2150
+ }
2151
+ /** Apply-time options for `port.pluginMigrations.apply`. */
2152
+ interface IPluginApplyOptions {
2153
+ /** No actual writes; surfaces what would run. Default false. */
2154
+ dryRun?: boolean;
2155
+ }
2156
+ /** Result of `port.pluginMigrations.apply`. */
2157
+ interface IPluginApplyResult {
2158
+ pluginId: string;
2159
+ applied: IPluginMigrationFile[];
2160
+ /** Catalog intrusions caught by Layer 3 (post-apply sweep). Empty when clean. */
2161
+ intrusions: string[];
2162
+ }
2163
+
2164
+ /**
2165
+ * `StoragePort` — the kernel's persistence boundary. Driving adapters
2166
+ * (CLI, future server, in-memory test harness) consume this surface
2167
+ * exclusively; nothing in `cli/**` should reach into the SQLite
2168
+ * adapter's internal helpers (free functions on
2169
+ * `kernel/adapters/sqlite/*`) directly. Phase F of the
2170
+ * storage-port-promotion refactor finishes that hardening; A-E grow
2171
+ * the port enough that the CLI has somewhere to land.
2172
+ *
2173
+ * The port is namespaced by domain (`scans`, `issues`, `enrichments`,
2174
+ * etc.) — explicitly NOT a generic `port.query<T>(sql)`. Each
2175
+ * namespace's methods name an operation the kernel cares about; the
2176
+ * adapter translates to its persistence engine's idioms.
2177
+ *
2178
+ * Phase A lands the **scans / issues / enrichments / transaction**
2179
+ * namespaces — the core scan pipeline. The remaining namespaces
2180
+ * (history / jobs / pluginConfig / migrations / pluginMigrations)
2181
+ * arrive in subsequent phases. The port shape declared here is the
2182
+ * Phase A subset; later phases extend it without reshaping what
2183
+ * lands today.
2184
+ */
2185
+
2186
+ /**
2187
+ * Subset of `StoragePort` exposed inside a `transaction(fn)` callback.
2188
+ * Lifecycle methods are intentionally omitted — a transaction that
2189
+ * tries to `init()` the adapter mid-flight is a category error.
2190
+ *
2191
+ * Every callable in the subset MUST run on the same underlying
2192
+ * transaction handle the adapter opened for the callback. Adapters
2193
+ * are responsible for that wiring; consumers only see the namespace
2194
+ * surfaces.
2195
+ */
2196
+ interface ITransactionalStorage {
2197
+ scans: {
2198
+ persist(result: ScanResult, opts?: IPersistOptions): Promise<void>;
2199
+ };
2200
+ issues: {
2201
+ deleteById(id: number): Promise<void>;
2202
+ insert(issue: Issue): Promise<void>;
2203
+ };
2204
+ enrichments: {
2205
+ /**
2206
+ * Upsert a batch of fresh enrichment records produced by an
2207
+ * extractor pass. Composite PK is `(nodePath, extractorId)`;
2208
+ * conflict → replace. Every row lands with `stale = 0` (the
2209
+ * caller just refreshed it; ROADMAP §B.10 — staleness is
2210
+ * computed downstream when the body hash changes again).
2211
+ */
2212
+ upsertMany(records: IEnrichmentRecord[]): Promise<void>;
2213
+ };
2214
+ history: {
2215
+ /**
2216
+ * Repoint every `state_*` reference from `fromPath` to `toPath`.
2217
+ * Atomic across the four state tables; the report flags any
2218
+ * composite-PK collisions so callers can diagnose them.
2219
+ * `sm orphans reconcile` / `undo-rename` and the scan-time
2220
+ * rename heuristic are the canonical consumers.
2221
+ */
2222
+ migrateNodeFks(from: string, to: string): Promise<IMigrateNodeFksReport>;
2223
+ };
2224
+ }
2225
+ interface StoragePort {
2226
+ init(): Promise<void>;
2227
+ close(): Promise<void>;
2228
+ scans: {
2229
+ /**
2230
+ * Persist a fresh `ScanResult` (replace-all on the scan zone).
2231
+ * Called by `sm scan` after the orchestrator returns. The renames /
2232
+ * extractor-runs / enrichments side bags ride along inside the
2233
+ * same transaction — the call is atomic from the caller's view.
2234
+ */
2235
+ persist(result: ScanResult, opts?: IPersistOptions): Promise<void>;
2236
+ /**
2237
+ * Hydrate the persisted `ScanResult`. Returns the snapshot the
2238
+ * scan zone holds today (including external-Provider kinds —
2239
+ * `node.kind` is open string per `node.schema.json`).
2240
+ */
2241
+ load(): Promise<ScanResult>;
2242
+ /**
2243
+ * Spec § A.9 — fine-grained extractor-runs cache breadcrumbs.
2244
+ * Returns `Map<nodePath, Map<qualifiedExtractorId, bodyHashAtRun>>`.
2245
+ */
2246
+ loadExtractorRuns(): Promise<Map<string, Map<string, string>>>;
2247
+ /** Universal enrichment layer — every persisted `(node, extractor)` pair. */
2248
+ loadNodeEnrichments(): Promise<IPersistedEnrichment[]>;
2249
+ /**
2250
+ * Row counts for `scan_nodes` / `scan_links` / `scan_issues`.
2251
+ * Used by `sm scan`'s "refusing to wipe a populated DB" guard.
2252
+ */
2253
+ countRows(): Promise<INodeCounts>;
2254
+ /** Row-level filter for `sm list`. Open `kind` (matches `Node.kind`). */
2255
+ findNodes(filter: INodeFilter): Promise<Node[]>;
2256
+ /**
2257
+ * Bundled fetch for `sm show <path>`. Returns `null` if the node
2258
+ * is not in the persisted scan.
2259
+ */
2260
+ findNode(path: string): Promise<INodeBundle | null>;
2261
+ };
2262
+ issues: {
2263
+ /** Every issue from the latest scan, in insertion order. */
2264
+ listAll(): Promise<Issue[]>;
2265
+ /**
2266
+ * Issue rows whose runtime `Issue` shape passes `predicate`.
2267
+ * `port.issues.findActive((i) => i.ruleId === 'orphan')` is the
2268
+ * canonical use; `sm orphans` consumes this. The returned shape
2269
+ * carries the DB-assigned `id` so a follow-up
2270
+ * `transaction(tx => tx.issues.deleteById(row.id))` can target
2271
+ * a specific row.
2272
+ */
2273
+ findActive(predicate: (issue: Issue) => boolean): Promise<IIssueRow[]>;
2274
+ };
2275
+ pluginConfig: {
2276
+ /**
2277
+ * Upsert the per-plugin enabled override into `config_plugins`.
2278
+ * Caller is `sm plugins enable / disable`.
2279
+ */
2280
+ set(pluginId: string, enabled: boolean): Promise<void>;
2281
+ /** Read a single override; `undefined` when no row exists. */
2282
+ get(pluginId: string): Promise<boolean | undefined>;
2283
+ /** Every override row, sorted by `pluginId` for stable rendering. */
2284
+ list(): Promise<IPluginConfigRow[]>;
2285
+ /** Drop a single override row (no-op when absent). */
2286
+ delete(pluginId: string): Promise<void>;
2287
+ /**
2288
+ * Load every override into a map for quick lookup by id. Used by
2289
+ * `loadPluginRuntime` to layer the DB overrides over the
2290
+ * `settings.json` defaults at scan boot.
2291
+ */
2292
+ loadOverrideMap(): Promise<Map<string, boolean>>;
2293
+ };
2294
+ jobs: {
2295
+ /**
2296
+ * Delete `state_jobs` rows in terminal `status` whose `finishedAt`
2297
+ * is older than `cutoffMs` (Unix ms). Returns the deleted count
2298
+ * plus every non-null `filePath` from the deleted rows so the
2299
+ * caller can unlink the on-disk MD files. Caller computes
2300
+ * `cutoffMs` from the configured retention.
2301
+ */
2302
+ pruneTerminal(status: 'completed' | 'failed', cutoffMs: number): Promise<IPruneResult>;
2303
+ /**
2304
+ * Same SELECT side as `pruneTerminal` but without the DELETE.
2305
+ * Powers `sm job prune --dry-run` previews so the dry-run output
2306
+ * names exactly the rows the live mode would delete.
2307
+ */
2308
+ listTerminalCandidates(status: 'completed' | 'failed', cutoffMs: number): Promise<IPruneResult>;
2309
+ /**
2310
+ * Read every `state_jobs.filePath` currently set, normalized through
2311
+ * `path.resolve()`. The CLI's `sm job prune --orphan-files` flow
2312
+ * pairs this set with `kernel/jobs/orphan-files.ts:findOrphanJobFiles`
2313
+ * (which walks the directory) to compute the MD files on disk that
2314
+ * no row references — keeps the storage layer FS-free.
2315
+ */
2316
+ listReferencedFilePaths(): Promise<Set<string>>;
2317
+ };
2318
+ history: {
2319
+ /** List `state_executions` rows (paginated by filter). */
2320
+ list(filter: IListExecutionsFilter): Promise<ExecutionRecord[]>;
2321
+ /**
2322
+ * Aggregate counters / period buckets / top-nodes / error rates
2323
+ * over `state_executions`. Body matches the spec
2324
+ * `history-stats.schema.json` shape minus `range`/`elapsedMs`
2325
+ * (the verb fills those in around the call).
2326
+ */
2327
+ aggregateStats(range: IHistoryStatsRange, period: THistoryStatsPeriod, topN: number): Promise<Omit<HistoryStats, 'elapsedMs' | 'range'> & {
2328
+ rangeMs: {
2329
+ sinceMs: number | null;
2330
+ untilMs: number;
2331
+ };
2332
+ }>;
2333
+ };
2334
+ migrations: {
2335
+ /** Enumerate kernel migration files bundled with this build. */
2336
+ discover(): IMigrationFile[];
2337
+ /**
2338
+ * Compute the apply / pending plan against the current `config_
2339
+ * schema_versions` ledger. Read-only; safe under `--dry-run`.
2340
+ */
2341
+ plan(files?: IMigrationFile[]): IMigrationPlan;
2342
+ /**
2343
+ * Apply pending migrations in order. Each runs inside its own
2344
+ * `BEGIN/COMMIT` (per `kernel/adapters/sqlite/migrations.ts`); a
2345
+ * partial failure rolls back to the prior state. Returns the
2346
+ * applied list + backup path (when `backup: true`).
2347
+ */
2348
+ apply(options?: IApplyOptions, files?: IMigrationFile[]): IApplyResult;
2349
+ /**
2350
+ * WAL-checkpoint + atomic file copy of the DB to `destPath`.
2351
+ * Caller composes the path. Returns the destination on success,
2352
+ * or `null` for in-memory DBs (no file to copy).
2353
+ */
2354
+ writeBackup(destPath: string): string | null;
2355
+ /**
2356
+ * Read `PRAGMA user_version` from the underlying DB. The migrations
2357
+ * runner keeps that pragma in sync with the latest applied kernel
2358
+ * migration, so this is the canonical "current schema version"
2359
+ * read for `sm version --json`'s `dbSchema` field. Returns `null`
2360
+ * on engine quirks (non-numeric / null pragma).
2361
+ */
2362
+ currentSchemaVersion(): number | null;
2363
+ };
2364
+ pluginMigrations: {
2365
+ /** Path to the plugin's `migrations/` directory, or `null` when absent. */
2366
+ resolveDir(plugin: IDiscoveredPlugin): string | null;
2367
+ /** Discover the plugin's migration files. */
2368
+ discover(plugin: IDiscoveredPlugin): IPluginMigrationFile[];
2369
+ /**
2370
+ * Plan against `config_schema_versions` for the plugin's
2371
+ * `(scope='plugin', ownerId=plugin.id)`.
2372
+ */
2373
+ plan(plugin: IDiscoveredPlugin, files?: IPluginMigrationFile[]): IPluginMigrationPlan;
2374
+ /** Apply pending plugin migrations. Same per-file BEGIN/COMMIT pattern. */
2375
+ apply(plugin: IDiscoveredPlugin, options?: IPluginApplyOptions, files?: IPluginMigrationFile[]): IPluginApplyResult;
2376
+ };
2377
+ /**
2378
+ * Open a transaction. The callback receives a transactional subset
2379
+ * of the port; the adapter commits on resolution and rolls back on
2380
+ * rejection. `sm orphans reconcile / undo-rename` and `sm refresh`
2381
+ * are the canonical consumers.
2382
+ */
2383
+ transaction<T>(fn: (tx: ITransactionalStorage) => Promise<T>): Promise<T>;
2384
+ }
2385
+
2386
+ /**
2387
+ * `FilesystemPort` — walks roots, reads nodes, writes job files.
2388
+ *
2389
+ * Shape-only. The real adapter ships with the scan end-to-end pipeline.
2390
+ */
2391
+ interface NodeStat {
2392
+ path: string;
2393
+ sizeBytes: number;
2394
+ mtimeMs: number;
2395
+ }
2396
+ interface IWalkOptions {
2397
+ ignore?: string[];
2398
+ }
2399
+ interface FilesystemPort {
2400
+ walk(roots: string[], options?: IWalkOptions): AsyncIterable<NodeStat>;
2401
+ readNode(path: string): Promise<string>;
2402
+ stat(path: string): Promise<NodeStat>;
2403
+ writeJobFile(path: string, content: string): Promise<void>;
2404
+ ensureDir(path: string): Promise<void>;
2405
+ }
2406
+
2407
+ /**
2408
+ * `RunnerPort` — executes an action against a rendered job file.
2409
+ *
2410
+ * Shape-only. `ClaudeCliRunner` + `MockRunner` land with the job subsystem
2411
+ * (job subsystem + first summarizer).
2412
+ */
2413
+ interface IRunOptions {
2414
+ timeoutMs?: number;
2415
+ model?: string;
2416
+ }
2417
+ interface IRunResult {
2418
+ reportPath: string;
2419
+ tokensIn: number;
2420
+ tokensOut: number;
2421
+ durationMs: number;
2422
+ exitCode: number;
2423
+ }
2424
+ interface RunnerPort {
2425
+ run(jobFilePath: string, options?: IRunOptions): Promise<IRunResult>;
2426
+ }
2427
+
2428
+ /**
2429
+ * `LoggerPort` — structured logging port for the kernel.
2430
+ *
2431
+ * The kernel must NOT write to stdout/stderr directly. Anything that
2432
+ * would historically have been a `console.log` / `console.error` goes
2433
+ * through this port; the adapter (CLI, server, test harness) decides
2434
+ * format, level filter, and destination.
2435
+ *
2436
+ * Levels follow the conventional ordering, lowest = most verbose:
2437
+ *
2438
+ * trace < debug < info < warn < error < silent
2439
+ *
2440
+ * `silent` is a sentinel for filtering only — it never appears as a
2441
+ * `LogRecord.level`. Setting an adapter to `silent` disables every
2442
+ * method.
2443
+ */
2444
+ type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
2445
+ type LogMethodLevel = Exclude<LogLevel, 'silent'>;
2446
+ declare const LOG_LEVELS: readonly LogLevel[];
2447
+ declare function logLevelRank(level: LogLevel): number;
2448
+ declare function isLogLevel(value: unknown): value is LogLevel;
2449
+ /**
2450
+ * Parse a string into a `LogLevel`. Returns `null` for invalid input
2451
+ * (incl. `undefined` / `null` / empty). Case-insensitive; trims
2452
+ * whitespace.
2453
+ */
2454
+ declare function parseLogLevel(value: string | undefined | null): LogLevel | null;
2455
+ interface LogRecord {
2456
+ level: LogMethodLevel;
2457
+ /** ISO 8601 timestamp produced at the moment the log call was made. */
2458
+ timestamp: string;
2459
+ message: string;
2460
+ /** Optional structured context. Caller-owned; serialization is up to the formatter. */
2461
+ context?: Record<string, unknown>;
2462
+ }
2463
+ interface LoggerPort {
2464
+ trace(message: string, context?: Record<string, unknown>): void;
2465
+ debug(message: string, context?: Record<string, unknown>): void;
2466
+ info(message: string, context?: Record<string, unknown>): void;
2467
+ warn(message: string, context?: Record<string, unknown>): void;
2468
+ error(message: string, context?: Record<string, unknown>): void;
2469
+ }
2470
+
2471
+ /**
2472
+ * No-op `LoggerPort`. Default when the kernel is invoked without a
2473
+ * logger (tests, embedded usage). Equivalent in spirit to
2474
+ * `InMemoryProgressEmitter`: callers that don't care get a working
2475
+ * implementation that does nothing.
2476
+ *
2477
+ * Every method is intentionally empty — that IS the contract of this
2478
+ * class. We disable `no-empty-function` for the whole file because
2479
+ * adding `// eslint-disable-next-line` to each method would be noise.
2480
+ */
2481
+
2482
+ declare class SilentLogger implements LoggerPort {
2483
+ trace(): void;
2484
+ debug(): void;
2485
+ info(): void;
2486
+ warn(): void;
2487
+ error(): void;
2488
+ }
2489
+
2490
+ /**
2491
+ * Module-level singleton `LoggerPort`. The kernel emits warnings /
2492
+ * info / debug through `log.*`; the active implementation defaults to
2493
+ * `SilentLogger` (no output) and is swapped by the driving adapter at
2494
+ * boot time via `configureLogger(...)`.
2495
+ *
2496
+ * Why a singleton (vs. per-call injection):
2497
+ * - Logging crosses every layer; threading a `logger` argument
2498
+ * through every kernel function costs a lot of plumbing for a
2499
+ * side-channel concern.
2500
+ * - The active impl is a pointer; the exported `log` is a stable
2501
+ * proxy. Imports made before `configureLogger` runs still see the
2502
+ * new impl on every call — no "captured stale logger" bugs.
2503
+ *
2504
+ * Tradeoffs accepted:
2505
+ * - Tests must call `resetLogger()` (or replace the active impl) in
2506
+ * teardown to avoid cross-test bleed.
2507
+ * - Concurrent scans share the same logger; per-scan logging requires
2508
+ * reintroducing an explicit `logger` argument on the call path.
2509
+ */
2510
+
2511
+ /** Stable proxy. Methods always delegate to the current `active` impl. */
2512
+ declare const log: LoggerPort;
2513
+ /** Install a logger as the active implementation. Idempotent. */
2514
+ declare function configureLogger(impl: LoggerPort): void;
2515
+ /** Restore the default `SilentLogger`. Call from test teardown. */
2516
+ declare function resetLogger(): void;
2517
+ /** Inspect the active logger. Test-only — production code uses `log`. */
2518
+ declare function getActiveLogger(): LoggerPort;
2519
+
2520
+ /**
2521
+ * Kernel entry point. `createKernel()` returns a shell with an empty registry
2522
+ * and no bound ports. Driving adapters (CLI, Server, Skill) are expected to
2523
+ * wire adapters before invoking use cases.
2524
+ */
2525
+
2526
+ interface Kernel {
2527
+ registry: Registry;
2528
+ }
2529
+ declare function createKernel(): Kernel;
2530
+
2531
+ 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 };