@skill-map/spec 0.29.0 → 0.31.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.
- package/CHANGELOG.md +215 -0
- package/architecture.md +41 -4
- package/cli-contract.md +10 -0
- package/conformance/cases/plugin-missing-ui-rejected.json +4 -3
- package/conformance/coverage.md +1 -0
- package/db-schema.md +2 -0
- package/index.json +12 -11
- package/package.json +1 -1
- package/plugin-author-guide.md +26 -22
- package/schemas/link.schema.json +4 -3
- package/schemas/node.schema.json +10 -1
- package/schemas/project-config.schema.json +18 -8
- package/schemas/signal.schema.json +89 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,220 @@
|
|
|
1
1
|
# Spec changelog
|
|
2
2
|
|
|
3
|
+
## 0.31.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 29fb253: Active-provider lens model, Signal IR scaffold, numeric `Confidence`, MCP virtual nodes, OpenAI Codex provider, and the Phase 4b extractor mudanza in one coherent migration.
|
|
8
|
+
|
|
9
|
+
**Spec foundation**
|
|
10
|
+
|
|
11
|
+
- New `activeProvider` field on `project-config.schema.json` (per-project lens id, drives the new drop+rescan lifecycle and the per-extension `precondition.provider` gating).
|
|
12
|
+
- New `signal.schema.json` defining the multi-candidate `Signal` IR emitted by extractors via `ctx.emitSignal()`.
|
|
13
|
+
- `Node.virtual` (boolean) and `Node.derivedFrom` (string[]) on `node.schema.json` so synthetic / derived entities (MCP servers, future Codex AGENTS.md cascade) coexist with filesystem-backed nodes.
|
|
14
|
+
- `link.confidence` migrates from the `'high' | 'medium' | 'low'` string union to `number` in `[0..1]`. The SQL column flips to `REAL` with a range check.
|
|
15
|
+
- `spec/architecture.md` gets a top-level **Active Provider Lens** section and an **Extractor · Signal IR (opt-in)** subsection. `spec/cli-contract.md` documents the lens-change UX. `spec/db-schema.md` notes the scan-zone drop on lens change. Coverage matrix gains a row for the Signal schema.
|
|
16
|
+
|
|
17
|
+
**Kernel**
|
|
18
|
+
|
|
19
|
+
- `Signal` / `SignalCandidate` / `SignalRange` / `SignalContext` types alongside `Link` in `kernel/types.ts`. `ConfidenceTier` constants (`HIGH = 0.9`, `MEDIUM = 0.6`, `LOW = 0.3`) preserve bucket-thinking call sites.
|
|
20
|
+
- New `IExtractorCallbacks.emitSignal` and `IExtractorCallbacks.emitNode` plus the `IEmittedNode` payload. Orchestrator wires both with validators that drop off-spec emissions via `extension.error` events; the walker merges virtual nodes into the accumulator with first-wins dedup.
|
|
21
|
+
- New `orchestrator/resolver.ts` scaffold (first-candidate-wins today; provider `resolverRules` land later).
|
|
22
|
+
- `frontmatter-toml` parser registered in the kernel parser registry (`smol-toml@1.6.1` pinned).
|
|
23
|
+
- `precondition.provider` is now enforced in `computeCacheDecision`: an extractor whose manifest declares `['claude']` only runs on nodes the `claude` provider classified.
|
|
24
|
+
|
|
25
|
+
**Lens model wiring**
|
|
26
|
+
|
|
27
|
+
- `src/core/config/active-provider.ts`: `resolveActiveProvider(cwd)` with filesystem auto-detect (`.claude/` / `.gemini/` / `.codex/` / `AGENTS.md` / `.cursor/`) and `isExtensionActiveUnderLens` predicate.
|
|
28
|
+
- `src/cli/util/scan-zone-drop.ts`: atomic `DELETE FROM scan_*` helper, shared by `sm db reset` and the new `sm config set activeProvider` hook.
|
|
29
|
+
- `sm config set activeProvider <id>` drops the scan zone and prints the lens-switch receipt with a hint to rescan.
|
|
30
|
+
- BFF: `GET / PATCH /api/active-provider` with the same drop-on-switch semantic, registered before the catch-all.
|
|
31
|
+
- Settings UI: new "Active provider" row in the Project tab (now at the top of the section), confirm dialog before the destructive switch, post-switch announcement.
|
|
32
|
+
|
|
33
|
+
**Numeric `Confidence` migration**
|
|
34
|
+
|
|
35
|
+
Atomic across spec / kernel / SQL / extractors / tests:
|
|
36
|
+
|
|
37
|
+
- `Confidence = number` in `kernel/types.ts`; `enum-parsers.ts` range-checks `[0..1]`.
|
|
38
|
+
- `validateLink` rejects out-of-range values with a `link-confidence-out-of-range` extension.error.
|
|
39
|
+
- The 5 universal extractors emit calibrated numeric values (`annotations` 1.0, `markdown-link` 0.95, `external-url-counter` 0.3, `at-directive` 0.85/0.5, `slash` 0.8).
|
|
40
|
+
- `sm show` renders confidence as a percent (`85%`).
|
|
41
|
+
- `link-conflict` `rankConfidence` reduces to identity; resolver's `bucketConfidence` bridge removed.
|
|
42
|
+
- Rename heuristic uses `ConfidenceTier.HIGH/MEDIUM` and exposes `renameTierLabel` for the analyzer-id mapping.
|
|
43
|
+
- DB column flips from `TEXT` (enum check) to `REAL` (range check) in `001_initial.sql`.
|
|
44
|
+
|
|
45
|
+
**MCP virtual nodes**
|
|
46
|
+
|
|
47
|
+
- New `core/mcp-tools` extractor: detects `tools: [mcp__<server>__<tool>]` in agent/skill/command frontmatter and emits one virtual MCP node (`mcp://<server>`, `kind: 'mcp'`, `virtual: true`, `derivedFrom: [source]`) plus a `references` link from the source. Idempotent: N skills referencing the same server collapse into one node.
|
|
48
|
+
- Claude provider registers the `mcp` kind in its catalog (violet, server icon).
|
|
49
|
+
|
|
50
|
+
**OpenAI Codex provider (MVP)**
|
|
51
|
+
|
|
52
|
+
- New `src/plugins/openai/` bundle: declarative `read: { extensions: ['.toml'], parser: 'toml' }`, classifies `.codex/agents/*.toml` as `agent`. Schema mirrors Codex sub-agent fields (`name`, `description`, `model`, `instructions`, `tools`, `mcp_servers`, `approval_policy`, `sandbox_mode`).
|
|
53
|
+
- `BUNDLE_ORDER` extended in the built-ins generator. 5 bundles, 29 built-in extensions total.
|
|
54
|
+
|
|
55
|
+
**Phase 4b extractor mudanza**
|
|
56
|
+
|
|
57
|
+
- `at-directive` and `slash` move from `src/plugins/core/extractors/` to `src/plugins/claude/extractors/`. Both declare `pluginId: 'claude'` and `precondition: { provider: ['claude'] }`. Universal extractors (markdown-link, external-url-counter, annotations, tools-count, mcp-tools) stay in `core/`.
|
|
58
|
+
|
|
59
|
+
**Settings UI polish**
|
|
60
|
+
|
|
61
|
+
- `<sm-settings-project>` reorders to put the Active provider selector first.
|
|
62
|
+
- `<sm-kind-palette>` filters kinds with zero nodes; the left palette no longer shows always-empty buttons for kinds the loaded set has no instances of. New `__tests__/kind-palette.spec.ts` regression-guards the filter.
|
|
63
|
+
|
|
64
|
+
## User-facing
|
|
65
|
+
|
|
66
|
+
Settings → Project: new Active provider dropdown switches the runtime lens (Claude / Gemini / Codex / Cursor). Tools matching `mcp__name__*` surface MCP nodes in the graph. Codex `.toml` sub-agents under `.codex/agents/` are classified. Link confidence renders as percent.
|
|
67
|
+
|
|
68
|
+
## 0.30.0
|
|
69
|
+
|
|
70
|
+
### Minor Changes
|
|
71
|
+
|
|
72
|
+
- 5f4b181: Remove `@skill-map/testkit` and `examples/hello-world` from the monorepo.
|
|
73
|
+
The packaged plugin-author helper layer is retired. Plugin authors test
|
|
74
|
+
extensions by building fake `ctx` literals against the public types
|
|
75
|
+
re-exported from `@skill-map/cli` (`IExtractor`, `IAnalyzer`,
|
|
76
|
+
`IFormatter`, the matching `*Context` shapes, `Node`, `Link`, `Issue`).
|
|
77
|
+
Reason: zero downstream consumers in the public ecosystem after Step
|
|
78
|
+
9.3; the maintenance cost of an independently-versioned npm package +
|
|
79
|
+
its own changesets, validate phases, and narrative outweighed the value
|
|
80
|
+
of a thin packaged helper layer.
|
|
81
|
+
|
|
82
|
+
**`spec/plugin-author-guide.md`:**
|
|
83
|
+
|
|
84
|
+
- §Testing rewritten as "Testing your plugin": shows the fake-`ctx`
|
|
85
|
+
pattern inline (extractor + analyzer + formatter + probabilistic
|
|
86
|
+
runner), with the public types coming from `@skill-map/cli`.
|
|
87
|
+
- §Stability footer updated to reference Step 10 for future
|
|
88
|
+
Action / Hook testing patterns instead of testkit coverage.
|
|
89
|
+
- §Providers / Actions advisory wording no longer references the
|
|
90
|
+
testkit roadmap.
|
|
91
|
+
|
|
92
|
+
**`spec/architecture.md`:**
|
|
93
|
+
|
|
94
|
+
- `src/` directory tree drops the `testkit/` row.
|
|
95
|
+
- Qualified-id example list swaps `hello-world/greet` for the
|
|
96
|
+
generic `my-plugin/my-extractor`.
|
|
97
|
+
|
|
98
|
+
**Monorepo plumbing** (no end-user impact):
|
|
99
|
+
|
|
100
|
+
- `pnpm-workspace.yaml`, root `package.json`, `Dockerfile`, and
|
|
101
|
+
`scripts/check-changeset.js` drop the `testkit/` and
|
|
102
|
+
`examples/hello-world/` entries.
|
|
103
|
+
- `context/scripts.md`, `context/kernel.md`, `context/notebooklm.md`,
|
|
104
|
+
`ROADMAP.md`, `CONTRIBUTING.md`, `AGENTS.md`, `.claude/agents/commit.md`,
|
|
105
|
+
and `scripts/build-user-changelog.js` updated to reflect the
|
|
106
|
+
two-public-package surface (`@skill-map/spec` + `@skill-map/cli`).
|
|
107
|
+
- `src/__tests__/integration/dockerfile-demo-assets.spec.ts` drops
|
|
108
|
+
the obsolete `COPY` assertions for both removed workspaces.
|
|
109
|
+
- JSDoc in `src/kernel/registry.ts` replaces the `hello-world/greet`
|
|
110
|
+
example with `my-plugin/my-extractor`.
|
|
111
|
+
|
|
112
|
+
**`web/modules/roadmap.js`:**
|
|
113
|
+
|
|
114
|
+
- Step 9 card (EN + ES, release tag + brief) drops the
|
|
115
|
+
`@skill-map/testkit` mention.
|
|
116
|
+
|
|
117
|
+
**Post-merge action required**: run
|
|
118
|
+
`/usr/bin/npm deprecate "@skill-map/testkit@*" "Subsumed: plugin authors
|
|
119
|
+
test against @skill-map/cli types directly. See
|
|
120
|
+
https://github.com/crystian/skill-map/blob/main/spec/plugin-author-guide.md."`
|
|
121
|
+
against the real `npm` binary (NOT the `pnpm`-aliased `npm` in the
|
|
122
|
+
maintainer's shell, which fails with `ERR_PNPM_REGISTRY_ERROR: 404 Not
|
|
123
|
+
Found` on the deprecate endpoint). `/usr/bin/` bypasses the zsh alias;
|
|
124
|
+
`command npm` and `\npm` are equivalent escapes. Latest published
|
|
125
|
+
version is `0.5.2`; the wildcard range covers every prior tag so anyone
|
|
126
|
+
with the package pinned sees the deprecation notice.
|
|
127
|
+
|
|
128
|
+
- d95e5b8: Remove the `scan.extraFolders` config key. Project-local persistent
|
|
129
|
+
extension of the indexed scan no longer exists; to walk a directory
|
|
130
|
+
outside the project root pass it as a positional argument to
|
|
131
|
+
`sm scan [roots...]` (per-invocation, not persisted). The narrower
|
|
132
|
+
`scan.referencePaths` key (validate links against on-disk files
|
|
133
|
+
without indexing them) is unaffected.
|
|
134
|
+
|
|
135
|
+
**Spec (`spec/`):**
|
|
136
|
+
|
|
137
|
+
- `spec/schemas/project-config.schema.json`: `extraFolders` block
|
|
138
|
+
deleted. `scan.referencePaths` description trimmed of cross-
|
|
139
|
+
references and now reads stand-alone.
|
|
140
|
+
- `spec/architecture.md` §Config layering: `PROJECT_LOCAL_ONLY_KEYS`
|
|
141
|
+
catalogue drops `scan.extraFolders`.
|
|
142
|
+
- `spec/plugin-author-guide.md`: the "the only way to scan paths
|
|
143
|
+
outside the project is `scan.extraFolders`" sentence rewrites to
|
|
144
|
+
point at positional roots.
|
|
145
|
+
- `spec/index.json` regenerated.
|
|
146
|
+
|
|
147
|
+
**Kernel + config (`src/kernel/`, `src/config/`, `src/core/config/`):**
|
|
148
|
+
|
|
149
|
+
- `IScanConfig` drops `extraFolders: string[]`.
|
|
150
|
+
- `PROJECT_LOCAL_ONLY_KEYS` and `PRIVACY_SENSITIVE_KEYS` lose the
|
|
151
|
+
entry.
|
|
152
|
+
- `projectPathExposure` collapses the two-branch list-check to one.
|
|
153
|
+
- `defaults.json` drops the `extraFolders: []` line.
|
|
154
|
+
|
|
155
|
+
**Runtime (`src/core/runtime/`):**
|
|
156
|
+
|
|
157
|
+
- `resolveScanRoots(inputs)` simplifies to `{ positionalRoots } =>
|
|
158
|
+
string[]`; no more `IScanRootsInputs.extraFolders`,
|
|
159
|
+
`IScanRootsResolution.fromExtra`, or `emitRootsAdvisory()`.
|
|
160
|
+
- The `includingExtraFoldersAdvisory` text catalog entry is removed.
|
|
161
|
+
|
|
162
|
+
**CLI (`src/cli/`):**
|
|
163
|
+
|
|
164
|
+
- `sm scan` help text loses the extraFolders sentence; positional
|
|
165
|
+
roots are now the documented way to extend the scan.
|
|
166
|
+
- `sm serve` boot banner reads only `scan.referencePaths` from the
|
|
167
|
+
effective config; the banner row labelled `Extras` (and the
|
|
168
|
+
matching shape on `IBannerInput` / `IFigletInput`) is removed.
|
|
169
|
+
`Refs` stays.
|
|
170
|
+
- `sm config set --yes` description trimmed to reflect the single
|
|
171
|
+
privacy-sensitive key remaining.
|
|
172
|
+
|
|
173
|
+
**Server (`src/server/`):**
|
|
174
|
+
|
|
175
|
+
- `WatcherService` no longer reads config to compute roots; it walks
|
|
176
|
+
`['.']` unconditionally. `loadConfig` and `resolveScanRoots`
|
|
177
|
+
imports drop. `restart()` is still useful (and still wired by
|
|
178
|
+
`PATCH /api/project-preferences`) so the side-set walk picks up
|
|
179
|
+
fresh `scan.referencePaths` on the next batch.
|
|
180
|
+
- `PATCH /api/project-preferences`: AJV body schema, `IPatchBody`,
|
|
181
|
+
`IProjectPreferencesEnvelope`, `IPlannedWrite.key`, `collectWrites`
|
|
182
|
+
all collapse to a single `referencePaths` branch.
|
|
183
|
+
- Catalog strings adjusted (the `extraFolders` example dropped from
|
|
184
|
+
`projectPrefsScanNotObject` etc).
|
|
185
|
+
|
|
186
|
+
**UI (`ui/src/`):**
|
|
187
|
+
|
|
188
|
+
- Settings → Project drops the entire `extraFolders` row (HTML, TS
|
|
189
|
+
signal + computed + add/remove handlers, i18n strings, mocks).
|
|
190
|
+
- `IProjectPreferencesApi` and `IProjectPreferencesPatchApi` lose
|
|
191
|
+
`extraFolders`.
|
|
192
|
+
- Test mocks (`app.spec.ts`, `graph-view.spec.ts`,
|
|
193
|
+
`inspector-view.spec.ts`) updated.
|
|
194
|
+
|
|
195
|
+
**Tests:**
|
|
196
|
+
|
|
197
|
+
- `server/routes/__tests__/project-preferences-route.spec.ts`: 5
|
|
198
|
+
PATCH cases remapped from `extraFolders` to `referencePaths`.
|
|
199
|
+
- `kernel/config/__tests__/config-loader.spec.ts`: strip-test
|
|
200
|
+
renamed and split.
|
|
201
|
+
- `core/runtime/__tests__/scan-roots.spec.ts`: drops 3 cases that
|
|
202
|
+
passed `extraFolders`; keeps the positional + default cases.
|
|
203
|
+
- `core/config/__tests__/config-helper.spec.ts`:
|
|
204
|
+
`PROJECT_LOCAL_ONLY_KEYS` catalogue assertion narrowed; the
|
|
205
|
+
`target=project` rejection test now targets `scan.referencePaths`.
|
|
206
|
+
|
|
207
|
+
**Backward compatibility note**: existing `settings.local.json` files
|
|
208
|
+
that still carry `scan.extraFolders` keep loading without error. The
|
|
209
|
+
loader's per-key resilience drops the unknown key with a generic
|
|
210
|
+
"unknown key ignored" warning; nothing crashes, the rest of the file
|
|
211
|
+
takes effect. Operators who relied on the key should switch to
|
|
212
|
+
positional roots on `sm scan`.
|
|
213
|
+
|
|
214
|
+
## User-facing
|
|
215
|
+
|
|
216
|
+
We removed `scan.extraFolders`. To extend the scan beyond the project root, pass folders as positional arguments to `sm scan [roots...]`. The `scan.referencePaths` key (validates links against on-disk files without indexing) is unchanged. Existing entries are silently ignored.
|
|
217
|
+
|
|
3
218
|
## 0.29.0
|
|
4
219
|
|
|
5
220
|
### Minor Changes
|
package/architecture.md
CHANGED
|
@@ -39,6 +39,32 @@ Any conforming implementation, reference or third-party, MUST respect these boun
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## Active Provider Lens
|
|
43
|
+
|
|
44
|
+
A skill-map project sees its filesystem through exactly one **active provider lens** at any time. The lens is the provider whose extractors, classifiers, and resolution rules apply to the whole project during a scan. All other enabled providers stay registered but their provider-specific extractors are skipped.
|
|
45
|
+
|
|
46
|
+
The lens is project-scope state. It lives in `.skill-map/settings.json` as the `activeProvider` key (see [`project-config.schema.json`](./schemas/project-config.schema.json#/properties/activeProvider)). When absent, the kernel auto-detects on first scan from filesystem heuristics (`.claude/` → `claude`, `.gemini/` → `gemini`, `.codex/` or root `AGENTS.md` → `openai`, etc.) and persists the result; if the heuristic is ambiguous or yields no result, the CLI and UI prompt the user to pick one of the enabled providers.
|
|
47
|
+
|
|
48
|
+
### Consequence: one graph per project at a time
|
|
49
|
+
|
|
50
|
+
The persisted scan graph (`scan_*` zone) reflects the project as the active lens sees it. No cross-provider merging happens at storage time. A repo with both `.claude/` and `.gemini/` does NOT show "everyone's nodes at once"; it shows the active lens's view.
|
|
51
|
+
|
|
52
|
+
### Consequence: lens change is destructive of the scan zone
|
|
53
|
+
|
|
54
|
+
Switching the active provider drops the `scan_*` zone atomically (nodes, links, issues, scan-result meta) and triggers a fresh scan under the new lens. The `state_*` zone (jobs, executions, summaries, enrichments, plugin KV, favorites) and the `config_*` zone survive untouched. Annotations (`.sm` sidecars on disk) are filesystem state and are also unaffected; the next scan re-derives the in-DB overlay from them.
|
|
55
|
+
|
|
56
|
+
This is a deliberate trade-off. Keeping two scan graphs simultaneously persisted (one per lens) would re-introduce the cross-provider coordination complexity the lens model exists to avoid. The drop+rescan UX is honest: changing lens means changing the world the graph represents, and the graph regenerates from the source of truth (the filesystem) under the new rules.
|
|
57
|
+
|
|
58
|
+
### Cross-provider read at the provider level
|
|
59
|
+
|
|
60
|
+
A provider plugin MAY declare it reads source files belonging to ANOTHER provider's territory. The canonical example: Cursor's runtime consumes `.claude/skills/` and `.codex/skills/` natively; a Cursor provider in skill-map can therefore claim those paths from its own classifier so that under the Cursor lens, those files appear as Cursor-managed nodes with Cursor's interpretation rules. This is provider-internal logic, not a kernel feature; the lens model neither encourages nor prevents it.
|
|
61
|
+
|
|
62
|
+
### Universal extractors and per-provider extractors
|
|
63
|
+
|
|
64
|
+
The lens does NOT gate the universal extractors that ship under `core/` (markdown links, external URLs, sidecar annotations). Those run regardless of the active provider because their semantics are provider-agnostic. Provider-specific extractors (Claude's `@`-directive parser, Gemini's three-surface `@`-parsers, Cursor's picker-derived references, the future Codex AGENTS.md walker) declare `precondition: { provider: '<id>' }` on their manifest; the orchestrator only invokes them when the node's provider matches AND the provider is the active lens.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
42
68
|
## Ports
|
|
43
69
|
|
|
44
70
|
An implementation MUST expose these five ports. Each is an interface (TypeScript, in the reference impl; equivalent in other languages).
|
|
@@ -72,7 +98,7 @@ The loader enforces two id-uniqueness analyzers during discovery (see [`plugin-a
|
|
|
72
98
|
1. **Directory name == manifest id.** A plugin lives at `<root>/<id>/plugin.json`. A mismatch surfaces as status `invalid-manifest`. This analyzer eliminates same-root collisions by construction.
|
|
73
99
|
2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (e.g. the project default `<cwd>/.skill-map/plugins/` and any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence analyzer applies, coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
|
|
74
100
|
|
|
75
|
-
In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `core/slash`, `core/broken-ref`, `
|
|
101
|
+
In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `core/slash`, `core/broken-ref`, `my-plugin/my-extractor`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts`, `core/` for kernel-internal primitives (every analyzer, the formatter, the cross-vendor extractors `annotations` / `slash` / `at-directive` / `markdown-link` / `external-url-counter` / `stability`) and vendor-specific bundles such as `claude/` (the Claude provider) for Provider integrations whose territory is platform-bound. If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
|
|
76
102
|
|
|
77
103
|
Each plugin (and each built-in bundle) declares a **granularity** that controls how its extensions are toggled. `granularity: 'bundle'` (the default) means the plugin id is the only enable/disable key; `granularity: 'extension'` means each extension is independently toggle-able under its qualified id. The loader's pre-import `resolveEnabled(pluginId)` short-circuit is always coarse (bundle level), when a granularity=`extension` bundle is partially enabled, the import work proceeds and the runtime composer (the CLI's `composeScanExtensions` / `composeFormatters` in `src/cli/util/plugin-runtime.ts`) drops the disabled extensions before they reach the orchestrator. Vendor Provider bundles (`claude`, `gemini`, `agent-skills`) ship as granularity=`bundle` (the platform integration is on or off as a whole); the `core` bundle is granularity=`extension` (every kernel built-in is removable, satisfying §Boot invariant: "no extension is privileged"). See [`plugin-author-guide.md` §Granularity, bundle vs extension](./plugin-author-guide.md#granularity--bundle-vs-extension) for the author-facing summary.
|
|
78
104
|
|
|
@@ -238,6 +264,19 @@ The `Extractor` runtime contract is `extract(ctx) → void`. The extractor emits
|
|
|
238
264
|
|
|
239
265
|
Extractors are deterministic-only; `ctx.runner` is NOT exposed on the Extractor context. LLM-driven enrichment of a node is an Action concern (queued as a job), not an Extractor concern.
|
|
240
266
|
|
|
267
|
+
### Extractor · Signal IR (opt-in)
|
|
268
|
+
|
|
269
|
+
In addition to the `emitLink` path, Extractors MAY emit **Signals** via `ctx.emitSignal(signal)`. A Signal is a candidate detection: one or many alternative interpretations of the same body or frontmatter location, each carrying its own kind, target, confidence, and rationale. See [`signal.schema.json`](./schemas/signal.schema.json) for the full contract. The Signal IR is opt-in; an extractor whose detection is unambiguous (sidecar `supersedes`, `[text](file.md)` markdown links, plain `https://…` URLs) is encouraged to keep emitting Links directly with `ctx.emitLink`. Signals exist for the cases the resolver actually helps: detections where a single body token can plausibly mean several things and the active provider's rules need to decide.
|
|
270
|
+
|
|
271
|
+
The kernel's **resolver phase** runs after extraction completes and before analysis starts. For each Signal, the resolver:
|
|
272
|
+
|
|
273
|
+
1. Filters candidates whose `extractorId` is not enabled (per `plugins.<id>.extensions.<extId>.enabled` overrides).
|
|
274
|
+
2. Applies the active Provider's resolution rules (declared on `IProvider.resolverRules`) to rank surviving candidates: priority order, tie-break by confidence, then by longest range, then by `extractorId` declaration order.
|
|
275
|
+
3. Materialises the winning candidate as a Link (indistinguishable from a Link emitted directly via `emitLink`). The rejected candidates remain accessible to analyzers via `IAnalyzerContext.signals` for collision-detection and conflict-visualization use cases.
|
|
276
|
+
4. Rejects all candidates and emits no Link if every interpretation has confidence below the configured floor.
|
|
277
|
+
|
|
278
|
+
The Signal's `range` field (byte offsets in the source) powers two cross-extractor analyses no Link can support today: collision detection (two extractors emitting Signals with overlapping ranges) and fragmentation detection (an authored intent split across several adjacent Signals). Both surface as analyzer issues, not silent merges.
|
|
279
|
+
|
|
241
280
|
### Extractor · enrichment layer
|
|
242
281
|
|
|
243
282
|
`ctx.enrichNode(partial)` is the only writable surface the Extractor pipeline has on a node. The author's frontmatter on `scan_nodes.frontmatter_json` is read-only from any Extractor. Implementations MUST:
|
|
@@ -408,7 +447,6 @@ src/
|
|
|
408
447
|
├── kernel/ Registry, Orchestrator, domain types, use cases, port interfaces
|
|
409
448
|
├── cli/ Clipanion commands, thin wrappers over kernel
|
|
410
449
|
├── server/ Hono + WebSocket, thin wrapper over kernel
|
|
411
|
-
├── testkit/ Kernel mocks for plugin authors
|
|
412
450
|
└── adapters/
|
|
413
451
|
├── sqlite/ node:sqlite + Kysely + CamelCasePlugin (StoragePort)
|
|
414
452
|
├── filesystem/ real fs (FilesystemPort)
|
|
@@ -459,10 +497,9 @@ One locality class constrains which layers a given key MAY live in. It is enforc
|
|
|
459
497
|
|
|
460
498
|
Members:
|
|
461
499
|
- `allowEditSmFiles`, per-project consent to create / modify `.sm` sidecars.
|
|
462
|
-
- `scan.extraFolders`, additional scan paths (the ONLY way to extend the scan beyond the project root).
|
|
463
500
|
- `scan.referencePaths`, additional link-validation paths.
|
|
464
501
|
|
|
465
|
-
|
|
502
|
+
Both describe disk access the local operator opted into; sharing them via the repo would silently expand every collaborator's scan surface to paths that only make sense on the original author's machine.
|
|
466
503
|
|
|
467
504
|
Adding a new entry is a behaviour change for older installs that wrote the key into a committed file, the value gets stripped at read time. The changeset that adds the entry MUST document the migration.
|
|
468
505
|
|
package/cli-contract.md
CHANGED
|
@@ -76,6 +76,16 @@ etc.). Constraints:
|
|
|
76
76
|
|
|
77
77
|
Everything else under `$HOME` MUST NOT be touched.
|
|
78
78
|
|
|
79
|
+
### Active provider lens
|
|
80
|
+
|
|
81
|
+
The project sees its filesystem through exactly one **active provider lens** at any time. The lens is persisted as `activeProvider` in `<cwd>/.skill-map/settings.json` (see [`project-config.schema.json`](./schemas/project-config.schema.json#/properties/activeProvider) and the [`architecture.md` §Active Provider Lens](./architecture.md#active-provider-lens) section for the full architectural rationale).
|
|
82
|
+
|
|
83
|
+
CLI surfaces:
|
|
84
|
+
|
|
85
|
+
- **Auto-detect on first scan**: when `activeProvider` is absent, `sm scan` and `sm watch` run a filesystem heuristic (`.claude/` → `claude`, `.gemini/` → `gemini`, `.codex/` or root `AGENTS.md` → `openai`, etc.). On unambiguous match, the result is persisted to `settings.json` and the scan proceeds; on no match, the CLI exits non-zero with a "no provider detected, set `activeProvider` in settings or install a provider plugin" message. On ambiguous match (multiple providers detected), the CLI prompts the user interactively (or fails with exit code 2 under `--yes` if no default is configured).
|
|
86
|
+
- **Manual override**: `sm config set activeProvider <id>` switches the lens. The verb drops the `scan_*` zone atomically (see [`db-schema.md`](./db-schema.md#zones)) and triggers an immediate rescan under the new lens. `state_*` and `config_*` zones survive.
|
|
87
|
+
- **No per-scan flag**: there is no `sm scan --provider=<id>` flag. The lens is a project-level decision, not a per-invocation parameter. The drop+rescan cost makes per-invocation switching the wrong default UX.
|
|
88
|
+
|
|
79
89
|
---
|
|
80
90
|
|
|
81
91
|
## Targeted fan-out flags
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"assertions": [
|
|
11
11
|
{ "type": "exit-code", "value": 0 },
|
|
12
12
|
{ "type": "stderr-matches", "pattern": "plugin bad-provider:.*invalid.*must have required property 'ui'" },
|
|
13
|
-
{ "type": "json-path", "path": "$.providers.length", "equals":
|
|
13
|
+
{ "type": "json-path", "path": "$.providers.length", "equals": 5 },
|
|
14
14
|
{ "type": "json-path", "path": "$.providers[0]", "equals": "claude" },
|
|
15
15
|
{ "type": "json-path", "path": "$.providers[1]", "equals": "gemini" },
|
|
16
|
-
{ "type": "json-path", "path": "$.providers[2]", "equals": "
|
|
17
|
-
{ "type": "json-path", "path": "$.providers[3]", "equals": "
|
|
16
|
+
{ "type": "json-path", "path": "$.providers[2]", "equals": "openai" },
|
|
17
|
+
{ "type": "json-path", "path": "$.providers[3]", "equals": "agent-skills" },
|
|
18
|
+
{ "type": "json-path", "path": "$.providers[4]", "equals": "markdown" },
|
|
18
19
|
{ "type": "json-path", "path": "$.nodes.length", "equals": 1 }
|
|
19
20
|
]
|
|
20
21
|
}
|
package/conformance/coverage.md
CHANGED
|
@@ -44,6 +44,7 @@ This file is hand-maintained. A CI check before spec release compares the schema
|
|
|
44
44
|
| 33 | `plugins-doctor.schema.json` |, | 🔴 missing | Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts plus structured issue / warning lists. Direct conformance case pending: prime a scope with one healthy + one invalid-manifest drop-in plugin, run `sm plugins doctor --json`, assert the envelope validates and the invalid plugin appears under `issues[]`. Implementation tests at `src/test/plugins-cli.test.ts` cover the runtime behaviour. |
|
|
45
45
|
| 34 | `conformance-result.schema.json` |, | 🔴 missing | Machine-readable output of `sm conformance run --json`. Self-referential by design (a conformance case would invoke the verb against itself); a direct case is deferred until the runner gains a meta-loopback mode. Implementation tests at `src/test/conformance-cli.test.ts` cover the envelope shape today. |
|
|
46
46
|
| 35 | `user-settings.schema.json` | (indirect via `no-global-scope`) | 🟡 partial | Per-user / per-machine settings file at `~/.skill-map/settings.json` (the narrow `$HOME` exception, see `cli-contract.md` §User-settings file). Direct case is not added because alt-impls MAY choose to not ship an update-check feature, requiring them to produce this file would over-prescribe. The implementation-side AJV round-trip is covered by `src/test/user-settings-store.test.ts` (15 cases: defaults, malformed JSON, schemaVersion mismatch, wrong-type fields, unknown top-level keys, deep-merge writes, off-shape rejection). The behavioral counterpart (no global / user scope) lives at `no-global-scope` in the non-schema table below. |
|
|
47
|
+
| 37 | `signal.schema.json` |, | 🔴 missing | Intermediate Representation (IR) emitted by extractors via `ctx.emitSignal()`; the kernel resolver phase consumes Signals and materialises Links. Opt-in: the existing `ctx.emitLink()` path coexists. Cases required (2): (a) `extractor-emits-signal`, an extractor emits a multi-candidate Signal and the resolver picks the highest-confidence candidate per the active Provider's `resolverRules`; (b) `signal-collision-detection`, two extractors emit Signals with overlapping `range` and the resolver surfaces the collision to analyzers via `IAnalyzerContext.signals`. Blocked by the kernel resolver phase landing in Phase 2 of the active-lens migration. |
|
|
47
48
|
|
|
48
49
|
> **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`skill`, `agent`, `command`, `note` for the built-in Claude Provider; other Providers MAY declare different kinds) live with the Provider that emits them, for the built-in Claude Provider, under `src/extensions/providers/claude/schemas/`. Those schemas are NOT counted in the spec's coverage matrix above; they belong to the Provider's own conformance suite at `src/extensions/providers/claude/conformance/coverage.md`. The same split applies to the cases that exercise Provider-specific kinds (`basic-scan`, `rename-high`, `orphan-detection`), they live in the Provider's `cases/` directory.
|
|
49
50
|
|
package/db-schema.md
CHANGED
|
@@ -36,6 +36,8 @@ Every kernel table belongs to exactly one zone, identified by a mandatory name p
|
|
|
36
36
|
|
|
37
37
|
`sm db reset` drops `scan_*` only (non-destructive, equivalent to forcing the next scan from a clean slate). `sm db reset --state` also drops `state_*` (destructive to operational history). `sm db reset --hard` deletes the DB file entirely. `sm db backup` preserves `state_*` + `config_*`; `scan_*` is always regenerated on demand and is never included in backups.
|
|
38
38
|
|
|
39
|
+
**Active-provider lens change**: switching the `activeProvider` setting (see [`cli-contract.md` §Active provider lens](./cli-contract.md#active-provider-lens) and [`architecture.md` §Active Provider Lens](./architecture.md#active-provider-lens)) drops the `scan_*` zone atomically and triggers a fresh scan under the new lens. Identical effect to `sm db reset` followed by `sm scan`, but bundled as a single transaction so the user never sees an empty graph between the two. `state_*` and `config_*` are preserved across the switch. The `config_plugins` and `config_preferences` rows survive (including the new `activeProvider` value itself).
|
|
40
|
+
|
|
39
41
|
---
|
|
40
42
|
|
|
41
43
|
## Naming conventions (normative)
|
package/index.json
CHANGED
|
@@ -174,21 +174,21 @@
|
|
|
174
174
|
}
|
|
175
175
|
]
|
|
176
176
|
},
|
|
177
|
-
"specPackageVersion": "0.
|
|
177
|
+
"specPackageVersion": "0.31.0",
|
|
178
178
|
"integrity": {
|
|
179
179
|
"algorithm": "sha256",
|
|
180
180
|
"files": {
|
|
181
|
-
"CHANGELOG.md": "
|
|
181
|
+
"CHANGELOG.md": "6c55bfe4d35bb532342ce36284ce682ff9610a03ccb997c90f95bd41b783ef73",
|
|
182
182
|
"README.md": "54c4649fa9742bf2f74423ea78788a7474ce09649cbe1e72a270b606cf16a0a5",
|
|
183
|
-
"architecture.md": "
|
|
184
|
-
"cli-contract.md": "
|
|
183
|
+
"architecture.md": "d7c01c6de2fb49959056bf01847fc290fafe26e094a8660690a9bb404de5694d",
|
|
184
|
+
"cli-contract.md": "196ad728c41ef5467ccfcdfdc983ef937d25a7aca426f622a70b300cd1d68c66",
|
|
185
185
|
"conformance/README.md": "6871dde25b5770ed945284c9e0f749e0768ec3f5ba4966bdb215985789e43887",
|
|
186
186
|
"conformance/cases/kernel-empty-boot.json": "2a5be9c93143d07a16d998df09dcc8fa4ea2d2f9a0bff6417573ed5a770352c1",
|
|
187
187
|
"conformance/cases/no-global-scope.json": "1284763988026d924c0bd78ba8a9f417dc88f5b7e9f4c2b642ae0c447758bfd4",
|
|
188
188
|
"conformance/cases/orphan-markdown-fallback.json": "8ef6e49b7e6532bd845d9f54974a16e537cf98d355f0c5e4f4fb06abac3adcc5",
|
|
189
|
-
"conformance/cases/plugin-missing-ui-rejected.json": "
|
|
189
|
+
"conformance/cases/plugin-missing-ui-rejected.json": "f2fb673ad01308b018f0a5ed0d3d2085b8ffab25230e60bf31569859c5c583cc",
|
|
190
190
|
"conformance/cases/sidecar-end-to-end.json": "dbb3640f95769a36b881855a261f918481edadea13a7eb0765c6090f2417a142",
|
|
191
|
-
"conformance/coverage.md": "
|
|
191
|
+
"conformance/coverage.md": "feb75cd38ddffa6252ed277dd23b8be4228b1ef2f37ce50871d00283bae4f108",
|
|
192
192
|
"conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
|
|
193
193
|
"conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "d6b6e18d4b963b26a292de73348c3396fd4710ab4c4bdd6cf094e581f99ec8d6",
|
|
194
194
|
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json": "6676a89bae5197e23cf50f1c11d596db558ac80f7334a7208fe57d8b92422251",
|
|
@@ -202,11 +202,11 @@
|
|
|
202
202
|
"conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm": "cb04f7f3103b4218b09fd4da92f7ea429588b04c1dac6a9547ce362263b11224",
|
|
203
203
|
"conformance/fixtures/sidecar-example/agent-example.md": "741131403e8c9580d0b7a8c2446cb4502d01f80053b7a2092663de92431aaa82",
|
|
204
204
|
"conformance/fixtures/sidecar-example/agent-example.sm": "8329950d49c69a1199bbe6c06e32b8513973e64207b0db8756b67301e6a1f1e2",
|
|
205
|
-
"db-schema.md": "
|
|
205
|
+
"db-schema.md": "3c34768a6ba34f6d77da84e0cdf73b977dca68ef50679decd131f2a5e20fb593",
|
|
206
206
|
"interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
|
|
207
207
|
"job-events.md": "84206168ac12b536d34470d62f8c8cba95dab181fee66d23203c2cf5dfbee716",
|
|
208
208
|
"job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
|
|
209
|
-
"plugin-author-guide.md": "
|
|
209
|
+
"plugin-author-guide.md": "bf7e01b1a36bfe0287d552f12d2211e76de33daf3cd172b841d46e49f7394878",
|
|
210
210
|
"plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
|
|
211
211
|
"prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
|
|
212
212
|
"schemas/annotations.schema.json": "e39990d47f53e25a1b3a5587a5714486d0b819b8eeaac10d42783a675296aee1",
|
|
@@ -228,16 +228,17 @@
|
|
|
228
228
|
"schemas/input-types.schema.json": "c713b768d0b0e3d0c764afb401189f7fb624a82b4e988b73aab015cf9c67c01f",
|
|
229
229
|
"schemas/issue.schema.json": "fa3344e75f1c3a5304291ca355bb973046552a68871ad6eb4edafca1cd9e1be8",
|
|
230
230
|
"schemas/job.schema.json": "e43e1761c99920beffe1de12ef8f32fe29f97838bd8686742b637c19c4dbb395",
|
|
231
|
-
"schemas/link.schema.json": "
|
|
232
|
-
"schemas/node.schema.json": "
|
|
231
|
+
"schemas/link.schema.json": "2450732829652ece58c853ca97711a8bbb64ac65e52e89e3b51024c073dddc9a",
|
|
232
|
+
"schemas/node.schema.json": "8d0635a80c8e6f22be7fa04071654e857fc052869de15839f4b29593aa4527a3",
|
|
233
233
|
"schemas/plugins-doctor.schema.json": "c1d92f30fdb0080e8cd8f7dc5d43e01aae02a16640bc5eb04811c337a275de58",
|
|
234
234
|
"schemas/plugins-registry.schema.json": "cca7ae65f0c22510ea27ea5ae34e0074f5beb5871a57b005b6b831e6ceaff5c0",
|
|
235
|
-
"schemas/project-config.schema.json": "
|
|
235
|
+
"schemas/project-config.schema.json": "1357e14026b038ac097d0528ee135728b1ddeb383db6a8cb1e345804e62311e5",
|
|
236
236
|
"schemas/refresh-report.schema.json": "54519b8caf86ba84c182f9565be9b5084bc1631ae05e9217ee18f34c0039fff3",
|
|
237
237
|
"schemas/report-base-deterministic.schema.json": "9d318d0181d121097c906ef3af1c52d71c782740bd04cf23418d7627ce2c3ed5",
|
|
238
238
|
"schemas/report-base.schema.json": "a1021e9a59b4df9f99cd92454d797e88469766e7d49f52d231c4645ffdfdad8f",
|
|
239
239
|
"schemas/scan-result.schema.json": "214bc12fbb9946642cbba3b23513dade60e7d6a5b6a9ed3dd0818f135b450185",
|
|
240
240
|
"schemas/sidecar.schema.json": "8856c387477340efbdd0a585d74bfb07a99ba15b9ce593cc67d9efebc67c6bfc",
|
|
241
|
+
"schemas/signal.schema.json": "2540c0014a78ebc902eb71b6815c35fa006c714b57d07dcb7415bd3c3da185b5",
|
|
241
242
|
"schemas/summaries/agent.schema.json": "bf540f9a804f2b43756ab33b7deb0462620d26e88cc9379c75a5f87d3b1b47d8",
|
|
242
243
|
"schemas/summaries/command.schema.json": "c26f6965f77c5058608feb5e7b9f807395de8e015b0dea5efcdb44cb1820551a",
|
|
243
244
|
"schemas/summaries/hook.schema.json": "58420ec485e152fdd21fa3d87337ad74b0d81a48d3b83dd072d4a2d196f78573",
|
package/package.json
CHANGED
package/plugin-author-guide.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plugin author guide
|
|
2
2
|
|
|
3
|
-
How to ship a third-party `skill-map` plugin: directory layout, manifest fields, the six extension kinds, storage choice, version compatibility, dual-mode posture, and how to test the result
|
|
3
|
+
How to ship a third-party `skill-map` plugin: directory layout, manifest fields, the six extension kinds, storage choice, version compatibility, dual-mode posture, and how to unit-test the result against the kernel's public types.
|
|
4
4
|
|
|
5
5
|
This guide is **descriptive prose**, not the normative contract. The normative pieces live in the schemas and the architecture document, every claim here is cross-linked to its source. When the two disagree, [`architecture.md`](./architecture.md) wins.
|
|
6
6
|
|
|
@@ -485,7 +485,7 @@ export default {
|
|
|
485
485
|
|
|
486
486
|
### Providers / Actions
|
|
487
487
|
|
|
488
|
-
These ship later in the v1.x line as bundled built-ins; the spec already pins their manifest shapes. Until
|
|
488
|
+
These ship later in the v1.x line as bundled built-ins; the spec already pins their manifest shapes. Until Step 10 lands the job subsystem, authors are encouraged to test them with a live kernel via `sm scan` against a fixture directory rather than in unit tests.
|
|
489
489
|
|
|
490
490
|
#### Provider, `kinds` catalog
|
|
491
491
|
|
|
@@ -497,7 +497,7 @@ Every Provider declares one required top-level field beyond the manifest base: `
|
|
|
497
497
|
- **`defaultRefreshAction`**, qualified action id (`<plugin-id>/<action-id>`) the UI's `🧠 prob` button dispatches. The action MUST exist in the registry; a dangling reference disables the Provider with `invalid-manifest`.
|
|
498
498
|
- **`ui`**, presentation block: `{ label, color, colorDark?, emoji?, icon? }`. The UI ships every `ui` block to the front-end via the `kindRegistry` envelope so built-in and user-plugin kinds render identically. `icon` is a discriminated union (`{ kind: 'pi'; id }` for PrimeIcons, `{ kind: 'svg'; path }` for raw SVG). The `ui` block is required (not optional) so the UI never has to invent visuals for unknown kinds. See [`architecture.md` §Provider · `ui` presentation](./architecture.md#provider--ui-presentation) for the field-by-field contract.
|
|
499
499
|
|
|
500
|
-
The Provider's walker hardcodes the paths it scans within the project (e.g. `.claude/`, `.cursor/rules`). The kernel does NOT extend the scan into the user's HOME based on Provider hints; the only way to scan paths outside the project is
|
|
500
|
+
The Provider's walker hardcodes the paths it scans within the project (e.g. `.claude/`, `.cursor/rules`). The kernel does NOT extend the scan into the user's HOME based on Provider hints; the only way to scan paths outside the project is by passing them as positional roots to `sm scan [roots...]` (per-invocation, not persisted).
|
|
501
501
|
|
|
502
502
|
```jsonc
|
|
503
503
|
{
|
|
@@ -704,45 +704,49 @@ The full per-kind capability matrix lives in [`architecture.md` §Execution mode
|
|
|
704
704
|
|
|
705
705
|
---
|
|
706
706
|
|
|
707
|
-
## Testing
|
|
707
|
+
## Testing your plugin
|
|
708
708
|
|
|
709
|
-
|
|
710
|
-
npm install --save-dev @skill-map/testkit
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
The testkit ships builders, per-kind context factories, in-memory KV / runner fakes, and high-level `runExtractorOnFixture` / `runAnalyzerOnGraph` / `runFormatterOnGraph` helpers. Most plugin tests reduce to one line per assertion.
|
|
709
|
+
Plugin extensions are plain ESM modules with a single entry point per kind (`extract` / `evaluate` / `format` / `run` / `on`); their inputs are well-typed context objects from `@skill-map/cli`. That makes them straightforward to unit-test without a kernel or DB: build a fake `ctx` literal, call the entry point, assert on what it captured.
|
|
714
710
|
|
|
715
711
|
```javascript
|
|
716
712
|
import { test } from 'node:test';
|
|
717
713
|
import { strictEqual } from 'node:assert';
|
|
718
|
-
import { runExtractorOnFixture, node } from '@skill-map/testkit';
|
|
719
714
|
|
|
720
715
|
import extractor from '../extractors/my-extractor/index.js';
|
|
721
716
|
|
|
722
717
|
test('emits one reference per [[ref:<name>]] token', async () => {
|
|
723
|
-
const
|
|
718
|
+
const links = [];
|
|
719
|
+
await extractor.extract({
|
|
720
|
+
node: { path: 'a.md', kind: 'skill', provider: 'claude' },
|
|
724
721
|
body: 'Talk to [[ref:architect]] or [[ref:sre]].',
|
|
725
|
-
|
|
722
|
+
frontmatter: {},
|
|
723
|
+
settings: {},
|
|
724
|
+
emitLink: (link) => links.push(link),
|
|
725
|
+
enrichNode: () => {},
|
|
726
|
+
emitContribution: () => {},
|
|
726
727
|
});
|
|
727
728
|
strictEqual(links.length, 2);
|
|
728
729
|
strictEqual(links[0].target, 'architect');
|
|
729
730
|
});
|
|
730
731
|
```
|
|
731
732
|
|
|
732
|
-
For
|
|
733
|
+
For analyzers, the same pattern applies: build a `ctx` with `nodes`, `links`, an `emitContribution` spy if you assert on view contributions, and call `analyzer.evaluate(ctx)`, it returns the issue array. Formatters take `{ nodes, links, issues }` and return a string from `formatter.format(ctx)`.
|
|
733
734
|
|
|
734
|
-
For probabilistic extensions
|
|
735
|
+
For probabilistic extensions (Actions / Hooks running in `mode: 'probabilistic'`), shape a fake `ctx.runner` that records the calls your test cares about:
|
|
735
736
|
|
|
736
737
|
```javascript
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
738
|
+
const calls = [];
|
|
739
|
+
const runner = {
|
|
740
|
+
async run(call) {
|
|
741
|
+
calls.push(call);
|
|
742
|
+
return { text: 'mocked response' };
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
await myAction.run({ runner, /* … */ });
|
|
746
|
+
strictEqual(calls[0].action, 'skill-summarizer');
|
|
743
747
|
```
|
|
744
748
|
|
|
745
|
-
|
|
749
|
+
The public TypeScript types (`IExtractor`, `IAnalyzer`, `IFormatter`, `IExtractorContext`, `IAnalyzerContext`, `IFormatterContext`, `Node`, `Link`, `Issue`, …) are re-exported from `@skill-map/cli` so authors can type-check their fakes against the same surface the kernel consumes.
|
|
746
750
|
|
|
747
751
|
---
|
|
748
752
|
|
|
@@ -1218,7 +1222,7 @@ Companion verbs:
|
|
|
1218
1222
|
|
|
1219
1223
|
## Stability
|
|
1220
1224
|
|
|
1221
|
-
- Document status: **stable** as of spec v1.0.0. Future minor revisions add new sections (e.g.
|
|
1225
|
+
- Document status: **stable** as of spec v1.0.0. Future minor revisions add new sections (e.g. Action / Hook testing patterns once Step 10 lands the job subsystem); breaking edits to the documented surface require a major bump per [`versioning.md`](./versioning.md).
|
|
1222
1226
|
- The six plugin statuses (`loaded` / `disabled` / `incompatible-spec` / `invalid-manifest` / `load-error` / `id-collision`) are stable; adding a seventh status is a minor bump.
|
|
1223
1227
|
- The structural analyzer **directory name MUST equal manifest id** is stable; relaxing it (allowing mismatch) is a major bump.
|
|
1224
1228
|
- The cross-root id-collision analyzer (both sides blocked, no precedence) is stable; introducing precedence (e.g. project root wins over global) is a major bump.
|
package/schemas/link.schema.json
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
"description": "Nature of the relation. `invokes` = execution-level call (e.g. slash command). `references` = explicit link (e.g. wikilink, @-directive). `mentions` = informal textual mention. `supersedes` = replaces another node (from `metadata.supersedes`)."
|
|
22
22
|
},
|
|
23
23
|
"confidence": {
|
|
24
|
-
"type": "
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
"type": "number",
|
|
25
|
+
"minimum": 0,
|
|
26
|
+
"maximum": 1,
|
|
27
|
+
"description": "Extractor's self-assessed confidence `[0..1]`. Drives UI edge opacity (more confident = more opaque). Migrated from the legacy string union `'high' | 'medium' | 'low'` to a numeric range so callers can express finer granularity than three buckets. Reference scoring: `1.0` = structured input (sidecar annotation), `0.95` = unambiguous syntax (`[text](file.md)`), `0.85` = strong signal with one inference (`@file.md`), `0.5` = genuine ambiguity (`@bare-handle`). The named tiers `HIGH = 0.9`, `MEDIUM = 0.6`, `LOW = 0.3` are exposed on the kernel side as the `ConfidenceTier` constants for callers that want to think in buckets. Analyzers MAY filter by confidence threshold."
|
|
27
28
|
},
|
|
28
29
|
"sources": {
|
|
29
30
|
"type": "array",
|
package/schemas/node.schema.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/node.schema.json",
|
|
4
4
|
"title": "Node",
|
|
5
|
-
"description": "A single
|
|
5
|
+
"description": "A single entity in the graph. Typically a file on disk (a markdown skill, an agent, a TOML sub-agent definition, a plain-markdown note), but MAY also be a **virtual / derived** entity that lives only in memory and is reconstructed from one or more source files on every scan (e.g. an MCP server node derived from `settings.json` / `mcp.json` / `config.toml`). Virtual nodes carry `virtual: true` and use a synthetic `path` scheme (`mcp://<name>`, etc.). The `kind` is whatever the classifying Provider declares, open by design; the **built-in Claude Provider** emits `skill` / `agent` / `command` / `markdown` today, but external Providers (Cursor, Obsidian, …) MAY emit their own. Format-named kinds (`markdown`, future `toml`, future `json`) are reserved for the generic fallback only, when a file matches a specific role (agent / command / skill) that classification prevails over format naming.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["path", "kind", "provider", "bodyHash", "frontmatterHash", "bytes", "linksOutCount", "linksInCount", "externalRefsCount"],
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -66,6 +66,15 @@
|
|
|
66
66
|
"isFavorite": {
|
|
67
67
|
"type": "boolean",
|
|
68
68
|
"description": "Per-node favorite flag set by the local user from the UI. Sourced from `state_node_favorites` (zone `state_`, persistent across scans). Decorated by the BFF on every `/api/nodes` response via in-memory Set lookup against the favorites table, no SQL JOIN against `scan_nodes`. Absent on emissions that don't carry per-user state (e.g. `sm export --json`); consumers that don't recognise the field MUST ignore it."
|
|
69
|
+
},
|
|
70
|
+
"virtual": {
|
|
71
|
+
"type": "boolean",
|
|
72
|
+
"description": "When `true`, this node is synthetic / derived: it does not correspond to a single file on disk. Reconstructed on every scan from the file(s) listed in `derivedFrom`. Use a synthetic `path` scheme (e.g. `mcp://github`) so the identifier is stable and visibly non-filesystem. Examples: MCP server nodes derived from `settings.json` / `mcp.json` / `config.toml`. When absent or `false`, the node is a normal filesystem-backed entity. Stability: experimental."
|
|
73
|
+
},
|
|
74
|
+
"derivedFrom": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": { "type": "string" },
|
|
77
|
+
"description": "Paths of the source files from which this node was derived. Required (and only meaningful) when `virtual: true`. Drives invalidation: if any listed source changes between scans, the virtual node's hashes change and the rename / drift machinery surfaces it. Empty / absent when the node is a regular filesystem-backed entity (the `path` itself is the source)."
|
|
69
78
|
}
|
|
70
79
|
},
|
|
71
80
|
"$defs": {
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
},
|
|
22
22
|
"providers": {
|
|
23
23
|
"type": "array",
|
|
24
|
-
"description": "Provider ids to enable, in priority order when multiple match. Empty/absent = use all registered.",
|
|
24
|
+
"description": "Provider ids to enable, in priority order when multiple match. Empty/absent = use all registered. Note: the `activeProvider` field selects ONE of these as the project's active lens; the rest stay enabled-but-inactive until the user switches.",
|
|
25
25
|
"items": { "type": "string" }
|
|
26
26
|
},
|
|
27
|
+
"activeProvider": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "The active provider lens for this project. Exactly one provider id (from the enabled `providers` list) sees the project at any time. All extractors, classifiers, and resolution rules belonging to other providers are skipped during scan. Changing this triggers an atomic drop of the `scan_*` DB zone followed by a fresh scan under the new lens; `state_*` and `config_*` zones survive the switch. When absent on a fresh project, the kernel auto-detects from filesystem (presence of `.claude/`, `.gemini/`, `.codex/`, AGENTS.md, etc.) and prompts via the CLI / UI if the heuristic is ambiguous. Stability: experimental."
|
|
30
|
+
},
|
|
27
31
|
"roots": {
|
|
28
32
|
"type": "array",
|
|
29
33
|
"description": "Directories (relative to the config file) to scan. Defaults to the scope root.",
|
|
@@ -58,15 +62,10 @@
|
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
},
|
|
61
|
-
"extraFolders": {
|
|
62
|
-
"type": "array",
|
|
63
|
-
"items": { "type": "string" },
|
|
64
|
-
"description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project, opens disk access there. Default `[]`. Additional directories appended to the scan roots; same parsing / indexing as the project root. Paths starting with `~` resolve against the user home; relative paths resolve against the project root. Reference impl gates writes that introduce out-of-project paths behind `--yes` (CLI) and a confirm dialog (UI). **Stripped with a warning when found in the committed `project` layer**, paths are inherently per-machine and must not travel via the shared repo. This is the ONLY mechanism to extend the scan beyond the project root: skill-map does NOT auto-include the user's HOME based on Provider hints, every out-of-project path must be listed here explicitly."
|
|
65
|
-
},
|
|
66
65
|
"referencePaths": {
|
|
67
66
|
"type": "array",
|
|
68
67
|
"items": { "type": "string" },
|
|
69
|
-
"description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project, opens read-only disk access for link validation only. Default `[]`. Directories walked in parallel by the scan to collect existing absolute paths into a side set; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken-ref` can resolve a link against the filesystem when the in-graph lookup misses. Files under these paths are NOT parsed and NOT indexed as nodes, the only effect is suppressing `broken-ref` warnings for targets that exist on disk outside the scan.
|
|
68
|
+
"description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project, opens read-only disk access for link validation only. Default `[]`. Directories walked in parallel by the scan to collect existing absolute paths into a side set; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken-ref` can resolve a link against the filesystem when the in-graph lookup misses. Files under these paths are NOT parsed and NOT indexed as nodes, the only effect is suppressing `broken-ref` warnings for targets that exist on disk outside the scan. Reference impl gates writes that introduce out-of-project paths behind `--yes` (CLI) and a confirm dialog (UI). **Stripped with a warning when found in the committed `project` layer**, paths are inherently per-machine and must not travel via the shared repo."
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
},
|
|
@@ -78,7 +77,18 @@
|
|
|
78
77
|
"additionalProperties": false,
|
|
79
78
|
"properties": {
|
|
80
79
|
"enabled": { "type": "boolean" },
|
|
81
|
-
"config": { "type": "object", "description": "Plugin-specific config passed to extensions at load time. Shape defined by the plugin.", "additionalProperties": true }
|
|
80
|
+
"config": { "type": "object", "description": "Plugin-specific config passed to extensions at load time. Shape defined by the plugin.", "additionalProperties": true },
|
|
81
|
+
"extensions": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"description": "Per-extension enable/disable overrides within this plugin. Keys are extension ids (the `id` declared by the extractor / analyzer / etc. inside the plugin). Absent = use the extension's manifest default (enabled). Allows a user to keep a provider plugin active but disable an individual extractor whose output is noisy in their project, without disabling the whole bundle.",
|
|
84
|
+
"additionalProperties": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"additionalProperties": false,
|
|
87
|
+
"properties": {
|
|
88
|
+
"enabled": { "type": "boolean" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
},
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://skill-map.dev/spec/v0/signal.schema.json",
|
|
4
|
+
"title": "Signal",
|
|
5
|
+
"description": "Intermediate Representation (IR) emitted by extractors during a scan. A Signal is a *candidate* detection: zero, one, or many interpretations of the same piece of source text or structured data. The kernel's resolver phase consumes `Signal[]` and produces final `Link[]` by selecting a winning candidate per Signal (or rejecting all and emitting none) using the active Provider's resolution rules. Opt-in for plugin authors: an extractor MAY emit `Signal`s via `ctx.emitSignal()` when the detection carries genuine ambiguity (multiple plausible kinds, multiple plausible targets, byte-range awareness for collision detection), OR continue calling `ctx.emitLink()` directly when its detection is unambiguous. The two paths coexist; resolved Link rows look identical regardless of origin. Stability: experimental.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["source", "scope", "candidates"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"source": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "`node.path` of the originating node (the file or virtual entity in which the signal was detected)."
|
|
13
|
+
},
|
|
14
|
+
"scope": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["body", "frontmatter", "sidecar"],
|
|
17
|
+
"description": "Where in the source the signal was detected. `body` = markdown body or equivalent prose payload. `frontmatter` = parsed metadata block at the top of the file. `sidecar` = co-located `.sm` overlay. Extractors specialized for non-prose sources (config files, AGENTS.md cascade) emit either `body` (if they treat the whole file as prose) or `frontmatter` (if they read structured fields)."
|
|
18
|
+
},
|
|
19
|
+
"range": {
|
|
20
|
+
"type": ["object", "null"],
|
|
21
|
+
"description": "Byte-range location within the source. Required for `scope: 'body'`, optional otherwise. Powers collision detection between detectors (two extractors emitting Signals with overlapping ranges) and code-block awareness (the orchestrator can mark ranges that fall inside code spans).",
|
|
22
|
+
"required": ["start", "end"],
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"properties": {
|
|
25
|
+
"start": { "type": "integer", "minimum": 0, "description": "Inclusive byte offset of the first character." },
|
|
26
|
+
"end": { "type": "integer", "minimum": 0, "description": "Exclusive byte offset one past the last character." }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"fieldPath": {
|
|
30
|
+
"type": ["array", "null"],
|
|
31
|
+
"description": "Structured-data location within `frontmatter` or `sidecar` scopes. Each entry is a step of the path: object keys are strings, array indices are integers serialized as strings. Example: `['tools', '0']` points to the first entry of the `tools` array. Null when the signal is body-scoped or when the extractor doesn't track field locations.",
|
|
32
|
+
"items": { "type": "string" }
|
|
33
|
+
},
|
|
34
|
+
"raw": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Verbatim matched text (for body scope) or stringified value (for frontmatter / sidecar scope). Used for debugging, UI tooltips, and collision-key dedup."
|
|
37
|
+
},
|
|
38
|
+
"context": {
|
|
39
|
+
"type": ["string", "null"],
|
|
40
|
+
"enum": ["code-block", "inline-code", "escaped", null],
|
|
41
|
+
"description": "Provider-determined surface context. `code-block` = inside a fenced code block (most providers ignore these). `inline-code` = inside backticks. `escaped` = preceded by `\\` or otherwise marked literal. Null when the signal is in normal prose or when the context concept doesn't apply (frontmatter / sidecar scopes). Drives both extraction filtering and the resolver's confidence weighting."
|
|
42
|
+
},
|
|
43
|
+
"candidates": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"minItems": 1,
|
|
46
|
+
"description": "One or more alternative interpretations of the same signal. The resolver picks ONE as the winner (becomes a Link) or rejects all (no Link emitted). Multiple candidates from the same `extractorId` are allowed (e.g. one detector may emit both a `references` and a `mentions` hypothesis for the same `@token` and let the resolver decide).",
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["extractorId", "kind", "target", "confidence"],
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"properties": {
|
|
52
|
+
"extractorId": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Id of the extractor that contributed this candidate."
|
|
55
|
+
},
|
|
56
|
+
"kind": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["invokes", "references", "mentions", "supersedes"],
|
|
59
|
+
"description": "Proposed link kind, matching `link.schema.json#/properties/kind/enum`. Closed enum in v1; provider-specific kinds wait until a concrete need emerges."
|
|
60
|
+
},
|
|
61
|
+
"target": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Proposed `node.path` of the destination. MAY refer to a missing node (the resolver does not validate existence); the `broken-ref` analyzer reports the gap downstream."
|
|
64
|
+
},
|
|
65
|
+
"confidence": {
|
|
66
|
+
"type": "number",
|
|
67
|
+
"minimum": 0,
|
|
68
|
+
"maximum": 1,
|
|
69
|
+
"description": "Extractor's self-assessed probability that this interpretation is the correct one. Reference scoring (guideline, not contract): `1.0` = structured input (sidecar annotation, parsed JSON pointer), `0.95` = unambiguous syntax (`[text](file.md)`, `https://...`), `0.85` = strong signal with one degree of inference (`@file.md` with known extension), `0.5` = genuine ambiguity (`@bare-handle` could be agent, file, or generic mention). Drives UI edge opacity downstream."
|
|
70
|
+
},
|
|
71
|
+
"rationale": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Optional human-readable explanation of WHY this candidate has the assigned confidence. Surfaced in inspector tooltips and debug output. Keep it short, e.g. `'ends in .md'`, `'no extension, no path prefix'`, `'declared in tools[] array'`."
|
|
74
|
+
},
|
|
75
|
+
"trigger": {
|
|
76
|
+
"type": ["object", "null"],
|
|
77
|
+
"description": "Trigger-style metadata when this candidate represents a textual invocation (`@x`, `/y`). Null otherwise. Mirrors `link.schema.json#/properties/trigger`.",
|
|
78
|
+
"required": ["originalTrigger", "normalizedTrigger"],
|
|
79
|
+
"additionalProperties": false,
|
|
80
|
+
"properties": {
|
|
81
|
+
"originalTrigger": { "type": "string" },
|
|
82
|
+
"normalizedTrigger": { "type": "string" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|