@skill-map/spec 0.19.0 → 0.21.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 +607 -6
- package/README.md +6 -6
- package/architecture.md +129 -57
- package/cli-contract.md +71 -25
- package/conformance/README.md +2 -2
- package/conformance/cases/kernel-empty-boot.json +2 -2
- package/conformance/cases/sidecar-end-to-end.json +3 -3
- package/conformance/coverage.md +5 -5
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +1 -1
- package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
- package/db-schema.md +22 -18
- package/index.json +36 -36
- package/interfaces/security-scanner.md +2 -2
- package/job-events.md +12 -12
- package/job-lifecycle.md +1 -1
- package/package.json +1 -1
- package/plugin-author-guide.md +131 -82
- package/plugin-kv-api.md +6 -6
- package/prompt-preamble.md +1 -1
- package/schemas/annotations.schema.json +4 -4
- package/schemas/api/rest-envelope.schema.json +4 -4
- package/schemas/conformance-case.schema.json +2 -2
- package/schemas/extensions/analyzer.schema.json +43 -0
- package/schemas/extensions/base.schema.json +5 -5
- package/schemas/extensions/extractor.schema.json +1 -1
- package/schemas/extensions/hook.schema.json +6 -4
- package/schemas/issue.schema.json +6 -6
- package/schemas/link.schema.json +2 -2
- package/schemas/plugins-registry.schema.json +1 -1
- package/schemas/project-config.schema.json +19 -1
- package/schemas/sidecar.schema.json +2 -2
- package/schemas/summaries/agent.schema.json +1 -1
- package/schemas/summaries/command.schema.json +1 -1
- package/schemas/summaries/hook.schema.json +1 -1
- package/schemas/{view-contracts.schema.json → view-slots.schema.json} +93 -55
- package/schemas/extensions/rule.schema.json +0 -43
|
@@ -1,47 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://skill-map.dev/spec/v0/view-
|
|
4
|
-
"title": "
|
|
5
|
-
"description": "Closed catalog of view
|
|
3
|
+
"$id": "https://skill-map.dev/spec/v0/view-slots.schema.json",
|
|
4
|
+
"title": "ViewSlots",
|
|
5
|
+
"description": "Closed catalog of view slots. A view slot is a kernel-published handle that names a visual surface in the UI, fixes the renderer that draws there, and fixes the payload shape the plugin emits. The plugin author picks ONE slot per view contribution; the kernel validates `ctx.emitContribution(id, payload)` against that slot's payload schema in `$defs.payloads`. There is no separate notion of a 'contract' — the slot IS the contract. Closed catalog by design: every new slot requires a spec change + UI renderer mount + scaffolder support + conformance fixtures + tests. Compounds catalog evolution cost; see ROADMAP.md §UI contribution system → 'Known limitations carried forward'. Slots are versioned via the manifest field `catalogCompat` (semver against the catalog as a whole), not per-slot.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"$defs": {
|
|
8
|
-
"
|
|
8
|
+
"SlotName": {
|
|
9
9
|
"type": "string",
|
|
10
10
|
"enum": [
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"node
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
11
|
+
"card.title.right",
|
|
12
|
+
"card.subtitle.left",
|
|
13
|
+
"card.footer.left",
|
|
14
|
+
"card.footer.right",
|
|
15
|
+
"graph.node.alert",
|
|
16
|
+
"inspector.header.badge.counter",
|
|
17
|
+
"inspector.header.badge.tag",
|
|
18
|
+
"inspector.body.panel.breakdown",
|
|
19
|
+
"inspector.body.panel.records",
|
|
20
|
+
"inspector.body.panel.tree",
|
|
21
|
+
"inspector.body.panel.key-values",
|
|
22
|
+
"inspector.body.panel.link-list",
|
|
23
|
+
"inspector.body.panel.markdown",
|
|
24
|
+
"topbar.nav.start"
|
|
21
25
|
],
|
|
22
|
-
"description": "Closed enum of
|
|
26
|
+
"description": "Closed enum of slot identifiers. Adding an entry requires the full spec/UI/scaffolder/conformance round-trip per ROADMAP.md §UI contribution system. Removing or renaming an entry is a catalog-major-bump and triggers `sm plugins upgrade` migration. Slots that share a payload shape (e.g. `card.subtitle.left`, `card.footer.right`, and `card.footer.left` all carry a counter) reference the same payload schema in `$defs.payloads`."
|
|
23
27
|
},
|
|
24
28
|
"Severity": {
|
|
25
29
|
"type": "string",
|
|
26
30
|
"enum": ["info", "warn", "success", "danger"],
|
|
27
|
-
"description": "Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>` severities. Used by
|
|
31
|
+
"description": "Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>` severities. Used by counter, tag, alert, and icon slots for color/contrast hints. The UI maps each severity to a theme-aware tint; plugins do not pick raw colors."
|
|
28
32
|
},
|
|
29
33
|
"IconString": {
|
|
30
34
|
"type": "string",
|
|
31
35
|
"minLength": 1,
|
|
32
36
|
"maxLength": 64,
|
|
33
|
-
"
|
|
37
|
+
"pattern": "^(?:pi pi-[a-z0-9-]+|pi-[a-z0-9-]+|fa-(?:solid|regular|brands) fa-[a-z0-9-]+|fa-[a-z0-9-]+|[^a-zA-Z].*)$",
|
|
38
|
+
"description": "Single string, prefix-discriminated by the UI. Four valid shapes: (1) emoji — any value starting with a non-ASCII-letter codepoint renders as text; (2) PrimeIcons — `pi-foo` or `pi pi-foo` renders as `<i class=\"pi pi-foo\">`; (3) FontAwesome explicit family — `fa-solid fa-foo` / `fa-regular fa-foo` / `fa-brands fa-foo` passes through as-is; (4) FontAwesome shorthand — `fa-foo` (no family token) defaults to `fa-solid fa-foo`. Bare class names without a `pi-` / `fa-` prefix are rejected at manifest load (invalid-manifest). Unknown PrimeIcons / FontAwesome names render no icon (silent fallback) plus a console warning."
|
|
34
39
|
},
|
|
35
40
|
"IViewContribution": {
|
|
36
41
|
"type": "object",
|
|
37
42
|
"additionalProperties": false,
|
|
38
|
-
"required": ["
|
|
43
|
+
"required": ["slot"],
|
|
39
44
|
"properties": {
|
|
40
|
-
"
|
|
45
|
+
"slot": { "$ref": "#/$defs/SlotName" },
|
|
41
46
|
"label": {
|
|
42
47
|
"type": "string",
|
|
43
48
|
"maxLength": 64,
|
|
44
|
-
"description": "Short human-readable label. Used as metadata (docs / plugin-doctor / aria-label) and, for some
|
|
49
|
+
"description": "Short human-readable label. Used as metadata (docs / plugin-doctor / aria-label) and, for some slots, rendered in the chip / panel header / table label. English-only per AGENTS.md (`Externalized texts, not internationalized`). Per-slot notes specify whether it is rendered inline; counter slots keep it as metadata only."
|
|
45
50
|
},
|
|
46
51
|
"tooltip": {
|
|
47
52
|
"type": "string",
|
|
@@ -52,12 +57,12 @@
|
|
|
52
57
|
"emptyText": {
|
|
53
58
|
"type": "string",
|
|
54
59
|
"maxLength": 128,
|
|
55
|
-
"description": "Text shown in the empty placeholder when the
|
|
60
|
+
"description": "Text shown in the empty placeholder when the slot permits an empty payload. Defaults to a UI-supplied generic 'No data.' string. English-only."
|
|
56
61
|
},
|
|
57
62
|
"emitWhenEmpty": {
|
|
58
63
|
"type": "boolean",
|
|
59
64
|
"default": false,
|
|
60
|
-
"description": "When false (default), the kernel drops emissions whose payload is structurally empty (zero counter value, empty array, empty tree, etc.) so the slot stays silent. When true, the renderer surfaces an empty placeholder instead. Per-
|
|
65
|
+
"description": "When false (default), the kernel drops emissions whose payload is structurally empty (zero counter value, empty array, empty tree, etc.) so the slot stays silent. When true, the renderer surfaces an empty placeholder instead. Per-slot definition of 'empty' lives in the payload schema notes."
|
|
61
66
|
},
|
|
62
67
|
"priority": {
|
|
63
68
|
"type": "number",
|
|
@@ -67,15 +72,47 @@
|
|
|
67
72
|
},
|
|
68
73
|
"allOf": [
|
|
69
74
|
{
|
|
70
|
-
"if": {
|
|
71
|
-
|
|
75
|
+
"if": {
|
|
76
|
+
"properties": {
|
|
77
|
+
"slot": {
|
|
78
|
+
"enum": [
|
|
79
|
+
"card.subtitle.left",
|
|
80
|
+
"card.footer.left",
|
|
81
|
+
"card.footer.right",
|
|
82
|
+
"inspector.header.badge.counter"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"then": { "required": ["slot", "icon"] }
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"if": { "properties": { "slot": { "const": "card.title.right" } } },
|
|
91
|
+
"then": { "required": ["slot", "icon"] }
|
|
72
92
|
}
|
|
73
93
|
],
|
|
74
|
-
"description": "Manifest-side declaration of a single view contribution, keyed in `IExtensionBase.viewContributions[<contributionId>]`. The plugin author picks the
|
|
94
|
+
"description": "Manifest-side declaration of a single view contribution, keyed in `IExtensionBase.viewContributions[<contributionId>]`. The plugin author picks ONE slot from the closed `SlotName` enum; the slot fixes the renderer and the payload shape. Slots whose renderer is the counter chip require `icon` in the manifest; same for `card.title.right` (the standalone icon slot)."
|
|
75
95
|
},
|
|
76
96
|
"payloads": {
|
|
77
|
-
"description": "Per-
|
|
78
|
-
"
|
|
97
|
+
"description": "Per-slot payload schemas. The kernel validates `ctx.emitContribution(id, payload)` calls against the entry corresponding to the declared slot before persisting to `scan_contributions`. Off-shape payloads emit an `extension.error` event and drop silently (mirror of `emitLink` off-contract drop pattern). Slots that share a renderer share a payload shape; the schema is defined inline at each slot to keep lookups direct (no internal indirection).",
|
|
98
|
+
"card.title.right": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"additionalProperties": false,
|
|
101
|
+
"properties": {
|
|
102
|
+
"icon": {
|
|
103
|
+
"$ref": "#/$defs/IconString",
|
|
104
|
+
"description": "Optional payload-time icon override. When omitted the manifest-declared `icon` (which is required for this slot — see `IViewContribution.allOf`) is rendered instead. Lets a plugin emit a different icon per node without redeclaring the manifest."
|
|
105
|
+
},
|
|
106
|
+
"severity": { "$ref": "#/$defs/Severity" },
|
|
107
|
+
"tooltip": { "type": "string", "maxLength": 256 }
|
|
108
|
+
},
|
|
109
|
+
"description": "Single icon per node — small standalone marker rendered next to the card title. The manifest requires `icon`; the payload optionally overrides it per node and may add `severity` (color tint) and `tooltip`. No counts, no labels — for chip + number use a counter slot; for label + severity use a tag slot. 'Empty' for `emitWhenEmpty` is the absence of both payload `icon` and a manifest fallback (in practice never empty since the manifest icon is required)."
|
|
110
|
+
},
|
|
111
|
+
"card.subtitle.left": { "$ref": "#/$defs/payloads/_counter" },
|
|
112
|
+
"card.footer.left": { "$ref": "#/$defs/payloads/_counter" },
|
|
113
|
+
"card.footer.right": { "$ref": "#/$defs/payloads/_counter" },
|
|
114
|
+
"inspector.header.badge.counter": { "$ref": "#/$defs/payloads/_counter" },
|
|
115
|
+
"_counter": {
|
|
79
116
|
"type": "object",
|
|
80
117
|
"additionalProperties": false,
|
|
81
118
|
"required": ["value"],
|
|
@@ -91,9 +128,10 @@
|
|
|
91
128
|
"description": "Optional severity for visual tinting. The slot decides whether to honour it (`SLOT_REGISTRY[slot].respectSeverity`). The plugin emits the data; the UI decides the presentation."
|
|
92
129
|
}
|
|
93
130
|
},
|
|
94
|
-
"description": "Single icon + integer pair, modelled after the `.sm-gnode__stat` rows in the card footer. Manifest requires `icon
|
|
131
|
+
"description": "Single icon + integer pair, modelled after the `.sm-gnode__stat` rows in the card footer. Manifest requires `icon` (enforced by `IViewContribution.allOf` for every counter slot); payload carries `value`, optionally `severity` and `tooltip`. The manifest `label` is metadata (docs / plugin-doctor / aria-label) and is NOT rendered inline."
|
|
95
132
|
},
|
|
96
|
-
"
|
|
133
|
+
"inspector.header.badge.tag": { "$ref": "#/$defs/payloads/_tag" },
|
|
134
|
+
"_tag": {
|
|
97
135
|
"type": "object",
|
|
98
136
|
"additionalProperties": false,
|
|
99
137
|
"required": ["label"],
|
|
@@ -108,7 +146,23 @@
|
|
|
108
146
|
"tooltip": { "type": "string", "maxLength": 256 }
|
|
109
147
|
}
|
|
110
148
|
},
|
|
111
|
-
"node
|
|
149
|
+
"graph.node.alert": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"additionalProperties": false,
|
|
152
|
+
"properties": {
|
|
153
|
+
"icon": { "$ref": "#/$defs/IconString" },
|
|
154
|
+
"severity": { "$ref": "#/$defs/Severity" },
|
|
155
|
+
"count": {
|
|
156
|
+
"type": "integer",
|
|
157
|
+
"minimum": 1,
|
|
158
|
+
"maximum": 99,
|
|
159
|
+
"description": "Optional badge count rendered next to the icon (1-99; 99+ collapses to '99+'). Omit for an icon-only marker."
|
|
160
|
+
},
|
|
161
|
+
"tooltip": { "type": "string", "maxLength": 256 }
|
|
162
|
+
},
|
|
163
|
+
"description": "Decoration on the graph node (corner badge / pin). At least one of `icon`, `severity`, `count` is required. 'Empty' for `emitWhenEmpty` is the absence of `icon` AND `count`. Hard cap 1 marker per node per plugin extension (slot config enforces)."
|
|
164
|
+
},
|
|
165
|
+
"inspector.body.panel.breakdown": {
|
|
112
166
|
"type": "object",
|
|
113
167
|
"additionalProperties": false,
|
|
114
168
|
"required": ["entries"],
|
|
@@ -130,7 +184,7 @@
|
|
|
130
184
|
}
|
|
131
185
|
}
|
|
132
186
|
},
|
|
133
|
-
"
|
|
187
|
+
"inspector.body.panel.records": {
|
|
134
188
|
"type": "object",
|
|
135
189
|
"additionalProperties": false,
|
|
136
190
|
"required": ["columns", "rows"],
|
|
@@ -168,7 +222,7 @@
|
|
|
168
222
|
}
|
|
169
223
|
}
|
|
170
224
|
},
|
|
171
|
-
"
|
|
225
|
+
"inspector.body.panel.tree": {
|
|
172
226
|
"$ref": "#/$defs/payloads/_TreeNode",
|
|
173
227
|
"description": "Recursive tree rendered as an indented hierarchy. Hard caps: max depth 6, max 200 total nodes per tree (validator enforces). 'Empty' for `emitWhenEmpty` is the root having no `children`."
|
|
174
228
|
},
|
|
@@ -186,7 +240,7 @@
|
|
|
186
240
|
}
|
|
187
241
|
}
|
|
188
242
|
},
|
|
189
|
-
"
|
|
243
|
+
"inspector.body.panel.key-values": {
|
|
190
244
|
"type": "object",
|
|
191
245
|
"additionalProperties": false,
|
|
192
246
|
"required": ["entries"],
|
|
@@ -215,7 +269,7 @@
|
|
|
215
269
|
}
|
|
216
270
|
}
|
|
217
271
|
},
|
|
218
|
-
"
|
|
272
|
+
"inspector.body.panel.link-list": {
|
|
219
273
|
"type": "object",
|
|
220
274
|
"additionalProperties": false,
|
|
221
275
|
"required": ["entries"],
|
|
@@ -232,7 +286,7 @@
|
|
|
232
286
|
"type": "string",
|
|
233
287
|
"minLength": 1,
|
|
234
288
|
"maxLength": 512,
|
|
235
|
-
"description": "Node path within the scope. Resolved by the UI to a clickable link via `Router.navigate` — never rendered as a raw `[href]` (per the renderer attr-sanitization
|
|
289
|
+
"description": "Node path within the scope. Resolved by the UI to a clickable link via `Router.navigate` — never rendered as a raw `[href]` (per the renderer attr-sanitization analyzer)."
|
|
236
290
|
},
|
|
237
291
|
"label": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
238
292
|
"kind": {
|
|
@@ -247,7 +301,7 @@
|
|
|
247
301
|
}
|
|
248
302
|
}
|
|
249
303
|
},
|
|
250
|
-
"
|
|
304
|
+
"inspector.body.panel.markdown": {
|
|
251
305
|
"type": "object",
|
|
252
306
|
"additionalProperties": false,
|
|
253
307
|
"required": ["markdown"],
|
|
@@ -259,23 +313,7 @@
|
|
|
259
313
|
}
|
|
260
314
|
}
|
|
261
315
|
},
|
|
262
|
-
"
|
|
263
|
-
"type": "object",
|
|
264
|
-
"additionalProperties": false,
|
|
265
|
-
"properties": {
|
|
266
|
-
"icon": { "$ref": "#/$defs/IconString" },
|
|
267
|
-
"severity": { "$ref": "#/$defs/Severity" },
|
|
268
|
-
"count": {
|
|
269
|
-
"type": "integer",
|
|
270
|
-
"minimum": 1,
|
|
271
|
-
"maximum": 99,
|
|
272
|
-
"description": "Optional badge count rendered next to the icon (1-99; 99+ collapses to '99+'). Omit for an icon-only marker."
|
|
273
|
-
},
|
|
274
|
-
"tooltip": { "type": "string", "maxLength": 256 }
|
|
275
|
-
},
|
|
276
|
-
"description": "Decoration on the graph node (corner badge / pin). At least one of `icon`, `severity`, `count` is required. 'Empty' for `emitWhenEmpty` is the absence of `icon` AND `count`. Hard cap 1 marker per node per plugin extension (slot config enforces)."
|
|
277
|
-
},
|
|
278
|
-
"scope-stat": {
|
|
316
|
+
"topbar.nav.start": {
|
|
279
317
|
"type": "object",
|
|
280
318
|
"additionalProperties": false,
|
|
281
319
|
"required": ["value"],
|
|
@@ -285,13 +323,13 @@
|
|
|
285
323
|
{ "type": "integer", "minimum": 0 },
|
|
286
324
|
{ "type": "string", "minLength": 1, "maxLength": 64 }
|
|
287
325
|
],
|
|
288
|
-
"description": "Either a non-negative integer or a short string. The UI renders it as a single chip in
|
|
326
|
+
"description": "Either a non-negative integer or a short string. The UI renders it as a single chip in the topbar."
|
|
289
327
|
},
|
|
290
328
|
"label": { "type": "string", "minLength": 1, "maxLength": 32 },
|
|
291
329
|
"tooltip": { "type": "string", "maxLength": 256 },
|
|
292
330
|
"severity": { "$ref": "#/$defs/Severity" }
|
|
293
331
|
},
|
|
294
|
-
"description": "Single value summarizing the entire scope. Emitted ONCE per scan (not per node). Plugins use `ctx.emitScopeContribution(...)` (
|
|
332
|
+
"description": "Single value summarizing the entire scope. Emitted ONCE per scan (not per node). Plugins use `ctx.emitScopeContribution(...)` (analyzer context) — extractors do not see `emitScopeContribution`."
|
|
295
333
|
}
|
|
296
334
|
}
|
|
297
335
|
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://skill-map.dev/spec/v0/extensions/rule.schema.json",
|
|
4
|
-
"title": "ExtensionRule",
|
|
5
|
-
"description": "Manifest shape for a `Rule` extension. A rule consumes the full graph (nodes + links) after all extractors have run and emits `Issue[]`. Rules are dual-mode: `deterministic` rules 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` rules invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (`sm job submit rule:<id>`); their output MAY vary across runs and they NEVER participate in `sm scan`. See `architecture.md` §Execution modes for the full contract.",
|
|
6
|
-
"allOf": [
|
|
7
|
-
{ "$ref": "base.schema.json" }
|
|
8
|
-
],
|
|
9
|
-
"type": "object",
|
|
10
|
-
"required": ["id", "kind", "version", "emitsRuleIds", "defaultSeverity"],
|
|
11
|
-
"unevaluatedProperties": false,
|
|
12
|
-
"properties": {
|
|
13
|
-
"kind": { "const": "rule" },
|
|
14
|
-
"mode": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"enum": ["deterministic", "probabilistic"],
|
|
17
|
-
"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 rules that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
|
|
19
|
-
},
|
|
20
|
-
"emitsRuleIds": {
|
|
21
|
-
"type": "array",
|
|
22
|
-
"description": "List of `rule_id` values this rule may emit on issues. Typically a singleton (`trigger-collision` → emits `trigger-collision`). A rule emitting a `rule_id` not in this list → kernel logs `rule-id-violation` but keeps the issue (forward compatibility).",
|
|
23
|
-
"minItems": 1,
|
|
24
|
-
"items": { "type": "string" }
|
|
25
|
-
},
|
|
26
|
-
"defaultSeverity": {
|
|
27
|
-
"type": "string",
|
|
28
|
-
"enum": ["error", "warn", "info"],
|
|
29
|
-
"description": "Severity attached by default to emitted issues. Rules MAY override per-issue."
|
|
30
|
-
},
|
|
31
|
-
"consumes": {
|
|
32
|
-
"type": "string",
|
|
33
|
-
"enum": ["nodes", "links", "both"],
|
|
34
|
-
"default": "both",
|
|
35
|
-
"description": "Which slices of the graph the rule 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 rule reads its own config from `config_preferences` under the key `rules.<id>.<field>`. Implementations MAY surface these in `sm config`."
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|