@skill-map/cli 0.54.0 → 0.56.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.
Files changed (41) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +22 -24
  2. package/dist/cli/tutorial/sm-tutorial/references/_core.md +8 -7
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +21 -42
  4. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +15 -7
  5. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +2 -2
  6. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  7. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +9 -10
  8. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +563 -0
  9. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +5 -1
  10. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +7 -7
  11. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +24 -12
  12. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +2 -2
  13. package/dist/cli.js +1785 -1102
  14. package/dist/index.js +148 -14
  15. package/dist/kernel/index.d.ts +229 -97
  16. package/dist/kernel/index.js +148 -14
  17. package/dist/migrations/001_initial.sql +5 -0
  18. package/dist/ui/chunk-4ITL7E6U.js +1 -0
  19. package/dist/ui/chunk-DWBJCNC7.js +2 -0
  20. package/dist/ui/{chunk-CXTU4HQV.js → chunk-GHOVZAAV.js} +1 -1
  21. package/dist/ui/{chunk-GBKHMJ4B.js → chunk-H6O2DYVT.js} +13 -13
  22. package/dist/ui/chunk-HDKR6XHG.js +917 -0
  23. package/dist/ui/{chunk-GEI6INVH.js → chunk-JA4Z74I3.js} +1 -1
  24. package/dist/ui/chunk-RS3ANRT5.js +1 -0
  25. package/dist/ui/chunk-VUNP5KNI.js +3 -0
  26. package/dist/ui/chunk-W3Z3CZL4.js +1844 -0
  27. package/dist/ui/chunk-YHJL5LP3.js +913 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/ui/{main-HP3MOLI2.js → main-PL3BEVQI.js} +3 -3
  30. package/dist/ui/{styles-4SNVM34O.css → styles-RHEEXRHQ.css} +1 -1
  31. package/migrations/001_initial.sql +5 -0
  32. package/package.json +2 -2
  33. package/dist/cli/tutorial/sm-tutorial/references/part-live-site.md +0 -155
  34. package/dist/cli/tutorial/sm-tutorial/references/part-maintain.md +0 -284
  35. package/dist/cli/tutorial/sm-tutorial/references/part-run-harness.md +0 -181
  36. package/dist/ui/chunk-4CXAL43H.js +0 -1
  37. package/dist/ui/chunk-BUNPMGDX.js +0 -2205
  38. package/dist/ui/chunk-DSNBKMYU.js +0 -2
  39. package/dist/ui/chunk-JXRIGHET.js +0 -552
  40. package/dist/ui/chunk-MVRQGDZJ.js +0 -123
  41. package/dist/ui/chunk-WFLPMCK4.js +0 -392
@@ -1,87 +1,3 @@
1
- /**
2
- * Extension registry, six kinds, first-class, loaded through a single API.
3
- *
4
- * The `IExtension` shape is aligned with `spec/schemas/extensions/base.schema.json`.
5
- * Kind-specific manifests (provider / extractor / analyzer / action / formatter /
6
- * hook) extend this base structurally; the registry stores the base view
7
- * and each kind's code carries its own fuller type where needed.
8
- *
9
- * **Spec § A.6, qualified ids.** Every extension is keyed in the registry
10
- * by `<pluginId>/<id>` (e.g. `core/annotations`, `core/slash-command`,
11
- * `my-plugin/my-extractor`). `IExtension.id` carries the **short** id as authored;
12
- * `IExtension.pluginId` carries the namespace; the registry composes the
13
- * qualifier internally and exposes lookup APIs that operate on either form
14
- * (qualified for direct lookup, kind-scoped listing for enumeration).
15
- *
16
- * Boot invariant: `new Registry()` is empty. `registry.totalCount() === 0`
17
- * when the kernel boots with zero extensions. This is the data side of the
18
- * `kernel-empty-boot` conformance contract.
19
- */
20
- type ExtensionKind = 'provider' | 'extractor' | 'analyzer' | 'action' | 'formatter' | 'hook';
21
- declare const EXTENSION_KINDS: readonly ExtensionKind[];
22
- interface IExtension {
23
- /** Short (unqualified) extension id, injected by the loader from the leaf folder name. */
24
- id: string;
25
- /** Owning plugin namespace, injected by the loader from the plugin folder name. */
26
- pluginId: string;
27
- kind: ExtensionKind;
28
- version: string;
29
- /** Required short description; surfaced in `sm <kind>s list` and the UI. */
30
- description: string;
31
- entry?: string;
32
- }
33
- /**
34
- * Compose the qualified registry key for an extension. Single source of
35
- * truth so callers don't reinvent the format and a future change (e.g. a
36
- * different separator) lands in one place.
37
- */
38
- declare function qualifiedExtensionId(pluginId: string, id: string): string;
39
- declare class DuplicateExtensionError extends Error {
40
- constructor(kind: ExtensionKind, qualifiedId: string);
41
- }
42
- declare class Registry {
43
- #private;
44
- constructor();
45
- register(ext: IExtension): void;
46
- /**
47
- * Lookup by qualified id (`<pluginId>/<id>`). Returns `undefined` when
48
- * no extension of that kind is registered under the qualifier.
49
- */
50
- get(kind: ExtensionKind, qualifiedId: string): IExtension | undefined;
51
- /**
52
- * Convenience wrapper that composes the qualified id for the caller.
53
- * Equivalent to `get(kind, qualifiedExtensionId(pluginId, id))`.
54
- */
55
- find(kind: ExtensionKind, pluginId: string, id: string): IExtension | undefined;
56
- all(kind: ExtensionKind): IExtension[];
57
- count(kind: ExtensionKind): number;
58
- totalCount(): number;
59
- }
60
-
61
- /**
62
- * Step 9.6.6, runtime annotation-contribution catalog types.
63
- *
64
- * Lives in its own module (rather than `kernel/index.ts`) so consumers
65
- * deep inside the kernel, `IAnalyzerContext`, the BFF route factories,
66
- * future Action contexts, can depend on the catalog shape without
67
- * dragging the whole kernel barrel and risking a cycle.
68
- */
69
- /**
70
- * Single row of the runtime annotation-contribution catalog surfaced by
71
- * `kernel.getRegisteredAnnotationKeys()`. One row per (plugin × key)
72
- * tuple. Built-in catalog keys from `annotations.schema.json` are NOT
73
- * included, this catalog is plugin-only; the UI knows the built-in
74
- * catalog via the schema bundle.
75
- */
76
- interface IRegisteredAnnotationKey {
77
- pluginId: string;
78
- key: string;
79
- location: 'namespaced' | 'root';
80
- ownership: 'exclusive' | 'shared';
81
- /** Inline JSON Schema as declared in the manifest (not the AJV compiled validator). */
82
- schema: Record<string, unknown>;
83
- }
84
-
85
1
  /**
86
2
  * Closed enum of view slot names. Mirror of
87
3
  * `spec/schemas/view-slots.schema.json#/$defs/SlotName`.
@@ -91,7 +7,7 @@ type TSlotName = 'card.title.right' | 'card.subtitle.left' | 'card.footer.left'
91
7
  * Closed enum of input-type names for plugin settings. Mirror of
92
8
  * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.
93
9
  */
94
- type TInputTypeName = 'string-list' | 'single-string' | 'boolean-flag' | 'integer' | 'enum-pick' | 'enum-multipick' | 'path-glob' | 'regex' | 'secret' | 'key-value-list';
10
+ type TInputTypeName = 'string-list' | 'single-string' | 'boolean-flag' | 'integer' | 'number' | 'enum-pick' | 'enum-multipick' | 'path-glob' | 'regex' | 'secret' | 'key-value-list';
95
11
  /**
96
12
  * Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>` severities. Used by counter, tag, alert, and icon slots for color/contrast hints. The UI maps each severity to a theme-aware tint; plugins do not pick raw colors.
97
13
  */
@@ -199,7 +115,7 @@ interface ActionPrompt {
199
115
  /**
200
116
  * Input-type id from the closed catalog. The UI renders the matching control before dispatch (`single-string`, `enum-pick` and `string-list` today; other types degrade to a graceful 'unsupported' notice).
201
117
  */
202
- inputType: ('string-list' | 'single-string' | 'boolean-flag' | 'integer' | 'enum-pick' | 'enum-multipick' | 'path-glob' | 'regex' | 'secret' | 'key-value-list') & string;
118
+ inputType: ('string-list' | 'single-string' | 'boolean-flag' | 'integer' | 'number' | 'enum-pick' | 'enum-multipick' | 'path-glob' | 'regex' | 'secret' | 'key-value-list') & string;
203
119
  /**
204
120
  * Key under which the collected value is placed in the dispatch `input` body.
205
121
  */
@@ -474,6 +390,13 @@ interface ISetting_Integer extends ISettingCommon {
474
390
  max?: number;
475
391
  step?: number;
476
392
  }
393
+ interface ISetting_Number extends ISettingCommon {
394
+ type: 'number';
395
+ default?: number;
396
+ min?: number;
397
+ max?: number;
398
+ step?: number;
399
+ }
477
400
  interface ISetting_EnumOption {
478
401
  value: string;
479
402
  label: string;
@@ -530,7 +453,7 @@ interface ISetting_KeyValueList extends ISettingCommon {
530
453
  *
531
454
  * Mirror of `input-types.schema.json#/$defs/ISettingDeclaration`.
532
455
  */
533
- type TSettingDeclaration = ISetting_StringList | ISetting_SingleString | ISetting_BooleanFlag | ISetting_Integer | ISetting_EnumPick | ISetting_EnumMultipick | ISetting_PathGlob | ISetting_Regex | ISetting_Secret | ISetting_KeyValueList;
456
+ type TSettingDeclaration = ISetting_StringList | ISetting_SingleString | ISetting_BooleanFlag | ISetting_Integer | ISetting_Number | ISetting_EnumPick | ISetting_EnumMultipick | ISetting_PathGlob | ISetting_Regex | ISetting_Secret | ISetting_KeyValueList;
534
457
  /**
535
458
  * Runtime value type for a setting, derived from its declaration. The
536
459
  * kernel exposes settings to extractors as `Record<string, TSettingValue>`
@@ -554,13 +477,24 @@ type TSettingValue = string | string[] | boolean | number | ISetting_KeyValueLis
554
477
  */
555
478
 
556
479
  /**
557
- * Lifecycle label an extension manifest MAY declare. Presentation-only
558
- * metadata: the non-default values render as a badge next to the
559
- * extension in `sm plugins list` / `sm plugins show` and the Settings
560
- * plugins panel, and the kernel never gates behaviour on it (a
561
- * `deprecated` extension still runs). Default: missing == `stable`,
562
- * and `stable` (declared or defaulted) renders no badge. Mirrors
480
+ * Lifecycle label an extension manifest MAY declare. Renders as a badge
481
+ * next to the extension in `sm plugins list <id>` / `sm plugins show` and
482
+ * the Settings plugins panel for the non-default values.
483
+ *
484
+ * Two values ALSO change behaviour: `experimental` (not ready yet) and
485
+ * `deprecated` (on its way out) flip the extension's installed default
486
+ * to DISABLED, so the extension does not load (does not run, does not
487
+ * register) unless the operator opts in (`sm plugins enable
488
+ * <plugin>/<ext>`, the Settings toggle, or a `settings.json` /
489
+ * `config_plugins` override). The opt-in is a plain enable override,
490
+ * once set it wins over the installed default exactly like any other
491
+ * extension (so a deprecated extension can still be kept running during
492
+ * a migration). The remaining values are presentation-only and default
493
+ * to ENABLED: `beta` runs by default with a badge, `stable` (declared
494
+ * or defaulted) runs with no badge. Missing == `stable` == enabled, no
495
+ * badge. Mirrors
563
496
  * `spec/schemas/extensions/base.schema.json#/properties/stability`.
497
+ *
564
498
  * Deliberately a superset of the node-level annotations enum (which has
565
499
  * no `beta`): this describes the maturity of the extension itself, not
566
500
  * of a scanned node.
@@ -620,8 +554,9 @@ interface IExtensionBase {
620
554
  description: string;
621
555
  /**
622
556
  * Optional lifecycle label (`experimental` / `beta` / `stable` /
623
- * `deprecated`). Missing == `stable`, no badge rendered. See
624
- * `TExtensionStability` for the full semantics.
557
+ * `deprecated`). Missing == `stable`, no badge rendered. `experimental`
558
+ * and `deprecated` additionally flip the installed default to disabled.
559
+ * See `TExtensionStability` for the full semantics.
625
560
  */
626
561
  stability?: TExtensionStability;
627
562
  /**
@@ -657,14 +592,107 @@ interface IExtensionBase {
657
592
  * `viewContributions` with the structure-as-truth refactor. Each
658
593
  * entry maps a local contribution id (kebab-case, unique within the
659
594
  * extension) to an `IViewContribution` that picks a view slot by
660
- * name from the closed catalog. Only `extractor` and `analyzer` kinds
661
- * may declare this field.
595
+ * name from the closed catalog. Declared by `extractor` and
596
+ * `analyzer` kinds (emitted during scan / graph evaluation) and by
597
+ * `action` kinds (emitted from the Action's scan-time `project()`
598
+ * self-projection, see `IActionProjectionContext`).
662
599
  */
663
600
  ui?: Record<string, IViewContribution>;
664
601
  /** Runtime-only, absolute path of the extension entry file. */
665
602
  entry?: string;
666
603
  }
667
604
 
605
+ /**
606
+ * Extension registry, six kinds, first-class, loaded through a single API.
607
+ *
608
+ * The `IExtension` shape is aligned with `spec/schemas/extensions/base.schema.json`.
609
+ * Kind-specific manifests (provider / extractor / analyzer / action / formatter /
610
+ * hook) extend this base structurally; the registry stores the base view
611
+ * and each kind's code carries its own fuller type where needed.
612
+ *
613
+ * **Spec § A.6, qualified ids.** Every extension is keyed in the registry
614
+ * by `<pluginId>/<id>` (e.g. `core/annotations`, `core/slash-command`,
615
+ * `my-plugin/my-extractor`). `IExtension.id` carries the **short** id as authored;
616
+ * `IExtension.pluginId` carries the namespace; the registry composes the
617
+ * qualifier internally and exposes lookup APIs that operate on either form
618
+ * (qualified for direct lookup, kind-scoped listing for enumeration).
619
+ *
620
+ * Boot invariant: `new Registry()` is empty. `registry.totalCount() === 0`
621
+ * when the kernel boots with zero extensions. This is the data side of the
622
+ * `kernel-empty-boot` conformance contract.
623
+ */
624
+
625
+ type ExtensionKind = 'provider' | 'extractor' | 'analyzer' | 'action' | 'formatter' | 'hook';
626
+ declare const EXTENSION_KINDS: readonly ExtensionKind[];
627
+ interface IExtension {
628
+ /** Short (unqualified) extension id, injected by the loader from the leaf folder name. */
629
+ id: string;
630
+ /** Owning plugin namespace, injected by the loader from the plugin folder name. */
631
+ pluginId: string;
632
+ kind: ExtensionKind;
633
+ version: string;
634
+ /** Required short description; surfaced in `sm <kind>s list` and the UI. */
635
+ description: string;
636
+ /**
637
+ * Optional lifecycle label (`IExtensionBase.stability`). Carried on the
638
+ * registry view so the enabled-resolver can read it: `experimental`
639
+ * flips an extension's installed default to disabled. Absent == stable.
640
+ */
641
+ stability?: TExtensionStability;
642
+ entry?: string;
643
+ }
644
+ /**
645
+ * Compose the qualified registry key for an extension. Single source of
646
+ * truth so callers don't reinvent the format and a future change (e.g. a
647
+ * different separator) lands in one place.
648
+ */
649
+ declare function qualifiedExtensionId(pluginId: string, id: string): string;
650
+ declare class DuplicateExtensionError extends Error {
651
+ constructor(kind: ExtensionKind, qualifiedId: string);
652
+ }
653
+ declare class Registry {
654
+ #private;
655
+ constructor();
656
+ register(ext: IExtension): void;
657
+ /**
658
+ * Lookup by qualified id (`<pluginId>/<id>`). Returns `undefined` when
659
+ * no extension of that kind is registered under the qualifier.
660
+ */
661
+ get(kind: ExtensionKind, qualifiedId: string): IExtension | undefined;
662
+ /**
663
+ * Convenience wrapper that composes the qualified id for the caller.
664
+ * Equivalent to `get(kind, qualifiedExtensionId(pluginId, id))`.
665
+ */
666
+ find(kind: ExtensionKind, pluginId: string, id: string): IExtension | undefined;
667
+ all(kind: ExtensionKind): IExtension[];
668
+ count(kind: ExtensionKind): number;
669
+ totalCount(): number;
670
+ }
671
+
672
+ /**
673
+ * Step 9.6.6, runtime annotation-contribution catalog types.
674
+ *
675
+ * Lives in its own module (rather than `kernel/index.ts`) so consumers
676
+ * deep inside the kernel, `IAnalyzerContext`, the BFF route factories,
677
+ * future Action contexts, can depend on the catalog shape without
678
+ * dragging the whole kernel barrel and risking a cycle.
679
+ */
680
+ /**
681
+ * Single row of the runtime annotation-contribution catalog surfaced by
682
+ * `kernel.getRegisteredAnnotationKeys()`. One row per (plugin × key)
683
+ * tuple. Built-in catalog keys from `annotations.schema.json` are NOT
684
+ * included, this catalog is plugin-only; the UI knows the built-in
685
+ * catalog via the schema bundle.
686
+ */
687
+ interface IRegisteredAnnotationKey {
688
+ pluginId: string;
689
+ key: string;
690
+ location: 'namespaced' | 'root';
691
+ ownership: 'exclusive' | 'shared';
692
+ /** Inline JSON Schema as declared in the manifest (not the AJV compiled validator). */
693
+ schema: Record<string, unknown>;
694
+ }
695
+
668
696
  /**
669
697
  * Domain types, byte-aligned with `spec/schemas/{node,link,issue,scan-result}.schema.json`.
670
698
  *
@@ -897,6 +925,16 @@ interface Node {
897
925
  externalRefs?: IExternalRef[];
898
926
  frontmatter?: Record<string, unknown>;
899
927
  tokens?: TripleSplit;
928
+ /**
929
+ * File modification time (`mtime`) in Unix milliseconds, captured at
930
+ * scan time from the on-disk `lstat`. Absent for virtual / derived
931
+ * nodes (`virtual === true`, no backing file) and for nodes built by a
932
+ * Provider `walk()` that does not stat its sources. Persisted to
933
+ * `scan_nodes.modified_at_ms` and surfaced on `/api/nodes` /
934
+ * `/api/scan` so the UI can show and sort a "last modified" column.
935
+ * NOT content: never participates in `bodyHash` / `frontmatterHash`.
936
+ */
937
+ modifiedAtMs?: number;
900
938
  /**
901
939
  * Step 9.6.2, sidecar denormalisation surface. Populated by the
902
940
  * orchestrator at scan time; absent when the orchestrator did not
@@ -2190,6 +2228,15 @@ interface IRawNode {
2190
2228
  frontmatterRaw: string;
2191
2229
  /** Parsed frontmatter, or `{}` when absent / unparseable. */
2192
2230
  frontmatter: Record<string, unknown>;
2231
+ /**
2232
+ * File modification time (`mtime`) in Unix milliseconds, captured by
2233
+ * the kernel walker from the same `lstat` that guards the read (zero
2234
+ * extra syscalls). Threaded onto the persisted `Node` as
2235
+ * `modifiedAtMs`. Optional: a Provider that ships its own `walk()` and
2236
+ * does not stat its sources MAY omit it; virtual / derived nodes carry
2237
+ * no file and never set it.
2238
+ */
2239
+ modifiedAtMs?: number;
2193
2240
  /**
2194
2241
  * Parser diagnostics (audit L1). Populated by the walker when the
2195
2242
  * parser surfaced `IParseIssue` entries (e.g. malformed YAML).
@@ -2893,6 +2940,12 @@ interface IAnalyzerOrphanSidecar {
2893
2940
  interface IAnalyzerContext {
2894
2941
  nodes: Node[];
2895
2942
  links: Link[];
2943
+ /**
2944
+ * Resolved values of the analyzer's declared `settings`, populated
2945
+ * from project config + user overrides. Empty object when no settings
2946
+ * are declared.
2947
+ */
2948
+ settings: Record<string, unknown>;
2896
2949
  /**
2897
2950
  * Step 9.6.2, orphaned sidecars discovered during the scan walk.
2898
2951
  * Empty when sidecar discovery did not run (legacy callers) or
@@ -2984,6 +3037,20 @@ interface IAnalyzerContext {
2984
3037
  * `runScan` sites that never wired the field through).
2985
3038
  */
2986
3039
  reservedNodePaths?: ReadonlySet<string>;
3040
+ /**
3041
+ * Links the post-walk lift judged genuinely broken: target matches no
3042
+ * node `path` AND the stripped trigger matches no entry in the cross-
3043
+ * kind name index (`spec/architecture.md` §Provider · resolution
3044
+ * rules). Computed once per scan by the orchestrator from the same
3045
+ * `deriveNodeIdentifiers`-backed index the confidence-lift transform
3046
+ * uses, so a link that resolves only via a filename / dirname
3047
+ * identifier is NOT in the set. Membership is by object identity (the
3048
+ * orchestrator threads the SAME link objects). The single consumer is
3049
+ * `core/reference-broken`, which projects one issue per member (after
3050
+ * its `referenceablePaths` escape hatch). Absent for legacy callers
3051
+ * that never wired the field through, the rule then emits nothing.
3052
+ */
3053
+ brokenLinks?: ReadonlySet<Link>;
2987
3054
  /**
2988
3055
  * Absolute path of the scan's project root (cwd of the invocation).
2989
3056
  * Threaded into the analyzer pass so an analyzer that needs to
@@ -3126,6 +3193,32 @@ interface IActionContext {
3126
3193
  */
3127
3194
  settings: Record<string, unknown>;
3128
3195
  }
3196
+ /**
3197
+ * Read-only graph context handed to an Action's scan-time `project()`
3198
+ * method. Mirrors the Analyzer emit path (`IAnalyzerContext`): the
3199
+ * Action sees the full merged graph (`nodes` + `links`) and emits its
3200
+ * own per-node view contributions via `emitContribution`, supplying the
3201
+ * target node path explicitly because, like the Analyzer, it walks the
3202
+ * whole graph rather than running per-node.
3203
+ *
3204
+ * The contribution is declared in the Action's manifest `ui` map and
3205
+ * passed BY REFERENCE (same object-identity model as Extractor /
3206
+ * Analyzer emit). The orchestrator validates the payload against the
3207
+ * slot's schema at call time, dropping invalid emissions with an
3208
+ * `extension.error` event.
3209
+ *
3210
+ * `project()` is strictly DETERMINISTIC and side-effect-free: no writes,
3211
+ * no runner, no IO. It runs during the scan's contribution phase on
3212
+ * EVERY scan, exactly like an Analyzer's emit path, so its cost is the
3213
+ * same per-scan cost as today's projector analyzers. Even an Action
3214
+ * whose `invoke` is `mode: 'probabilistic'` MUST keep `project()`
3215
+ * deterministic, only `invoke` may be probabilistic.
3216
+ */
3217
+ interface IActionProjectionContext {
3218
+ readonly nodes: readonly Node[];
3219
+ readonly links: readonly Link[];
3220
+ emitContribution(nodePath: string, ref: IViewContribution, payload: unknown): void;
3221
+ }
3129
3222
  /**
3130
3223
  * Declarative filter applied by `--all` fan-out, UI button gating, and
3131
3224
  * `sm actions show`. Same shape used by Extractor and Analyzer so the
@@ -3182,6 +3275,23 @@ interface IAction extends IExtensionBase {
3182
3275
  * kernel materialises any returned `writes` after the call.
3183
3276
  */
3184
3277
  invoke?: <TInput, TReport>(input: TInput, ctx: IActionContext) => IActionResult<TReport>;
3278
+ /**
3279
+ * Optional scan-time self-projection. When present, the orchestrator
3280
+ * calls it during the contribution phase (right after the analyzer
3281
+ * pass) with read-only graph access, and the Action emits its OWN
3282
+ * `inspector.action.button` (or any declared `ui` contribution) per
3283
+ * node. This replaces the former "projector analyzer" pattern: the
3284
+ * button now lives with the Action that dispatches it, not in a
3285
+ * sibling Analyzer.
3286
+ *
3287
+ * MUST be deterministic and side-effect-free (no writes, no runner,
3288
+ * no IO), exactly like an Analyzer's emit path. The button declares
3289
+ * its own qualified id as `actionId` in the payload. Actions that ship
3290
+ * for the future probabilistic runner / record path leave it absent;
3291
+ * an Action MAY declare both `project` and `invoke` (advertiser +
3292
+ * executor), or only one.
3293
+ */
3294
+ project?(ctx: IActionProjectionContext): void;
3185
3295
  }
3186
3296
 
3187
3297
  /**
@@ -3202,6 +3312,12 @@ interface IFormatterContext {
3202
3312
  nodes: Node[];
3203
3313
  links: Link[];
3204
3314
  issues: Issue[];
3315
+ /**
3316
+ * Resolved values of the formatter's declared `settings`, populated
3317
+ * from project config + user overrides. Empty object when no settings
3318
+ * are declared.
3319
+ */
3320
+ settings: Record<string, unknown>;
3205
3321
  /**
3206
3322
  * Full persisted scan, when the caller has it on hand. Optional so
3207
3323
  * formatters that only consume (nodes, links, issues) keep working
@@ -3305,6 +3421,12 @@ declare const HOOK_TRIGGERS: readonly THookTrigger[];
3305
3421
  * Deterministic hooks SHOULD ignore the field.
3306
3422
  */
3307
3423
  interface IHookContext {
3424
+ /**
3425
+ * Resolved values of the hook's declared `settings`, populated from
3426
+ * project config + user overrides. Empty object when no settings are
3427
+ * declared.
3428
+ */
3429
+ settings: Record<string, unknown>;
3308
3430
  /** The raw event the dispatcher matched. */
3309
3431
  event: {
3310
3432
  type: THookTrigger;
@@ -3648,6 +3770,16 @@ interface IScanExtensions {
3648
3770
  * advisory until the job subsystem ships once the job subsystem ships.
3649
3771
  */
3650
3772
  hooks?: IHook[];
3773
+ /**
3774
+ * Optional enabled actions. When supplied, the orchestrator runs the
3775
+ * action-projection pass right after the analyzer pass: every action
3776
+ * carrying a scan-time `project()` self-projection emits its own view
3777
+ * contributions (e.g. `inspector.action.button`) onto the merged
3778
+ * graph. Actions without `project` (only `invoke`) ride along inert.
3779
+ * Absent → no projection pass runs (the gate is the composed enabled
3780
+ * set, so a disabled / experimental action never reaches here).
3781
+ */
3782
+ actions?: IAction[];
3651
3783
  }
3652
3784
  interface RunScanOptions {
3653
3785
  /**