@skill-map/spec 0.16.0 → 0.18.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 +360 -0
- package/README.md +1 -1
- package/architecture.md +69 -0
- package/cli-contract.md +113 -2
- package/conformance/cases/plugin-missing-ui-rejected.json +3 -1
- package/conformance/cases/sidecar-end-to-end.json +26 -0
- package/conformance/coverage.md +8 -4
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +6 -6
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +12 -0
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.md +8 -0
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm +20 -0
- package/conformance/fixtures/sidecar-example/agent-example.md +17 -0
- package/conformance/fixtures/sidecar-example/agent-example.sm +53 -0
- package/db-schema.md +17 -3
- package/index.json +34 -16
- package/package.json +8 -1
- package/plugin-author-guide.md +125 -0
- package/schemas/annotations.schema.json +79 -0
- package/schemas/api/rest-envelope.schema.json +111 -42
- package/schemas/bump-report.schema.json +29 -0
- package/schemas/extensions/base.schema.json +25 -0
- package/schemas/extensions/provider.schema.json +22 -0
- package/schemas/frontmatter/base.schema.json +2 -127
- package/schemas/node.schema.json +39 -8
- package/schemas/report-base-deterministic.schema.json +15 -0
- package/schemas/sidecar.schema.json +96 -0
- package/schemas/summaries/{note.schema.json → markdown.schema.json} +5 -5
package/plugin-author-guide.md
CHANGED
|
@@ -681,6 +681,131 @@ Full surface in `@skill-map/testkit/index.ts`.
|
|
|
681
681
|
|
|
682
682
|
---
|
|
683
683
|
|
|
684
|
+
## Annotation contributions
|
|
685
|
+
|
|
686
|
+
> **Status.** Ships with spec v0.18.0 (Step 9.6.6). Plugins that want to write first-class fields into a node's co-located `.sm` sidecar declare them in their extension manifest under `annotationContributions`. The kernel validates the contributions at load time, surfaces the runtime catalog via `kernel.getRegisteredAnnotationKeys()` (consumed by the BFF / UI for autocomplete), and treats two plugins claiming the same root-exclusive key as a fatal startup error.
|
|
687
|
+
|
|
688
|
+
### Manifest shape
|
|
689
|
+
|
|
690
|
+
`annotationContributions` is an object map keyed by the annotation key the extension wants to own. Each entry declares an inline JSON Schema for the value plus two policy fields:
|
|
691
|
+
|
|
692
|
+
```js
|
|
693
|
+
// my-plugin/extensions/extractor.js
|
|
694
|
+
export default {
|
|
695
|
+
id: 'my-extractor',
|
|
696
|
+
kind: 'extractor',
|
|
697
|
+
version: '1.0.0',
|
|
698
|
+
// ...rest of the extractor manifest...
|
|
699
|
+
annotationContributions: {
|
|
700
|
+
lastReviewedAt: {
|
|
701
|
+
schema: { type: 'string', format: 'date-time' },
|
|
702
|
+
// location and ownership default to 'namespaced' / 'shared'
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Field-by-field:
|
|
709
|
+
|
|
710
|
+
| Field | Type | Default | Meaning |
|
|
711
|
+
|--------------|-----------------------------------|----------------|------------------------------------------------------------------------------------------------------|
|
|
712
|
+
| `schema` | inline JSON Schema (object) | required | Validates the value the extension writes under this key. Compiled with AJV at load time. |
|
|
713
|
+
| `location` | `'namespaced'` \| `'root'` | `'namespaced'` | Where the key lands inside the sidecar (see below). |
|
|
714
|
+
| `ownership` | `'shared'` \| `'exclusive'` | `'shared'` | Conflict policy. REQUIRED to be `'exclusive'` when `location: 'root'`. |
|
|
715
|
+
|
|
716
|
+
The `schema` field is **inline** — an object literal in the manifest, not a `$ref` to a file. Aligns with how the extractor / rule / action schemas already declare other inline shapes; avoids an extra path-resolution step at load time.
|
|
717
|
+
|
|
718
|
+
### Namespacing default vs root opt-in
|
|
719
|
+
|
|
720
|
+
By default a contribution lands inside the plugin's `<plugin-id>:` block at the sidecar root. Two plugins can ship a contribution with the same key and never collide because the runtime path keeps them under separate namespaces:
|
|
721
|
+
|
|
722
|
+
```yaml
|
|
723
|
+
# .claude/agents/architect.sm
|
|
724
|
+
for:
|
|
725
|
+
path: .claude/agents/architect.md
|
|
726
|
+
bodyHash: ...
|
|
727
|
+
frontmatterHash: ...
|
|
728
|
+
annotations:
|
|
729
|
+
version: 3
|
|
730
|
+
|
|
731
|
+
# Plugin 'reviewer' contributes 'lastReviewedAt'
|
|
732
|
+
reviewer:
|
|
733
|
+
lastReviewedAt: 2026-05-06T10:00:00Z
|
|
734
|
+
|
|
735
|
+
# Plugin 'auditor' also contributes 'lastReviewedAt' — different namespace, no conflict
|
|
736
|
+
auditor:
|
|
737
|
+
lastReviewedAt: 2026-05-05T18:30:00Z
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Opting into a top-level (root) key requires `location: 'root'` AND `ownership: 'exclusive'`. The pair travels together — a top-level reserved key cannot be silently shared between plugins, because `.sm` writes deep-merge per the `SidecarStore` contract and a shared root key would route non-deterministically. Use root sparingly: for every plugin that contributes a root key, the kernel reserves that name across the whole installed-plugin surface.
|
|
741
|
+
|
|
742
|
+
```js
|
|
743
|
+
// compliance-plugin/extensions/rule.js
|
|
744
|
+
export default {
|
|
745
|
+
id: 'compliance-checker',
|
|
746
|
+
kind: 'rule',
|
|
747
|
+
// ...
|
|
748
|
+
annotationContributions: {
|
|
749
|
+
compliance: {
|
|
750
|
+
schema: {
|
|
751
|
+
type: 'object',
|
|
752
|
+
required: ['audit'],
|
|
753
|
+
properties: {
|
|
754
|
+
audit: { type: 'string' },
|
|
755
|
+
dueAt: { type: 'string', format: 'date-time' },
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
location: 'root',
|
|
759
|
+
ownership: 'exclusive',
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
};
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
The resulting sidecar block:
|
|
766
|
+
|
|
767
|
+
```yaml
|
|
768
|
+
# .claude/agents/architect.sm
|
|
769
|
+
for: { path: ..., bodyHash: ..., frontmatterHash: ... }
|
|
770
|
+
compliance:
|
|
771
|
+
audit: sox-2026
|
|
772
|
+
dueAt: 2026-12-31T23:59:59Z
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Ownership rules
|
|
776
|
+
|
|
777
|
+
- `shared` (default) — multiple plugins MAY write the same key. Every plugin gets its own namespaced block; `last-write-wins` is per-`(plugin, key)` tuple inside `FilesystemSidecarStore.applyPatch`. Two plugins on the SAME namespaced key from the same plugin id is structurally impossible (one extension per kind per plugin id by spec), so the only collision surface is intra-extension.
|
|
778
|
+
- `exclusive` — only this plugin may write the key. The kernel rejects any other plugin that tries to claim the same `(key, location: 'root')` tuple as `exclusive`. `exclusive` + `namespaced` is permitted but redundant in practice (the namespace already isolates by plugin id); use it as documentation when you want the manifest to scream "no other plugin should ever write this".
|
|
779
|
+
|
|
780
|
+
### Collision behaviour — hard fail, no boot
|
|
781
|
+
|
|
782
|
+
Two plugins claiming the same `(key, location: 'root', ownership: 'exclusive')` tuple is a **fatal startup error**. The kernel does NOT boot in this state — `loadPluginRuntime` throws `AnnotationContributionConflictError` and the host (CLI verb, BFF, watch mode) propagates the error and exits non-zero with a clear stderr message naming both offenders. Stricter than the default per-plugin `invalid-manifest` "disable just that plugin" path: annotation-namespace conflicts are non-recoverable because annotated `.sm` files would otherwise become non-deterministically routed.
|
|
783
|
+
|
|
784
|
+
This is the only fatal path on the plugin-load surface. Every other failure mode (manifest invalid, schema invalid, dynamic-import failure, id collision) is per-plugin and the kernel keeps booting on the survivors.
|
|
785
|
+
|
|
786
|
+
### Tier-1 typo guard (`core/unknown-field`)
|
|
787
|
+
|
|
788
|
+
The built-in `core/unknown-field` Rule walks every parsed `.sm` and emits a `warn` issue per truly-unknown key. Three surfaces are checked:
|
|
789
|
+
|
|
790
|
+
1. Inside `annotations:` — keys not in `annotations.schema.json`'s curated catalog (the ~25 conventional fields). Plugins do NOT contribute to `annotations:`; that block is skill-map-curated.
|
|
791
|
+
2. At the sidecar root — keys outside the four reserved blocks (`for`, `annotations`, `settings`, `audit`) that are also NOT a registered plugin namespace `<plugin-id>:` AND NOT a registered `location: 'root'` contribution.
|
|
792
|
+
3. Inside a registered `<plugin-id>:` namespace — values that fail the schema declared by the owning plugin's `annotationContributions[<key>].schema`.
|
|
793
|
+
|
|
794
|
+
The rule never blocks a scan; advisories surface through the standard issue channel (CLI, UI, REST). When you ship a contribution, the loader compiles your inline schema, the runtime catalog publishes it, and `core/unknown-field` automatically validates user writes against your declaration.
|
|
795
|
+
|
|
796
|
+
### Runtime catalog accessor
|
|
797
|
+
|
|
798
|
+
Once every plugin has loaded, the runtime catalog is reachable via `kernel.getRegisteredAnnotationKeys()`:
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
// Each entry: { pluginId, key, location, ownership, schema }
|
|
802
|
+
const keys = kernel.getRegisteredAnnotationKeys();
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Pure read; no side effects. Built-in catalog fields from `annotations.schema.json` are NOT included — this catalog is plugin-only. The UI knows the built-in catalog separately via the schema bundle. The (future) BFF endpoint surfaces this through `GET /api/annotations/catalog` for autocomplete.
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
684
809
|
## See also
|
|
685
810
|
|
|
686
811
|
- [`architecture.md`](./architecture.md) — extension contract, ports, execution modes.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://skill-map.dev/spec/v0/annotations.schema.json",
|
|
4
|
+
"title": "Annotations",
|
|
5
|
+
"description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` rule emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 14 fields below — versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), display (`hidden`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. Plugins that want first-class custom keys with their own validation declare `annotationContributions` in their manifest (see Step 9.6.6).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"minimum": 1,
|
|
12
|
+
"description": "Monotonic counter. Bumped via the built-in `bump` Action when the underlying node changes meaningfully. Orthogonal to `stability` — `stability` carries the lifecycle stage; `version` is just a counter. There is no major: a change so big it would justify a major bump uses the convention `create a new node, supersede the old one` instead. Default: missing == unversioned."
|
|
13
|
+
},
|
|
14
|
+
"stability": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["experimental", "stable", "deprecated"],
|
|
17
|
+
"description": "Lifecycle stage. Denormalized into `scan_nodes.stability` for fast queries (see `node.schema.json` #/properties/stability). Default: missing == unspecified."
|
|
18
|
+
},
|
|
19
|
+
"supersedes": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "string", "minLength": 1 },
|
|
22
|
+
"description": "Paths (relative to scope root) of nodes this node replaces. Consumed by the built-in `superseded` rule and surfaces in `sm list --superseded`."
|
|
23
|
+
},
|
|
24
|
+
"supersededBy": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 1,
|
|
27
|
+
"description": "Path (relative to scope root) of the node that replaces this one. When set, the current node is end-of-life and consumers should migrate."
|
|
28
|
+
},
|
|
29
|
+
"requires": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": { "type": "string", "minLength": 1 },
|
|
32
|
+
"description": "Paths (relative to scope root) of nodes this node depends on. Surfaces in the dependency graph; the `broken-ref` rule flags missing targets."
|
|
33
|
+
},
|
|
34
|
+
"conflictsWith": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string", "minLength": 1 },
|
|
37
|
+
"description": "Paths (relative to scope root) of nodes that cannot be active alongside this one. Surfaces in conflict detection (post-v0.5.0)."
|
|
38
|
+
},
|
|
39
|
+
"related": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "type": "string", "minLength": 1 },
|
|
42
|
+
"description": "Paths (relative to scope root) of conceptually related nodes. Soft link for navigation; no strong semantics, no rule enforcement."
|
|
43
|
+
},
|
|
44
|
+
"authors": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "type": "string", "minLength": 1 },
|
|
47
|
+
"description": "Multi-author list. Single-author files use a one-element array."
|
|
48
|
+
},
|
|
49
|
+
"license": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1,
|
|
52
|
+
"description": "SPDX identifier preferred (e.g. `MIT`, `Apache-2.0`); free-form accepted."
|
|
53
|
+
},
|
|
54
|
+
"source": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"format": "uri",
|
|
57
|
+
"description": "URL of the canonical upstream (e.g. GitHub raw URL). Consumed by the `github-enrichment` Action for hash verification."
|
|
58
|
+
},
|
|
59
|
+
"sourceVersion": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"minLength": 1,
|
|
62
|
+
"description": "Tag, branch, or full commit SHA at the upstream. Drives `github-enrichment` SHA pin / tag resolution."
|
|
63
|
+
},
|
|
64
|
+
"tags": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "type": "string", "minLength": 1 },
|
|
67
|
+
"description": "Free-form taxonomy tags. Consumed by `sm list --tag <name>` and UI faceted search."
|
|
68
|
+
},
|
|
69
|
+
"hidden": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"description": "When true, the node is excluded from default listings (still appears under `--all` / `--include-hidden`). Useful for in-progress nodes the author does not want surfaced yet."
|
|
72
|
+
},
|
|
73
|
+
"docsUrl": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"format": "uri",
|
|
76
|
+
"description": "Canonical docs URL for this node (separate from `source`, which points at the file's upstream)."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/api/rest-envelope.schema.json",
|
|
4
4
|
"title": "RestEnvelope",
|
|
5
|
-
"description": "Wrapper shape for REST responses under `/api/*` (Step 14.2).
|
|
5
|
+
"description": "Wrapper shape for REST responses under `/api/*` (Step 14.2). Five variants distinguished by the `kind` discriminator and which payload field is present (`items` for list kinds AND `'annotations.registered'`, `item` for single-resource kinds, `value` for `kind: 'config'` and `'sidecar.bumped'`). 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 list / single / config 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. Step 9.6 closes the `'sidecar.bumped'` (R7) and `'annotations.registered'` (R7) gaps — both are payload-bearing but carry their own variant shapes (sidecar.bumped: `value` + `elapsedMs`, no `filters`/`counts`/`kindRegistry`; annotations.registered: `items` + `counts.total`, no `filters`/`kindRegistry`) because they project read-only kernel surfaces orthogonal to the kindRegistry. The change keeps `schemaVersion` at `'1'` — the BFF is greenfield (no released consumers depend on the prior shape), so a versioned migration buys nothing.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["schemaVersion", "kind"],
|
|
8
8
|
"properties": {
|
|
@@ -13,8 +13,20 @@
|
|
|
13
13
|
},
|
|
14
14
|
"kind": {
|
|
15
15
|
"type": "string",
|
|
16
|
-
"enum": [
|
|
17
|
-
|
|
16
|
+
"enum": [
|
|
17
|
+
"nodes",
|
|
18
|
+
"links",
|
|
19
|
+
"issues",
|
|
20
|
+
"plugins",
|
|
21
|
+
"config",
|
|
22
|
+
"graph",
|
|
23
|
+
"node",
|
|
24
|
+
"health",
|
|
25
|
+
"scan",
|
|
26
|
+
"sidecar.bumped",
|
|
27
|
+
"annotations.registered"
|
|
28
|
+
],
|
|
29
|
+
"description": "Discriminator. List kinds (`nodes`, `links`, `issues`, `plugins`) carry `items` + `filters` + `counts` + `kindRegistry`. The `node` kind carries `item` + `kindRegistry`. The `config` kind carries `value` + `kindRegistry`. The `sidecar.bumped` kind (Step 9.6.5, BFF half) carries `value` + `elapsedMs` (no `filters` / `counts` / `kindRegistry` — it's an action-result projection orthogonal to the kindRegistry surface). The `annotations.registered` kind (Step 9.6.6, BFF half) carries `items` + `counts.total` (no `filters` / `kindRegistry` — pure read-only catalog projection). The `health` / `scan` / `graph` values are reserved for documentation parity with the routes that DON'T use this envelope."
|
|
18
30
|
},
|
|
19
31
|
"items": {
|
|
20
32
|
"type": "array",
|
|
@@ -26,7 +38,12 @@
|
|
|
26
38
|
},
|
|
27
39
|
"value": {
|
|
28
40
|
"type": "object",
|
|
29
|
-
"description": "Present when `kind` is `'config'`.
|
|
41
|
+
"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`)."
|
|
42
|
+
},
|
|
43
|
+
"elapsedMs": {
|
|
44
|
+
"type": "integer",
|
|
45
|
+
"minimum": 0,
|
|
46
|
+
"description": "Wall-clock milliseconds the BFF spent serving the request. Present on action-result envelopes (`kind: 'sidecar.bumped'`); absent elsewhere."
|
|
30
47
|
},
|
|
31
48
|
"filters": {
|
|
32
49
|
"type": "object",
|
|
@@ -34,49 +51,61 @@
|
|
|
34
51
|
},
|
|
35
52
|
"kindRegistry": {
|
|
36
53
|
"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.",
|
|
54
|
+
"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. Each entry MAY carry contributions from multiple Providers when several declare the same kind name (e.g. Claude `agent` and Gemini `agent`); the `providers` map keeps every contribution and `primaryProviderId` points at the one whose visuals drive the kind's primary CSS var. The kernel separately surfaces `provider-ambiguous` issues for files matched by more than one Provider; the UI may still receive the merged registry during the conflict window.",
|
|
38
55
|
"additionalProperties": {
|
|
39
56
|
"type": "object",
|
|
40
|
-
"required": ["
|
|
57
|
+
"required": ["primaryProviderId", "providers"],
|
|
41
58
|
"additionalProperties": false,
|
|
42
59
|
"properties": {
|
|
43
|
-
"
|
|
60
|
+
"primaryProviderId": {
|
|
44
61
|
"type": "string",
|
|
45
62
|
"minLength": 1,
|
|
46
|
-
"description": "Id of the Provider
|
|
63
|
+
"description": "Id of the Provider whose visuals drive the kind's primary CSS var (`--sm-kind-<kind>`). Set on first registration (first Provider in iteration order); subsequent contributors append to `providers` without overwriting the primary."
|
|
47
64
|
},
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
"providers": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"minProperties": 1,
|
|
68
|
+
"description": "Per-provider visuals for this kind name. Keyed by Provider id (e.g. `'claude'`, `'gemini'`). Lets the UI render a node painted with its own Provider's color via `entry.providers[node.provider]` when the kind name is shared across Providers.",
|
|
69
|
+
"additionalProperties": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"required": ["label", "color"],
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"properties": {
|
|
74
|
+
"label": { "type": "string", "minLength": 1 },
|
|
75
|
+
"color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
76
|
+
"colorDark": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
77
|
+
"emoji": { "type": "string", "minLength": 1, "maxLength": 8 },
|
|
78
|
+
"icon": {
|
|
79
|
+
"oneOf": [
|
|
80
|
+
{
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["kind", "id"],
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"properties": {
|
|
85
|
+
"kind": { "const": "pi" },
|
|
86
|
+
"id": { "type": "string", "pattern": "^pi-[a-z0-9]+(-[a-z0-9]+)*$" }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"type": "object",
|
|
91
|
+
"required": ["kind", "path"],
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"kind": { "const": "svg" },
|
|
95
|
+
"path": { "type": "string", "minLength": 1 }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
70
99
|
}
|
|
71
100
|
}
|
|
72
|
-
|
|
101
|
+
}
|
|
73
102
|
}
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
105
|
},
|
|
77
106
|
"counts": {
|
|
78
107
|
"type": "object",
|
|
79
|
-
"required": ["total"
|
|
108
|
+
"required": ["total"],
|
|
80
109
|
"properties": {
|
|
81
110
|
"total": {
|
|
82
111
|
"type": "integer",
|
|
@@ -86,7 +115,7 @@
|
|
|
86
115
|
"returned": {
|
|
87
116
|
"type": "integer",
|
|
88
117
|
"minimum": 0,
|
|
89
|
-
"description": "Rows actually carried in `items` (≤ `limit`)."
|
|
118
|
+
"description": "Rows actually carried in `items` (≤ `limit`). Present on list-kind envelopes (`nodes`, `links`, `issues`, `plugins`); absent on `'annotations.registered'` (no pagination, no filters — the catalog ships in its entirety in every response)."
|
|
90
119
|
},
|
|
91
120
|
"page": {
|
|
92
121
|
"type": "object",
|
|
@@ -108,25 +137,27 @@
|
|
|
108
137
|
}
|
|
109
138
|
},
|
|
110
139
|
"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)."
|
|
140
|
+
"description": "Tally + paging info. Present on every list / single envelope; absent on `health` / `scan` / `graph` responses (which don't use this envelope). The list variants additionally require `returned`; the `'annotations.registered'` variant only requires `total`."
|
|
112
141
|
}
|
|
113
142
|
},
|
|
114
143
|
"oneOf": [
|
|
115
144
|
{
|
|
116
|
-
"description": "List envelope — `items` payload + `counts` + `kindRegistry`.",
|
|
145
|
+
"description": "List envelope — `items` payload + `filters` + `counts` (with `returned`) + `kindRegistry`. Used by `/api/nodes`, `/api/links`, `/api/issues`, `/api/plugins`.",
|
|
117
146
|
"required": ["items", "counts", "filters", "kindRegistry"],
|
|
118
147
|
"properties": {
|
|
119
|
-
"kind": { "enum": ["nodes", "links", "issues", "plugins"] }
|
|
148
|
+
"kind": { "enum": ["nodes", "links", "issues", "plugins"] },
|
|
149
|
+
"counts": { "required": ["total", "returned"] }
|
|
120
150
|
},
|
|
121
151
|
"not": {
|
|
122
152
|
"anyOf": [
|
|
123
153
|
{ "required": ["item"] },
|
|
124
|
-
{ "required": ["value"] }
|
|
154
|
+
{ "required": ["value"] },
|
|
155
|
+
{ "required": ["elapsedMs"] }
|
|
125
156
|
]
|
|
126
157
|
}
|
|
127
158
|
},
|
|
128
159
|
{
|
|
129
|
-
"description": "Single-resource envelope — `item` payload + `kindRegistry`, no `counts` / `filters`.",
|
|
160
|
+
"description": "Single-resource envelope — `item` payload + `kindRegistry`, no `counts` / `filters`. Used by `/api/nodes/:pathB64`.",
|
|
130
161
|
"required": ["item", "kindRegistry"],
|
|
131
162
|
"properties": {
|
|
132
163
|
"kind": { "const": "node" }
|
|
@@ -134,12 +165,13 @@
|
|
|
134
165
|
"not": {
|
|
135
166
|
"anyOf": [
|
|
136
167
|
{ "required": ["items"] },
|
|
137
|
-
{ "required": ["value"] }
|
|
168
|
+
{ "required": ["value"] },
|
|
169
|
+
{ "required": ["elapsedMs"] }
|
|
138
170
|
]
|
|
139
171
|
}
|
|
140
172
|
},
|
|
141
173
|
{
|
|
142
|
-
"description": "Value envelope — `value` payload + `kindRegistry`, no `counts` / `filters`.",
|
|
174
|
+
"description": "Value envelope — `value` payload + `kindRegistry`, no `counts` / `filters`. Used by `/api/config`.",
|
|
143
175
|
"required": ["value", "kindRegistry"],
|
|
144
176
|
"properties": {
|
|
145
177
|
"kind": { "const": "config" }
|
|
@@ -147,7 +179,43 @@
|
|
|
147
179
|
"not": {
|
|
148
180
|
"anyOf": [
|
|
149
181
|
{ "required": ["items"] },
|
|
150
|
-
{ "required": ["item"] }
|
|
182
|
+
{ "required": ["item"] },
|
|
183
|
+
{ "required": ["elapsedMs"] }
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"description": "Action-result envelope — `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry`. 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 kindRegistry is intentionally absent — the action result is orthogonal to the kind catalog and the SPA already has it cached from a prior list call.",
|
|
189
|
+
"required": ["value", "elapsedMs"],
|
|
190
|
+
"properties": {
|
|
191
|
+
"kind": { "const": "sidecar.bumped" }
|
|
192
|
+
},
|
|
193
|
+
"not": {
|
|
194
|
+
"anyOf": [
|
|
195
|
+
{ "required": ["items"] },
|
|
196
|
+
{ "required": ["item"] },
|
|
197
|
+
{ "required": ["filters"] },
|
|
198
|
+
{ "required": ["counts"] },
|
|
199
|
+
{ "required": ["kindRegistry"] }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"description": "Catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `returned`. Used by `GET /api/annotations/registered` (Step 9.6.6, BFF half). The catalog is small (typically 0–50 entries), ships in its entirety on every response, and does not paginate; `counts.total` doubles as `items.length`.",
|
|
205
|
+
"required": ["items", "counts"],
|
|
206
|
+
"properties": {
|
|
207
|
+
"kind": { "const": "annotations.registered" },
|
|
208
|
+
"counts": {
|
|
209
|
+
"not": { "required": ["returned"] }
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"not": {
|
|
213
|
+
"anyOf": [
|
|
214
|
+
{ "required": ["item"] },
|
|
215
|
+
{ "required": ["value"] },
|
|
216
|
+
{ "required": ["filters"] },
|
|
217
|
+
{ "required": ["kindRegistry"] },
|
|
218
|
+
{ "required": ["elapsedMs"] }
|
|
151
219
|
]
|
|
152
220
|
}
|
|
153
221
|
},
|
|
@@ -161,7 +229,8 @@
|
|
|
161
229
|
{ "required": ["items"] },
|
|
162
230
|
{ "required": ["item"] },
|
|
163
231
|
{ "required": ["value"] },
|
|
164
|
-
{ "required": ["kindRegistry"] }
|
|
232
|
+
{ "required": ["kindRegistry"] },
|
|
233
|
+
{ "required": ["elapsedMs"] }
|
|
165
234
|
]
|
|
166
235
|
}
|
|
167
236
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://skill-map.dev/spec/v0/bump-report.schema.json",
|
|
4
|
+
"title": "BumpReport",
|
|
5
|
+
"description": "Report shape produced by the built-in deterministic `bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (the deterministic counterpart to `report-base.schema.json`, which carries the LLM-only `confidence` + `safety` fields). The bump Action returns one of three concrete shapes, distinguished by `ok` / `noop` / `reason`: success-with-write (`{ ok: true, version }`), silent-no-op under `force` (`{ ok: true, noop: true }`), or refusal (`{ ok: false, reason: 'fresh' }`).",
|
|
6
|
+
"allOf": [{ "$ref": "report-base-deterministic.schema.json" }],
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"properties": {
|
|
10
|
+
"noop": {
|
|
11
|
+
"type": "boolean",
|
|
12
|
+
"description": "True when the Action accepted the request but did not produce a write (the node was already fresh and `force: true` opted into the silent no-op path). Mutually exclusive with `version`."
|
|
13
|
+
},
|
|
14
|
+
"reason": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["fresh"],
|
|
17
|
+
"description": "Refusal reason, present only when `ok: false`. `fresh` = the node has no drift versus its sidecar and the caller did not pass `force: true`."
|
|
18
|
+
},
|
|
19
|
+
"version": {
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"minimum": 1,
|
|
22
|
+
"description": "The new `annotations.version` value the Action wrote. Present only when `ok: true` and `noop` is absent."
|
|
23
|
+
},
|
|
24
|
+
"createdSidecar": {
|
|
25
|
+
"type": "boolean",
|
|
26
|
+
"description": "True when this bump created the `.sm` file (no sidecar existed on disk before). Triggers the `audit.createdAt` / `audit.createdBy` fill-in."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -38,6 +38,31 @@
|
|
|
38
38
|
"entry": {
|
|
39
39
|
"type": "string",
|
|
40
40
|
"description": "Optional path to the module exporting this extension (relative to the plugin root). Absent → the kernel uses the path listed in the plugin manifest's `extensions[]` array."
|
|
41
|
+
},
|
|
42
|
+
"annotationContributions": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"required": ["schema"],
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"properties": {
|
|
49
|
+
"schema": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"description": "Inline JSON Schema for this contribution's value. Validated when the kernel routes a sidecar write through the extension's namespace (or root key, see `location`). Must be a valid JSON Schema document; an invalid `schema` rejects the extension at load with `invalid-manifest`."
|
|
52
|
+
},
|
|
53
|
+
"ownership": {
|
|
54
|
+
"enum": ["exclusive", "shared"],
|
|
55
|
+
"default": "shared",
|
|
56
|
+
"description": "Conflict policy for this key. `shared` (default) — the key is namespaced by default; multiple plugins MAY contribute to the same key, last-write-wins per the SidecarStore's deep-merge semantics. `exclusive` — only this plugin may write the key. REQUIRED when `location: 'root'` (a top-level reserved key cannot be silently shared between plugins)."
|
|
57
|
+
},
|
|
58
|
+
"location": {
|
|
59
|
+
"enum": ["namespaced", "root"],
|
|
60
|
+
"default": "namespaced",
|
|
61
|
+
"description": "Where the key lands inside the sidecar. `namespaced` (default) — written under the plugin's `<plugin-id>:` block at the sidecar root. `root` — written as a top-level key alongside the reserved blocks (`for`, `annotations`, `settings`, `audit`); requires `ownership: 'exclusive'` and is treated as elevated trust (two plugins claiming the same root key with `exclusive` is a hard-fail at orchestrator init). See `plugin-author-guide.md` §Annotation contributions."
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Rule emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
|
|
41
66
|
}
|
|
42
67
|
}
|
|
43
68
|
}
|
|
@@ -21,6 +21,28 @@
|
|
|
21
21
|
"description": "Path globs (relative to scope root) that this Provider SHOULD be consulted for. Advisory — the kernel walks all roots and consults every Provider regardless, but this field lets `sm doctor` warn when no file matched a specific Provider (i.e. the Provider was loaded for a platform that isn't in this scope).",
|
|
22
22
|
"items": { "type": "string" }
|
|
23
23
|
},
|
|
24
|
+
"read": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"required": ["extensions", "parser"],
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"description": "Declarative file-discovery config consumed by the kernel walker. When present, the kernel walks every root, includes files whose extension matches `extensions`, parses each with the parser registered as `parser`, and yields raw nodes the orchestrator consumes. When absent, the kernel applies the default `{ extensions: ['.md'], parser: 'frontmatter-yaml' }` so the most common Provider shape needs no configuration. When a Provider also declares the runtime `walk()` method (TypeScript-only — never appears in this manifest), `walk()` wins and `read` is ignored: the runtime field is the escape hatch for Providers with non-standard discovery requirements. Built-in parsers ship with the kernel (`frontmatter-yaml`, `plain`); the set is closed by design and user plugins cannot register their own.",
|
|
29
|
+
"properties": {
|
|
30
|
+
"extensions": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"minItems": 1,
|
|
33
|
+
"description": "File extensions the walker yields. Strings include the leading dot. Lowercase-only by convention — match is case-sensitive and Providers MUST list every casing they want recognised.",
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"pattern": "^\\.[a-z0-9]+$"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"parser": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1,
|
|
42
|
+
"description": "Identifier of a parser registered in the kernel-internal registry. Built-ins: `frontmatter-yaml` (markdown with `--- … ---` YAML frontmatter, prototype-pollution-safe, `js-yaml` JSON_SCHEMA-pinned), `plain` (entire body, empty frontmatter — for files carrying no frontmatter convention; the Provider derives `name` from the path inside `classify()`). Unknown ids surface as `UnknownParserError` from the walker; the orchestrator translates the error into a Provider issue with status `invalid-manifest`."
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
24
46
|
"kinds": {
|
|
25
47
|
"type": "object",
|
|
26
48
|
"description": "Catalog of node kinds this Provider emits. Keyed by kind name (e.g. `skill`, `agent`, `note`). Each entry declares the relative path to the kind's frontmatter schema (the kernel resolves it against the Provider's package directory) and the qualified `defaultRefreshAction` id the UI dispatches when the user requests a probabilistic refresh on a node of that kind. The map MUST be non-empty: a Provider that emits no kinds is meaningless. Kind names follow camelCase / lowerCase convention; the spec does not constrain the value space (a Cursor Provider could declare `rule`, an Obsidian Provider could declare `daily`).",
|