@skill-map/spec 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +672 -0
  2. package/README.md +1 -1
  3. package/architecture.md +281 -16
  4. package/cli-contract.md +122 -6
  5. package/conformance/cases/orphan-markdown-fallback.json +22 -0
  6. package/conformance/cases/plugin-missing-ui-rejected.json +4 -1
  7. package/conformance/cases/sidecar-end-to-end.json +25 -0
  8. package/conformance/coverage.md +9 -3
  9. package/conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md +6 -0
  10. package/conformance/fixtures/orphan-markdown/ARCHITECTURE.md +10 -0
  11. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +6 -6
  12. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +12 -0
  13. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.md +8 -0
  14. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm +20 -0
  15. package/conformance/fixtures/sidecar-example/agent-example.md +17 -0
  16. package/conformance/fixtures/sidecar-example/agent-example.sm +53 -0
  17. package/db-schema.md +73 -15
  18. package/index.json +42 -19
  19. package/package.json +1 -1
  20. package/plugin-author-guide.md +426 -27
  21. package/schemas/annotations.schema.json +75 -0
  22. package/schemas/api/rest-envelope.schema.json +159 -46
  23. package/schemas/bump-report.schema.json +29 -0
  24. package/schemas/extensions/base.schema.json +36 -1
  25. package/schemas/extensions/extractor.schema.json +3 -10
  26. package/schemas/extensions/provider.schema.json +23 -1
  27. package/schemas/frontmatter/base.schema.json +6 -1
  28. package/schemas/input-types.schema.json +260 -0
  29. package/schemas/node.schema.json +36 -23
  30. package/schemas/plugins-registry.schema.json +14 -2
  31. package/schemas/project-config.schema.json +11 -0
  32. package/schemas/report-base-deterministic.schema.json +15 -0
  33. package/schemas/sidecar.schema.json +96 -0
  34. package/schemas/summaries/{note.schema.json → markdown.schema.json} +5 -5
  35. package/schemas/view-contracts.schema.json +298 -0
@@ -0,0 +1,298 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/view-contracts.schema.json",
4
+ "title": "ViewContracts",
5
+ "description": "Closed catalog of view contracts. A view contract is a semantic spec a plugin author picks by name to surface per-node (or per-scope) data in the UI without shipping any UI code. The kernel publishes this catalog; plugin authors pick by `contract` name in their extension manifest's `viewContributions` map; the UI driving adapter maps `contract → slot(s) + renderer` (slot policy is UI-only, kernel does not know about slots). The plugin emits per-node payloads via `ctx.emitContribution(id, payload)` — payloads are validated at emit time against the contract's payload schema in `$defs.payloads`. Closed catalog by design: every new contract requires a spec change + UI renderer + scaffolder support + conformance fixtures + tests. Compounds catalog evolution cost; see ROADMAP.md §UI contribution system → 'Known limitations carried forward'. Contracts are versioned via the manifest field `catalogCompat` (semver against the catalog as a whole), not per-contract.",
6
+ "type": "object",
7
+ "$defs": {
8
+ "ContractName": {
9
+ "type": "string",
10
+ "enum": [
11
+ "node-counter",
12
+ "node-tag",
13
+ "node-breakdown",
14
+ "node-records",
15
+ "node-tree",
16
+ "node-key-values",
17
+ "node-link-list",
18
+ "node-markdown",
19
+ "node-alert",
20
+ "scope-stat"
21
+ ],
22
+ "description": "Closed enum of contract 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."
23
+ },
24
+ "Severity": {
25
+ "type": "string",
26
+ "enum": ["info", "warn", "success", "danger"],
27
+ "description": "Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>` severities. Used by `node-counter`, `node-tag`, and `node-alert` for color/contrast hints. The UI maps each severity to a theme-aware tint; plugins do not pick raw colors."
28
+ },
29
+ "IconString": {
30
+ "type": "string",
31
+ "minLength": 1,
32
+ "maxLength": 64,
33
+ "description": "Single string, dual-resolved by the UI. If the value matches a Unicode `\\p{Extended_Pictographic}` codepoint, render as emoji text. Otherwise resolve as a PrimeIcons class id (without the `pi-` prefix; the UI prepends it). Unknown PrimeIcons names render no icon (silent fallback) plus a console warning. The plugin author types either the emoji character or the icon name; the discrimination is automatic."
34
+ },
35
+ "IViewContribution": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["contract"],
39
+ "properties": {
40
+ "contract": { "$ref": "#/$defs/ContractName" },
41
+ "label": {
42
+ "type": "string",
43
+ "maxLength": 64,
44
+ "description": "Short human-readable label. Used as metadata (docs / plugin-doctor / aria-label) and, for some contracts, rendered in the chip / panel header / table label. English-only per AGENTS.md (`Externalized texts, not internationalized`). Per-contract notes specify whether it is rendered inline; `node-counter` keeps it as metadata only."
45
+ },
46
+ "tooltip": {
47
+ "type": "string",
48
+ "maxLength": 256,
49
+ "description": "Hover tooltip shown on the chip or panel header. English-only."
50
+ },
51
+ "icon": { "$ref": "#/$defs/IconString" },
52
+ "emptyText": {
53
+ "type": "string",
54
+ "maxLength": 128,
55
+ "description": "Text shown in the empty placeholder when the contract permits an empty payload. Defaults to a UI-supplied generic 'No data.' string. English-only."
56
+ },
57
+ "emitWhenEmpty": {
58
+ "type": "boolean",
59
+ "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-contract definition of 'empty' lives in the contract's payload schema notes."
61
+ },
62
+ "priority": {
63
+ "type": "number",
64
+ "default": 100,
65
+ "description": "Optional ordering hint. Slots configured with `order: 'priority'` sort contributions ASC by this value, with alphabetical tie-break by qualified id. Default 100 — plugins use it to suggest where their contribution belongs relative to others sharing the same slot. The slot has the final say on whether `priority` is honoured at all."
66
+ }
67
+ },
68
+ "allOf": [
69
+ {
70
+ "if": { "properties": { "contract": { "const": "node-counter" } } },
71
+ "then": { "required": ["contract", "icon"] }
72
+ }
73
+ ],
74
+ "description": "Manifest-side declaration of a single view contribution, keyed in `IExtensionBase.viewContributions[<contributionId>]`. The plugin author picks the contract by name; the rest is presentation tuning. The plugin NEVER picks a slot — the UI maps contract → slot(s). Some contracts add per-contract requireds (e.g. `node-counter` requires `icon`)."
75
+ },
76
+ "payloads": {
77
+ "description": "Per-contract payload schemas, keyed by contract name. The kernel validates `ctx.emitContribution(id, payload)` calls against the entry corresponding to the declared contract before persisting to `scan_contributions`. Off-contract payloads emit an `extension.error` event and drop silently (mirror of `emitLink` off-contract drop pattern).",
78
+ "node-counter": {
79
+ "type": "object",
80
+ "additionalProperties": false,
81
+ "required": ["value"],
82
+ "properties": {
83
+ "value": {
84
+ "type": "integer",
85
+ "minimum": 0,
86
+ "description": "Single non-negative integer for the chip / badge. 'Empty' for `emitWhenEmpty` purposes is `value === 0`."
87
+ },
88
+ "tooltip": { "type": "string", "maxLength": 256 },
89
+ "severity": {
90
+ "$ref": "#/$defs/Severity",
91
+ "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
+ }
93
+ },
94
+ "description": "Single icon + integer pair, modelled after the `.sm-gnode__stat` rows in the card footer. Manifest requires `icon`; payload carries `value`, optionally `severity` and `tooltip`. The manifest `label` is metadata (docs / plugin-doctor / aria-label) and is NOT rendered inline."
95
+ },
96
+ "node-tag": {
97
+ "type": "object",
98
+ "additionalProperties": false,
99
+ "required": ["label"],
100
+ "properties": {
101
+ "label": {
102
+ "type": "string",
103
+ "minLength": 1,
104
+ "maxLength": 32,
105
+ "description": "Tag text (e.g. 'fresh', 'stale', '7d ago'). 'Empty' for `emitWhenEmpty` is `label === ''`."
106
+ },
107
+ "severity": { "$ref": "#/$defs/Severity" },
108
+ "tooltip": { "type": "string", "maxLength": 256 }
109
+ }
110
+ },
111
+ "node-breakdown": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "required": ["entries"],
115
+ "properties": {
116
+ "entries": {
117
+ "type": "array",
118
+ "maxItems": 20,
119
+ "items": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "required": ["label", "value"],
123
+ "properties": {
124
+ "label": { "type": "string", "minLength": 1, "maxLength": 32 },
125
+ "value": { "type": "integer", "minimum": 0 },
126
+ "tooltip": { "type": "string", "maxLength": 256 }
127
+ }
128
+ },
129
+ "description": "Top-N labeled values rendered as a horizontal bar chart. Hard cap 20 entries (overflow rejected at validation, plugin should pre-truncate). 'Empty' for `emitWhenEmpty` is `entries.length === 0`."
130
+ }
131
+ }
132
+ },
133
+ "node-records": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "required": ["columns", "rows"],
137
+ "properties": {
138
+ "columns": {
139
+ "type": "array",
140
+ "minItems": 1,
141
+ "maxItems": 6,
142
+ "items": {
143
+ "type": "object",
144
+ "additionalProperties": false,
145
+ "required": ["key", "label"],
146
+ "properties": {
147
+ "key": { "type": "string", "minLength": 1, "maxLength": 32, "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$" },
148
+ "label": { "type": "string", "minLength": 1, "maxLength": 32 }
149
+ }
150
+ },
151
+ "description": "Column declarations (max 6). Each row's value at `key` is rendered under `label`."
152
+ },
153
+ "rows": {
154
+ "type": "array",
155
+ "maxItems": 50,
156
+ "items": {
157
+ "type": "object",
158
+ "additionalProperties": {
159
+ "oneOf": [
160
+ { "type": "string", "maxLength": 256 },
161
+ { "type": "number" },
162
+ { "type": "boolean" },
163
+ { "type": "null" }
164
+ ]
165
+ }
166
+ },
167
+ "description": "Tabular rows (max 50). Cell values are scalar only (string ≤256 chars, number, boolean, or null). 'Empty' for `emitWhenEmpty` is `rows.length === 0`."
168
+ }
169
+ }
170
+ },
171
+ "node-tree": {
172
+ "$ref": "#/$defs/payloads/_TreeNode",
173
+ "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
+ },
175
+ "_TreeNode": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "required": ["label"],
179
+ "properties": {
180
+ "label": { "type": "string", "minLength": 1, "maxLength": 64 },
181
+ "marker": { "$ref": "#/$defs/IconString" },
182
+ "tooltip": { "type": "string", "maxLength": 256 },
183
+ "children": {
184
+ "type": "array",
185
+ "items": { "$ref": "#/$defs/payloads/_TreeNode" }
186
+ }
187
+ }
188
+ },
189
+ "node-key-values": {
190
+ "type": "object",
191
+ "additionalProperties": false,
192
+ "required": ["entries"],
193
+ "properties": {
194
+ "entries": {
195
+ "type": "array",
196
+ "maxItems": 50,
197
+ "items": {
198
+ "type": "object",
199
+ "additionalProperties": false,
200
+ "required": ["key", "value"],
201
+ "properties": {
202
+ "key": { "type": "string", "minLength": 1, "maxLength": 64 },
203
+ "value": {
204
+ "oneOf": [
205
+ { "type": "string", "maxLength": 512 },
206
+ { "type": "number" },
207
+ { "type": "boolean" },
208
+ { "type": "null" }
209
+ ]
210
+ },
211
+ "tooltip": { "type": "string", "maxLength": 256 }
212
+ }
213
+ },
214
+ "description": "Flat key/value pairs (max 50). Renders as a definition list. 'Empty' for `emitWhenEmpty` is `entries.length === 0`."
215
+ }
216
+ }
217
+ },
218
+ "node-link-list": {
219
+ "type": "object",
220
+ "additionalProperties": false,
221
+ "required": ["entries"],
222
+ "properties": {
223
+ "entries": {
224
+ "type": "array",
225
+ "maxItems": 100,
226
+ "items": {
227
+ "type": "object",
228
+ "additionalProperties": false,
229
+ "required": ["path"],
230
+ "properties": {
231
+ "path": {
232
+ "type": "string",
233
+ "minLength": 1,
234
+ "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 rule)."
236
+ },
237
+ "label": { "type": "string", "minLength": 1, "maxLength": 128 },
238
+ "kind": {
239
+ "type": "string",
240
+ "minLength": 1,
241
+ "maxLength": 32,
242
+ "description": "Optional Provider kind id (informational). The UI may apply per-kind tinting from `kindRegistry`."
243
+ }
244
+ }
245
+ },
246
+ "description": "List of in-scope node paths (max 100). 'Empty' for `emitWhenEmpty` is `entries.length === 0`."
247
+ }
248
+ }
249
+ },
250
+ "node-markdown": {
251
+ "type": "object",
252
+ "additionalProperties": false,
253
+ "required": ["markdown"],
254
+ "properties": {
255
+ "markdown": {
256
+ "type": "string",
257
+ "maxLength": 4096,
258
+ "description": "Markdown text rendered with a sanitized allow-list (paragraphs, headings up to H3, lists, inline code, fenced code, emphasis, strong, blockquote). HTML, scripts, embedded SVG, image tags, and link autodetection are stripped. Hard cap 4096 chars to keep render cost bounded. 'Empty' for `emitWhenEmpty` is `markdown.trim() === ''`."
259
+ }
260
+ }
261
+ },
262
+ "node-alert": {
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": {
279
+ "type": "object",
280
+ "additionalProperties": false,
281
+ "required": ["value"],
282
+ "properties": {
283
+ "value": {
284
+ "oneOf": [
285
+ { "type": "integer", "minimum": 0 },
286
+ { "type": "string", "minLength": 1, "maxLength": 64 }
287
+ ],
288
+ "description": "Either a non-negative integer or a short string. The UI renders it as a single chip in `topbar.actions.indicator`."
289
+ },
290
+ "label": { "type": "string", "minLength": 1, "maxLength": 32 },
291
+ "tooltip": { "type": "string", "maxLength": 256 },
292
+ "severity": { "$ref": "#/$defs/Severity" }
293
+ },
294
+ "description": "Single value summarizing the entire scope. Emitted ONCE per scan (not per node). Plugins use `ctx.emitScopeContribution(...)` (rule context) — extractors do not see `emitScopeContribution`."
295
+ }
296
+ }
297
+ }
298
+ }