@skill-map/spec 0.26.0 → 0.28.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 +334 -0
- package/architecture.md +29 -30
- package/cli-contract.md +55 -20
- package/conformance/README.md +4 -0
- package/conformance/cases/no-global-scope.json +13 -0
- package/conformance/cases/sidecar-end-to-end.json +4 -4
- package/conformance/coverage.md +7 -4
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json +3 -0
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/schema.json +6 -0
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json +3 -2
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/providers/bad-provider/index.js +17 -0
- package/db-schema.md +7 -7
- package/index.json +26 -21
- package/package.json +1 -1
- package/plugin-author-guide.md +94 -47
- package/schemas/extensions/action.schema.json +28 -44
- package/schemas/extensions/analyzer.schema.json +34 -32
- package/schemas/extensions/base.schema.json +26 -55
- package/schemas/extensions/extractor.schema.json +35 -22
- package/schemas/extensions/formatter.schema.json +4 -14
- package/schemas/extensions/hook.schema.json +2 -9
- package/schemas/extensions/provider-kind.schema.json +71 -0
- package/schemas/extensions/provider.schema.json +3 -90
- package/schemas/plugins-registry.schema.json +11 -27
- package/schemas/project-config.schema.json +0 -11
- package/schemas/scan-result.schema.json +1 -6
- package/schemas/user-settings.schema.json +39 -0
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +0 -30
package/plugin-author-guide.md
CHANGED
|
@@ -12,23 +12,53 @@ This guide is **descriptive prose**, not the normative contract. The normative p
|
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
14
|
my-plugin/
|
|
15
|
-
├── plugin.json
|
|
16
|
-
└──
|
|
17
|
-
└── extractor
|
|
15
|
+
├── plugin.json ← bundle metadata (required)
|
|
16
|
+
└── extractors/ ← one folder per extension kind
|
|
17
|
+
└── my-extractor/
|
|
18
|
+
├── index.js ← extension entry (required)
|
|
19
|
+
├── text.ts ← user-facing strings (optional, see below)
|
|
20
|
+
└── my-extractor.test.ts ← tests live next to the code (optional)
|
|
18
21
|
```
|
|
19
22
|
|
|
23
|
+
The kernel auto-discovers extensions by walking
|
|
24
|
+
`<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}` for each known kind
|
|
25
|
+
(`providers`, `extractors`, `analyzers`, `actions`, `formatters`,
|
|
26
|
+
`hooks`). The folder layout IS the source of truth: bundle from the
|
|
27
|
+
top-level dir, kind from the subfolder name, extension id from the
|
|
28
|
+
extension folder name. The manifest no longer declares an
|
|
29
|
+
`extensions[]` array.
|
|
30
|
+
|
|
31
|
+
**Co-located files convention**: any siblings of `index.{js,mjs,ts}`
|
|
32
|
+
that the kernel does NOT recognise as an entry point are author
|
|
33
|
+
files (texts, tests, schemas, fixtures). Two names are blessed by
|
|
34
|
+
convention so consumers know where to look without grepping:
|
|
35
|
+
|
|
36
|
+
- **`text.ts`** holds the extension's externalised user-facing
|
|
37
|
+
strings (the `tx()`-fed templates, error messages, glyph labels).
|
|
38
|
+
One per extension; imported by `index.ts` as `./text.js`. Keeps
|
|
39
|
+
copy out of the code path and makes the surface review-friendly.
|
|
40
|
+
Plain TS module, no schema, no codegen.
|
|
41
|
+
- **`<extension-name>.test.ts`** (or `.test.mjs` / `.test.js`) is
|
|
42
|
+
the colocated test suite. Picked up by the workspace's test glob
|
|
43
|
+
(`plugins/**/*.test.ts`); no separate test directory.
|
|
44
|
+
|
|
45
|
+
Both files are optional. The kernel ignores everything that isn't
|
|
46
|
+
`index.{js,mjs,ts}`, so future per-extension fixtures, schemas, or
|
|
47
|
+
conformance scopes can live in the same folder without manifest
|
|
48
|
+
plumbing.
|
|
49
|
+
|
|
20
50
|
```jsonc
|
|
21
51
|
// my-plugin/plugin.json
|
|
22
52
|
{
|
|
23
53
|
"id": "my-plugin",
|
|
24
54
|
"version": "1.0.0",
|
|
25
55
|
"specCompat": "^1.0.0",
|
|
26
|
-
"
|
|
56
|
+
"granularity": "bundle"
|
|
27
57
|
}
|
|
28
58
|
```
|
|
29
59
|
|
|
30
60
|
```javascript
|
|
31
|
-
// my-plugin/
|
|
61
|
+
// my-plugin/extractors/my-extractor/index.js
|
|
32
62
|
export default {
|
|
33
63
|
id: 'my-extractor',
|
|
34
64
|
kind: 'extractor',
|
|
@@ -50,18 +80,19 @@ export default {
|
|
|
50
80
|
};
|
|
51
81
|
```
|
|
52
82
|
|
|
53
|
-
Drop the directory under
|
|
83
|
+
Drop the directory under `<cwd>/.skill-map/plugins/` and
|
|
84
|
+
`sm plugins list` will pick it up. The kernel injects `pluginId`
|
|
85
|
+
from `plugin.json#/id` at load time; do NOT hardcode it in the
|
|
86
|
+
extension export. A folder/kind mismatch (e.g. an extractor placed
|
|
87
|
+
under `analyzers/`) surfaces as `invalid-manifest`.
|
|
54
88
|
|
|
55
89
|
---
|
|
56
90
|
|
|
57
91
|
## Discovery
|
|
58
92
|
|
|
59
|
-
The kernel scans
|
|
60
|
-
|
|
61
|
-
1. `<project>/.skill-map/plugins/`, committed-with-the-repo plugins.
|
|
62
|
-
2. `~/.skill-map/plugins/`, user-level plugins available across every project.
|
|
93
|
+
The kernel scans one root: `<cwd>/.skill-map/plugins/`, committed-with-the-repo plugins. There is no implicit user-level discovery (see `cli-contract.md` §Scope is always project-local for the broader principle): plugins live with the project that uses them.
|
|
63
94
|
|
|
64
|
-
A plugin is any direct child directory containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to
|
|
95
|
+
A plugin is any direct child directory of that root containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to replace the default root with a custom directory (mostly for testing, or for loading a user-level plugin set the operator explicitly opts into).
|
|
65
96
|
|
|
66
97
|
After every change to the `plugins/` folder, run `sm plugins list` to see the load status of each. The six statuses are documented under [Diagnostics](#diagnostics) below.
|
|
67
98
|
|
|
@@ -113,7 +144,7 @@ The kernel guards against two foot-guns:
|
|
|
113
144
|
- If the extension file injects a `pluginId` field that doesn't match `plugin.json#/id`, the loader emits `invalid-manifest` with a directed reason. The composed qualifier MUST come from `plugin.json`, there is no second source of truth.
|
|
114
145
|
- The kebab-case pattern on the extension `id` deliberately forbids `/`. This keeps the analyzer "the qualifier always lives in the plugin id, never in the extension id" enforced by AJV.
|
|
115
146
|
|
|
116
|
-
For built-ins, the reference impl's `src/
|
|
147
|
+
For built-ins, the reference impl's `src/plugins/<bundle>/plugin.json` provides the bundle's `id` and the codegen at `scripts/generate-built-ins.js` inlines the `pluginId` injection at build time (the resulting `src/plugins/built-ins.ts` is auto-generated and committed). Authors never hardcode `pluginId` on the extension export.
|
|
117
148
|
|
|
118
149
|
### Granularity, bundle vs extension
|
|
119
150
|
|
|
@@ -142,48 +173,60 @@ Resolution order is the same as for plugin enabled-state: DB override (`config_p
|
|
|
142
173
|
|
|
143
174
|
`sm plugins enable/disable --all` operates only on top-level bundle ids (the default-enabled set every user can see); it never expands to qualified `<bundle>/<ext>` keys. The "disable every kernel built-in at once" intent is served by `--no-built-ins` on `sm scan` and friends; `--all` is the macro on user-toggle-able units, not on every individual extension.
|
|
144
175
|
|
|
145
|
-
|
|
176
|
+
Set `granularity` in your `plugin.json`. The folder layout supplies the extensions; the kernel discovers them automatically:
|
|
146
177
|
|
|
147
178
|
```jsonc
|
|
148
179
|
{
|
|
149
180
|
"id": "my-multi-tool",
|
|
150
181
|
"version": "1.0.0",
|
|
151
182
|
"specCompat": "^1.0.0",
|
|
152
|
-
"granularity": "extension"
|
|
153
|
-
"extensions": [
|
|
154
|
-
"./extensions/orphan-skill-analyzer.js",
|
|
155
|
-
"./extensions/csv-formatter.js"
|
|
156
|
-
]
|
|
183
|
+
"granularity": "extension"
|
|
157
184
|
}
|
|
158
185
|
```
|
|
159
186
|
|
|
187
|
+
```text
|
|
188
|
+
my-multi-tool/
|
|
189
|
+
├── plugin.json
|
|
190
|
+
├── analyzers/
|
|
191
|
+
│ └── orphan-skill/
|
|
192
|
+
│ └── index.js
|
|
193
|
+
└── formatters/
|
|
194
|
+
└── csv/
|
|
195
|
+
└── index.js
|
|
196
|
+
```
|
|
197
|
+
|
|
160
198
|
The default (`'bundle'`) is the right answer for almost every plugin, keep the manifest minimal until the plugin actually ships several independent capabilities.
|
|
161
199
|
|
|
162
|
-
### Extractor `
|
|
200
|
+
### Extractor `precondition`, narrow the pipeline
|
|
201
|
+
|
|
202
|
+
An `Extractor` extension MAY declare a `precondition` block on its manifest. When declared, the kernel runs the extractor **only** against nodes that satisfy every declared sub-filter, the filter is fail-fast (no extractor context, no method call) so the extractor wastes zero CPU on nodes it cannot meaningfully process. The same shape is shared by `Analyzer` and `Action`.
|
|
163
203
|
|
|
164
|
-
|
|
204
|
+
```ts
|
|
205
|
+
precondition?: {
|
|
206
|
+
kind?: string[]; // qualified `<plugin>/<kindName>` ids
|
|
207
|
+
provider?: string[]; // plugin ids
|
|
208
|
+
};
|
|
209
|
+
```
|
|
165
210
|
|
|
166
|
-
| `
|
|
211
|
+
| `precondition` | Behaviour |
|
|
167
212
|
|---|---|
|
|
168
213
|
| Absent (`undefined`) | **Default.** The extractor runs on every kind the loaded Providers emit. |
|
|
169
|
-
| `['skill']` | Runs only on skill nodes. |
|
|
170
|
-
| `['skill', '
|
|
171
|
-
| `[]` |
|
|
214
|
+
| `{ kind: ['claude/skill'] }` | Runs only on skill nodes from the Claude provider. |
|
|
215
|
+
| `{ kind: ['claude/skill', 'gemini/skill'] }` | Runs on skills from either provider. |
|
|
216
|
+
| `{ provider: ['claude'] }` | Coarser: runs on every kind the `claude` plugin declares. |
|
|
217
|
+
| `{ kind: ['claude/skill'], provider: ['claude'] }` | Both filters apply (AND). |
|
|
172
218
|
|
|
173
|
-
|
|
219
|
+
Use `precondition.kind` over `precondition.provider` when the filter is really about the kind, not the provider. There is no wildcard syntax, omitting the field IS the wildcard.
|
|
174
220
|
|
|
175
221
|
Use case, a deterministic frontmatter-tag extractor that only makes sense for skills:
|
|
176
222
|
|
|
177
223
|
```javascript
|
|
178
224
|
export default {
|
|
179
|
-
id
|
|
180
|
-
kind: 'extractor',
|
|
225
|
+
// id, kind, pluginId injected by the loader from the folder path
|
|
181
226
|
version: '1.0.0',
|
|
182
227
|
description: 'Lifts the `tags:` frontmatter array into `references` links for skill nodes.',
|
|
183
|
-
emitsLinkKinds: ['references'],
|
|
184
|
-
defaultConfidence: 'high',
|
|
185
228
|
scope: 'frontmatter',
|
|
186
|
-
|
|
229
|
+
precondition: { kind: ['claude/skill'] },
|
|
187
230
|
async extract(ctx) {
|
|
188
231
|
// Never invoked for agents, commands, hooks, or notes, the kernel
|
|
189
232
|
// skipped this node before reaching us.
|
|
@@ -203,7 +246,9 @@ export default {
|
|
|
203
246
|
|
|
204
247
|
> **Why no `mode` field?** Extractors are deterministic-only, they sit on `sm scan`'s synchronous loop, and the loop must stay fast and reproducible. If you need an LLM to infer something about a node (tags, summaries, suspicious imports), write an `Action` instead and let the user dispatch it via `sm job submit action:<id>`. The Action's report flows back through the job lifecycle, not through the Extractor pipeline.
|
|
205
248
|
|
|
206
|
-
**
|
|
249
|
+
> **Why no `emitsLinkKinds` / `defaultConfidence`?** Both fields were retired with the structure-as-truth refactor. Link kinds are constrained by the global closed enum (`invokes`, `references`, `mentions`, `supersedes`); off-enum emissions drop with `extension.error`. Confidence is declared per-emit on every `ctx.emitLink({ ..., confidence })` call (default `'medium'` if omitted).
|
|
250
|
+
|
|
251
|
+
**Unknown qualified kinds are non-blocking.** An extractor that lists a kind no installed Provider declares (typo, missing Provider plugin) still loads with status `enabled`; `sm plugins doctor` surfaces an informational warning so the author sees the mismatch. The exit code of `doctor` is NOT promoted to 1 by this warning, the corresponding Provider may legitimately arrive later (e.g. when the user installs the matching plugin), and the load contract favours forward compatibility over rigid checks.
|
|
207
252
|
|
|
208
253
|
### Module top-level side effects survive load timeouts
|
|
209
254
|
|
|
@@ -229,23 +274,26 @@ Required fields (see [`schemas/plugins-registry.schema.json#/$defs/PluginManifes
|
|
|
229
274
|
|
|
230
275
|
| Field | Type | Notes |
|
|
231
276
|
|---|---|---|
|
|
232
|
-
| `id` | kebab-case string | Globally unique. Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`. |
|
|
233
277
|
| `version` | semver | Plugin version, independent of `specCompat`. |
|
|
234
278
|
| `specCompat` | semver range | Spec versions this plugin is compatible with. Checked via `semver.satisfies(specVersion, this)` at load time. |
|
|
235
|
-
| `
|
|
279
|
+
| `catalogCompat` | semver range | Semver range against the view-slots + input-types catalog. Independent from `specCompat` because the catalog evolves on its own cadence. Required as of the structure-as-truth refactor (was optional). |
|
|
280
|
+
| `description` | string | Required short description shown in `sm plugins list` and the UI. |
|
|
236
281
|
|
|
237
282
|
Optional fields:
|
|
238
283
|
|
|
239
284
|
| Field | Type | Notes |
|
|
240
285
|
|---|---|---|
|
|
241
|
-
| `
|
|
242
|
-
| `granularity` | `'bundle' \| 'extension'` | Controls how `sm plugins enable / disable` operates on this plugin. Default `'bundle'`. See [Granularity, bundle vs extension](#granularity--bundle-vs-extension). |
|
|
286
|
+
| `granularity` | `'bundle' \| 'extension'` | Default `'extension'` (each extension toggleable by qualified id). Set to `'bundle'` when the plugin's extensions form a coherent unit a user would never want to toggle piecemeal. |
|
|
243
287
|
| `storage` | object | `{ "mode": "kv" }` or `{ "mode": "dedicated", "tables": [...], "migrations": [...] }`. Absent means the plugin does not persist state. |
|
|
244
288
|
| `author` | string | Free-form. |
|
|
245
289
|
| `license` | string | SPDX identifier. |
|
|
246
290
|
| `homepage` | string | URL. |
|
|
247
291
|
| `repository` | string | URL. |
|
|
248
292
|
|
|
293
|
+
**Structure-as-truth**: the plugin id is the directory name (`<root>/<id>/plugin.json`); it is NOT a manifest field. Manifests carrying an `id` literal are rejected as `invalid-manifest`. Settings moved out of `plugin.json` into each extension's own manifest with the same refactor (see [Extension manifest](#extension-manifest)).
|
|
294
|
+
|
|
295
|
+
The manifest does NOT list extensions. The kernel discovers each extension by walking `<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}`; the path is authoritative for both the kind and the local id. A Provider's kind catalog lives on disk at `<plugin>/kinds/<kindName>/{schema.json, kind.json}` (see [Providers](#providers--actions)).
|
|
296
|
+
|
|
249
297
|
### `specCompat` strategy
|
|
250
298
|
|
|
251
299
|
Pre-`v1.0.0` of the spec, narrow ranges are the defensive default, minor bumps **MAY** carry breaking changes per [`versioning.md`](./versioning.md). A plugin that spans minor boundaries can load successfully and crash at first use against a changed schema.
|
|
@@ -663,7 +711,7 @@ import { test } from 'node:test';
|
|
|
663
711
|
import { strictEqual } from 'node:assert';
|
|
664
712
|
import { runExtractorOnFixture, node } from '@skill-map/testkit';
|
|
665
713
|
|
|
666
|
-
import extractor from '../
|
|
714
|
+
import extractor from '../extractors/my-extractor/index.js';
|
|
667
715
|
|
|
668
716
|
test('emits one reference per [[ref:<name>]] token', async () => {
|
|
669
717
|
const { links } = await runExtractorOnFixture(extractor, {
|
|
@@ -703,7 +751,7 @@ Full surface in `@skill-map/testkit/index.ts`.
|
|
|
703
751
|
| `incompatible-spec` | manifest parsed but `semver.satisfies` failed against the installed spec. | Plugin built against an older / newer spec. |
|
|
704
752
|
| `invalid-manifest` | `plugin.json` missing, unparseable, AJV-fails, OR the directory name does not equal the manifest id. | Typo, missing required field, wrong shape, mismatched directory name. |
|
|
705
753
|
| `load-error` | manifest passed but an extension module failed to import or its default export failed schema validation. | Missing `kind` field, wrong `kind` for the file, runtime import error. |
|
|
706
|
-
| `id-collision` | two plugins reachable from different roots declared the same `id`. Both collided plugins receive this status; no precedence analyzer applies. |
|
|
754
|
+
| `id-collision` | two plugins reachable from different roots declared the same `id`. Both collided plugins receive this status; no precedence analyzer applies. | A project-local plugin and a `--plugin-dir` plugin (or two `--plugin-dir` plugins) sharing an id. Rename one and rerun. |
|
|
707
755
|
|
|
708
756
|
`sm plugins doctor` runs the full load pass and exits 1 if any plugin is in a non-`loaded` / non-`disabled` state (so any of `incompatible-spec` / `invalid-manifest` / `load-error` / `id-collision` trips it). Wire it into CI to catch breakage early.
|
|
709
757
|
|
|
@@ -718,7 +766,7 @@ Full surface in `@skill-map/testkit/index.ts`.
|
|
|
718
766
|
`annotationContributions` is an object map keyed by the annotation key the extension wants to own. Each entry declares an inline JSON Schema for the value plus two policy fields:
|
|
719
767
|
|
|
720
768
|
```js
|
|
721
|
-
// my-plugin/
|
|
769
|
+
// my-plugin/extractors/my-extractor/index.js
|
|
722
770
|
export default {
|
|
723
771
|
id: 'my-extractor',
|
|
724
772
|
kind: 'extractor',
|
|
@@ -768,7 +816,7 @@ auditor:
|
|
|
768
816
|
Opting into a top-level (root) key requires `location: 'root'` AND `ownership: 'exclusive'`. The pair travels together, a top-level reserved key cannot be silently shared between plugins, because `.sm` writes deep-merge per the `SidecarStore` contract and a shared root key would route non-deterministically. Use root sparingly: for every plugin that contributes a root key, the kernel reserves that name across the whole installed-plugin surface.
|
|
769
817
|
|
|
770
818
|
```js
|
|
771
|
-
// compliance-plugin/
|
|
819
|
+
// compliance-plugin/analyzers/compliance-checker/index.js
|
|
772
820
|
export default {
|
|
773
821
|
id: 'compliance-checker',
|
|
774
822
|
kind: 'analyzer',
|
|
@@ -1039,9 +1087,10 @@ Full plugin walkthrough:
|
|
|
1039
1087
|
|
|
1040
1088
|
```
|
|
1041
1089
|
plugins/acme-keyword-finder/
|
|
1042
|
-
├── plugin.json
|
|
1043
|
-
└──
|
|
1044
|
-
└──
|
|
1090
|
+
├── plugin.json ← manifest with settings + catalogCompat
|
|
1091
|
+
└── extractors/
|
|
1092
|
+
└── keyword-finder/
|
|
1093
|
+
└── index.js ← extract() with ctx.emitContribution
|
|
1045
1094
|
```
|
|
1046
1095
|
|
|
1047
1096
|
`plugin.json`:
|
|
@@ -1052,7 +1101,7 @@ plugins/acme-keyword-finder/
|
|
|
1052
1101
|
"version": "1.0.0",
|
|
1053
1102
|
"specCompat": "^0.20.0",
|
|
1054
1103
|
"catalogCompat": "^1.0.0",
|
|
1055
|
-
"
|
|
1104
|
+
"granularity": "bundle",
|
|
1056
1105
|
"settings": {
|
|
1057
1106
|
"keywords": {
|
|
1058
1107
|
"type": "string-list",
|
|
@@ -1064,17 +1113,15 @@ plugins/acme-keyword-finder/
|
|
|
1064
1113
|
}
|
|
1065
1114
|
```
|
|
1066
1115
|
|
|
1067
|
-
`
|
|
1116
|
+
`extractors/keyword-finder/index.js`:
|
|
1068
1117
|
|
|
1069
1118
|
```js
|
|
1070
|
-
export
|
|
1119
|
+
export default {
|
|
1071
1120
|
id: 'keyword-finder',
|
|
1072
|
-
pluginId: 'acme-keyword-finder',
|
|
1073
1121
|
kind: 'extractor',
|
|
1074
1122
|
version: '1.0.0',
|
|
1075
1123
|
description: 'Counts configured keywords per node.',
|
|
1076
1124
|
stability: 'stable',
|
|
1077
|
-
mode: 'deterministic',
|
|
1078
1125
|
emitsLinkKinds: [],
|
|
1079
1126
|
defaultConfidence: 'high',
|
|
1080
1127
|
scope: 'body',
|
|
@@ -2,78 +2,62 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/extensions/action.schema.json",
|
|
4
4
|
"title": "ExtensionAction",
|
|
5
|
-
"description": "Manifest shape for an `Action` extension. An action operates on one or more nodes in one of two modes: `deterministic` (code runs in-process, returns a report JSON directly) or `probabilistic` (kernel renders a prompt, a runner executes it against an LLM, the callback closes the job).
|
|
5
|
+
"description": "Manifest shape for an `Action` extension. An action operates on one or more nodes in one of two modes: `deterministic` (code runs in-process, returns a report JSON directly) or `probabilistic` (kernel renders a prompt, a runner executes it against an LLM, the callback closes the job). **Structure-as-truth files**: every Action carries `<action-dir>/report.schema.json` (the JSON Schema for the report, MUST extend `report-base.schema.json`); probabilistic Actions additionally carry `<action-dir>/prompt.md` (the prompt template). The kernel resolves both by convention; missing or mis-placed files surface as `load-error`. A deterministic Action with a `prompt.md` in its folder is also `load-error` (config inconsistent). **`prob*` prefix convention**: manifest fields that only apply when `mode=probabilistic` start with `prob`; if a deterministic-only field ever appears, it starts with `det`.",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": ["
|
|
7
|
+
"required": ["version", "description"],
|
|
8
8
|
"unevaluatedProperties": false,
|
|
9
9
|
"properties": {
|
|
10
|
-
"kind": { "const": "action" },
|
|
11
10
|
"mode": {
|
|
12
11
|
"type": "string",
|
|
13
12
|
"enum": ["deterministic", "probabilistic"],
|
|
14
|
-
"
|
|
13
|
+
"default": "deterministic",
|
|
14
|
+
"description": "`deterministic` (default): the plugin's code computes the report synchronously, no job file, no runner. `probabilistic`: the kernel renders `prompt.md` + preamble into a job file; a runner executes it via `RunnerPort`; `sm record` closes the job."
|
|
15
15
|
},
|
|
16
|
-
"
|
|
17
|
-
"type": "string",
|
|
18
|
-
"description": "Absolute or relative reference to the JSON Schema the report MUST validate against. MUST extend `report-base.schema.json` (directly or transitively). Validation failure → job transitions to `failed` with reason `report-invalid`."
|
|
19
|
-
},
|
|
20
|
-
"expectedDurationSeconds": {
|
|
16
|
+
"probExpectedDurationSeconds": {
|
|
21
17
|
"type": "integer",
|
|
22
18
|
"minimum": 1,
|
|
23
|
-
"description": "Best-effort estimate of wall-clock duration
|
|
24
|
-
},
|
|
25
|
-
"promptTemplateRef": {
|
|
26
|
-
"type": "string",
|
|
27
|
-
"description": "Path (relative to the extension file) to the prompt template the kernel renders at `sm job submit`. REQUIRED when `mode: probabilistic`; FORBIDDEN when `mode: deterministic`. The template MUST NOT interpolate user text outside `<user-content>` blocks (see `prompt-preamble.md`)."
|
|
19
|
+
"description": "Best-effort estimate of wall-clock duration when `mode=probabilistic`. Drives TTL (`ttl = max(probExpectedDurationSeconds × graceMultiplier, minimumTtlSeconds)`). Required for `probabilistic`; ignored otherwise. Renamed from `expectedDurationSeconds` with the structure-as-truth refactor, the `prob` prefix makes it clear at a glance which mode the field belongs to."
|
|
28
20
|
},
|
|
29
21
|
"precondition": {
|
|
30
22
|
"type": "object",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"description": "Declarative filter that nodes must satisfy for this action to be applicable. Consumed by `--all` fan-out, UI button gating, `sm actions show`. Also drives the inverse analyzer↔action relationship (Modelo B): `analyzerIds` declares which analyzers' findings this action is intended to resolve, replacing the deprecated `Analyzer.recommendedActions` map.",
|
|
33
25
|
"properties": {
|
|
34
26
|
"kind": {
|
|
35
27
|
"type": "array",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
28
|
+
"minItems": 1,
|
|
29
|
+
"uniqueItems": true,
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^[a-z][a-z0-9-]*/[a-z][a-zA-Z0-9]*$"
|
|
33
|
+
},
|
|
34
|
+
"description": "Qualified node kinds this action accepts (`<provider-plugin>/<kindName>`)."
|
|
38
35
|
},
|
|
39
36
|
"provider": {
|
|
40
37
|
"type": "array",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"type": "array",
|
|
46
|
-
"items": { "type": "string", "enum": ["experimental", "stable", "deprecated"] },
|
|
47
|
-
"description": "Node stability filter."
|
|
38
|
+
"minItems": 1,
|
|
39
|
+
"uniqueItems": true,
|
|
40
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
41
|
+
"description": "Provider ids whose nodes this action accepts."
|
|
48
42
|
},
|
|
49
|
-
"
|
|
43
|
+
"analyzerIds": {
|
|
50
44
|
"type": "array",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
45
|
+
"minItems": 1,
|
|
46
|
+
"uniqueItems": true,
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$"
|
|
50
|
+
},
|
|
51
|
+
"description": "Qualified analyzer ids whose findings this action is intended to resolve (Modelo B, replaces the deprecated `Analyzer.recommendedActions`). The UI surfaces matching actions in the node inspector under 'Resolve this issue' when the analyzer's id matches an entry here. Format `<plugin>/<analyzer>` or `<plugin>/<analyzer>:<sub-id>` when the analyzer emits sub-typed issues. Dangling references warn via `recommended-action-missing` in `sm plugins doctor` but do NOT block load."
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
},
|
|
56
|
-
"expectedTools": {
|
|
57
|
-
"type": "array",
|
|
58
|
-
"description": "Hint to a Skill agent / CLI runner about what tools the rendered prompt expects access to (`Bash`, `Read`, `WebSearch`, …). Host MAY filter or warn. No normative enforcement in v0.",
|
|
59
|
-
"items": { "type": "string" }
|
|
60
|
-
},
|
|
61
|
-
"fanOutPolicy": {
|
|
62
|
-
"type": "string",
|
|
63
|
-
"enum": ["per-node", "batch"],
|
|
64
|
-
"default": "per-node",
|
|
65
|
-
"description": "`per-node` (default): `sm job submit --all` produces one job per matching node. `batch`: produces one job whose prompt template receives the full list. Batch actions tend to hit context limits; use sparingly."
|
|
66
54
|
}
|
|
67
55
|
},
|
|
68
56
|
"allOf": [
|
|
69
57
|
{ "$ref": "base.schema.json" },
|
|
70
58
|
{
|
|
71
59
|
"if": { "properties": { "mode": { "const": "probabilistic" } } },
|
|
72
|
-
"then": { "required": ["
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"if": { "properties": { "mode": { "const": "deterministic" } } },
|
|
76
|
-
"then": { "not": { "required": ["promptTemplateRef"] } }
|
|
60
|
+
"then": { "required": ["probExpectedDurationSeconds"] }
|
|
77
61
|
}
|
|
78
62
|
]
|
|
79
63
|
}
|
|
@@ -2,51 +2,53 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/extensions/analyzer.schema.json",
|
|
4
4
|
"title": "ExtensionAnalyzer",
|
|
5
|
-
"description": "Manifest shape for an `Analyzer` extension. An analyzer consumes the full graph (nodes + links) after all extractors have run, emits `Issue[]`, and MAY emit view contributions to project findings into the UI. Analyzers are dual-mode: `deterministic` analyzers MUST be byte-for-byte reproducible (same graph in → same issues out; time, random, and network are forbidden) and run synchronously inside `sm check` / `sm scan
|
|
5
|
+
"description": "Manifest shape for an `Analyzer` extension. An analyzer consumes the full graph (nodes + links) after all extractors have run, emits `Issue[]`, and MAY emit view contributions to project findings into the UI. Analyzers are dual-mode: `deterministic` analyzers MUST be byte-for-byte reproducible (same graph in → same issues out; time, random, and network are forbidden) and run synchronously inside `sm check` / `sm scan`; `probabilistic` analyzers invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (`sm job submit analyzer:<id>`); their output MAY vary across runs and they NEVER participate in `sm scan`. Each issue emitted is tagged with `analyzer_id = <plugin-id>/<extension-id>` by default (the extension's qualified id, derived from structure); analyzers that need to discriminate sub-types append `:<sub-id>` at emit time. Severity is set per-emit (no manifest-level default).",
|
|
6
6
|
"allOf": [
|
|
7
7
|
{ "$ref": "base.schema.json" }
|
|
8
8
|
],
|
|
9
9
|
"type": "object",
|
|
10
|
-
"required": ["
|
|
10
|
+
"required": ["version", "description"],
|
|
11
11
|
"unevaluatedProperties": false,
|
|
12
12
|
"properties": {
|
|
13
|
-
"kind": { "const": "analyzer" },
|
|
14
13
|
"mode": {
|
|
15
14
|
"type": "string",
|
|
16
15
|
"enum": ["deterministic", "probabilistic"],
|
|
17
16
|
"default": "deterministic",
|
|
18
|
-
"description": "`deterministic` (default): pure code, byte-for-byte reproducible, runs during `sm check` and `sm scan`. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job; never participates in scan-time pipelines. The kernel rejects probabilistic analyzers that try to register scan-time hooks at load time.
|
|
17
|
+
"description": "`deterministic` (default): pure code, byte-for-byte reproducible, runs during `sm check` and `sm scan`. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job; never participates in scan-time pipelines. The kernel rejects probabilistic analyzers that try to register scan-time hooks at load time."
|
|
19
18
|
},
|
|
20
|
-
"
|
|
21
|
-
"type": "
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
19
|
+
"precondition": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"description": "Optional declarative filter. The analyzer runs only when the graph contains at least one node matching every declared sub-filter. Same shape used by Extractor and Action.",
|
|
23
|
+
"properties": {
|
|
24
|
+
"kind": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"minItems": 1,
|
|
27
|
+
"uniqueItems": true,
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"pattern": "^[a-z][a-z0-9-]*/[a-z][a-zA-Z0-9]*$"
|
|
31
|
+
},
|
|
32
|
+
"description": "Qualified node kinds the analyzer cares about (`<provider-plugin>/<kindName>`). Unknown qualified kinds load OK but emit a `precondition-kind-unknown` warning in `sm plugins doctor`."
|
|
33
|
+
},
|
|
34
|
+
"provider": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"minItems": 1,
|
|
37
|
+
"uniqueItems": true,
|
|
38
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
39
|
+
"description": "Provider ids whose nodes the analyzer cares about."
|
|
40
|
+
}
|
|
41
|
+
}
|
|
25
42
|
},
|
|
26
|
-
"
|
|
27
|
-
"type": "
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"enum": ["nodes", "links", "both"],
|
|
34
|
-
"default": "both",
|
|
35
|
-
"description": "Which slices of the graph the analyzer reads. The kernel MAY pass a restricted view when this is a strict subset."
|
|
36
|
-
},
|
|
37
|
-
"configurable": {
|
|
38
|
-
"type": "boolean",
|
|
39
|
-
"default": false,
|
|
40
|
-
"description": "If true, the analyzer reads its own config from `config_preferences` under the key `analyzers.<id>.<field>`. Implementations MAY surface these in `sm config`."
|
|
41
|
-
},
|
|
42
|
-
"recommendedActions": {
|
|
43
|
-
"type": "array",
|
|
44
|
-
"description": "Qualified `<pluginId>/<id>` action ids the analyzer recommends running to address its findings. Per-node Actions only (Actions are per-node by design, see `IActionPrecondition`). Distinct from `Action.precondition`: the precondition declares 'this action applies to nodes matching X'; `recommendedActions` declares 'when this analyzer fires, these are the relevant actions to resolve the finding'. The UI surfaces matching Actions in the node inspector under 'Recommended for issues' alongside the always-applicable list driven by `Action.precondition`. Project-level cleanup operations (e.g. orphan file prune, contribution relink) are CLI verbs, not Actions, and therefore are NOT linked through this field. Each entry MUST be the qualified id of a registered Action; the kernel logs `recommended-action-missing` when a referenced action is not loaded but keeps the analyzer registered.",
|
|
45
|
-
"items": {
|
|
46
|
-
"type": "string",
|
|
47
|
-
"pattern": "^[a-z0-9-]+/[a-z0-9-]+$"
|
|
43
|
+
"ui": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": {
|
|
46
|
+
"$ref": "../view-slots.schema.json#/$defs/IViewContribution"
|
|
47
|
+
},
|
|
48
|
+
"propertyNames": {
|
|
49
|
+
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
|
|
48
50
|
},
|
|
49
|
-
"
|
|
51
|
+
"description": "Plugin-contributed view contributions. Same contract as Extractor.ui (slot-driven, payload-validated). The analyzer emits per-node payloads via `ctx.emitContribution(<nodePath>, <contributionId>, payload)` during graph evaluation (signature differs from extractor: the nodePath is explicit because analyzers see the full graph, not a single node). Only `extractor` and `analyzer` kinds may declare this field. Renamed from `viewContributions` with the structure-as-truth refactor."
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -2,77 +2,48 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/extensions/base.schema.json",
|
|
4
4
|
"title": "ExtensionBase",
|
|
5
|
-
"description": "Base manifest shape common to every extension kind. Kind-specific schemas (`provider`, `extractor`, `analyzer`, `action`, `formatter`, `hook`) extend this via `allOf` and add a
|
|
5
|
+
"description": "Base manifest shape common to every extension kind. Kind-specific schemas (`provider`, `extractor`, `analyzer`, `action`, `formatter`, `hook`) extend this via `allOf` and add a kind-specific shape. Both `id` and `kind` are derived from the filesystem structure (`<plugin>/<kind-plural>/<id>/index.ts`, where the parent folder dictates the kind and the leaf folder dictates the id), so they are NOT manifest fields. Manifests carrying `id` or `kind` are rejected as `invalid-manifest`. Closed-content enforcement (unknown keys = bug) lives on the kind schemas via `unevaluatedProperties: false`; those see base's evaluated keys through the `allOf` composition.",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": ["
|
|
7
|
+
"required": ["version", "description"],
|
|
8
8
|
"properties": {
|
|
9
|
-
"id": {
|
|
10
|
-
"type": "string",
|
|
11
|
-
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
|
12
|
-
"description": "Kebab-case identifier unique within the extension's kind across the same plugin. The kernel registers every extension under the **qualified** id `<plugin-id>/<id>` (e.g. `core/annotations`, `core/slash`, `hello-world/greet`), see `architecture.md` §Extension kinds and `plugin-author-guide.md` §Qualified extension ids. Authors declare only the short id here; the qualifier is composed by the loader from the manifest's `id` (or, for built-ins, from the bundle declaration in `built-ins.ts`). The pattern is intentionally constrained to a single kebab-case segment without the `/` separator: the qualifier always lives in the plugin id, never in the extension id."
|
|
13
|
-
},
|
|
14
|
-
"kind": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"enum": ["provider", "extractor", "analyzer", "action", "formatter", "hook"],
|
|
17
|
-
"description": "Discriminant. MUST match the file exporting this manifest; kind mismatch → load-error."
|
|
18
|
-
},
|
|
19
9
|
"version": {
|
|
20
10
|
"type": "string",
|
|
21
11
|
"description": "Extension semver. Bumped independently from the plugin version; frozen into `state_executions.extension_version` on every run for reproducibility."
|
|
22
12
|
},
|
|
23
13
|
"description": {
|
|
24
14
|
"type": "string",
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
"stability": {
|
|
28
|
-
"type": "string",
|
|
29
|
-
"enum": ["experimental", "stable", "deprecated"],
|
|
30
|
-
"default": "stable",
|
|
31
|
-
"description": "Maturity tag. The kernel MAY warn when an `experimental` extension is used in CI (`--strict`)."
|
|
15
|
+
"minLength": 1,
|
|
16
|
+
"description": "Required short description (1-3 sentences) shown by `sm <kind>s list` and the UI inspector. English-only per AGENTS.md."
|
|
32
17
|
},
|
|
33
|
-
"
|
|
34
|
-
"type": "array",
|
|
35
|
-
"description": "Free-form predicates the kernel evaluates before offering this extension. Canonical keys: `kind=<node-kind>`, `provider=<provider-id>`, `frontmatter.<path>=<value>`. Unknown keys are skipped with a warning, not rejected.",
|
|
36
|
-
"items": { "type": "string" }
|
|
37
|
-
},
|
|
38
|
-
"entry": {
|
|
39
|
-
"type": "string",
|
|
40
|
-
"description": "Optional path to the module exporting this extension (relative to the plugin root). Absent → the kernel uses the path listed in the plugin manifest's `extensions[]` array."
|
|
41
|
-
},
|
|
42
|
-
"annotationContributions": {
|
|
18
|
+
"annotation": {
|
|
43
19
|
"type": "object",
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"description": "Where the key lands inside the sidecar. `namespaced` (default), written under the plugin's `<plugin-id>:` block at the sidecar root. `root`, written as a top-level key alongside the reserved blocks (`for`, `annotations`, `settings`, `audit`); requires `ownership: 'exclusive'` and is treated as elevated trust (two plugins claiming the same root key with `exclusive` is a hard-fail at orchestrator init). See `plugin-author-guide.md` §Annotation contributions."
|
|
62
|
-
}
|
|
20
|
+
"required": ["schema"],
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"description": "Optional, opt-in declaration of a single sidecar annotation key contributed by this extension. The key is the extension's id (i.e. the leaf folder name). Extensions that need multiple annotation keys split into multiple extensions. The runtime exposes the registered catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Analyzer emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contribution.",
|
|
23
|
+
"properties": {
|
|
24
|
+
"schema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "Inline JSON Schema for this contribution's value. The value can be a scalar, an array, or a rich object; the schema is full JSON Schema. Validated when the kernel routes a sidecar write through this extension's namespace (or root key, see `location`). An invalid schema rejects the extension at load with `invalid-manifest`."
|
|
27
|
+
},
|
|
28
|
+
"ownership": {
|
|
29
|
+
"enum": ["exclusive", "shared"],
|
|
30
|
+
"default": "shared",
|
|
31
|
+
"description": "Conflict policy for this key. `shared` (default): the key is namespaced by default; multiple plugins MAY contribute to the same key, last-write-wins per the SidecarStore's deep-merge semantics. `exclusive`: only this plugin may write the key. REQUIRED when `location: 'root'` (a top-level reserved key cannot be silently shared between plugins)."
|
|
32
|
+
},
|
|
33
|
+
"location": {
|
|
34
|
+
"enum": ["namespaced", "root"],
|
|
35
|
+
"default": "namespaced",
|
|
36
|
+
"description": "Where the key lands inside the sidecar. `namespaced` (default): written under the plugin's `<plugin-id>:` block at the sidecar root. `root`: written as a top-level key alongside the reserved blocks (`for`, `annotations`, `settings`, `audit`); requires `ownership: 'exclusive'` and is treated as elevated trust (two plugins claiming the same root key with `exclusive` is a hard-fail at orchestrator init)."
|
|
63
37
|
}
|
|
64
|
-
}
|
|
65
|
-
"description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Analyzer emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
|
|
38
|
+
}
|
|
66
39
|
},
|
|
67
|
-
"
|
|
40
|
+
"settings": {
|
|
68
41
|
"type": "object",
|
|
69
|
-
"additionalProperties": {
|
|
70
|
-
"$ref": "../view-slots.schema.json#/$defs/IViewContribution"
|
|
71
|
-
},
|
|
42
|
+
"additionalProperties": { "$ref": "../input-types.schema.json#/$defs/ISettingDeclaration" },
|
|
72
43
|
"propertyNames": {
|
|
73
44
|
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
|
|
74
45
|
},
|
|
75
|
-
"description": "
|
|
46
|
+
"description": "Extension user-configurable settings. Each entry picks an `input-type` from the closed catalog at `input-types.schema.json#/$defs/InputTypeName`. The extension author NEVER writes JSON Schema for settings, they pick by `type` name and provide per-type parameters (label, default, min/max, options for enums, etc.). The kernel exposes the resolved settings via `ctx.settings.<settingId>` to the extension's runtime methods (`extract`, `evaluate`, `invoke`, etc.); the UI generates a form per declaration; the CLI's `sm plugins config <plugin>/<extension>` exposes the same surface. Settings are read once at extension invocation; changing a setting requires `sm scan` to re-emit (per ROADMAP.md decision D4). Settings live per-extension (not per-plugin) since the structure-as-truth refactor."
|
|
76
47
|
}
|
|
77
48
|
}
|
|
78
49
|
}
|