@skill-map/spec 0.27.0 → 0.29.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 +250 -0
- package/architecture.md +18 -17
- package/conformance/cases/sidecar-end-to-end.json +4 -4
- package/conformance/coverage.md +5 -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 +1 -0
- package/index.json +20 -17
- package/package.json +1 -1
- package/plugin-author-guide.md +99 -43
- 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 +10 -26
- 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,7 +80,11 @@ 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
|
|
|
@@ -110,7 +144,7 @@ The kernel guards against two foot-guns:
|
|
|
110
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.
|
|
111
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.
|
|
112
146
|
|
|
113
|
-
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.
|
|
114
148
|
|
|
115
149
|
### Granularity, bundle vs extension
|
|
116
150
|
|
|
@@ -139,48 +173,60 @@ Resolution order is the same as for plugin enabled-state: DB override (`config_p
|
|
|
139
173
|
|
|
140
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.
|
|
141
175
|
|
|
142
|
-
|
|
176
|
+
Set `granularity` in your `plugin.json`. The folder layout supplies the extensions; the kernel discovers them automatically:
|
|
143
177
|
|
|
144
178
|
```jsonc
|
|
145
179
|
{
|
|
146
180
|
"id": "my-multi-tool",
|
|
147
181
|
"version": "1.0.0",
|
|
148
182
|
"specCompat": "^1.0.0",
|
|
149
|
-
"granularity": "extension"
|
|
150
|
-
"extensions": [
|
|
151
|
-
"./extensions/orphan-skill-analyzer.js",
|
|
152
|
-
"./extensions/csv-formatter.js"
|
|
153
|
-
]
|
|
183
|
+
"granularity": "extension"
|
|
154
184
|
}
|
|
155
185
|
```
|
|
156
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
|
+
|
|
157
198
|
The default (`'bundle'`) is the right answer for almost every plugin, keep the manifest minimal until the plugin actually ships several independent capabilities.
|
|
158
199
|
|
|
159
|
-
### Extractor `
|
|
200
|
+
### Extractor `precondition`, narrow the pipeline
|
|
160
201
|
|
|
161
|
-
An `Extractor` extension MAY declare
|
|
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`.
|
|
162
203
|
|
|
163
|
-
|
|
204
|
+
```ts
|
|
205
|
+
precondition?: {
|
|
206
|
+
kind?: string[]; // qualified `<plugin>/<kindName>` ids
|
|
207
|
+
provider?: string[]; // plugin ids
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| `precondition` | Behaviour |
|
|
164
212
|
|---|---|
|
|
165
213
|
| Absent (`undefined`) | **Default.** The extractor runs on every kind the loaded Providers emit. |
|
|
166
|
-
| `['skill']` | Runs only on skill nodes. |
|
|
167
|
-
| `['skill', '
|
|
168
|
-
| `[]` |
|
|
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). |
|
|
169
218
|
|
|
170
|
-
|
|
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.
|
|
171
220
|
|
|
172
221
|
Use case, a deterministic frontmatter-tag extractor that only makes sense for skills:
|
|
173
222
|
|
|
174
223
|
```javascript
|
|
175
224
|
export default {
|
|
176
|
-
id
|
|
177
|
-
kind: 'extractor',
|
|
225
|
+
// id, kind, pluginId injected by the loader from the folder path
|
|
178
226
|
version: '1.0.0',
|
|
179
227
|
description: 'Lifts the `tags:` frontmatter array into `references` links for skill nodes.',
|
|
180
|
-
emitsLinkKinds: ['references'],
|
|
181
|
-
defaultConfidence: 'high',
|
|
182
228
|
scope: 'frontmatter',
|
|
183
|
-
|
|
229
|
+
precondition: { kind: ['claude/skill'] },
|
|
184
230
|
async extract(ctx) {
|
|
185
231
|
// Never invoked for agents, commands, hooks, or notes, the kernel
|
|
186
232
|
// skipped this node before reaching us.
|
|
@@ -200,7 +246,9 @@ export default {
|
|
|
200
246
|
|
|
201
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.
|
|
202
248
|
|
|
203
|
-
**
|
|
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.
|
|
204
252
|
|
|
205
253
|
### Module top-level side effects survive load timeouts
|
|
206
254
|
|
|
@@ -226,23 +274,26 @@ Required fields (see [`schemas/plugins-registry.schema.json#/$defs/PluginManifes
|
|
|
226
274
|
|
|
227
275
|
| Field | Type | Notes |
|
|
228
276
|
|---|---|---|
|
|
229
|
-
| `id` | kebab-case string | Globally unique. Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`. |
|
|
230
277
|
| `version` | semver | Plugin version, independent of `specCompat`. |
|
|
231
278
|
| `specCompat` | semver range | Spec versions this plugin is compatible with. Checked via `semver.satisfies(specVersion, this)` at load time. |
|
|
232
|
-
| `
|
|
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. |
|
|
233
281
|
|
|
234
282
|
Optional fields:
|
|
235
283
|
|
|
236
284
|
| Field | Type | Notes |
|
|
237
285
|
|---|---|---|
|
|
238
|
-
| `
|
|
239
|
-
| `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. |
|
|
240
287
|
| `storage` | object | `{ "mode": "kv" }` or `{ "mode": "dedicated", "tables": [...], "migrations": [...] }`. Absent means the plugin does not persist state. |
|
|
241
288
|
| `author` | string | Free-form. |
|
|
242
289
|
| `license` | string | SPDX identifier. |
|
|
243
290
|
| `homepage` | string | URL. |
|
|
244
291
|
| `repository` | string | URL. |
|
|
245
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
|
+
|
|
246
297
|
### `specCompat` strategy
|
|
247
298
|
|
|
248
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.
|
|
@@ -286,7 +337,13 @@ Extractors are deterministic-only and never see `ctx.runner`. If an Extractor ne
|
|
|
286
337
|
|
|
287
338
|
You can read `ctx.node.sidecar.*` freely, the kernel's per-`(node, extractor)` cache hashes the sidecar `annotations` block alongside the `.md` body, so a `.sm`-only edit invalidates the cached run automatically. No manifest flag, no opt-in: just read what you need.
|
|
288
339
|
|
|
289
|
-
> **Pick a syntax that doesn't collide with built-ins.** The built-in `at-directive`
|
|
340
|
+
> **Pick a syntax that doesn't collide with built-ins.** The built-in `at-directive` and `slash` extractors claim the `@` and `/` prefixes with LLM-aligned semantics:
|
|
341
|
+
>
|
|
342
|
+
> - **`core/at-directive`**: bare handles (`@team-lead`) and namespaced agents (`@my-plugin/foo-extractor`, `@skill-map:explore`) emit `mentions` links; file-flavoured tokens (`@docs/api/v1.md`, `@./readme.md`, `@../parent.md`, `@/abs/path.md`) emit `references` links so the graph treats them as file pointers, not entity mentions, the same way Claude Code / Gemini CLI / Cursor would resolve them. The kind dispatch keys on (a) an explicit relative / absolute path prefix or (b) a known file extension at the tail.
|
|
343
|
+
> - **`core/slash`**: bare commands (`/scan`, `/skill-map:explore`) emit `invokes`; tokens whose next character is another `/` or any other identifier char are dropped as path segments (`/Volumes/disk`, `/api/v1/items`).
|
|
344
|
+
> - **Both extractors strip fenced code blocks and inline backticks before matching**, so author-marked literal payload never registers as invocation surface.
|
|
345
|
+
>
|
|
346
|
+
> A new extractor that also matches one of those prefixes will likely fire on the same input, and if the two emit different `target` shapes the kernel raises a `trigger-collision` error. The example below uses a wikilink-style `[[ref:<name>]]` pattern to side-step this; reserve `@` and `/` for the built-ins.
|
|
290
347
|
|
|
291
348
|
```javascript
|
|
292
349
|
import { normalizeTrigger } from '@skill-map/cli';
|
|
@@ -660,7 +717,7 @@ import { test } from 'node:test';
|
|
|
660
717
|
import { strictEqual } from 'node:assert';
|
|
661
718
|
import { runExtractorOnFixture, node } from '@skill-map/testkit';
|
|
662
719
|
|
|
663
|
-
import extractor from '../
|
|
720
|
+
import extractor from '../extractors/my-extractor/index.js';
|
|
664
721
|
|
|
665
722
|
test('emits one reference per [[ref:<name>]] token', async () => {
|
|
666
723
|
const { links } = await runExtractorOnFixture(extractor, {
|
|
@@ -700,7 +757,7 @@ Full surface in `@skill-map/testkit/index.ts`.
|
|
|
700
757
|
| `incompatible-spec` | manifest parsed but `semver.satisfies` failed against the installed spec. | Plugin built against an older / newer spec. |
|
|
701
758
|
| `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. |
|
|
702
759
|
| `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. |
|
|
703
|
-
| `id-collision` | two plugins reachable from different roots declared the same `id`. Both collided plugins receive this status; no precedence analyzer applies. |
|
|
760
|
+
| `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. |
|
|
704
761
|
|
|
705
762
|
`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.
|
|
706
763
|
|
|
@@ -715,7 +772,7 @@ Full surface in `@skill-map/testkit/index.ts`.
|
|
|
715
772
|
`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:
|
|
716
773
|
|
|
717
774
|
```js
|
|
718
|
-
// my-plugin/
|
|
775
|
+
// my-plugin/extractors/my-extractor/index.js
|
|
719
776
|
export default {
|
|
720
777
|
id: 'my-extractor',
|
|
721
778
|
kind: 'extractor',
|
|
@@ -765,7 +822,7 @@ auditor:
|
|
|
765
822
|
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.
|
|
766
823
|
|
|
767
824
|
```js
|
|
768
|
-
// compliance-plugin/
|
|
825
|
+
// compliance-plugin/analyzers/compliance-checker/index.js
|
|
769
826
|
export default {
|
|
770
827
|
id: 'compliance-checker',
|
|
771
828
|
kind: 'analyzer',
|
|
@@ -1036,9 +1093,10 @@ Full plugin walkthrough:
|
|
|
1036
1093
|
|
|
1037
1094
|
```
|
|
1038
1095
|
plugins/acme-keyword-finder/
|
|
1039
|
-
├── plugin.json
|
|
1040
|
-
└──
|
|
1041
|
-
└──
|
|
1096
|
+
├── plugin.json ← manifest with settings + catalogCompat
|
|
1097
|
+
└── extractors/
|
|
1098
|
+
└── keyword-finder/
|
|
1099
|
+
└── index.js ← extract() with ctx.emitContribution
|
|
1042
1100
|
```
|
|
1043
1101
|
|
|
1044
1102
|
`plugin.json`:
|
|
@@ -1049,7 +1107,7 @@ plugins/acme-keyword-finder/
|
|
|
1049
1107
|
"version": "1.0.0",
|
|
1050
1108
|
"specCompat": "^0.20.0",
|
|
1051
1109
|
"catalogCompat": "^1.0.0",
|
|
1052
|
-
"
|
|
1110
|
+
"granularity": "bundle",
|
|
1053
1111
|
"settings": {
|
|
1054
1112
|
"keywords": {
|
|
1055
1113
|
"type": "string-list",
|
|
@@ -1061,17 +1119,15 @@ plugins/acme-keyword-finder/
|
|
|
1061
1119
|
}
|
|
1062
1120
|
```
|
|
1063
1121
|
|
|
1064
|
-
`
|
|
1122
|
+
`extractors/keyword-finder/index.js`:
|
|
1065
1123
|
|
|
1066
1124
|
```js
|
|
1067
|
-
export
|
|
1125
|
+
export default {
|
|
1068
1126
|
id: 'keyword-finder',
|
|
1069
|
-
pluginId: 'acme-keyword-finder',
|
|
1070
1127
|
kind: 'extractor',
|
|
1071
1128
|
version: '1.0.0',
|
|
1072
1129
|
description: 'Counts configured keywords per node.',
|
|
1073
1130
|
stability: 'stable',
|
|
1074
|
-
mode: 'deterministic',
|
|
1075
1131
|
emitsLinkKinds: [],
|
|
1076
1132
|
defaultConfidence: 'high',
|
|
1077
1133
|
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
|
}
|