@skill-map/spec 0.46.0 → 0.48.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 +68 -0
  2. package/architecture.md +33 -11
  3. package/cli-contract.md +7 -12
  4. package/conformance/README.md +1 -0
  5. package/conformance/cases/backtick-path-extraction.json +20 -0
  6. package/conformance/cases/plugin-missing-ui-rejected.json +1 -1
  7. package/conformance/cases/view-action-button.json +21 -0
  8. package/conformance/cases/view-contribution-payloads.json +19 -0
  9. package/conformance/cases/view-slots-all.json +15 -0
  10. package/conformance/coverage.md +1 -1
  11. package/conformance/fixtures/backtick-path/docs/target.md +5 -0
  12. package/conformance/fixtures/backtick-path/source.md +14 -0
  13. package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/analyzers/good-badges/index.js +46 -0
  14. package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/plugin.json +6 -0
  15. package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/analyzers/header-counter/index.js +28 -0
  16. package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/plugin.json +6 -0
  17. package/conformance/fixtures/view-action-button/notes/example.md +6 -0
  18. package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/analyzers/panels/index.js +37 -0
  19. package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json +6 -0
  20. package/conformance/fixtures/view-contribution-payloads/notes/example.md +5 -0
  21. package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js +35 -0
  22. package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json +6 -0
  23. package/db-schema.md +1 -1
  24. package/index.json +34 -18
  25. package/package.json +1 -1
  26. package/plugin-author-guide.md +48 -9
  27. package/schemas/api/rest-envelope.schema.json +12 -3
  28. package/schemas/extensions/action.schema.json +32 -0
  29. package/schemas/extensions/base.schema.json +9 -0
  30. package/schemas/extensions/provider.schema.json +1 -1
  31. package/schemas/link.schema.json +2 -2
  32. package/schemas/plugins-doctor.schema.json +45 -2
  33. package/schemas/plugins-registry.schema.json +4 -0
  34. package/schemas/signal.schema.json +1 -1
  35. package/schemas/view-slots.schema.json +112 -23
@@ -0,0 +1,35 @@
1
+ // Conformance fixture: an analyzer whose `ui` map declares a contribution to
2
+ // EVERY one of the 14 view slots in the closed catalog. The counter slots
3
+ // (`card.subtitle.left`, `card.footer.left`, `card.footer.right`) and the
4
+ // standalone icon slot (`card.title.right`) require `icon` in the manifest;
5
+ // the rest only need `slot`. The `ui` keys are kebab-case (the manifest schema
6
+ // constrains contribution ids). The companion case `view-slots-all.json`
7
+ // asserts the plugin loads clean (`sm plugins doctor` reports ok), locking
8
+ // that every catalog slot id is a valid manifest declaration. `evaluate`
9
+ // emits nothing: this case exercises manifest validation, not emission.
10
+ export default {
11
+ version: '0.1.0',
12
+ description: 'analyzer declaring a contribution to every one of the 14 view slots',
13
+ mode: 'deterministic',
14
+
15
+ ui: {
16
+ 'card-title': { slot: 'card.title.right', icon: 'pi-flag' },
17
+ 'card-subtitle': { slot: 'card.subtitle.left', icon: 'pi-hashtag' },
18
+ 'card-footer-left': { slot: 'card.footer.left', icon: 'pi-download' },
19
+ 'card-footer-right': { slot: 'card.footer.right', icon: 'pi-upload' },
20
+ 'graph-alert': { slot: 'graph.node.alert' },
21
+ 'header-badge': { slot: 'inspector.header.badge' },
22
+ 'action-button': { slot: 'inspector.action.button' },
23
+ 'breakdown': { slot: 'inspector.body.panel.breakdown' },
24
+ 'records': { slot: 'inspector.body.panel.records' },
25
+ 'tree': { slot: 'inspector.body.panel.tree' },
26
+ 'key-values': { slot: 'inspector.body.panel.key-values' },
27
+ 'link-list': { slot: 'inspector.body.panel.link-list' },
28
+ 'markdown': { slot: 'inspector.body.panel.markdown' },
29
+ 'scope-stat': { slot: 'topbar.nav.start' },
30
+ },
31
+
32
+ evaluate() {
33
+ return [];
34
+ },
35
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "specCompat": "*",
4
+ "catalogCompat": "*",
5
+ "description": "Conformance fixture: one analyzer whose ui map declares a contribution to every one of the 14 view slots, so the loader must accept every slot id in the closed catalog."
6
+ }
package/db-schema.md CHANGED
@@ -104,7 +104,7 @@ One row per detected link, matching [`schemas/link.schema.json`](./schemas/link.
104
104
  | `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | |
105
105
  | `source_path` | TEXT | NOT NULL | FK semantically; MAY be unenforced for performance. |
106
106
  | `target_path` | TEXT | NOT NULL | MAY point to a missing node (broken ref). |
107
- | `kind` | TEXT | NOT NULL, CHECK in (`invokes`, `references`, `mentions`, `supersedes`) | |
107
+ | `kind` | TEXT | NOT NULL, CHECK in (`invokes`, `references`, `mentions`, `supersedes`, `points`) | |
108
108
  | `confidence` | TEXT | NOT NULL, CHECK in (`high`, `medium`, `low`) | |
109
109
  | `sources_json` | TEXT | NOT NULL | JSON array of extractor ids. |
110
110
  | `original_trigger` | TEXT | NULL | |
package/index.json CHANGED
@@ -174,23 +174,29 @@
174
174
  }
175
175
  ]
176
176
  },
177
- "specPackageVersion": "0.46.0",
177
+ "specPackageVersion": "0.48.0",
178
178
  "integrity": {
179
179
  "algorithm": "sha256",
180
180
  "files": {
181
- "CHANGELOG.md": "f144432840afc98bb65fb0982865d1f7ed233cae2ec0b0eaea94c84d2299f771",
181
+ "CHANGELOG.md": "e7fc58aaced4c555405dd1e41a860b016cfb4fca7da6dbb29dfa6b39fddcba79",
182
182
  "README.md": "a7505a7b0672c39a8b011e3c5e7d41826306476ee63768249bba4bdb3c03d4d1",
183
- "architecture.md": "b45089d52095177228c443c037ea699061f59ba4fb2b70231ebf8218ab49561d",
184
- "cli-contract.md": "92259a45962f4c0374f707b33b814bce6430eca7936d2ff52e7095484c634279",
185
- "conformance/README.md": "0c69bd9becf511ada9175b1e428ba183e31d1c8a49ff09eedf4c950bb831ec4d",
183
+ "architecture.md": "961a1aedf037dc9a2e6bdef1944d04e35bf826fdf8579ee64b0aff9f6d9f70da",
184
+ "cli-contract.md": "f2d5bbe15c19646b69fd1aaff8a380b7044966dad049a180445e5c2130ec051c",
185
+ "conformance/README.md": "4ec22ca3cc8e4282fe0bfd111f22b121e0781e2b525867cd092258b8f58ae1e1",
186
+ "conformance/cases/backtick-path-extraction.json": "4620e7f8bc161fc57cb44001e9d99879c7e22b4865a0c27a20dc28969cd936d9",
186
187
  "conformance/cases/extractor-emits-signal.json": "0115c7bb62a7a705f72e9d8048b3f0396e5caaeb3d04dea204415e279e58479d",
187
188
  "conformance/cases/kernel-empty-boot.json": "9b51b85ff62479cd0eee37cad260245208d94f6d79644f7ee40945a934960913",
188
189
  "conformance/cases/no-global-scope.json": "1c83343422144be2ad9e3d27d2062e61af87c7c1c1f3b051b6b9f687d845ac7b",
189
190
  "conformance/cases/orphan-markdown-fallback.json": "506119323ddde85c1fb4c986c7f6f40a345d44adb06de8d84002591df0e479ee",
190
- "conformance/cases/plugin-missing-ui-rejected.json": "7c910b74e6f718ab5c1a590cd3544602f056559251d18995a26bca0a0648a2fa",
191
+ "conformance/cases/plugin-missing-ui-rejected.json": "2074fd71937feae136c999f76da81f334f2caf8b65bfe8dc9d7fb800699fb85c",
191
192
  "conformance/cases/sidecar-end-to-end.json": "0a0d941ab50bd7619e1021a6c6d6dc92918429c2efcf25236b42b5fac9eab901",
192
193
  "conformance/cases/signal-collision-detection.json": "c5e39a406ded6928a14c1a22b84f7b3cd49805bec56bd65de83130d9e419c09e",
193
- "conformance/coverage.md": "70cfa9a5736f1e12845da46c4c217b8a6061148f54548b67a30f1c74e3381bc5",
194
+ "conformance/cases/view-action-button.json": "51331f725be1c3655351f8fca6fc9d3d301ae68ea1741ff6c79998332ba2dfeb",
195
+ "conformance/cases/view-contribution-payloads.json": "e8f54ed62e64a2a0f86729866e507abb1f4246683f0e60d538280536f7cd3ecc",
196
+ "conformance/cases/view-slots-all.json": "05284e0324dd2da72b6b21d397c11b355802229a68053e9dddc323f69b3a1eba",
197
+ "conformance/coverage.md": "f93676ede774003c4d15ccf8d3bf2f65b5d032d75ae572df01dff892aeb1a8cf",
198
+ "conformance/fixtures/backtick-path/docs/target.md": "a09ae2cb4c96358a2e0692215f172b0f8c48028b6b123e4e83424b28302e644c",
199
+ "conformance/fixtures/backtick-path/source.md": "217f78b12b3ff47a938a5cc9c1ff7d6989d6a1db82bd1ddf3656787f31efb902",
194
200
  "conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
195
201
  "conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "ec903666440bae65da3796b1158c92cfcdce22e0e09c3b20bb690176881a6ac4",
196
202
  "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json": "6676a89bae5197e23cf50f1c11d596db558ac80f7334a7208fe57d8b92422251",
@@ -208,50 +214,60 @@
208
214
  "conformance/fixtures/signal-ir-collision/.claude/agents/architect.md": "acc46b5b2dff73d98a354e4d53b5041164595deae466a4e2ce41d7c5a72f28fb",
209
215
  "conformance/fixtures/signal-ir-single-signal/source.md": "1eda417b4c6eed372b66870e385c8d8cd631372b77cab7e996bb711e22218f89",
210
216
  "conformance/fixtures/signal-ir-single-signal/target.md": "527137f2b4f46c0034b0edc8932cf8613d2bf22ffaaf78f01085c82a3baaebe3",
211
- "db-schema.md": "f74ce6766bf7f2dcda187a49f82e1768bc1c091d9492846e718903a379610e2e",
217
+ "conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/analyzers/good-badges/index.js": "943fc3f11c328d2ccc4f8474106f4ae92077d353d02bd0207153efd1d0a1cf42",
218
+ "conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/plugin.json": "ed7b048c140d3d5acdf4456678acba8d9d55fd63511013c8621122b7062f40be",
219
+ "conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/analyzers/header-counter/index.js": "7e097f3f2efcf1175dd02c926a8872f9d2de584c1e6a09fcceb56d603a4386ce",
220
+ "conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/plugin.json": "1ed0decf76d195da2caa5949d6b11fc8fea097416e263d77ed294e5d158304dc",
221
+ "conformance/fixtures/view-action-button/notes/example.md": "c8fad69ee251b25080869c36d84c1f6b697773526a1cd8bda5a577e2027ebb55",
222
+ "conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/analyzers/panels/index.js": "b313ec830ab48bd72e9f347e9d161469a56b8a805cf0861061d0012a452a2706",
223
+ "conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json": "18b7d246f004829ed3e86fa40654b34ac2e1ab416aa083fa17ae2e4f13ac3c0d",
224
+ "conformance/fixtures/view-contribution-payloads/notes/example.md": "312b1919cd7fd0f233648b053acfb2975662ede3c65dd391cc508204b67ad6fb",
225
+ "conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js": "ea0022fec7f0fd5a26ba12db1310335f434f2f820682206a3a9542d98db0d346",
226
+ "conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json": "c48e8a0574947ade0b4eb189d6bc27a48e24f92f616aacdc177f2d22d472a599",
227
+ "db-schema.md": "9f99e1c2b73570a12021dd2cd640afd4b1f78ac31f898f0485bba7ed86adaac6",
212
228
  "interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
213
229
  "job-events.md": "9d5b35d4c451a7f8eef9915d85316d924ac52f1c026a316cdda5f1099d496854",
214
230
  "job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
215
- "plugin-author-guide.md": "b0c510529eb660c753cedfa6397fbf0520135f83f6c8e3cb3816fbfa1c9537a9",
231
+ "plugin-author-guide.md": "ab40dd384186e02d7123f0a202e6ce4cd1a11870112e1b94937a5026ce2d9133",
216
232
  "plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
217
233
  "prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
218
234
  "schemas/annotations.schema.json": "8c639b149cad675fdd4e7d6be2b47e920cfdd24087b41361d6e1b8280f646322",
219
- "schemas/api/rest-envelope.schema.json": "cbab82381a49a0a7db796d61a11977abd51d62be423cc19af988a5301a55354f",
235
+ "schemas/api/rest-envelope.schema.json": "8eeb1c2d79fb69eaef23737a2231d48d67e59b8b19aad816239ab4680e2c4752",
220
236
  "schemas/bump-report.schema.json": "c763e1f89f2665c479d6a4985c1d324c65e5278331ebab82220287a07e4c4429",
221
237
  "schemas/conformance-case.schema.json": "958b316d646d0c64a715a7a28cee66d2c2d2498a60dbfc5ae8970687c2a96954",
222
238
  "schemas/conformance-result.schema.json": "14f983a8f4e62cd4ff964688c9b2b026a3bee3a0b762b64091c8c34db5b75777",
223
239
  "schemas/execution-record.schema.json": "db0eb16153493ad9f13ea0ecede44191e4a8536979adffd17ca278ddf8786c77",
224
- "schemas/extensions/action.schema.json": "dc4f52d23c163c6239a487fa1c1ad9c09685cf38833d3962c604d5872716cff9",
240
+ "schemas/extensions/action.schema.json": "8b300532c0217c0f65c454edd6df86d1fe4245590fb5e0974944ce9e593f7f28",
225
241
  "schemas/extensions/analyzer.schema.json": "8def4a5ca4934197c34abde97da70704b2751041a443c859eddd4b783e2fe1db",
226
- "schemas/extensions/base.schema.json": "49baa06a4ce8a6ce75fec52b650d9bf3566e5de0b1053b06f73a71ce103e4fdf",
242
+ "schemas/extensions/base.schema.json": "c78bdf1057cd19cf370d1343c801a0deeaf38d745e9ec40ec141de52b658243a",
227
243
  "schemas/extensions/extractor.schema.json": "ee44bf562b19318c93116c574a811857cdef1f4119326a9a604fa408889dd230",
228
244
  "schemas/extensions/formatter.schema.json": "880dc379ad545a62404403533a01eda5171edba0390561fc46ec6e986e0b9bd3",
229
245
  "schemas/extensions/hook.schema.json": "f56aef59e9986ffdf7d86aa2e048dccccf217000a358b8c64737cbd911c48dad",
230
246
  "schemas/extensions/provider-kind.schema.json": "499b2418bbe6d8a84a1608e26c56b52c2652a30ce314bc2989094418797dc1e6",
231
- "schemas/extensions/provider.schema.json": "75a565b8be6f1a08f0dbfec34e10c5d4d7c990489842bf338519a7d4b97dfe8f",
247
+ "schemas/extensions/provider.schema.json": "bea1d73897dc8fa8499ba7c77ce535337473e5ecb3702ebca9966c08afc920f4",
232
248
  "schemas/frontmatter/base.schema.json": "cff81510ed94824dfd12ab8b30ce9fbac65e42d61ae0edf3fbb6bbb6bb8bcb8c",
233
249
  "schemas/history-stats.schema.json": "436aa0ffe744bdb699000447e86b45724fbd2cc4642781074eb1527522b9058c",
234
250
  "schemas/input-types.schema.json": "1c81704783627c5e89dd40cb20368d9e9aa94a15f32c2f929964e392cf2a12b6",
235
251
  "schemas/issue.schema.json": "d173aa5c5312b3d2a2cd249f55c10943c8f3cd5799e4645ae3c66316221e12d1",
236
252
  "schemas/job.schema.json": "dbcedf137de03fde38f74686f594e600c627bf808f2aad23511a26617a663a02",
237
- "schemas/link.schema.json": "10f700feb3e23d89453d4d11cbe559bcc0b31f6edf08fffbe9e6773e58120512",
253
+ "schemas/link.schema.json": "df1466499e78f68056b302dc2a5a1bf3bdcc0ffa6b7b01ffe89111c78e1b2c3f",
238
254
  "schemas/node.schema.json": "14ed2e4c44d01e3f662e240219819895cca06dead374a5cadccfd423c520ed69",
239
- "schemas/plugins-doctor.schema.json": "2238266f31402a446b313af16f933e395a02eca70128e39ab99a11de90a4735f",
240
- "schemas/plugins-registry.schema.json": "6d850d06cdf70e233f20d0d7968bb0c34306f11f30ce2505cec173cd9fa784e5",
255
+ "schemas/plugins-doctor.schema.json": "03e2dc51c052a09bf0198c80e2c26e6129734ada4a748e483245de3dd8576c42",
256
+ "schemas/plugins-registry.schema.json": "211d081691fc83526e1593c79ed9741ad8a5dbd4db1a756f72141b0cced2ea15",
241
257
  "schemas/project-config.schema.json": "0a4a12a3409f900bd19b47c34588c77ac894b944d21a9beebb91ae1e9c0f3d01",
242
258
  "schemas/refresh-report.schema.json": "47184d4f6b15e9b7671dc178b3b3886a64422da198898508ecdb2cb27876db04",
243
259
  "schemas/report-base-deterministic.schema.json": "59785fe6f3ceb34814bbbd03d10fa7336a32835ce598946f2923d469b32aa32a",
244
260
  "schemas/report-base.schema.json": "e4d25f055e24f18ae0f77c24661c1bddc87ff2e43b001b6a827fcb14f9753f44",
245
261
  "schemas/scan-result.schema.json": "9fb81f496d6f8bdcb82131d0b2eb532da1addb801e7d27bd192a0c286a28c2c0",
246
262
  "schemas/sidecar.schema.json": "f9d914e61b2d04495b84dc90e55240aca959e6f16137e5bfa4c0e10ada33ecbe",
247
- "schemas/signal.schema.json": "57baf52e55fc9a6f122fb9b33395b5a2790e7f5b7d461cf576099b68a8a17159",
263
+ "schemas/signal.schema.json": "39dd0e6989a1141bf7769bbb26b3d750b6ebcd8e3215ebe50efd0ad30ccb46fc",
248
264
  "schemas/summaries/agent.schema.json": "5b26b95fb082b73d302c8aa6489ab09488a155ccfbb8943dfc47079509d35122",
249
265
  "schemas/summaries/command.schema.json": "7f522c682d0fdf5a40172c7fc8fcd23e60a0ab0253354146525bd3a3d417f1f8",
250
266
  "schemas/summaries/hook.schema.json": "6a1ceecda7a7173dfcd8b5f705d84be1792c4bb5a2269ff666088128c02c888a",
251
267
  "schemas/summaries/markdown.schema.json": "369d75a18710eb6c7ee4220b2899767ddbc07dada24f81b611827fe2e3a2977a",
252
268
  "schemas/summaries/skill.schema.json": "85d68056054bade62391948cc038fcfa70cdcf465e2b295f69cd520bbdba0134",
253
269
  "schemas/user-settings.schema.json": "d155552ffca9c7dd4c6e31398aff4950dd9721d2a1f4b308cf0fe33000ca31b5",
254
- "schemas/view-slots.schema.json": "4623cf8d774f44435f960b1dd3ad4c8b241e37b234ce8eab5b390b9ae8a2acb1",
270
+ "schemas/view-slots.schema.json": "886487a1f38fd7e4270fa6213653664c0cf906043e8aa9e832017149932bf6a2",
255
271
  "telemetry.md": "fa659b47c59e692f50c7a091470888d5e7c98dcf858978fa549af25b2562803f",
256
272
  "versioning.md": "28a13f165f837921fe5066f4bfce61012cd9f1b7c451f88eeb67252e39a0981a"
257
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-map/spec",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "JSON Schemas, prose contracts, and conformance suite for the skill-map specification.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -114,13 +114,14 @@ Concrete examples for the reference impl's built-in extensions:
114
114
  | Slash-command extractor | `slash-command` | `claude/slash-command` |
115
115
  | At-directive extractor | `at-directive` | `claude/at-directive` |
116
116
  | Markdown-link extractor | `markdown-link` | `core/markdown-link` |
117
+ | Backtick-path extractor | `backtick-path` | `core/backtick-path` |
117
118
  | External-URL counter | `external-url-counter` | `core/external-url-counter` |
118
119
  | Reference-broken analyzer | `reference-broken` | `core/reference-broken` |
119
120
  | ASCII formatter | `ascii` | `core/ascii` |
120
121
 
121
122
  Built-ins split between two namespaces:
122
123
 
123
- - **`core/`**, kernel-internal primitives, platform-agnostic: every built-in analyzer, the ASCII formatter, the cross-vendor extractors (`annotations`, `markdown-link`, `external-url-counter`), the universal `markdown` Provider fallback, and the `update-check` hook.
124
+ - **`core/`**, kernel-internal primitives, platform-agnostic: every built-in analyzer, the ASCII formatter, the cross-vendor extractors (`annotations`, `markdown-link`, `backtick-path`, `external-url-counter`), the universal `markdown` Provider fallback, and the `update-check` hook.
124
125
  - **`claude/`**, the Claude Code Provider plugin: the Provider plus the Claude-flavoured extractors (`slash-command`, `at-directive`). Other vendor plugins (`antigravity`, `openai`, `agent-skills`) follow the same shape (Provider only).
125
126
 
126
127
  ### Extension id shape
@@ -241,20 +242,22 @@ The kernel knows six categories. Each has a JSON Schema under [`schemas/extensio
241
242
 
242
243
  The runtime instance you `export default` includes both the manifest fields (`version`, `description`, plus kind-specific metadata) AND the runtime method. The kernel strips function-typed properties before AJV-validating the manifest, so the method lives alongside metadata.
243
244
 
245
+ Base manifest fields shared by every kind (normative shape in [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json)): `version` (required for external plugins), `description` (required), and the optional `stability`, `order`, `annotation`, `settings`. `stability` (`'experimental' | 'beta' | 'stable' | 'deprecated'`, default `stable`) is a presentation-only lifecycle label: the non-default values render as a badge next to the extension in `sm plugins list` / `sm plugins show` and the Settings plugins panel, and the kernel never gates behaviour on it (a `deprecated` extension still runs). A stable extension simply omits the field; declaring `stability: 'stable'` is valid but renders nothing.
246
+
244
247
  ### Extractors
245
248
 
246
249
  Pure single-node analysis. **Never** read another node, the graph, or the database, cross-node reasoning is for analyzers. Manifest fields beyond the base: `scope` (`'frontmatter'` | `'body'` | `'both'`), optional `precondition`, optional `ui` (view contributions). Spec at [`schemas/extensions/extractor.schema.json`](./schemas/extensions/extractor.schema.json).
247
250
 
248
251
  `extract(ctx) → void`. Output flows through callbacks the kernel binds onto `ctx`:
249
252
 
250
- - **`ctx.emitLink(link)`**, append a `Link`. The kernel validates `link.kind` against the **global closed enum** (`invokes`, `references`, `mentions`, `supersedes`); off-enum kinds drop as `extension.error`. Confidence is declared per emit (default `'medium'`). URL-shaped targets are partitioned into `node.externalRefsCount` and never persisted. (There is no per-extractor `emitsLinkKinds` allowlist anymore.)
253
+ - **`ctx.emitLink(link)`**, append a `Link`. The kernel validates `link.kind` against the **global closed enum** (`invokes`, `references`, `mentions`, `supersedes`, `points`); off-enum kinds drop as `extension.error`. Confidence is declared per emit (default `'medium'`). URL-shaped targets are partitioned into `node.externalRefsCount` and never persisted. (There is no per-extractor `emitsLinkKinds` allowlist anymore.)
251
254
  - **`ctx.enrichNode(partial)`**, merge kernel-curated properties onto the node's enrichment layer (persisted into `node_enrichments`). **Strictly separate from the author frontmatter**, which is immutable from any Extractor. Use it for inferred facts (computed titles, summaries) the author did not write.
252
255
  - **`ctx.emitContribution(id, payload)`**, view contributions (see [View contributions](#view-contributions)).
253
256
  - **`ctx.store`**, plugin-scoped persistence, present only when `plugin.json` declares `storage.mode`. See [`plugin-kv-api.md`](./plugin-kv-api.md).
254
257
 
255
258
  You can read `ctx.node.sidecar.*` freely: the per-`(node, extractor)` cache hashes the sidecar `annotations` block alongside the body, so a `.sm`-only edit invalidates the cached run automatically.
256
259
 
257
- > **Pick a syntax that doesn't collide with built-ins.** `core/at-directive` claims `@`, `core/slash-command` claims `/`, both with LLM-aligned semantics (and both strip fenced code blocks + inline backticks before matching). A new extractor matching one of those prefixes will fire on the same input and, if it emits a different `target` shape, raises a `trigger-collision`. The example below uses a wikilink-style `[[ref:<name>]]` pattern to side-step this. See [`architecture.md` §Extractor · trigger normalization](./architecture.md#extractor--trigger-normalization) for the normalization pipeline.
260
+ > **Pick a syntax that doesn't collide with built-ins.** `core/at-directive` claims `@`, `core/slash-command` claims `/`, both with LLM-aligned semantics (and both strip fenced code blocks + inline backticks before matching). `core/backtick-path` is the deliberate inverse: it matches relative `.md` paths ONLY inside those stripped code regions, so by construction it cannot overlap the prose-side extractors. A new extractor matching one of those prefixes will fire on the same input and, if it emits a different `target` shape, raises a `trigger-collision`. The example below uses a wikilink-style `[[ref:<name>]]` pattern to side-step this. See [`architecture.md` §Extractor · trigger normalization](./architecture.md#extractor--trigger-normalization) for the normalization pipeline.
258
261
 
259
262
  ```javascript
260
263
  export default {
@@ -557,6 +560,27 @@ Inside an extractor or analyzer manifest, declare a `ui` map (sibling of `annota
557
560
  }
558
561
  ```
559
562
 
563
+ In TypeScript, declare each contribution as a module-level const with `satisfies IViewContribution` and build `ui` by shorthand. You then emit by passing the SAME object by reference (see [Emit path](#emit-path)) and get a typed payload for free:
564
+
565
+ ```ts
566
+ import type { IViewContribution } from '@skill-map/cli';
567
+
568
+ const breakdown = {
569
+ slot: 'inspector.body.panel.breakdown', label: 'Keyword hits', emptyText: 'No matches.',
570
+ } satisfies IViewContribution;
571
+ const total = {
572
+ slot: 'card.footer.left', icon: '🔍', label: 'kw', emitWhenEmpty: false,
573
+ } satisfies IViewContribution;
574
+
575
+ export default {
576
+ // ...
577
+ ui: { breakdown, total },
578
+ // ...
579
+ };
580
+ ```
581
+
582
+ The `ui` **key** (kebab-case per the manifest schema) is the contribution id; the const's variable name is incidental, because the kernel matches an emission to its declaration by object identity, not by name. Plain `.js` plugins use the same shape without `satisfies` (they get the runtime check, not the compile-time one).
583
+
560
584
  Field reference (full schema in [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/IViewContribution`):
561
585
 
562
586
  | Field | Required | Notes |
@@ -596,8 +620,8 @@ The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape
596
620
  | `card.footer.left` | counter chip (icon required) |
597
621
  | `card.footer.right` | counter chip (icon required) |
598
622
  | `graph.node.alert` | graph corner badge (reserved, see `view-slots.md`) |
599
- | `inspector.header.badge.counter` | counter chip (icon required) |
600
- | `inspector.header.badge.tag` | tag chip |
623
+ | `inspector.header.badge` | unified header badge (icon and/or label and/or count) |
624
+ | `inspector.action.button` | action button (dispatches an Action, see `view-slots.md`) |
601
625
  | `inspector.body.panel.breakdown` | bar chart panel |
602
626
  | `inspector.body.panel.records` | table panel |
603
627
  | `inspector.body.panel.tree` | tree panel |
@@ -606,6 +630,19 @@ The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape
606
630
  | `inspector.body.panel.markdown` | sanitized markdown panel |
607
631
  | `topbar.nav.start` | scope chip |
608
632
 
633
+ ### Inspector grouping and `order`
634
+
635
+ The six `inspector.body.panel.*` contributions are not rendered in a shared drawer. The inspector groups them **one collapsible section per plugin**, titled by the plugin id (host-applied from the trusted contribution `pluginId`, never the payload) and **collapsed by default**. A plugin's bricks only ever land in its own section: a plugin cannot contribute into another plugin's space.
636
+
637
+ Two optional, inspector-only `order` hints (both `number`, default `100`) control layout:
638
+
639
+ | Field | Where | Effect |
640
+ |---|---|---|
641
+ | `order` | `plugin.json` (plugin level) | Sorts the plugin sections, ASC, tie-break by plugin id. |
642
+ | `order` | extension manifest (extension level) | Sorts the bricks inside a plugin's section, ASC, tie-break by the contribution `priority` then qualified id. |
643
+
644
+ `order` is purely presentational and never affects execution order (analyzer `phase` + registration order govern that). It only applies to the inspector body sections; `priority` still governs ordering within the card / header / action slots.
645
+
609
646
  ### Chip vs Issue
610
647
 
611
648
  For analyzers, a per-node card surfaces a finding through two independent channels: the `Issue` returned by `evaluate(ctx)` feeds the aggregated stats and the scan / check exit code; a view contribution to a card slot is **purely presentational** (its `severity` controls only the chip's own colour, never the count, never the exit code). The colour rule, when a chip may paint `warn` / `danger`, and the reserved status of `graph.node.alert` are documented in [`view-slots.md` §Chip vs Issue](./view-slots.md). Breaking the colour rule produces visually misleading cards and is caught in code review, not by the schema.
@@ -614,14 +651,14 @@ For analyzers, a per-node card surfaces a finding through two independent channe
614
651
 
615
652
  ```ts
616
653
  // Extractor (per-node walk): nodePath is implicit (ctx.node.path)
617
- ctx.emitContribution('breakdown', { entries: [...] });
618
- ctx.emitContribution('total', { value: total });
654
+ ctx.emitContribution(breakdown, { bars: [...] });
655
+ ctx.emitContribution(total, { value });
619
656
 
620
657
  // Analyzer (post-merge graph): explicit nodePath, the analyzer sees every node at once
621
- ctx.emitContribution(nodePath, 'breakdown', { ... });
658
+ ctx.emitContribution(nodePath, breakdown, { bars: [...] });
622
659
  ```
623
660
 
624
- The first id argument is the **manifest `ui` key**, NOT the slot name; the kernel composes the qualified id from your plugin id, extension id, and the key, and looks up the declared slot to validate the payload against `view-slots.schema.json#/$defs/payloads/<slot>`. Off-shape payloads emit an `extension.error` and drop silently, same posture as `emitLink`. For `topbar.nav.start`, analyzers use `ctx.emitScopeContribution(id, payload)` (reserved in the spec; the runtime callback lands when the first scope-level adopter arrives).
661
+ Pass the contribution **object you declared in `ui`, by reference** (the `const` above), not a string id. The kernel recovers the contribution id (the `ui` key) by object identity and looks up the declared slot to validate the payload against `view-slots.schema.json#/$defs/payloads/<slot>`. The payload argument is typed from `ref.slot` (`SlotPayload<C['slot']>`), so a wrong-shape payload is a **compile error** in TypeScript. At runtime, a ref that is not one of your declared `ui` objects (a spread copy, an inline literal) or an off-shape payload emits an `extension.error` and drops, same posture as `emitLink`. For `topbar.nav.start`, analyzers use `ctx.emitScopeContribution(ref, payload)` (reserved in the spec; the runtime callback lands when the first scope-level adopter arrives).
625
662
 
626
663
  To surface the same data in two surfaces, declare two contributions (one per slot) and emit twice, there is no broadcast.
627
664
 
@@ -705,6 +742,8 @@ Analyzers take a `ctx` with `nodes`, `links`, and (if you assert on view contrib
705
742
 
706
743
  `sm plugins doctor` runs the full load pass and exits `1` if any plugin is in a non-`loaded` / non-`disabled` state. Wire it into CI.
707
744
 
745
+ Beyond load status, `sm plugins doctor` also reports **runtime contribution errors from the last scan**: view contributions rejected at emit time (an undeclared ref, or a payload that fails the slot's schema) are persisted per scan and surfaced in a "Runtime contribution errors (last scan)" section grouped by plugin, and any present also promote the exit code to `1`. A plugin can be `loaded` (clean manifest) yet still have runtime rejections, a healthy `list` status does not mean your chips actually rendered. The same errors appear per-plugin in the Settings plugin panel (a warning badge plus a collapsible diagnostics list). Re-run `sm scan` after a fix to clear them.
746
+
708
747
  ---
709
748
 
710
749
  ## Scaffolder
@@ -24,6 +24,7 @@
24
24
  "health",
25
25
  "scan",
26
26
  "sidecar.bumped",
27
+ "action.applied",
27
28
  "annotations.registered",
28
29
  "contributions.registered"
29
30
  ],
@@ -39,7 +40,7 @@
39
40
  },
40
41
  "value": {
41
42
  "type": "object",
42
- "description": "Present when `kind` is `'config'` or `'sidecar.bumped'`. For `'config'`, carries the merged effective config object. For `'sidecar.bumped'`, carries `{ nodePath, version, status }` (the Action-result payload from `POST /api/sidecar/bump`)."
43
+ "description": "Present when `kind` is `'config'`, `'sidecar.bumped'`, or `'action.applied'`. For `'config'`, carries the merged effective config object. For `'sidecar.bumped'`, carries `{ nodePath, version, status }` (the bump report from `POST /api/sidecar/bump`). For `'action.applied'`, carries `{ actionId, nodePath, report }` (the generic Action-result payload from `POST /api/actions/:id`)."
43
44
  },
44
45
  "elapsedMs": {
45
46
  "type": "integer",
@@ -124,6 +125,14 @@
124
125
  "priority": {
125
126
  "type": "number",
126
127
  "description": "Optional ordering hint (default 100 when omitted). Slots whose `order` is `'priority'` sort contributions ASC by this value with alphabetical tie-break by qualified id. Mirror of `IViewContribution.priority` in `view-slots.schema.json#/$defs/ViewContribution`; propagated so the UI can apply the manifest-declared order without a second round-trip."
128
+ },
129
+ "pluginOrder": {
130
+ "type": "number",
131
+ "description": "Optional inspector-only ordering hint, denormalised from the owning plugin's `plugin.json` `order` field (default 100). The inspector groups `inspector.body.panel.*` contributions into one collapsible section per plugin and sorts those sections ASC by this value, tie-break by plugin id. Every contribution of a given plugin carries the same value."
132
+ },
133
+ "extensionOrder": {
134
+ "type": "number",
135
+ "description": "Optional inspector-only ordering hint, denormalised from the owning extension's `order` manifest field (default 100). Inside a plugin's inspector section, bricks are sorted ASC by this value, tie-break by `priority` then qualified id. Every contribution of a given extension carries the same value."
127
136
  }
128
137
  }
129
138
  }
@@ -251,10 +260,10 @@
251
260
  }
252
261
  },
253
262
  {
254
- "description": "Action-result envelope, `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry` / `providerRegistry` / `contributionsRegistry`. Used by `POST /api/sidecar/bump` (Step 9.6.5, BFF half) where the response carries the bump report (`{ nodePath, version, status }`) plus the wall-clock duration. The registries are intentionally absent, the action result is orthogonal to every catalog and the SPA already has them cached from a prior list call.",
263
+ "description": "Action-result envelope, `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry` / `providerRegistry` / `contributionsRegistry`. Used by `POST /api/actions/:id` (`action.applied`, carries `{ actionId, nodePath, report }`) and the legacy `POST /api/sidecar/bump` (`sidecar.bumped`, Step 9.6.5, carries `{ nodePath, version, status }`), in both cases plus the wall-clock duration. The registries are intentionally absent, the action result is orthogonal to every catalog and the SPA already has them cached from a prior list call.",
255
264
  "required": ["value", "elapsedMs"],
256
265
  "properties": {
257
- "kind": { "const": "sidecar.bumped" }
266
+ "kind": { "enum": ["sidecar.bumped", "action.applied"] }
258
267
  },
259
268
  "not": {
260
269
  "anyOf": [
@@ -51,6 +51,38 @@
51
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."
52
52
  }
53
53
  }
54
+ },
55
+ "prompt": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["inputType", "paramKey", "label"],
59
+ "properties": {
60
+ "inputType": {
61
+ "$ref": "../input-types.schema.json#/$defs/InputTypeName",
62
+ "description": "Input-type id from the closed catalog. The UI renders the matching control before dispatch."
63
+ },
64
+ "paramKey": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "maxLength": 48,
68
+ "description": "Key under which the UI-collected value is placed in the dispatch `input` body."
69
+ },
70
+ "label": { "type": "string", "minLength": 1, "maxLength": 64 },
71
+ "options": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["value", "label"],
77
+ "properties": {
78
+ "value": { "type": "string" },
79
+ "label": { "type": "string" }
80
+ }
81
+ },
82
+ "description": "Choice list for `enum-pick` / `enum-multipick` input types."
83
+ }
84
+ },
85
+ "description": "Reserved (Steps 3+). When set, a parametrized Action declares the single user input it needs; the UI renders the matching input-type control before dispatch and places the value under `paramKey` in the dispatch body. Deterministic no-prompt actions (e.g. `node-bump`) omit it. Mirrors `view-slots.schema.json#/$defs/payloads/_ActionPrompt`."
54
86
  }
55
87
  },
56
88
  "allOf": [
@@ -15,6 +15,15 @@
15
15
  "minLength": 1,
16
16
  "description": "Required short description (1-3 sentences) shown by `sm <kind>s list` and the UI inspector. English-only per AGENTS.md."
17
17
  },
18
+ "stability": {
19
+ "type": "string",
20
+ "enum": ["experimental", "beta", "stable", "deprecated"],
21
+ "description": "Optional lifecycle label for the extension. Presentation-only metadata: it drives a badge next to the extension in `sm plugins list` / `sm plugins show` and the Settings plugins panel, and the loader never gates behaviour on it (a `deprecated` extension still runs). Default: missing == `stable`. Only the non-default values (`experimental`, `beta`, `deprecated`) render a badge; `stable`, declared or defaulted, renders nothing, so authors only declare the field while the extension is NOT stable. Deliberately a superset of the node-level enum at `annotations.schema.json#/properties/stability` (which has no `beta`): this field describes the maturity of the extension itself, not of a scanned node."
22
+ },
23
+ "order": {
24
+ "type": "number",
25
+ "description": "Optional visual ordering hint, inspector-only. Inside a plugin's inspector section (which groups the plugin's `inspector.body.panel.*` contributions), the bricks contributed by each extension are sorted ASC by this value (default 100), tie-break by the contribution's `priority` then qualified id. Does NOT affect execution order, which is governed by `phase` (analyzers) and registration order."
26
+ },
18
27
  "annotation": {
19
28
  "type": "object",
20
29
  "required": ["schema"],
@@ -127,7 +127,7 @@
127
127
  "description": "When present, the resolver ranks candidates whose `kind` appears earlier in this array ABOVE candidates whose `kind` appears later. Candidates whose `kind` is absent from the array drop to the end (after every listed kind). Example: a Provider that wants `invokes` edges to win against `mentions` and `references` of the same range declares `['invokes', 'references', 'mentions']`. Ties inside the same `kindPriority` bucket fall through to the confidence -> range length -> declaration order tiebreaks.",
128
128
  "items": {
129
129
  "type": "string",
130
- "enum": ["invokes", "references", "mentions", "supersedes"]
130
+ "enum": ["invokes", "references", "mentions", "supersedes", "points"]
131
131
  }
132
132
  }
133
133
  }
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "kind": {
19
19
  "type": "string",
20
- "enum": ["invokes", "references", "mentions", "supersedes"],
21
- "description": "Nature of the relation. `invokes` = execution-level call (e.g. slash command). `references` = explicit link (e.g. wikilink, @-directive). `mentions` = informal textual mention. `supersedes` = replaces another node (from `metadata.supersedes`)."
20
+ "enum": ["invokes", "references", "mentions", "supersedes", "points"],
21
+ "description": "Nature of the relation. `invokes` = execution-level call (e.g. slash command). `references` = explicit link (e.g. wikilink, @-directive). `mentions` = informal textual mention. `supersedes` = replaces another node (from `metadata.supersedes`). `points` = relative file path written inside a code region (backtick span / fenced block); coexists with `references` on the same `(source, target)` pair as a separate Link row (no merge, and `core/link-conflict` does not treat the pair as a conflict)."
22
22
  },
23
23
  "confidence": {
24
24
  "type": "number",
@@ -2,9 +2,9 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.ai/spec/v0/plugins-doctor.schema.json",
4
4
  "title": "PluginsDoctorReport",
5
- "description": "Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts across built-in and drop-in plugins plus the structured issue / warning lists the human renderer produces. The `elapsedMs` top-level field is the command's own wall-clock (see `cli-contract.md` §Elapsed time).",
5
+ "description": "Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts across built-in and drop-in plugins plus the structured issue / warning lists the human renderer produces, and the runtime contribution rejections persisted by the last scan. The `elapsedMs` top-level field is the command's own wall-clock (see `cli-contract.md` §Elapsed time).",
6
6
  "type": "object",
7
- "required": ["ok", "kind", "counts", "issues", "warnings", "elapsedMs"],
7
+ "required": ["ok", "kind", "counts", "issues", "warnings", "contributionErrors", "elapsedMs"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "ok": {
@@ -88,6 +88,49 @@
88
88
  }
89
89
  }
90
90
  },
91
+ "contributionErrors": {
92
+ "type": "array",
93
+ "description": "View contributions the last persisted scan REJECTED at emit time (the \"off-shape visible\" follow-up): an `ctx.emitContribution(...)` call whose ref was not a declared contribution, or whose payload failed the target slot's payload schema. Read from `scan_contribution_errors`; empty when the last scan had no rejected emissions, or when no scan has run yet (fresh project / missing DB). Each entry gates the exit code (any contribution error → exit 1). Iteration order matches the human renderer (`pluginId`, `extensionId`, `nodePath`, `emittedAt` ASC).",
94
+ "items": {
95
+ "type": "object",
96
+ "required": ["pluginId", "extensionId", "nodePath", "reason", "message"],
97
+ "additionalProperties": false,
98
+ "properties": {
99
+ "pluginId": {
100
+ "type": "string",
101
+ "minLength": 1,
102
+ "description": "Plugin id of the extension whose emission was rejected."
103
+ },
104
+ "extensionId": {
105
+ "type": "string",
106
+ "minLength": 1,
107
+ "description": "Extension id (within the plugin) that emitted the rejected contribution."
108
+ },
109
+ "nodePath": {
110
+ "type": "string",
111
+ "minLength": 1,
112
+ "description": "Target node path the contribution was emitted against."
113
+ },
114
+ "reason": {
115
+ "type": "string",
116
+ "minLength": 1,
117
+ "description": "Rejection reason: the literal `undeclared-contribution-ref`, or the AJV error string when the payload failed the slot's payload schema."
118
+ },
119
+ "message": {
120
+ "type": "string",
121
+ "description": "Sanitised human-readable diagnostic (mirrors the ephemeral `extension.error` event of kind `contribution-rejected`)."
122
+ },
123
+ "contributionId": {
124
+ "type": "string",
125
+ "description": "Resolved contribution id. Absent for the `undeclared-contribution-ref` shape (no contribution was resolved)."
126
+ },
127
+ "slot": {
128
+ "type": "string",
129
+ "description": "Resolved target slot. Absent for the `undeclared-contribution-ref` shape."
130
+ }
131
+ }
132
+ }
133
+ },
91
134
  "elapsedMs": {
92
135
  "type": "integer",
93
136
  "minimum": 0,
@@ -32,6 +32,10 @@
32
32
  "minLength": 1,
33
33
  "description": "Required short description shown in `sm plugins list` and the UI. English-only per AGENTS.md."
34
34
  },
35
+ "order": {
36
+ "type": "number",
37
+ "description": "Optional visual ordering hint, inspector-only. The inspector renders one collapsible section per plugin (grouping the plugin's `inspector.body.panel.*` contributions); sections are sorted ASC by this value (default 100), tie-break by plugin id. Does NOT affect extension execution order, which is governed by `phase` (analyzers) and registration order."
38
+ },
35
39
  "storage": {
36
40
  "type": "object",
37
41
  "description": "Persistence mode for this plugin. Absent = plugin does not persist state.",
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "kind": {
58
58
  "type": "string",
59
- "enum": ["invokes", "references", "mentions", "supersedes"],
59
+ "enum": ["invokes", "references", "mentions", "supersedes", "points"],
60
60
  "description": "Proposed link kind, matching `link.schema.json#/properties/kind/enum`. Closed enum in v1; provider-specific kinds wait until a concrete need emerges."
61
61
  },
62
62
  "target": {