@skill-map/spec 0.25.0 → 0.27.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 CHANGED
@@ -1,5 +1,121 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f1efd1b: Remove the `-g/--global` flag and every implicit `$HOME` read from
8
+ skill-map. The CLI now operates exclusively on the project scope
9
+ (`<cwd>/.skill-map/`); there is no global / user scope, no
10
+ `SKILL_MAP_SCOPE` env var, no silent merge of user-level config or
11
+ plugins.
12
+
13
+ The user extends the scan beyond the project root via the existing
14
+ `scan.extraFolders` setting in project-local config (privacy-gated
15
+ through `sm config set --yes` or the Settings UI confirm dialog).
16
+ Plugins outside the project install per-project at
17
+ `<cwd>/.skill-map/plugins/` or load via the `--plugin-dir <path>`
18
+ escape hatch on the `sm plugins …` verb family.
19
+
20
+ **Narrow documented exception**: a single `~/.skill-map/settings.json`
21
+ file (validated by `user-settings.schema.json`) holds genuinely
22
+ per-machine preferences. Today it carries the update-check toggle +
23
+ its throttle bookkeeping; future per-machine settings (locale, theme)
24
+ extend it under their own sub-keys. There is no `.local` partner.
25
+ The file is NOT part of the project config layer system; it is read
26
+ directly by the module that owns each feature. `src/cli/util/user-settings-store.ts`
27
+ is the only module that calls `os.homedir()` for this file. The two
28
+ remaining `os.homedir()` callsites (`core/config/helper.ts`,
29
+ `core/runtime/reference-paths-walker.ts`) handle user-typed `~/foo`
30
+ expansion inside `scan.extraFolders` / `scan.referencePaths`, the
31
+ read is user-authored per invocation, not skill-map's own default.
32
+
33
+ Removed surface (`@skill-map/cli`):
34
+
35
+ - `-g/--global` flag inherited by every `SmCommand` verb (`bump`,
36
+ `check`, `config`, `export`, `graph`, `history`, `init`, `jobs`,
37
+ `list`, `orphans`, `refresh`, `scan`, `serve`, `show`, `sidecar`,
38
+ `watch`, every `plugins` subcommand). Calling any verb with
39
+ `-g/--global` now exits 2 with Clipanion's "unknown option" error.
40
+ - `SKILL_MAP_SCOPE=global` env var translation.
41
+ - `sm serve --scope project|global` flag.
42
+ - `sm config --source global` literal in `--source` outputs (the
43
+ source set is now `default | project | project-local | env | flag`).
44
+ - `IRuntimeContext.homedir` field.
45
+ - `IDbLocationOptions.global` field; `resolveDbPath` reduces to
46
+ `db ?? defaultProjectDbPath(ctx)`.
47
+ - `defaultUserPluginsDir` helper.
48
+ - `loadConfig` `scope: 'project' | 'global'` parameter and the
49
+ `user` / `user-local` file-pair iteration; the layer list is now
50
+ `defaults` → `project` → `project-local` → `override`.
51
+ - `USER_ONLY_KEYS` constant and the per-key locality enforcement
52
+ pinned to it. `updateCheck.enabled` is no longer part of the
53
+ config layer system; its toggle lives alongside the throttle
54
+ cache.
55
+ - `GET /api/health` response field `scope: 'project'|'global'`.
56
+ - `GET /api/plugins` item field `source: 'built-in'|'project'|'global'`
57
+ reduces to `'built-in'|'project'`.
58
+ - `scan_meta.scope` SQLite column and the matching `IScanResult.scope`
59
+ kernel field.
60
+
61
+ Removed surface (`@skill-map/spec`):
62
+
63
+ - `spec/cli-contract.md` § Global flags row for `-g/--global` and
64
+ the `SKILL_MAP_SCOPE` row in the env-var table.
65
+ - `spec/cli-contract.md` § serve flag table `--scope project|global`
66
+ row.
67
+ - `spec/architecture.md` § Config layering layers `user` and
68
+ `user-local`; `USER_ONLY_KEYS` set.
69
+ - `spec/db-schema.md` two-scope diagram; `scan_meta.scope` column;
70
+ `scope: 'global'` from `--source` enum text.
71
+ - `spec/schemas/scan-result.schema.json` `scope` property (was in
72
+ `required`).
73
+ - `spec/schemas/project-config.schema.json` `updateCheck`
74
+ description rewritten as the documented exception.
75
+ - `spec/schemas/plugins-registry.schema.json` status description's
76
+ `project / global / --plugin-dir` reference.
77
+
78
+ Added surface:
79
+
80
+ - `spec/cli-contract.md` § "Scope is always project-local"
81
+ normative paragraph at the top of the file, stating the
82
+ no-`$HOME`-reads principle and the update-check exception.
83
+ - `AGENTS.md` § Analyzers gains the matching operating rule for
84
+ agents working in the repo, "Skill-map MUST NEVER read `$HOME`
85
+ by default…".
86
+ - Regression test at `src/test/global-flag-removed.test.ts`
87
+ asserting Clipanion's "unknown option" error on `sm scan -g`.
88
+
89
+ Migration (no compat shim): pre-1.0, greenfield. Users who relied
90
+ on `~/.skill-map/skill-map.db`, `~/.skill-map/settings*.json`, or
91
+ `~/.skill-map/plugins/` move the files into their project
92
+ (`<cwd>/.skill-map/`) or pass `--plugin-dir <path>` per invocation.
93
+ Older DBs are not migrated, a fresh `sm init` regenerates without
94
+ the `scope` column.
95
+
96
+ ## User-facing
97
+
98
+ `-g/--global` is gone. `sm` reads only the current project
99
+ (`<cwd>/.skill-map/`). To scan outside the project, add paths via
100
+ `scan.extraFolders` in Settings. User-scope plugins move to
101
+ `<cwd>/.skill-map/plugins/` or load with `--plugin-dir <path>`.
102
+
103
+ ## 0.26.0
104
+
105
+ ### Minor Changes
106
+
107
+ - 48800d4: Drop `requires`, `related`, and `conflictsWith` from the curated annotation catalog.
108
+
109
+ The three fields collapsed into the same edge kind (`references`), which made it impossible to tell from the graph whether an arrow meant "depends on", "is in conflict with", or "soft-related". The catalog now ships 10 fields instead of 13: versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), and docs (`docsUrl`).
110
+
111
+ The extractor `core/annotations` now declares `emitsLinkKinds: ['supersedes']` (no longer emits `references` from the sidecar). Path-style `references` edges still surface from `core/markdown-link` over `[text](path)` syntax in the body.
112
+
113
+ The schema keeps `additionalProperties: true`, so sidecars that still carry `requires` / `related` / `conflictsWith` continue to parse without errors, but those keys produce no edges and the built-in `unknown-field` analyzer surfaces them as warnings.
114
+
115
+ ## User-facing
116
+
117
+ The `.sm` annotation catalog shrinks from 13 to 10 fields. `requires`, `related`, and `conflictsWith` were dropped, their edges all rendered as plain `references` and added no extra meaning. Existing sidecars keep working; the three keys are now flagged by `unknown-field`.
118
+
3
119
  ## 0.25.0
4
120
 
5
121
  ### Minor Changes
package/README.md CHANGED
@@ -39,7 +39,7 @@ These are implementation decisions. The reference impl picks them (see [`../AGEN
39
39
  Two analyzers govern every identifier in the spec. They are **normative**.
40
40
 
41
41
  - **Filesystem artefacts use kebab-case.** Every file and directory in `spec/` (and in any conforming implementation), `scan-result.schema.json`, `job-lifecycle.md`, `report-base.schema.json`, `auto-rename-medium` (as an `issue.analyzerId` value), `direct-override` (as a `safety.injectionType` enum value), and so on, is kebab-case lowercase. Enum values and issue analyzer ids follow the same convention so they can be echoed back into URLs, filenames, and log keys without escaping.
42
- - **JSON content uses camelCase.** Every key inside a JSON Schema, frontmatter block, config file, plugin manifest, action manifest, job record, report, event payload, or API response is camelCase: `whatItDoes`, `injectionDetected`, `expectedTools`, `conflictsWith`, `docsUrl`, `examplesUrl`, `ttlSeconds`, `runId`, `jobId`. This matches the JS/TS ecosystem the reference impl ships in and the Kysely `CamelCasePlugin` that bridges to the `snake_case` SQL layer, but the analyzer is spec-level, not implementation-level: an alternative implementation in any language still exposes camelCase JSON keys.
42
+ - **JSON content uses camelCase.** Every key inside a JSON Schema, frontmatter block, config file, plugin manifest, action manifest, job record, report, event payload, or API response is camelCase: `whatItDoes`, `injectionDetected`, `expectedTools`, `supersededBy`, `docsUrl`, `examplesUrl`, `ttlSeconds`, `runId`, `jobId`. This matches the JS/TS ecosystem the reference impl ships in and the Kysely `CamelCasePlugin` that bridges to the `snake_case` SQL layer, but the analyzer is spec-level, not implementation-level: an alternative implementation in any language still exposes camelCase JSON keys.
43
43
 
44
44
  The SQL persistence layer is the sole exception: tables, columns, and migration filenames use `snake_case` (see `db-schema.md`). That boundary is crossed only inside a storage adapter; nothing that leaves the kernel should ever be `snake_case`.
45
45
 
package/architecture.md CHANGED
@@ -70,7 +70,7 @@ Operations: `discover(scopes)`, `load(pluginPath)`, `validateManifest(json)`.
70
70
  The loader enforces two id-uniqueness analyzers during discovery (see [`plugin-author-guide.md` §Plugin id uniqueness](./plugin-author-guide.md#plugin-id-uniqueness) for the author-facing summary):
71
71
 
72
72
  1. **Directory name == manifest id.** A plugin lives at `<root>/<id>/plugin.json`. A mismatch surfaces as status `invalid-manifest`. This analyzer eliminates same-root collisions by construction.
73
- 2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (project + global, or any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence analyzer applies, coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
73
+ 2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (e.g. the project default `<cwd>/.skill-map/plugins/` and any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence analyzer applies, coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
74
74
 
75
75
  In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `core/slash`, `core/broken-ref`, `hello-world/greet`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts`, `core/` for kernel-internal primitives (every analyzer, the formatter, the cross-vendor extractors `annotations` / `slash` / `at-directive` / `markdown-link` / `external-url-counter` / `stability`) and vendor-specific bundles such as `claude/` (the Claude provider) for Provider integrations whose territory is platform-bound. If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
76
76
 
@@ -362,7 +362,7 @@ Hooks introduce no new persisted state and do NOT participate in the determinist
362
362
 
363
363
  ### Locality
364
364
 
365
- - **Drop-in**: extensions live inside plugins, discovered at boot from `.skill-map/plugins/<id>/` and `~/.skill-map/plugins/<id>/`.
365
+ - **Drop-in**: extensions live inside plugins, discovered at boot from `<cwd>/.skill-map/plugins/<id>/` only. The `--plugin-dir <path>` escape hatch on the `sm plugins …` verb family loads a custom directory per invocation when the user explicitly opts in.
366
366
  - **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five analyzers, one formatter, one hook). The fifth analyzer, `core/validate-all`, replays every scanned node and link through the authoritative spec schemas via AJV, the kernel-side guard against persisting non-conforming graph rows. The first built-in Hook is `core/update-check`, which subscribes to `shutdown` and runs the once-per-day "update available" probe + banner that lived on the CLI entry path before the Hook kind had concrete consumers. These are loaded from `src/extensions/` and are indistinguishable from plugin-supplied extensions from the kernel's point of view.
367
367
 
368
368
  ---
@@ -440,23 +440,21 @@ This is what makes "CLI-first" a coherent analyzer: every CLI verb is a kernel f
440
440
  | # | Layer | Source | Audience |
441
441
  |---|---|---|---|
442
442
  | 1 | `defaults` | Bundled `defaults.json` (ships in the CLI binary). | Every install. |
443
- | 2 | `user` | `~/.skill-map/settings.json` | Per-machine, per-user; lives in `$HOME` (never in the repo). |
444
- | 3 | `user-local` | `~/.skill-map/settings.local.json` | Same audience as `user`; intended for values the user might want to keep out of dotfile sync. |
445
- | 4 | `project` | `<cwd>/.skill-map/settings.json` | **Committed to the repo**, values are shared with every collaborator and CI. |
446
- | 5 | `project-local` | `<cwd>/.skill-map/settings.local.json` | **Gitignored**, values are per-checkout, never travel via the repo. |
447
- | 6 | `override` | Caller-supplied (env vars, CLI flags). | Process-scoped, ephemeral. |
443
+ | 2 | `project` | `<cwd>/.skill-map/settings.json` | **Committed to the repo**, values are shared with every collaborator and CI. |
444
+ | 3 | `project-local` | `<cwd>/.skill-map/settings.local.json` | **Gitignored**, values are per-checkout, never travel via the repo. |
445
+ | 4 | `override` | Caller-supplied (env vars, CLI flags). | Process-scoped, ephemeral. |
448
446
 
449
447
  The merge is per dot-path: a value declared at a higher layer replaces the value at lower layers; objects recurse, arrays replace. The loader records which layer last wrote each key in a `sources` map so `sm config show --source` can attribute every effective value.
450
448
 
451
- Layers 1, 2, 3, 5, 6 carry **per-user / per-machine state**. Only layer 4 (`project`) travels via the shared repo, so values landing in `project` are part of the contract every collaborator inherits.
449
+ Only layer 2 (`project`) travels via the shared repo, so values landing in `project` are part of the contract every collaborator inherits. Layers 1, 3, 4 carry **per-machine / per-checkout state** that never leaves the project.
452
450
 
453
- ### Per-key locality
451
+ Skill-map deliberately has **no user-scope config layer**: there is no merge of `$HOME` state on top of the project. The CLI honours the principle "never read `$HOME` by default" (see `cli-contract.md` §Scope is always project-local). The narrow exception, `~/.skill-map/settings.json`, holds genuinely per-machine preferences (the update-check toggle + its throttle bookkeeping today; future locale / theme) but is **NOT** part of the config layer system: it is read directly by the module that owns the feature, never merged into the project layers above. See `cli-contract.md` §User-settings file for the contract.
454
452
 
455
- Two locality classes constrain which layers a given key MAY live in. Both are enforced in code (reference impl: `core/config/helper.ts`), not in the JSON Schema, the schema stays additive so older settings files keep validating even when a key is reclassified.
453
+ ### Per-key locality
456
454
 
457
- - **`USER_ONLY_KEYS`**, keys describing per-user preferences that have no project meaning. Read forces `scope: 'global'` (project layers ignored); write rejects `target: 'project'` with a directed error. Today: `updateCheck.enabled`.
455
+ One locality class constrains which layers a given key MAY live in. It is enforced in code (reference impl: `core/config/helper.ts`), not in the JSON Schema, the schema stays additive so older settings files keep validating even when a key is reclassified.
458
456
 
459
- - **`PROJECT_LOCAL_ONLY_KEYS`**, keys describing per-user-per-project preferences. Valid in layers 1, 2, 3, 5, 6. **Stripped (with a warning) from layer 4 (`project`)** because the value is inherently per-user and must not be shared via the committed repo. Writes target `project-local` (`<cwd>/.skill-map/settings.local.json`); `sm config set` rejects `--scope project` for these keys.
457
+ - **`PROJECT_LOCAL_ONLY_KEYS`**, keys describing per-user-per-project preferences. Valid in layers 1, 3, 4. **Stripped (with a warning) from layer 2 (`project`)** because the value is inherently per-user and must not be shared via the committed repo. Writes target `project-local` (`<cwd>/.skill-map/settings.local.json`); `sm config set` rejects writes to `project` for these keys with a directed error.
460
458
 
461
459
  Members:
462
460
  - `allowEditSmFiles`, per-project consent to create / modify `.sm` sidecars.
@@ -465,7 +463,7 @@ Two locality classes constrain which layers a given key MAY live in. Both are en
465
463
 
466
464
  All three describe disk access the local operator opted into; sharing them via the repo would silently expand every collaborator's scan surface to paths that only make sense on the original author's machine.
467
465
 
468
- Adding a new entry to either set is a behaviour change for older installs that wrote the key into a committed file, the value gets stripped (PROJECT_LOCAL_ONLY) or ignored (USER_ONLY) at read time. The changeset that adds the entry MUST document the migration.
466
+ Adding a new entry is a behaviour change for older installs that wrote the key into a committed file, the value gets stripped at read time. The changeset that adds the entry MUST document the migration.
469
467
 
470
468
  ---
471
469
 
@@ -476,7 +474,7 @@ Skill-map's own metadata layer (versioning, supersession, provenance, taxonomy,
476
474
  Two schemas describe the wire shape:
477
475
 
478
476
  - [`schemas/sidecar.schema.json`](./schemas/sidecar.schema.json), root shape with reserved blocks `identity` (anchor + drift hashes), `annotations` (the conventional catalog), `settings` (reserved), `audit` (write trail), plus opt-in `<plugin-id>:` namespacing.
479
- - [`schemas/annotations.schema.json`](./schemas/annotations.schema.json), curated 13-field catalog: versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. `additionalProperties: true` so plugins or users add custom keys without coordination; the built-in `unknown-field` analyzer warns on truly unrecognized keys (typo guard).
477
+ - [`schemas/annotations.schema.json`](./schemas/annotations.schema.json), curated 10-field catalog: versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. `additionalProperties: true` so plugins or users add custom keys without coordination; the built-in `unknown-field` analyzer warns on truly unrecognized keys (typo guard).
480
478
 
481
479
  ### Identity and drift
482
480
 
package/cli-contract.md CHANGED
@@ -19,7 +19,6 @@ These flags apply to every verb unless marked otherwise.
19
19
 
20
20
  | Flag | Shape | Purpose |
21
21
  |---|---|---|
22
- | `-g` / `--global` | boolean | Operate on the global scope (`~/.skill-map/`) instead of project scope (`./.skill-map/`). |
23
22
  | `--json` | boolean | Emit machine-readable output on stdout. Suppresses pretty printing. Human progress goes to stderr. |
24
23
  | `-v` / `--verbose` | count | Increase log level (`-v` = info, `-vv` = debug, `-vvv` = trace). Logs to stderr. |
25
24
  | `-q` / `--quiet` | boolean | Suppress all non-error stderr output. Does not affect stdout. |
@@ -31,13 +30,52 @@ Env-var equivalents are normative:
31
30
 
32
31
  | Env var | Equivalent flag |
33
32
  |---|---|
34
- | `SKILL_MAP_SCOPE=global` | `-g` |
35
33
  | `SKILL_MAP_JSON=1` | `--json` |
36
34
  | `NO_COLOR=1` | `--no-color` (also honored per the NO_COLOR standard) |
37
35
  | `SKILL_MAP_DB=<path>` | `--db <path>` |
38
36
 
39
37
  CLI flag wins over env var. Env var wins over config file.
40
38
 
39
+ ### Scope is always project-local
40
+
41
+ Every `sm` verb operates on the **project scope** (`<cwd>/.skill-map/`).
42
+ There is no opt-in global scope, no `-g/--global` flag, no
43
+ `SKILL_MAP_SCOPE` env var. Skill-map MUST NOT read anything from
44
+ `$HOME` by default. The only mechanism the user has to extend the
45
+ scan beyond the project root is the `scan.extraFolders` setting (see
46
+ §Scan), which lives in `<cwd>/.skill-map/settings.local.json` and is
47
+ written under an explicit privacy gate (`sm config set --yes` or the
48
+ Settings UI confirm dialog). Plugins load from
49
+ `<cwd>/.skill-map/plugins/` by default; an arbitrary external
50
+ location MAY be loaded via the `--plugin-dir <path>` escape hatch on
51
+ the `sm plugins …` verb family, again user-explicit per invocation.
52
+
53
+ ### User-settings file (narrow, documented exception)
54
+
55
+ Genuinely per-user, per-machine preferences live in a **single file**
56
+ at `~/.skill-map/settings.json`, validated against
57
+ [`user-settings.schema.json`](./schemas/user-settings.schema.json).
58
+ The file holds preferences that have no project meaning (the update-
59
+ check toggle + its throttle bookkeeping today; future locale, theme,
60
+ etc.). Constraints:
61
+
62
+ - **One file, no `.local` partner**: values here are already
63
+ per-machine, so the project / project-local split has no meaning.
64
+ - **NOT part of the config layer system**: the project config loader
65
+ (`defaults` → `project` → `project-local` → `override`) MUST NOT
66
+ read or merge this file. Modules that own a user-scope feature
67
+ read it directly through a dedicated helper.
68
+ - **Narrow scope**: implementations SHOULD keep the key set small
69
+ (only preferences whose value is meaningless inside a project).
70
+ Anything that belongs to a project goes in
71
+ `<cwd>/.skill-map/settings.json` instead.
72
+ - **Closed list of writers**: today only the update-check store
73
+ (`src/cli/util/update-check-store.ts` in the reference impl)
74
+ reads and writes the file. New user-scope features extend that
75
+ module rather than opening new home access points.
76
+
77
+ Everything else under `$HOME` MUST NOT be touched.
78
+
41
79
  ---
42
80
 
43
81
  ## Targeted fan-out flags
@@ -93,9 +131,9 @@ Dry-run is **per-verb opt-in**. The flag is not global; verbs that do not declar
93
131
 
94
132
  #### `sm init`
95
133
 
96
- Bootstrap the current scope.
134
+ Bootstrap the project scope.
97
135
 
98
- - Creates `./.skill-map/` (project) or `~/.skill-map/` (global).
136
+ - Creates `./.skill-map/`.
99
137
  - Provisions the database.
100
138
  - Runs migrations.
101
139
  - Runs a first scan.
@@ -118,7 +156,6 @@ Common behaviour for both variants:
118
156
  - Writes the chosen file at the top level (single file, no subdirectory).
119
157
  - Content is the canonical `SKILL.md` shipped with the implementation. Any conforming implementation MUST embed equivalent tutorial sources (the prose itself is informative; what is normative is that the verb produces a single readable file at the chosen path that a Claude Code skill can consume).
120
158
  - Does NOT require an initialized project, runs in any directory, including empty ones, and never reads or writes `.skill-map/`.
121
- - Is NOT scope-aware: `-g` is accepted (inherited global flag) but has no effect; the file is always written under the cwd.
122
159
 
123
160
  Flags: `--force` (overwrite the existing target file, whichever variant was selected, without prompting).
124
161
 
@@ -185,11 +222,11 @@ Consumers: docs generator, shell completion, Web UI form generation, IDE extensi
185
222
  |---|---|
186
223
  | `sm config list` | Effective config after layered merge. |
187
224
  | `sm config get <key>` | Single value. |
188
- | `sm config set <key> <value> [--yes]` | Write to user config (scope-aware: `-g` writes to global). Privacy-sensitive keys require `--yes` to confirm, see §Privacy-sensitive config below. |
189
- | `sm config reset <key>` | Remove user override; revert to default or higher-scope value. |
190
- | `sm config show <key> --source` | Reveals origin: `default` / `project` / `global` / `env` / `flag`. |
225
+ | `sm config set <key> <value> [--yes]` | Write to project config. Privacy-sensitive keys require `--yes` to confirm, see §Privacy-sensitive config below. |
226
+ | `sm config reset <key>` | Remove user override; revert to default. |
227
+ | `sm config show <key> --source` | Reveals origin: `default` / `project` / `project-local` / `env` / `flag`. |
191
228
 
192
- Config precedence (lowest → highest): library defaults → user config → env vars → CLI flags.
229
+ Config precedence (lowest → highest): library defaults → project config → project-local config → env vars → CLI flags.
193
230
 
194
231
  Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys → exit 5.
195
232
 
@@ -207,9 +244,8 @@ The Settings UI's Project section enforces the same analyzer via a confirm dialo
207
244
 
208
245
  The three privacy-sensitive keys above PLUS `allowEditSmFiles` are members of `PROJECT_LOCAL_ONLY_KEYS` (see [`architecture.md` §Config layering · Per-key locality](./architecture.md#per-key-locality)). The values are per-user-per-project and MUST NOT travel via the committed repo:
209
246
 
210
- - `sm config set` writes them to `<cwd>/.skill-map/settings.local.json` (gitignored) by default. The user MAY also set them at user scope with `-g` (writes to `~/.skill-map/settings.json`).
211
- - Attempting to write any of them with `--scope project` (or equivalent forcing flag) is REJECTED with exit `2` and a directed message pointing to `settings.local.json` or `-g`.
212
- - The loader strips them (with a warning) when found in the committed `project` layer. An older install that wrote one of these keys to `settings.json` keeps validating against the schema, but the value is ignored at read time and `sm config show --source` surfaces the warning.
247
+ - `sm config set` writes them to `<cwd>/.skill-map/settings.local.json` (gitignored).
248
+ - The loader strips them (with a warning) when found in the committed `project` layer (`settings.json`). An older install that wrote one of these keys to `settings.json` keeps validating against the schema, but the value is ignored at read time and `sm config show --source` surfaces the warning.
213
249
 
214
250
  ---
215
251
 
@@ -231,7 +267,7 @@ The three privacy-sensitive keys above PLUS `allowEditSmFiles` are members of `P
231
267
  **Effective roots** (one-shot `sm scan`):
232
268
 
233
269
  - `sm scan [roots...]`: when positional roots are given, they ARE the effective roots (verbatim). When omitted: the effective roots are `[cwd]` plus the appended set below.
234
- - `scan.extraFolders[]` is appended verbatim (entries starting with `~` resolve against the user home; relative entries resolve against the project root). This is the ONLY way to extend the scan beyond the project: there is no implicit HOME walk and Providers cannot opt their own directory in.
270
+ - `scan.extraFolders[]` (project-local config) is appended verbatim (entries starting with `~` resolve against the user home; relative entries resolve against the project root). This is the ONLY way to extend the scan beyond the project: there is no implicit `$HOME` walk, no opt-in global scope, and Providers cannot opt their own directory in. See §Scope is always project-local at the top of this file for the broader principle.
235
271
 
236
272
  **Reference paths** (`scan.referencePaths[]`): walked in parallel by the scan to collect existing absolute paths into a side set. Files there are NOT parsed and NOT indexed as nodes; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken-ref` can resolve a link against the filesystem when the in-graph lookup misses.
237
273
 
@@ -486,7 +522,7 @@ Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interacti
486
522
 
487
523
  | Command | Purpose |
488
524
  |---|---|
489
- | `sm serve [--port N] [--host ...] [--scope project\|global] [--db <path>] [--no-built-ins] [--no-plugins] [--open\|--no-open] [--dev-cors] [--ui-dist <path>] [--no-ui] [--no-watcher]` | Start Hono + WebSocket for the Web UI. Single-port mandate: SPA + REST + WS under one listener. Default port 4242, default host 127.0.0.1 (loopback-only through v0.6.0; multi-host deferred, see §Server). The watcher is on by default (Decision #121: a server with stale DB is a footgun); pass `--no-watcher` for CI / read-only deployments. `--no-ui` skips the SPA bundle (dev workflow alongside the Angular dev server); see §Server flags. |
525
+ | `sm serve [--port N] [--host ...] [--db <path>] [--no-built-ins] [--no-plugins] [--open\|--no-open] [--dev-cors] [--ui-dist <path>] [--no-ui] [--no-watcher]` | Start Hono + WebSocket for the Web UI. Single-port mandate: SPA + REST + WS under one listener. Default port 4242, default host 127.0.0.1 (loopback-only through v0.6.0; multi-host deferred, see §Server). The watcher is on by default (Decision #121: a server with stale DB is a footgun); pass `--no-watcher` for CI / read-only deployments. `--no-ui` skips the SPA bundle (dev workflow alongside the Angular dev server); see §Server flags. |
490
526
 
491
527
  #### Server
492
528
 
@@ -503,15 +539,15 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
503
539
 
504
540
  **Boot output**: after the listener binds, `sm serve` writes a startup banner to **stderr**. Stdout is reserved for `--json` payloads on other verbs and stays empty here. The banner shape depends on `isTTY(stderr)` and the standard color toggles (`NO_COLOR`, `FORCE_COLOR`, `--no-color`):
505
541
 
506
- - **TTY + color**: an ASCII-art figlet logo split into a violet upper half and a green lower half, a dim version line right-aligned under the logo, then a dim-labelled data block (`Server <url>`, `Scope <project|global>`, `Path <cwd>`, `DB <path>`) and the `Press Ctrl+C to stop.` hint. The `Path` row shows the cwd the verb is running from; when it sits under the user's home, the prefix is replaced with `~` for legibility. The URL value is rendered in green with an underline. Implementations MAY choose any figlet-style rendering and any palette consistent with the violet-upper / green-lower split; the reference impl uses xterm 256-color codes (`\x1b[38;5;141m` violet, `\x1b[38;5;42m` green) and does NOT degrade to 16-color terminals, users on legacy terminals MUST set `NO_COLOR`.
542
+ - **TTY + color**: an ASCII-art figlet logo split into a violet upper half and a green lower half, a dim version line right-aligned under the logo, then a dim-labelled data block (`Server <url>`, `Path <cwd>`, `DB <path>`) and the `Press Ctrl+C to stop.` hint. The `Path` row shows the cwd the verb is running from; when it sits under the user's home, the prefix is replaced with `~` for legibility. The URL value is rendered in green with an underline. Implementations MAY choose any figlet-style rendering and any palette consistent with the violet-upper / green-lower split; the reference impl uses xterm 256-color codes (`\x1b[38;5;141m` violet, `\x1b[38;5;42m` green) and does NOT degrade to 16-color terminals, users on legacy terminals MUST set `NO_COLOR`.
507
543
  - **TTY + `NO_COLOR` (or `--no-color`)**: same figlet block + version + data block, with zero ANSI escapes.
508
- - **Non-TTY (pipes / redirects)**: banner suppressed; the verb emits two flat lines, `sm serve: listening on http://<host>:<port> (scope=<scope>, db=<path>)` followed by `sm serve: opening <url>/ in your browser. Press Ctrl+C to stop.` (or `sm serve: visit <url>/ ...` under `--no-open`). This shape is **stable**; tooling that scrapes those lines (CI capture, `tee log.txt`) MUST keep working across releases.
544
+ - **Non-TTY (pipes / redirects)**: banner suppressed; the verb emits two flat lines, `sm serve: listening on http://<host>:<port> (db=<path>)` followed by `sm serve: opening <url>/ in your browser. Press Ctrl+C to stop.` (or `sm serve: visit <url>/ ...` under `--no-open`). This shape is **stable**; tooling that scrapes those lines (CI capture, `tee log.txt`) MUST keep working across releases.
509
545
 
510
546
  **Endpoints (v14.2 surface)**:
511
547
 
512
548
  | Path | Status | Shape |
513
549
  |---|---|---|
514
- | `GET /api/health` | implemented | `{ ok: true, schemaVersion, specVersion, implVersion, scope: 'project'\|'global', db: 'present'\|'missing', cwd: string, dbPath: string }`. `cwd` is the absolute project root the BFF resolves against (`runtimeContext.cwd`); `dbPath` is the absolute project DB path (`IServerOptions.dbPath`). Both are surfaced so the SPA's About panel can show "you are looking at <project>" + the DB location without a second endpoint. |
550
+ | `GET /api/health` | implemented | `{ ok: true, schemaVersion, specVersion, implVersion, db: 'present'\|'missing', cwd: string, dbPath: string }`. `cwd` is the absolute project root the BFF resolves against (`runtimeContext.cwd`); `dbPath` is the absolute project DB path (`IServerOptions.dbPath`). Both are surfaced so the SPA's About panel can show "you are looking at <project>" + the DB location without a second endpoint. |
515
551
  | `GET /api/scan` | implemented | latest persisted `ScanResult` (1:1 with `scan-result.schema.json`; byte-equal to `sm scan --json` modulo whitespace). DB absent → empty `ScanResult` shape (zero `nodes` / `links` / `issues`). |
516
552
  | `GET /api/scan?fresh=1` | implemented | runs an in-memory scan and returns the produced `ScanResult` without persistence. Rejects with `bad-query` (400) when the server was started with `--no-built-ins` or `--no-plugins` (would yield empty / partial results). |
517
553
  | `POST /api/scan` | implemented | Run a fresh scan **and persist it** through the same `runScanWithRenames` + `persistScanResult` pipeline the watcher uses. Body is empty (`{}` or no body). Response: the persisted `ScanResult` inline (same shape as `GET /api/scan`). Side effects: broadcasts `scan.started` then `scan.completed` over `/ws` so other connected clients can refresh, the per-batch sequence is identical to a watcher-driven batch. **Concurrency**: only one scan may run at a time across the whole BFF process. A POST that arrives while a watcher batch is in flight (or while another POST is in flight) is rejected with `409 scan-busy` so the caller can decide whether to retry. **Pipeline gate**: rejected with `400 bad-query` when the server was started with `--no-built-ins` or `--no-plugins` (a partial pipeline would persist a misleading DB the next watcher boot would have to reconcile). **DB gate**: rejected with `500 db-missing` when the project DB file is absent, the read-side `/api/scan` degrades to the empty shape, but a write path cannot, so it fails fast. |
@@ -521,7 +557,7 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
521
557
  | `GET /api/issues?severity=&analyzerId=&node=` | implemented | `RestEnvelope` (`kind: 'issues'`), list of issues. Filters: `severity` (CSV from `error\|warn\|info`), `analyzerId` (CSV; qualified or short suffix per `sm check --analyzers`), `node` (filter to issues whose `nodeIds` includes the path). No pagination at v14.2. |
522
558
  | `GET /api/graph?format=ascii\|json\|md` | implemented | formatter-rendered graph. `Content-Type` per format: `text/plain` (ascii), `application/json` (json), `text/markdown` (md / mermaid). Default `format=ascii`. Unknown format → 400 `bad-query`. |
523
559
  | `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`), merged effective config (defaults → user → user-local → project → project-local → override). |
524
- | `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`), list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project'\|'global', granularity: 'bundle'\|'extension', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, locked?: boolean }> }`. The `granularity` field reflects the manifest declaration (built-ins: hardcoded per `built-in-plugins/built-ins.ts`; drop-ins: from `plugin.json#/granularity`, default `'bundle'`). The `description` field on the bundle item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInBundle`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its extension manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`). The SPA's Settings list renders the descriptions as muted secondary text and includes them in its substring-search index alongside the ids. The `extensions` array is present **only** when `granularity === 'extension'` AND the plugin loaded successfully; each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). For `granularity: 'bundle'` plugins the array is omitted (the bundle is the only toggle-able key). The optional `locked: true` flag is stamped when the bundle id (or qualified extension id) appears in the host's lock-list (`src/server/locked-plugins.ts`); locked items render the toggle disabled in the SPA and any `PATCH` against them returns `403 locked`. The flag is omitted when false. The optional `startsAsDisabled: true` flag is stamped on drop-in plugins (never built-ins) whose discovery-time `status` was `'disabled'`, that is, the user had them disabled in `config_plugins` / `settings.json` at `sm serve` boot, so their handlers were never bucketed into the runtime. The SPA renders a per-row hint when this flag is set AND the user toggles the row back on, since re-enabling them requires `sm serve` restart (the rest of the toggle pipeline applies live). The flag is omitted when false. |
560
+ | `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`), list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project', granularity: 'bundle'\|'extension', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, locked?: boolean }> }`. The `granularity` field reflects the manifest declaration (built-ins: hardcoded per `built-in-plugins/built-ins.ts`; drop-ins: from `plugin.json#/granularity`, default `'bundle'`). The `description` field on the bundle item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInBundle`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its extension manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`). The SPA's Settings list renders the descriptions as muted secondary text and includes them in its substring-search index alongside the ids. The `extensions` array is present **only** when `granularity === 'extension'` AND the plugin loaded successfully; each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). For `granularity: 'bundle'` plugins the array is omitted (the bundle is the only toggle-able key). The optional `locked: true` flag is stamped when the bundle id (or qualified extension id) appears in the host's lock-list (`src/server/locked-plugins.ts`); locked items render the toggle disabled in the SPA and any `PATCH` against them returns `403 locked`. The flag is omitted when false. The optional `startsAsDisabled: true` flag is stamped on drop-in plugins (never built-ins) whose discovery-time `status` was `'disabled'`, that is, the user had them disabled in `config_plugins` / `settings.json` at `sm serve` boot, so their handlers were never bucketed into the runtime. The SPA renders a per-row hint when this flag is set AND the user toggles the row back on, since re-enabling them requires `sm serve` restart (the rest of the toggle pipeline applies live). The flag is omitted when false. |
525
561
  | `PATCH /api/plugins/:id` | implemented | Toggle one plugin's user override. `:id` MUST be a top-level bundle id; qualified-id form (`bundle/extension`) is the sibling route below. Body `{ enabled: boolean }` (JSON). Persists to `config_plugins` via `IConfigPluginsPort.set`, same path the CLI's `sm plugins enable\|disable` uses. Response is the canonical `{ id, version, kinds, status, reason, source }` row for the affected plugin (post-write `status` reflects the new override resolution). **Granularity**, rejected with 400 `bad-query` when the target bundle declares `granularity: 'extension'` (only the qualified-id form is toggle-able for those). **Lock**, rejected with 403 `locked` when the bundle id is in the host lock-list (`src/server/locked-plugins.ts`). **Apply window**, the override applies on the next scan (manual via `POST /api/scan` or `sm scan`, automatic via watcher batch); both the BFF and the watcher build a fresh resolver from `config_plugins` before composing extensions, so the toggle is honoured without restarting `sm serve`. The endpoint purges `scan_contributions` rows for the plugin immediately on disable so the UI stops rendering its chips before the next scan. **Exception**, drop-in plugins whose discovery-time `status` was `'disabled'` (carried as `startsAsDisabled: true` on the read shape) are NOT in the runtime extension buckets; re-enabling them via PATCH persists the override but requires `sm serve` restart for the handlers to be loaded. The SPA surfaces this per-row when the user re-enables such a plugin. The endpoint does NOT broadcast a WS event today. |
526
562
  | `PATCH /api/plugins/:bundleId/extensions/:extensionId` | implemented | Qualified-id form for `granularity: 'extension'` bundles (today: `core` + any user plugin that opts in). Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:bundleId` or `:extensionId`). Rejected with 400 `bad-query` when the target bundle declares `granularity: 'bundle'` (use the sibling route above). **Lock**, rejected with 403 `locked` when either the bundle id or the qualified `bundleId/extensionId` appears in the host lock-list. Same persistence + apply-window semantics as the bundle form (including the `startsAsDisabled` exception). |
527
563
  | `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare bundle id or a qualified `<bundle>/<extension>` id (the dispatcher branches on the slash exactly like the single-id routes above). Empty `changes` array is accepted as a no-op (returns the current `GET /api/plugins` envelope). The route validates the **entire batch** before writing, any invalid entry (`unknown-plugin` / `granularity-mismatch` / `locked`) rejects the whole request with the offending id in `error.details.id`, and the DB is not touched. Valid batches are applied in **one SQLite transaction**: `IConfigPluginsPort.set` per entry, then one grouped `scan_contributions` purge per disabled plugin (enables skip the purge). Response is the same `RestEnvelope` (`kind: 'plugins'`) shape as `GET /api/plugins`, reflecting the post-write state, the SPA replaces its modal state from this envelope. Apply-window and `startsAsDisabled` exception semantics match the per-id routes. The single-id `PATCH /api/plugins/:id` and qualified-id sibling stay available for CLI / external automation; the bulk variant exists so the SPA can stage edits in a buffered modal and ship the final delta atomically. |
@@ -569,8 +605,7 @@ Error code sources at v14.2:
569
605
  |---|---|---|
570
606
  | `--port N` | `4242` | Listening port. `0` = OS-assigned (handle reports the bound port). |
571
607
  | `--host <ip>` | `127.0.0.1` | Listening host. Implementations MUST NOT bind `0.0.0.0` by default. |
572
- | `--scope project\|global` | `project` | Effective scope for `/api/*` reads. Alias for `-g/--global`. |
573
- | `--db <path>` | resolved per spec § Global flags | Override the DB file location. Missing explicit `--db` exits 5. |
608
+ | `--db <path>` | `<cwd>/.skill-map/skill-map.db` | Override the DB file location. Missing explicit `--db` exits 5. |
574
609
  | `--no-built-ins` | off | Skip built-in plugin registration (parity with `sm scan --no-built-ins`). |
575
610
  | `--no-plugins` | off | Skip drop-in plugin discovery. |
576
611
  | `--open` / `--no-open` | `--open` | Auto-open the SPA in the user's default browser after listen. |
@@ -119,6 +119,10 @@ Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Im
119
119
  | Id | Verifies |
120
120
  |---|---|
121
121
  | `kernel-empty-boot` | With every Provider/Extractor/Analyzer disabled, scanning an empty scope returns a valid empty graph. |
122
+ | `no-global-scope` | The `-g/--global` flag does not exist. Implementations MUST reject it on every verb (exit `2`, "unknown option"). Guards `cli-contract.md` §Scope is always project-local. |
123
+ | `orphan-markdown-fallback` | Multi-Provider corpus where one node lands via the universal `core/markdown` fallback and another via vendor-specific claude classification. Locks the orchestrator's path-dedup contract. |
124
+ | `plugin-missing-ui-rejected` | Drop-in Provider whose `kinds[*]` entry omits the required `ui` block fails AJV validation with `invalid-manifest` while the rest of the pipeline keeps running. |
125
+ | `sidecar-end-to-end` | Co-located `.sm` sidecar shape, stale / orphan detection, populated `Node.sidecar` overlay, both `annotation-stale` and `annotation-orphan` issues emitted. |
122
126
 
123
127
  Cases explicitly referenced elsewhere in the spec (landing before v1.0):
124
128
 
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
3
+ "id": "no-global-scope",
4
+ "description": "Skill-map operates exclusively on the project scope. Implementations MUST NOT expose a `-g/--global` flag (the historical opt-in for a global / user scope) on any verb. Passing the flag to any verb MUST be rejected as an unknown option (exit `2`, usage error) without writing any state. Guards the principle declared in `cli-contract.md` §Scope is always project-local.",
5
+ "invoke": {
6
+ "verb": "scan",
7
+ "flags": ["-g", "--json"]
8
+ },
9
+ "assertions": [
10
+ { "type": "exit-code", "value": 2 },
11
+ { "type": "stderr-matches", "pattern": "[Uu]nknown option" }
12
+ ]
13
+ }
@@ -42,6 +42,7 @@ This file is hand-maintained. A CI check before spec release compares the schema
42
42
  | 32 | `refresh-report.schema.json` |, | 🔴 missing | Machine-readable output of `sm refresh <node.path> --json` and `sm refresh --stale --json`. Reports the count of enrichment rows persisted across targeted nodes (universal enrichment layer per `architecture.md` §A.8). Direct conformance case pending: seed a fixture with one Provider-classified node, run `sm refresh <node> --json`, assert the envelope validates and `refreshed >= 0`. Implementation tests at `src/test/node-enrichments.test.ts` cover the runtime behaviour today. |
43
43
  | 33 | `plugins-doctor.schema.json` |, | 🔴 missing | Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts plus structured issue / warning lists. Direct conformance case pending: prime a scope with one healthy + one invalid-manifest drop-in plugin, run `sm plugins doctor --json`, assert the envelope validates and the invalid plugin appears under `issues[]`. Implementation tests at `src/test/plugins-cli.test.ts` cover the runtime behaviour. |
44
44
  | 34 | `conformance-result.schema.json` |, | 🔴 missing | Machine-readable output of `sm conformance run --json`. Self-referential by design (a conformance case would invoke the verb against itself); a direct case is deferred until the runner gains a meta-loopback mode. Implementation tests at `src/test/conformance-cli.test.ts` cover the envelope shape today. |
45
+ | 35 | `user-settings.schema.json` | (indirect via `no-global-scope`) | 🟡 partial | Per-user / per-machine settings file at `~/.skill-map/settings.json` (the narrow `$HOME` exception, see `cli-contract.md` §User-settings file). Direct case is not added because alt-impls MAY choose to not ship an update-check feature, requiring them to produce this file would over-prescribe. The implementation-side AJV round-trip is covered by `src/test/user-settings-store.test.ts` (15 cases: defaults, malformed JSON, schemaVersion mismatch, wrong-type fields, unknown top-level keys, deep-merge writes, off-shape rejection). The behavioral counterpart (no global / user scope) lives at `no-global-scope` in the non-schema table below. |
45
46
 
46
47
  > **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`skill`, `agent`, `command`, `note` for the built-in Claude Provider; other Providers MAY declare different kinds) live with the Provider that emits them, for the built-in Claude Provider, under `src/extensions/providers/claude/schemas/`. Those schemas are NOT counted in the spec's coverage matrix above; they belong to the Provider's own conformance suite at `src/extensions/providers/claude/conformance/coverage.md`. The same split applies to the cases that exercise Provider-specific kinds (`basic-scan`, `rename-high`, `orphan-detection`), they live in the Provider's `cases/` directory.
47
48
 
@@ -65,6 +66,7 @@ These have their own conformance cases even though they are not JSON Schemas.
65
66
  | J | Plugin DDL rejection |, | 🔴 missing | Blocked by Step 9. Plugin migration referencing `state_jobs` → disabled with `invalid-manifest`. |
66
67
  | K | Plugin prefix injection |, | 🔴 missing | Blocked by Step 9. Plugin declares `CREATE TABLE foo` → kernel applies as `plugin_<id>_foo`. |
67
68
  | L | Elapsed-time reporting |, | 🔴 missing | Blocked by Step 4 (first real verb work). Run any in-scope verb; stderr last line MUST match `/^done in (\d+ms\|\d+\.\d+s\|\d+m \d+s)$/`. In-scope verb with `--json` returning an object MUST carry `elapsedMs`. Exempt verb (`sm version`) MUST NOT emit the line. |
69
+ | M | No global / user scope (no `-g/--global` flag) | `no-global-scope` | 🟢 covered | Skill-map operates exclusively on the project scope (see `cli-contract.md` §Scope is always project-local). Implementations MUST reject `-g/--global` on every verb as an unknown option (exit `2`). The case invokes `sm scan -g --json` and asserts exit code `2` plus a stderr line matching `/(?i)unknown option/`. The matching implementation-side coverage lives at `src/test/global-flag-removed.test.ts` (regression guard) and `src/test/no-implicit-home-reads.test.ts` (no implicit `$HOME` reads from any verb on the default path). |
68
70
 
69
71
  ## Release gates
70
72
 
@@ -32,10 +32,6 @@ annotations:
32
32
  - quality
33
33
  hidden: false
34
34
  docsUrl: https://skill-map.dev/examples/code-reviewer
35
- requires:
36
- - .skill-map/agents/diff-reader.md
37
- related:
38
- - .skill-map/agents/security-reviewer.md
39
35
 
40
36
  settings: {}
41
37
 
package/db-schema.md CHANGED
@@ -12,16 +12,15 @@ The spec assumes a relational, SQL-like store but is **engine-agnostic**. The re
12
12
 
13
13
  ## Scope and location
14
14
 
15
- Two scopes. Each has its own database file and its own migration ledger.
15
+ One scope. Skill-map operates on the project scope only (`<cwd>/.skill-map/`). There is no global / user-level DB, the CLI never reads `$HOME` by default (see `cli-contract.md` §Scope is always project-local). To extend the scan beyond the current repository the user adds explicit paths via `scan.extraFolders` in the project-local config; the scan walks those paths against the same project DB.
16
16
 
17
17
  | Scope | Default DB location | Scan roots |
18
18
  |---|---|---|
19
- | `project` (default) | `./.skill-map/skill-map.db` | The current repository. |
20
- | `global` (`-g`) | `~/.skill-map/skill-map.db` | User-level skill directories (e.g. `~/.claude/`). |
19
+ | `project` | `<cwd>/.skill-map/skill-map.db` | The current repository, plus any paths the user added to `scan.extraFolders`. |
21
20
 
22
- The project DB is gitignored by default. Teams MAY opt in to sharing it by setting `history.share: true` in `.skill-map/settings.json`, the file is then committed and the execution log becomes a team artifact. Both zones use the same schema.
21
+ The project DB is gitignored by default. Teams MAY opt in to sharing it by setting `history.share: true` in `.skill-map/settings.json`, the file is then committed and the execution log becomes a team artifact.
23
22
 
24
- The `--db <path>` CLI flag overrides location for both scopes as an escape hatch.
23
+ The `--db <path>` CLI flag overrides the DB location as an escape hatch (debugging, custom layouts).
25
24
 
26
25
  ---
27
26
 
@@ -135,14 +134,13 @@ Indexes: `ix_scan_issues_analyzer_id`, `ix_scan_issues_severity`.
135
134
 
136
135
  ### `scan_meta`
137
136
 
138
- Single-row table holding the metadata of the last persisted scan. Lets `loadScanResult` return the real `scope` / `roots` / `scannedAt` / `scannedBy` / `providers` / `stats.filesWalked|filesSkipped|durationMs` instead of synthesising them. Replaced atomically with the rest of the `scan_*` zone on every `sm scan`.
137
+ Single-row table holding the metadata of the last persisted scan. Lets `loadScanResult` return the real `roots` / `scannedAt` / `scannedBy` / `providers` / `stats.filesWalked|filesSkipped|durationMs` instead of synthesising them. Replaced atomically with the rest of the `scan_*` zone on every `sm scan`.
139
138
 
140
139
  `nodesCount` / `linksCount` / `issuesCount` are not stored here, they derive from `COUNT(*)` of the sibling tables.
141
140
 
142
141
  | Column | Type | Constraint |
143
142
  |---|---|---|
144
143
  | `id` | INTEGER | PRIMARY KEY, CHECK `id = 1` |
145
- | `scope` | TEXT | NOT NULL, CHECK in (`project`, `global`) |
146
144
  | `roots_json` | TEXT | NOT NULL | JSON array of strings (filesystem roots walked). |
147
145
  | `scanned_at` | INTEGER | NOT NULL | Unix milliseconds. |
148
146
  | `scanned_by_name` | TEXT | NOT NULL |
@@ -153,6 +151,8 @@ Single-row table holding the metadata of the last persisted scan. Lets `loadScan
153
151
  | `stats_files_skipped` | INTEGER | NOT NULL |
154
152
  | `stats_duration_ms` | INTEGER | NOT NULL |
155
153
 
154
+ The `scope` column was removed pre-1.0 along with the `-g/--global` flag (see `cli-contract.md` §Scope is always project-local); every persisted scan is project-scoped so the column never carried any information worth round-tripping. Older DBs are not migrated, the column drop is a greenfield change and a fresh `sm init` regenerates the schema.
155
+
156
156
  No indexes (single row).
157
157
 
158
158
  ### `scan_extractor_runs`
package/index.json CHANGED
@@ -174,20 +174,21 @@
174
174
  }
175
175
  ]
176
176
  },
177
- "specPackageVersion": "0.25.0",
177
+ "specPackageVersion": "0.27.0",
178
178
  "integrity": {
179
179
  "algorithm": "sha256",
180
180
  "files": {
181
- "CHANGELOG.md": "fe77519b1e25579650c086077946e39bf492ab904e23eaba72d46220da52dff1",
182
- "README.md": "76c5d5afa1c08dbfe9206e141c810ea063f5bcb2f2069d80ace311905ca3c2c3",
183
- "architecture.md": "ebb5370040cc72300803c4f153512127e21279c80834f701f722e72411e5b8ed",
184
- "cli-contract.md": "461e8eaa29600edaf2637ef1fa5fda1412efdd3c7b253e5c7aacb7ce8d414f0c",
185
- "conformance/README.md": "7a0f6d6a4057349d35004c68c92bacd6528bb7489ad942406a035be1b84bf360",
181
+ "CHANGELOG.md": "609e04963f4a7302161bc01718643fa60c358b62cf564cd683e68892be6a65de",
182
+ "README.md": "54c4649fa9742bf2f74423ea78788a7474ce09649cbe1e72a270b606cf16a0a5",
183
+ "architecture.md": "7c735b2d305798d610760d23b03a450361131927109d7271ef78257fbf36b1f4",
184
+ "cli-contract.md": "c22f7c82d460714efaf34a04a2d2367d21eb04985100aef1291071e6726cbc64",
185
+ "conformance/README.md": "6871dde25b5770ed945284c9e0f749e0768ec3f5ba4966bdb215985789e43887",
186
186
  "conformance/cases/kernel-empty-boot.json": "2a5be9c93143d07a16d998df09dcc8fa4ea2d2f9a0bff6417573ed5a770352c1",
187
+ "conformance/cases/no-global-scope.json": "1284763988026d924c0bd78ba8a9f417dc88f5b7e9f4c2b642ae0c447758bfd4",
187
188
  "conformance/cases/orphan-markdown-fallback.json": "8ef6e49b7e6532bd845d9f54974a16e537cf98d355f0c5e4f4fb06abac3adcc5",
188
189
  "conformance/cases/plugin-missing-ui-rejected.json": "bdebee810436e6be88edf2fe38ddc6939fd3f53e6a12dc1d66da051c4922f1e9",
189
190
  "conformance/cases/sidecar-end-to-end.json": "24a73e7c857709d001cf7013b8fe5ccad4027e064b39533dda33697d80b56e7a",
190
- "conformance/coverage.md": "bdf0043707d447417d2a0d03b97fdc30e5335759b4b82a1e894c2ffb00fb7940",
191
+ "conformance/coverage.md": "0d202baf257f6690c107e1e20aff0c7db0399a53fcd72536e2d87ab7641510ab",
191
192
  "conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
192
193
  "conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "d6b6e18d4b963b26a292de73348c3396fd4710ab4c4bdd6cf094e581f99ec8d6",
193
194
  "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "4d78af6f12faa9d131e2a19f1dbb8f250baacc525978f3a8c858932b95da4ff6",
@@ -198,15 +199,15 @@
198
199
  "conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.md": "cb3a95777cba530d47e6040c5601b6dcd34b5fc653dd69f183369eb6bdd956b5",
199
200
  "conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm": "cb04f7f3103b4218b09fd4da92f7ea429588b04c1dac6a9547ce362263b11224",
200
201
  "conformance/fixtures/sidecar-example/agent-example.md": "741131403e8c9580d0b7a8c2446cb4502d01f80053b7a2092663de92431aaa82",
201
- "conformance/fixtures/sidecar-example/agent-example.sm": "41200387e74a120c554a34dfabc50dd2151067a1c6599695c59412d8eab38bb4",
202
- "db-schema.md": "ff792e4b8d4fcd122e43cf7718b72b7780aaefadc6f839441bdc06fa89197d7c",
202
+ "conformance/fixtures/sidecar-example/agent-example.sm": "8329950d49c69a1199bbe6c06e32b8513973e64207b0db8756b67301e6a1f1e2",
203
+ "db-schema.md": "51fb96bb372ca20de0478b3d6596e39a87f882a561264c4857ec55a23c422548",
203
204
  "interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
204
205
  "job-events.md": "84206168ac12b536d34470d62f8c8cba95dab181fee66d23203c2cf5dfbee716",
205
206
  "job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
206
- "plugin-author-guide.md": "4612258f05b57c60a0f660bc86d20677bcc8403be3659d93101d966c4bc17e4d",
207
+ "plugin-author-guide.md": "03c2f666d7856eae76594484a1580968e1292f247fb10c8abcff5d99c9d38f01",
207
208
  "plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
208
209
  "prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
209
- "schemas/annotations.schema.json": "a446a03ffc5df443c10464caee547014533f56c2106bf897f204ca7247876431",
210
+ "schemas/annotations.schema.json": "e39990d47f53e25a1b3a5587a5714486d0b819b8eeaac10d42783a675296aee1",
210
211
  "schemas/api/rest-envelope.schema.json": "bf735dbea44725545a33c001c08bf9b0395995f71c70d46a4cc215d276e19038",
211
212
  "schemas/bump-report.schema.json": "c2d853715d5f50098567bc23382a4e81baf78d589c6e1baf67d3b841e7f7d8ae",
212
213
  "schemas/conformance-case.schema.json": "f6d4c9fb92e79cb516eeeb9d042223572a3bd5ff8e7871a0becce13916f20cf6",
@@ -227,18 +228,19 @@
227
228
  "schemas/link.schema.json": "7fc429d03aca7e4c0b9a28241712c1aa2a5275870cea5ed938c2f97e8cccb081",
228
229
  "schemas/node.schema.json": "e5da06c9262cc0f2f7584d5733ebc1c08acd75487952ed7b4d6035fb417aaa4b",
229
230
  "schemas/plugins-doctor.schema.json": "c1d92f30fdb0080e8cd8f7dc5d43e01aae02a16640bc5eb04811c337a275de58",
230
- "schemas/plugins-registry.schema.json": "c79b134e25575b0046fd583b5b8fd8fc3413ca91502002cf556ac0fe4217d4a0",
231
- "schemas/project-config.schema.json": "c866a64282199fd9ead5dc2b889e6cca27ffb289e656fafa0a22d1b715c86e95",
231
+ "schemas/plugins-registry.schema.json": "3d8a29aa045d46d70be127aabbc59831da0c71a54f18eae752676e452577ae3d",
232
+ "schemas/project-config.schema.json": "7bb695476015b6b43026db78208aedf67350f4bc2c796c822fa87d0c9093b13f",
232
233
  "schemas/refresh-report.schema.json": "54519b8caf86ba84c182f9565be9b5084bc1631ae05e9217ee18f34c0039fff3",
233
234
  "schemas/report-base-deterministic.schema.json": "9d318d0181d121097c906ef3af1c52d71c782740bd04cf23418d7627ce2c3ed5",
234
235
  "schemas/report-base.schema.json": "a1021e9a59b4df9f99cd92454d797e88469766e7d49f52d231c4645ffdfdad8f",
235
- "schemas/scan-result.schema.json": "d1a8782e198bc9bb92dad247437aefa1b02f92ff8dca8562eaf2348fd7c5cf0c",
236
+ "schemas/scan-result.schema.json": "214bc12fbb9946642cbba3b23513dade60e7d6a5b6a9ed3dd0818f135b450185",
236
237
  "schemas/sidecar.schema.json": "8856c387477340efbdd0a585d74bfb07a99ba15b9ce593cc67d9efebc67c6bfc",
237
238
  "schemas/summaries/agent.schema.json": "bf540f9a804f2b43756ab33b7deb0462620d26e88cc9379c75a5f87d3b1b47d8",
238
239
  "schemas/summaries/command.schema.json": "c26f6965f77c5058608feb5e7b9f807395de8e015b0dea5efcdb44cb1820551a",
239
240
  "schemas/summaries/hook.schema.json": "58420ec485e152fdd21fa3d87337ad74b0d81a48d3b83dd072d4a2d196f78573",
240
241
  "schemas/summaries/markdown.schema.json": "f7b2b5ae9e4836c94bb6a84edc730412f19296a6ef13552016690d7ba6f5391d",
241
242
  "schemas/summaries/skill.schema.json": "a2e23e35fe1545fb7bb8a6bb87dcad2248afa5a0f23aebc089eb5464201f4654",
243
+ "schemas/user-settings.schema.json": "969638f7235b2de2bbe38f291e3f576fc54f2f2f22e30b37d1578fd9e8538b51",
242
244
  "schemas/view-slots.schema.json": "b151740016585ab2ba6cde376b071fc79ef1a1b484032216d841afeb593d06fd",
243
245
  "versioning.md": "bab36cda6deb3edc29d7d40d97399ea4a213551257b0dd3321ddf95e2a60bff2"
244
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-map/spec",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "JSON Schemas, prose contracts, and conformance suite for the skill-map specification.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,12 +56,9 @@ Drop the directory under one of the discovery roots and `sm plugins list` will p
56
56
 
57
57
  ## Discovery
58
58
 
59
- The kernel scans two roots, in this order:
59
+ The kernel scans one root: `<cwd>/.skill-map/plugins/`, committed-with-the-repo plugins. There is no implicit user-level discovery (see `cli-contract.md` §Scope is always project-local for the broader principle): plugins live with the project that uses them.
60
60
 
61
- 1. `<project>/.skill-map/plugins/`, committed-with-the-repo plugins.
62
- 2. `~/.skill-map/plugins/`, user-level plugins available across every project.
63
-
64
- A plugin is any direct child directory containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to override both roots (mostly for testing).
61
+ A plugin is any direct child directory of that root containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to replace the default root with a custom directory (mostly for testing, or for loading a user-level plugin set the operator explicitly opts into).
65
62
 
66
63
  After every change to the `plugins/` folder, run `sm plugins list` to see the load status of each. The six statuses are documented under [Diagnostics](#diagnostics) below.
67
64
 
@@ -815,7 +812,7 @@ This is the only fatal path on the plugin-load surface. Every other failure mode
815
812
 
816
813
  The built-in `core/unknown-field` Analyzer walks every parsed `.sm` and emits a `warn` issue per truly-unknown key. Three surfaces are checked:
817
814
 
818
- 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.
815
+ 1. Inside `annotations:`, keys not in `annotations.schema.json`'s curated catalog (the 10 conventional fields). Plugins do NOT contribute to `annotations:`; that block is skill-map-curated.
819
816
  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.
820
817
  3. Inside a registered `<plugin-id>:` namespace, values that fail the schema declared by the owning plugin's `annotationContributions[<key>].schema`.
821
818
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/annotations.schema.json",
4
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` analyzer emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 13 fields below, versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), 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).",
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` analyzer emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 10 fields below, versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), 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
6
  "type": "object",
7
7
  "additionalProperties": true,
8
8
  "properties": {
@@ -26,21 +26,6 @@
26
26
  "minLength": 1,
27
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
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` analyzer 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 analyzer enforcement."
43
- },
44
29
  "authors": {
45
30
  "type": "array",
46
31
  "items": { "type": "string", "minLength": 1 },
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/plugins-registry.schema.json",
4
4
  "title": "PluginsRegistry",
5
- "description": "Two shapes in one file: (1) the per-plugin manifest that authors ship as `plugin.json` (see `$defs/PluginManifest`); (2) the aggregate registry the implementation produces on disk (e.g. `~/.skill-map/plugins.json`), which lists all discovered plugins with their compat status. Both shapes are normative. camelCase keys throughout.",
5
+ "description": "Two shapes in one file: (1) the per-plugin manifest that authors ship as `plugin.json` (see `$defs/PluginManifest`); (2) the aggregate registry the implementation produces on disk (`<cwd>/.skill-map/plugins.json`), which lists all discovered plugins with their compat status. Both shapes are normative. camelCase keys throughout.",
6
6
  "type": "object",
7
7
  "oneOf": [
8
8
  { "$ref": "#/$defs/PluginsRegistry" },
@@ -114,7 +114,7 @@
114
114
  "status": {
115
115
  "type": "string",
116
116
  "enum": ["enabled", "disabled", "incompatible-spec", "incompatible-catalog", "invalid-manifest", "load-error", "id-collision"],
117
- "description": "Resolved state after discovery. `disabled` = user-disabled via config; `id-collision` = two plugins (any combination of project / global / --plugin-dir) declared the same `id`, both blocked, no precedence; `incompatible-catalog` = manifest's `catalogCompat` does not satisfy the kernel's catalog version (resolved via `sm plugins upgrade`); others = automatic."
117
+ "description": "Resolved state after discovery. `disabled` = user-disabled via config; `id-collision` = two plugins (any combination of project + --plugin-dir overrides) declared the same `id`, both blocked, no precedence; `incompatible-catalog` = manifest's `catalogCompat` does not satisfy the kernel's catalog version (resolved via `sm plugins upgrade`); others = automatic."
118
118
  },
119
119
  "statusReason": {
120
120
  "type": ["string", "null"],
@@ -143,17 +143,6 @@
143
143
  "allowEditSmFiles": {
144
144
  "type": "boolean",
145
145
  "description": "**Project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`). Grants this project permission to create / modify `.sm` annotation sidecars next to source files. Default `false`. The first time a verb or BFF route attempts a `.sm` write while this is `false`, the kernel raises `EConsentRequiredError`. The CLI surfaces it as an interactive `confirm()` prompt (or `--yes` bypass); the BFF returns 412 `confirm-required` so the UI can open a `ConfirmationService` dialog. On accept the flag is persisted to `<cwd>/.skill-map/settings.local.json` (gitignored, per-checkout) and never asked again. On decline the operation aborts WITHOUT persisting the rejection, the next attempt re-asks. **Stripped with a warning when found in the committed `project` layer** (`<cwd>/.skill-map/settings.json`), each developer consents independently."
146
- },
147
- "updateCheck": {
148
- "type": "object",
149
- "additionalProperties": false,
150
- "description": "Controls the once-per-day notification when a newer @skill-map/cli release is published on npm. Disabled in CI, when SM_NO_UPDATE_CHECK=1, when stderr is not a TTY, or when the project DB is missing. **User-scope only**: this key SHOULD live in `~/.skill-map/settings.json` and the reference implementation forces user-scope reads via `core/config/helper:USER_ONLY_KEYS`, a project-layer entry from an older install continues to validate but is silently ignored at read time. `sm config set` rejects writes to the project layer for this key (rerun with `-g`); the Settings UI's General section persists toggles to the user layer.",
151
- "properties": {
152
- "enabled": {
153
- "type": "boolean",
154
- "description": "Default true. Set to false to disable both the npm registry probe and the CLI / UI banner."
155
- }
156
- }
157
146
  }
158
147
  }
159
148
  }
@@ -4,7 +4,7 @@
4
4
  "title": "ScanResult",
5
5
  "description": "Canonical output of `sm scan --json` (and the data shape sent over WebSocket scan events). Self-describing and versioned; consumers MUST check `schemaVersion` before parsing.",
6
6
  "type": "object",
7
- "required": ["schemaVersion", "scannedAt", "scope", "roots", "nodes", "links", "issues", "stats"],
7
+ "required": ["schemaVersion", "scannedAt", "roots", "nodes", "links", "issues", "stats"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "schemaVersion": {
@@ -26,11 +26,6 @@
26
26
  "specVersion": { "type": "string", "description": "Spec version that this scan conforms to (e.g. `0.1.0`)." }
27
27
  }
28
28
  },
29
- "scope": {
30
- "type": "string",
31
- "enum": ["project", "global"],
32
- "description": "Scan scope. `project` walks the cwd repo; `global` walks user-level skill directories."
33
- },
34
29
  "roots": {
35
30
  "type": "array",
36
31
  "description": "Filesystem roots that were walked during this scan, as absolute or scope-root-relative paths.",
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/user-settings.schema.json",
4
+ "title": "UserSettings",
5
+ "description": "Per-user, per-machine settings file persisted at `~/.skill-map/settings.json`. Holds the small set of preferences that genuinely belong to the operator (not to a project) plus the bookkeeping each one needs. The file is NOT part of the project config layer system (no merge, no PROJECT_LOCAL_ONLY_KEYS interaction); it is read directly by the few modules that own a user-scope feature. See `spec/cli-contract.md` §Scope is always project-local for the broader principle: skill-map never reads `$HOME` by default, this file is the narrow, documented exception. There is intentionally no `.local` partner; values here are already per-machine, so the project / project-local split would have no meaning.",
6
+ "type": "object",
7
+ "required": ["schemaVersion"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "type": "integer",
12
+ "const": 1,
13
+ "description": "Shape version of this file. Bumped only on breaking changes to the on-disk shape. Pre-1.0 the value stays `1`; future migrations land alongside the bump."
14
+ },
15
+ "updateCheck": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "description": "User toggle + boot-time throttle bookkeeping for the once-per-day 'new version available' probe. The toggle is a real preference; the timestamps are opaque bookkeeping the CLI maintains so it does not spam the user. Both live in the same sub-object because they belong to the same feature.",
19
+ "properties": {
20
+ "enabled": {
21
+ "type": "boolean",
22
+ "description": "Operator opt-out toggle. Default `true` when absent. The CLI / Settings UI persists changes through `PATCH /api/preferences`."
23
+ },
24
+ "latestVersion": {
25
+ "type": ["string", "null"],
26
+ "description": "Latest @skill-map/cli version observed at the last npm-registry probe. `null` (or absent) when never probed."
27
+ },
28
+ "checkedAt": {
29
+ "type": ["integer", "null"],
30
+ "description": "Unix milliseconds of the last npm-registry probe. `null` (or absent) when never probed. Used to throttle the daily probe."
31
+ },
32
+ "shownAt": {
33
+ "type": ["integer", "null"],
34
+ "description": "Unix milliseconds of the last banner emission to stderr. `null` (or absent) when never shown. Used so a single probe does not re-emit the banner across back-to-back `sm` invocations."
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }