@skill-map/spec 0.12.0 → 0.13.1

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.
@@ -0,0 +1,170 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/api/rest-envelope.schema.json",
4
+ "title": "RestEnvelope",
5
+ "description": "Wrapper shape for REST responses under `/api/*` (Step 14.2). Three variants distinguished by the `kind` discriminator and which payload field is present (`items` for list kinds, `item` for single-resource kinds, `value` for `kind: 'config'`). The `/api/scan` and `/api/health` responses are exempt — they carry the underlying `ScanResult` / `IHealthResponse` shape directly. The `/api/graph` response is also exempt — it returns the formatter's native textual output (text/plain or text/markdown). Step 14.5.d adds the required `kindRegistry` field on every payload-bearing variant so the UI can render Provider-declared kinds (label, color, icon) without hardcoding visuals; sentinel kinds (`health`, `scan`, `graph`) stay exempt because they don't carry an envelope payload. The change keeps `schemaVersion` at `'1'` — the BFF is greenfield (no released consumers run against `'1'` without `kindRegistry`), so a versioned migration buys nothing.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "kind"],
8
+ "properties": {
9
+ "schemaVersion": {
10
+ "type": "string",
11
+ "const": "1",
12
+ "description": "Envelope shape version. Bumped only on breaking changes. The Step 14.5.d addition of `kindRegistry` keeps the version at `'1'` because there are no released consumers in the wild."
13
+ },
14
+ "kind": {
15
+ "type": "string",
16
+ "enum": ["nodes", "links", "issues", "plugins", "config", "graph", "node", "health", "scan"],
17
+ "description": "Discriminator. List kinds (`nodes`, `links`, `issues`, `plugins`) carry `items`. The `node` kind carries `item`. The `config` kind carries `value`. The `health` / `scan` / `graph` values are reserved for documentation parity with the routes that DON'T use this envelope."
18
+ },
19
+ "items": {
20
+ "type": "array",
21
+ "description": "Present when `kind` is one of the list kinds (`nodes`, `links`, `issues`, `plugins`). Empty array is valid and means the filter matched zero rows."
22
+ },
23
+ "item": {
24
+ "type": "object",
25
+ "description": "Present when `kind` is `'node'`. Single-resource envelope payload."
26
+ },
27
+ "value": {
28
+ "type": "object",
29
+ "description": "Present when `kind` is `'config'`. Carries the merged effective config object."
30
+ },
31
+ "filters": {
32
+ "type": "object",
33
+ "description": "Echo of the URL filters the server applied, normalized into a JSON-friendly shape (arrays for multi-value filters, `null` for absent ones). Helps the client correlate the response with the request."
34
+ },
35
+ "kindRegistry": {
36
+ "type": "object",
37
+ "description": "Catalog of node kinds active in the current scope, keyed by kind name. Built once per server boot from every enabled Provider's `kinds` map and embedded into every payload-bearing envelope so the UI can render kind tags / palette swatches / graph nodes against Provider-declared visuals (label, color, icon) without ever hardcoding a closed kind enum. Sentinel envelopes (`health`, `scan`, `graph`) are exempt.",
38
+ "additionalProperties": {
39
+ "type": "object",
40
+ "required": ["providerId", "label", "color"],
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "providerId": {
44
+ "type": "string",
45
+ "minLength": 1,
46
+ "description": "Id of the Provider that contributed this kind. Lets the UI scope kind ownership and disambiguate when two Providers happen to declare the same kind name (kernel surfaces this as `provider-ambiguous` but the UI may still receive the merged registry during the conflict window)."
47
+ },
48
+ "label": { "type": "string", "minLength": 1 },
49
+ "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
50
+ "colorDark": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
51
+ "emoji": { "type": "string", "minLength": 1, "maxLength": 8 },
52
+ "icon": {
53
+ "oneOf": [
54
+ {
55
+ "type": "object",
56
+ "required": ["kind", "id"],
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "kind": { "const": "pi" },
60
+ "id": { "type": "string", "pattern": "^pi-[a-z0-9]+(-[a-z0-9]+)*$" }
61
+ }
62
+ },
63
+ {
64
+ "type": "object",
65
+ "required": ["kind", "path"],
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "kind": { "const": "svg" },
69
+ "path": { "type": "string", "minLength": 1 }
70
+ }
71
+ }
72
+ ]
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "counts": {
78
+ "type": "object",
79
+ "required": ["total", "returned"],
80
+ "properties": {
81
+ "total": {
82
+ "type": "integer",
83
+ "minimum": 0,
84
+ "description": "Total rows after filtering, before pagination is applied."
85
+ },
86
+ "returned": {
87
+ "type": "integer",
88
+ "minimum": 0,
89
+ "description": "Rows actually carried in `items` (≤ `limit`)."
90
+ },
91
+ "page": {
92
+ "type": "object",
93
+ "required": ["offset", "limit"],
94
+ "properties": {
95
+ "offset": {
96
+ "type": "integer",
97
+ "minimum": 0,
98
+ "description": "Pagination window offset (zero-based)."
99
+ },
100
+ "limit": {
101
+ "type": "integer",
102
+ "minimum": 0,
103
+ "description": "Maximum items the page may carry."
104
+ }
105
+ },
106
+ "additionalProperties": false,
107
+ "description": "Pagination window. Present when the endpoint paginates (today: `/api/nodes` only)."
108
+ }
109
+ },
110
+ "additionalProperties": false,
111
+ "description": "Tally + paging info. Present on every list / single envelope; absent on `health` / `scan` / `graph` responses (which don't use this envelope)."
112
+ }
113
+ },
114
+ "oneOf": [
115
+ {
116
+ "description": "List envelope — `items` payload + `counts` + `kindRegistry`.",
117
+ "required": ["items", "counts", "filters", "kindRegistry"],
118
+ "properties": {
119
+ "kind": { "enum": ["nodes", "links", "issues", "plugins"] }
120
+ },
121
+ "not": {
122
+ "anyOf": [
123
+ { "required": ["item"] },
124
+ { "required": ["value"] }
125
+ ]
126
+ }
127
+ },
128
+ {
129
+ "description": "Single-resource envelope — `item` payload + `kindRegistry`, no `counts` / `filters`.",
130
+ "required": ["item", "kindRegistry"],
131
+ "properties": {
132
+ "kind": { "const": "node" }
133
+ },
134
+ "not": {
135
+ "anyOf": [
136
+ { "required": ["items"] },
137
+ { "required": ["value"] }
138
+ ]
139
+ }
140
+ },
141
+ {
142
+ "description": "Value envelope — `value` payload + `kindRegistry`, no `counts` / `filters`.",
143
+ "required": ["value", "kindRegistry"],
144
+ "properties": {
145
+ "kind": { "const": "config" }
146
+ },
147
+ "not": {
148
+ "anyOf": [
149
+ { "required": ["items"] },
150
+ { "required": ["item"] }
151
+ ]
152
+ }
153
+ },
154
+ {
155
+ "description": "Sentinel kinds — reserved for routes that do NOT carry an envelope payload at the wire level (`health`, `scan`, `graph`). They do not carry `kindRegistry` either; clients that need it must call any payload-bearing endpoint at boot.",
156
+ "properties": {
157
+ "kind": { "enum": ["health", "scan", "graph"] }
158
+ },
159
+ "not": {
160
+ "anyOf": [
161
+ { "required": ["items"] },
162
+ { "required": ["item"] },
163
+ { "required": ["value"] },
164
+ { "required": ["kindRegistry"] }
165
+ ]
166
+ }
167
+ }
168
+ ],
169
+ "additionalProperties": false
170
+ }
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "additionalProperties": {
33
33
  "type": "object",
34
- "required": ["schema", "defaultRefreshAction"],
34
+ "required": ["schema", "defaultRefreshAction", "ui"],
35
35
  "additionalProperties": false,
36
36
  "properties": {
37
37
  "schema": {
@@ -43,6 +43,66 @@
43
43
  "type": "string",
44
44
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*/[a-z][a-z0-9]*(-[a-z0-9]+)*$",
45
45
  "description": "Qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The action MUST exist in the registry by the time the graph is queried; a dangling reference disables the Provider with status `invalid-manifest`."
46
+ },
47
+ "ui": {
48
+ "type": "object",
49
+ "required": ["label", "color"],
50
+ "additionalProperties": false,
51
+ "description": "Presentation metadata the UI uses to render nodes of this kind (palette swatches, list tags, graph nodes, filter chips). Required so the UI never has to invent visuals for a kind a Provider declares. The Provider declares intent (label + base color, optional dark variant + emoji + icon); the UI derives bg/fg tints from `color` per theme via a deterministic helper. Reaches the UI via the `kindRegistry` field embedded in REST envelopes (`api/rest-envelope.schema.json`).",
52
+ "properties": {
53
+ "label": {
54
+ "type": "string",
55
+ "minLength": 1,
56
+ "description": "Plural human-readable label for groups of this kind (e.g. `'Skills'`, `'Agents'`, `'Cursor Rules'`). Used in filter dropdowns, palette tooltips, and any list grouping."
57
+ },
58
+ "color": {
59
+ "type": "string",
60
+ "pattern": "^#[0-9a-fA-F]{6}$",
61
+ "description": "Base hex color (light theme). The UI derives `bg` and `fg` tints from this value at runtime; declaring a single base value (instead of three) keeps the manifest small and lets the UI control accessibility-driven contrast."
62
+ },
63
+ "colorDark": {
64
+ "type": "string",
65
+ "pattern": "^#[0-9a-fA-F]{6}$",
66
+ "description": "Optional dark-theme variant of `color`. When absent, the UI falls back to `color`. Declared explicitly because a luminosity flip rarely matches the brand intent for kinds that should stand out in dark mode."
67
+ },
68
+ "emoji": {
69
+ "type": "string",
70
+ "minLength": 1,
71
+ "maxLength": 8,
72
+ "description": "Optional decorative emoji used as a fallback when `icon` is absent or fails to render. Bound to a small length so the UI can lay it out predictably alongside text."
73
+ },
74
+ "icon": {
75
+ "description": "Optional discriminated icon descriptor. The UI prefers `icon` over `emoji`; when both are absent, the UI falls back to the first letter of `label` colored with `color`.",
76
+ "oneOf": [
77
+ {
78
+ "type": "object",
79
+ "required": ["kind", "id"],
80
+ "additionalProperties": false,
81
+ "properties": {
82
+ "kind": { "const": "pi" },
83
+ "id": {
84
+ "type": "string",
85
+ "pattern": "^pi-[a-z0-9]+(-[a-z0-9]+)*$",
86
+ "description": "PrimeIcons identifier (e.g. `pi-cog`, `pi-bolt`). Matched verbatim against the `pi pi-<id>` class the UI emits."
87
+ }
88
+ }
89
+ },
90
+ {
91
+ "type": "object",
92
+ "required": ["kind", "path"],
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "kind": { "const": "svg" },
96
+ "path": {
97
+ "type": "string",
98
+ "minLength": 1,
99
+ "description": "Raw SVG path data (the `d` attribute of one or more `<path>` elements, joined). The UI wraps it in `<svg viewBox=\"0 0 24 24\"><path d=\"...\"/></svg>` and tints it with `currentColor`."
100
+ }
101
+ }
102
+ }
103
+ ]
104
+ }
105
+ }
46
106
  }
47
107
  }
48
108
  }