@skill-map/spec 0.41.0 → 0.43.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 +34 -0
- package/README.md +1 -1
- package/architecture.md +6 -6
- package/cli-contract.md +29 -19
- package/db-schema.md +5 -5
- package/index.json +13 -13
- package/package.json +1 -1
- package/plugin-author-guide.md +11 -11
- package/schemas/extensions/base.schema.json +1 -1
- package/schemas/extensions/provider.schema.json +20 -0
- package/schemas/plugins-doctor.schema.json +1 -1
- package/schemas/project-config.schema.json +3 -43
- package/schemas/scan-result.schema.json +21 -0
- package/schemas/signal.schema.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Spec changelog
|
|
2
2
|
|
|
3
|
+
## 0.43.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `sm <namespace> --help` (and `sm help <namespace>`) now render a namespace overview, header, USAGE, an optional DESCRIPTION, and a COMMANDS list of the subcommands, for command prefixes that own subcommands but are not themselves runnable (`plugins`, `db`, `config`, `job`, `actions`, `sidecar`, `hooks`, `conformance`, plus nested ones like `plugins slots`). Previously these fell through to Clipanion's terse "Multiple commands match" listing. Leaf verbs and unknown names are unchanged.
|
|
8
|
+
|
|
9
|
+
## User-facing
|
|
10
|
+
|
|
11
|
+
`sm plugins --help` (and `db`, `config`, `job`, and the other command groups) now print a tidy overview with a one-line description and a list of their subcommands, matching the look of `sm scan --help`, instead of a terse internal list.
|
|
12
|
+
|
|
13
|
+
- Removed seven project-config keys that had no runtime consumer: `i18n.locale`, `providers` (the enabled-list; `activeProvider` stays), `history.share`, the `autoMigrate` config key (the `sm db migrate` / `backup` adapter option is untouched), `plugins.<id>.config`, `plugins.<id>.extensions`, and `scan.followSymlinks` (the walker always hard-skips symlinks). Dropping `plugins.<id>.config` closed the last open subtree, so project-config is now fully `additionalProperties: false`.
|
|
14
|
+
|
|
15
|
+
## User-facing
|
|
16
|
+
|
|
17
|
+
**Config cleanup.** Several settings.json keys that never did anything (`i18n`, `providers`, `history`, `autoMigrate`, `scan.followSymlinks`, per-plugin `config` / `extensions`) were removed. If still present they are now ignored and reported with a warning on load.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Normalize plugin terminology: "bundle" is no longer used as a synonym for "plugin". The installable unit is now consistently called a "plugin" everywhere (types, identifiers, spec prose, CLI output, and Settings labels); the word "bundle" is reserved exclusively for the aggregate toggle that flips all of a plugin's extensions at once (the "bundle macro"). No behavior or wire-shape changes.
|
|
22
|
+
|
|
23
|
+
## User-facing
|
|
24
|
+
|
|
25
|
+
`sm plugins list` / `show` and the Settings → Plugins UI now consistently say "plugin" instead of "bundle". The only place "bundle" remains is the name for toggling a whole plugin (all its extensions) at once.
|
|
26
|
+
|
|
27
|
+
## 0.42.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- `sm tutorial` now materializes the walkthrough skill into the chosen agent's territory instead of always `.claude/skills/`. Providers declare an optional `scaffold` block (`skillDir` plus display-only `aka` names); the destination comes from `--for <provider>` or a prompt defaulting to Claude. It now also requires an empty cwd, seeding a self-contained scenario the tester can later delete wholesale, so a non-empty directory is refused (exit 2) unless `--force` is passed.
|
|
32
|
+
|
|
33
|
+
## User-facing
|
|
34
|
+
|
|
35
|
+
`sm tutorial` can now target other agents: `--for agent-skills` (open-standard layout, used by Antigravity and OpenAI Codex) or `--for claude` (default). It now requires an empty directory: run it in a fresh folder, or pass `--force` to seed into the current one.
|
|
36
|
+
|
|
3
37
|
## 0.41.0
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ spec/ ← published as @skill-map/spec
|
|
|
112
112
|
|
|
113
113
|
## Relationship to the reference implementation
|
|
114
114
|
|
|
115
|
-
The reference implementation ([`../src/`](../src/README.md)) is one conforming consumer of this spec. It ships the CLI binary `sm`, a built-in SQLite storage adapter, and a
|
|
115
|
+
The reference implementation ([`../src/`](../src/README.md)) is one conforming consumer of this spec. It ships the CLI binary `sm`, a built-in SQLite storage adapter, and a set of default extensions.
|
|
116
116
|
|
|
117
117
|
The reference impl has no privileged access to the spec. Breaking changes to the spec must follow [`versioning.md`](./versioning.md) regardless of reference-impl convenience.
|
|
118
118
|
|
package/architecture.md
CHANGED
|
@@ -155,9 +155,9 @@ The loader enforces two id-uniqueness analyzers during discovery (see [`plugin-a
|
|
|
155
155
|
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.
|
|
156
156
|
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.
|
|
157
157
|
|
|
158
|
-
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-command`, `core/reference-broken`, `my-plugin/my-extractor`). 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
|
|
158
|
+
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-command`, `core/reference-broken`, `my-plugin/my-extractor`). 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 plugins 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.
|
|
159
159
|
|
|
160
|
-
Every extension (built-in or drop-in) is independently toggle-able by its qualified id `<
|
|
160
|
+
Every extension (built-in or drop-in) is independently toggle-able by its qualified id `<plugin>/<ext-id>`. The plugin row is a presentational grouping; the granular toggle target is the extension, while toggling a bare plugin id is the **bundle** (aggregate) macro that fans across every extension. The loader's pre-import `resolveEnabled(pluginId)` short-circuit only fires when EVERY extension of the plugin is disabled (the plugin "starts as disabled"); partial enables let the imports proceed and the runtime composer (`composeScanExtensions` / `composeFormatters` in `src/core/runtime/plugin-runtime/composer.ts`) drops the per-extension disabled rows before they reach the orchestrator. The `core` plugin exercises the per-extension axis explicitly (every kernel built-in is removable, satisfying §Boot invariant: "no extension is privileged"); vendor Provider plugins (`claude`, `antigravity`, `openai`, `agent-skills`) currently have most operators leaving every extension enabled, but the same per-extension toggle surface applies. See [`plugin-author-guide.md` §Toggle model](./plugin-author-guide.md#toggle-model) for the author-facing summary.
|
|
161
161
|
|
|
162
162
|
### `RunnerPort`
|
|
163
163
|
|
|
@@ -309,7 +309,7 @@ Each Provider ALSO declares a top-level `presentation` block (`provider.schema.j
|
|
|
309
309
|
The dispatch contract has two consequences implementations MUST honour:
|
|
310
310
|
|
|
311
311
|
1. **First-claim-wins**. A vendor Provider that classifies a file inside its territory (e.g. claude's `.claude/agents/foo.md` → `agent`) is authoritative; later Providers cannot reclassify it to a different kind. This locks vendor ownership of vendor paths and removes the historical `provider-ambiguous` failure mode for non-overlapping territories.
|
|
312
|
-
2. **`core/markdown` is the universal fallback for unclaimed `.md` files**. The built-in `core/markdown` Provider's `classify` returns `'markdown'` unconditionally (it does NOT inspect the path). Combined with the dedup guarantee above and its terminal position in the iteration order, it picks up exactly the `.md` files no vendor Provider claimed, a `.md` at the project root, under `.claude/hooks/`, `notes/`, `CLAUDE.md`, `GEMINI.md`, or anywhere else outside a known vendor territory. The fallback is **not privileged kernel code**: it ships as a regular built-in Provider under the `core`
|
|
312
|
+
2. **`core/markdown` is the universal fallback for unclaimed `.md` files**. The built-in `core/markdown` Provider's `classify` returns `'markdown'` unconditionally (it does NOT inspect the path). Combined with the dedup guarantee above and its terminal position in the iteration order, it picks up exactly the `.md` files no vendor Provider claimed, a `.md` at the project root, under `.claude/hooks/`, `notes/`, `CLAUDE.md`, `GEMINI.md`, or anywhere else outside a known vendor territory. The fallback is **not privileged kernel code**: it ships as a regular built-in Provider under the `core` plugin, so a user who explicitly does not want it can disable it via `sm plugins disable core/markdown` and the scan reverts to "vendor-only", orphan `.md` files become silently invisible, matching pre-spec-0.9.0 behaviour.
|
|
313
313
|
|
|
314
314
|
The fallback exists because the format-named generic kind `markdown` is provider-agnostic: no vendor owns the universal markdown format. Keeping the fallback as a Provider (rather than a kernel-level special case) preserves the boot invariant that no extension is privileged, when a future vendor Provider (Codex, Cursor, Roo) lands, it slots into the iteration order before `core/markdown` and the fallback semantics stay invariant.
|
|
315
315
|
|
|
@@ -331,7 +331,7 @@ Implementations MUST treat an absent `identifiers` field exactly like `[]`: the
|
|
|
331
331
|
|
|
332
332
|
### Provider · resolution rules
|
|
333
333
|
|
|
334
|
-
Each Provider MAY declare an optional `resolution: Record<linkKind, targetKind[]>` map listing, for each `link.kind` an Extractor in this Provider's
|
|
334
|
+
Each Provider MAY declare an optional `resolution: Record<linkKind, targetKind[]>` map listing, for each `link.kind` an Extractor in this Provider's plugin emits, the set of target `node.kind` values that count as a valid resolution for the post-walk confidence-lift transform. Absent = no link.kind bumps under this Provider via the name path (path-match always fires).
|
|
335
335
|
|
|
336
336
|
The transform runs after `dedupeLinks` and before the analyzer pipeline. For each link below `confidence: 1.0`:
|
|
337
337
|
|
|
@@ -382,7 +382,7 @@ In addition to the `emitLink` path, Extractors MAY emit **Signals** via `ctx.emi
|
|
|
382
382
|
|
|
383
383
|
The kernel's **resolver phase** runs after extraction completes and before analysis starts. For each Signal, the resolver:
|
|
384
384
|
|
|
385
|
-
1. (Phase 4+, not yet wired) Filters candidates whose `extractorId` is disabled
|
|
385
|
+
1. (Phase 4+, not yet wired) Filters candidates whose `extractorId` is disabled by a per-extension enable filter. The config surface that toggles individual extensions is not defined yet (the earlier `plugins.<id>.extensions.<extId>.enabled` placeholder was removed); it will be specified when this filter lands. When the filter empties every candidate, the Signal carries `resolution.outcome = 'rejected'` with `extractorDisabled = { extractorId }`.
|
|
386
386
|
2. Ranks the surviving candidates inside the Signal by the active Provider's `resolverRules.kindPriority` (when declared), then `confidence` DESC, then `range` length (`end - start`) DESC, then `extractorId` declaration order. The chosen index is recorded as `resolution.winnerIndex` and (provisionally) `resolution.outcome = 'materialised'`.
|
|
387
387
|
3. For body-scoped Signals with a `range`, the resolver builds overlap clusters per source (transitive closure of range intersection). Clusters of size 1 keep their winner. For clusters of size 2+, the resolver re-applies the same four-step tiebreak to each Signal's winning candidate to pick a cluster winner. Losers flip to `resolution.outcome = 'rejected'` with `rejectedBy = { source, range, extractorId, reason }`, where `reason` names the tiebreak step that decided it: `kind-priority`, `higher-confidence`, `longer-range`, or `earlier-declaration`. External pseudo-link clusters (every member targets `http://` / `https://`) skip cross-cluster ranking, every member materialises (URL-targeted Signals can never conflict with internal-target Signals or with each other because they leave the local graph).
|
|
388
388
|
4. Materialises every Signal whose final `outcome === 'materialised'` as a Link, identical in shape to a Link emitted directly via `emitLink`. The materialised Link's `sources[]` carries the winning candidate's `extractorId` so attribution survives the resolver.
|
|
@@ -808,7 +808,7 @@ PK `(plugin_id, extension_id, node_path, contribution_id)` so re-emission upsert
|
|
|
808
808
|
**NOT pure replace-all** (the way `scan_links` / `scan_issues` are). The watcher's cached pass leaves the contributions buffer empty for cached nodes, the orchestrator skips `extract()` when the per-(node, extractor) cache hits, so no `emitContribution` fires. A naive wipe-all would silently drop the prior valid rows on every watcher boot. The persist runs four passes inside the same transaction:
|
|
809
809
|
|
|
810
810
|
1. **Orphan sweep**, drops every row whose `node_path` is NOT in the current live node set. Disappeared nodes lose their contributions.
|
|
811
|
-
2. **Catalog sweep**, drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (uninstalled-on-disk plugins, removed contributions). Disabled
|
|
811
|
+
2. **Catalog sweep**, drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (uninstalled-on-disk plugins, removed contributions). Disabled plugins are normally purged eagerly by `sm plugins disable` (see `StoragePort.contributions.purgeByPlugin`); the catalog sweep here is the fallback for the rare "config flipped between scans without going through the CLI" case.
|
|
812
812
|
3. **Per-tuple sweep**, for every `(pluginId, extensionId, nodePath)` tuple where the extension actually RAN against that node in this scan (extractor cache miss, OR analyzer, analyzers always run), drop any row carrying that triple whose `contribution_id` is NOT present in the buffer for that triple. This catches the "extractor used to emit, now does not" case (e.g. a node body change that removes the trigger). Cached-extractor tuples are NOT in the set, so their rows survive untouched.
|
|
813
813
|
4. **Upsert**, `INSERT ... ON CONFLICT DO UPDATE SET payload_json = excluded.payload_json, slot = excluded.slot` for every row in the buffer. PK conflict refreshes payload + `slot` + `emitted_at`.
|
|
814
814
|
|
package/cli-contract.md
CHANGED
|
@@ -182,22 +182,30 @@ Exit: 0 on success, 2 on failure.
|
|
|
182
182
|
|
|
183
183
|
#### `sm tutorial [variant]`
|
|
184
184
|
|
|
185
|
-
Materialize an interactive tester tutorial as a
|
|
185
|
+
Materialize an interactive tester tutorial as a skill folder under the chosen agent's on-disk territory. Companion to the `sm-tutorial` and `sm-master` skills: a tester drops into an empty directory, runs `sm tutorial` (or `sm tutorial master`) to seed the skill, then opens their agent there and triggers it by speaking one of its trigger phrases (the agent auto-discovers `<skillDir>/<slug>/SKILL.md` on boot).
|
|
186
186
|
|
|
187
187
|
The optional positional `variant` argument selects which skill gets materialised. Valid values are:
|
|
188
188
|
|
|
189
|
-
- `tutorial` (default, also the behaviour when no argument is passed):
|
|
190
|
-
- `master`:
|
|
189
|
+
- `tutorial` (default, also the behaviour when no argument is passed): the basic onboarding walkthrough, slug `sm-tutorial`.
|
|
190
|
+
- `master`: the advanced walkthrough (plugin tour, plugin authoring, settings + view-slots), slug `sm-master`, includes the `references/` sub-folder.
|
|
191
|
+
|
|
192
|
+
The destination directory is the selected Provider's `scaffold.skillDir` (e.g. `.claude/skills` for Claude, `.agents/skills` for the open standard adopted by Antigravity); the verb writes `<cwd>/<skillDir>/<slug>/`. Provider selection:
|
|
193
|
+
|
|
194
|
+
- `--for <provider-id>` selects the destination Provider explicitly (e.g. `--for claude`, `--for agent-skills`). The id MUST be a registered Provider that declares `scaffold.skillDir`; any other value is a usage error.
|
|
195
|
+
- Without `--for`, the default Provider is the first scaffold-capable Provider in catalog order (Claude). The verb requires an empty cwd (see below), so there is no marker to detect: provider auto-detection does not apply.
|
|
196
|
+
- Without `--for`, on an interactive stdin the verb prompts with a numbered list of the Providers that declare `scaffold.skillDir`, marking the default option (Claude); an empty answer accepts it. Each option shows the Provider label plus any `scaffold.aka` agents in parentheses (e.g. the open standard lists Antigravity and OpenAI Codex). The `aka` strings are display-only and are NOT accepted by `--for`.
|
|
197
|
+
- Without `--for`, on a non-interactive stdin (pipes, CI) the verb selects the default Provider without prompting, so the verb stays scriptable.
|
|
191
198
|
|
|
192
199
|
Common behaviour for both variants:
|
|
193
200
|
|
|
194
|
-
- Writes the
|
|
195
|
-
- Content is the canonical `SKILL.md`
|
|
196
|
-
-
|
|
201
|
+
- Writes the full skill folder (`SKILL.md` plus any `references/` sub-folder) under the resolved `<skillDir>/<slug>/`.
|
|
202
|
+
- Content is the canonical skill shipped with the implementation. The `SKILL.md` payload is host-agnostic; only the destination directory varies per Provider. Any conforming implementation MUST embed equivalent tutorial sources (the prose itself is informative; what is normative is that the verb produces a readable skill folder a compatible agent can consume).
|
|
203
|
+
- Requires the cwd to be empty (a directory listing including dotfiles returns nothing). The tutorial seeds a self-contained scenario and the skill later lays its fixtures and `.skill-map/` directly in the cwd, so the tester can delete the whole directory afterwards without losing prior work; that guarantee only holds when the directory started empty. A non-empty cwd is refused (exit 2) unless `--force` is passed.
|
|
204
|
+
- Does NOT require an initialized project and never reads or writes `.skill-map/`. It is a pre-bootstrap helper: Provider selection reads the built-in Provider catalog directly, not project config.
|
|
197
205
|
|
|
198
|
-
Flags: `--force` (
|
|
206
|
+
Flags: `--for <provider-id>` (destination Provider, skips the prompt); `--force` (proceed even when the cwd is not empty, overwriting any existing target folder, without prompting).
|
|
199
207
|
|
|
200
|
-
Exit: `0` on success; `2` if the
|
|
208
|
+
Exit: `0` on success; `2` if the cwd is not empty and `--force` was not passed (operational error, refusing to seed the tutorial into a directory that already holds content); `2` if the positional `variant` is set to a value other than `tutorial` or `master`; `2` if `--for` names a Provider that does not exist or declares no `scaffold.skillDir`; `2` on any I/O failure.
|
|
201
209
|
|
|
202
210
|
#### `sm version`
|
|
203
211
|
|
|
@@ -231,8 +239,8 @@ Exit: 0 if all green, 1 if warnings, 2 if any `error`-level problem.
|
|
|
231
239
|
|
|
232
240
|
Self-describing introspection.
|
|
233
241
|
|
|
234
|
-
- `human` (default): pretty terminal output.
|
|
235
|
-
- `md`: canonical markdown for documentation sites. Implementations MUST NOT hand-maintain equivalent markdown; it is generated on demand from this output.
|
|
242
|
+
- `human` (default): pretty terminal output. With no argument: the compact overview of every verb grouped by category. With a verb (`sm help scan`, `sm scan --help`): that verb's detail view. With a **command namespace** (a prefix that owns subcommands but is not itself runnable, e.g. `sm help plugins`, `sm plugins --help`, `sm plugins slots --help`): a namespace overview, header line, USAGE, optional DESCRIPTION, then a COMMANDS list of the subcommands. An argument that is neither a verb nor a namespace exits `5` with an unknown-verb message.
|
|
243
|
+
- `md`: canonical markdown for documentation sites. Implementations MUST NOT hand-maintain equivalent markdown; it is generated on demand from this output. With a verb or namespace argument, the output is scoped to that verb (or the namespace's subcommands).
|
|
236
244
|
- `json`: structured surface dump. Shape:
|
|
237
245
|
|
|
238
246
|
```json
|
|
@@ -313,6 +321,8 @@ The watcher subscribes to the same roots that `sm scan` walks and respects `.ski
|
|
|
313
321
|
|
|
314
322
|
**Node cap** (`--max-nodes <N>`): on `sm scan` and `sm watch` (alias `sm scan --watch`), a hard cap on the number of files the walker accepts after `.skillmapignore` filtering, before extractors run. Default comes from `scan.maxNodes` (default 256). The flag is a full override of the setting and is **bidirectional**: it can raise the cap (`--max-nodes 1000` on a 312-file repo) or lower it (`--max-nodes 100` cuts deeper than the default). When the walker reaches the cap, additional files are dropped in stable provider-walker order and the scan is marked oversized in `scan_meta` (columns `recommended_node_limit` and `override_max_nodes`), the resulting `ScanResult` envelope carries `recommendedNodeLimit` and `overrideMaxNodes` so the UI raises a persistent banner pointing at the `.skillmapignore` editor in Settings → Project. The CLI prints a human-mode notice naming both escapes: edit `.skillmapignore` (preferred, trims permanently) or re-run with `--max-nodes <N>` (force, graph quality may degrade past the recommended limit). `sm refresh` operates on a single already-classified node, so the cap does not apply there. Validation: integer ≥ 1, anything else exits `2` operational.
|
|
315
323
|
|
|
324
|
+
**File-size skip** (`scan.maxFileSizeBytes`, default 1 MiB): the walker checks each candidate file's on-disk size before reading it and skips any file larger than the limit. The skip happens at the source (the file is never read, parsed, or indexed as a node), so an accidental binary drop or generated artefact cannot poison the graph. Every skipped file is reported in the `ScanResult` envelope as `oversizedFiles` (each entry the root-relative, forward-slash path plus the byte size) and counted in `stats.filesOversized`. When at least one file is skipped, `sm scan`, `sm watch` (per batch), and `sm serve` (initial scan and every batch) print a **WARN**-level terminal notice listing the skipped files with a human-readable size, plus a hint pointing at `scan.maxFileSizeBytes` and `.skillmapignore`; the UI raises a matching banner. Unlike the node cap, the limit is config-only (no per-invocation flag).
|
|
325
|
+
|
|
316
326
|
**Schema-drift rebuild (pre-1.0)**: before persisting, `sm scan` and `sm watch` compare `scan_meta.scanned_by_version` against the running CLI. A minor or major difference means the local cache predates a schema change, so the DB is deleted and rebuilt from scratch by this run (`.sm` sidecars are untouched, they are the source of truth). On an interactive terminal the rebuild is confirmed first; `--yes` (and every non-interactive caller: piped stdin, CI, the BFF, the watcher) rebuilds without prompting. Declining aborts the scan (exit `2`) without deleting anything. Patch-level differences are compatible and never trigger a rebuild. Read-only verbs keep the version-skew advisory instead of rebuilding. See [`db-schema.md` §Schema drift (pre-1.0)](./db-schema.md#schema-drift-pre-10).
|
|
317
327
|
|
|
318
328
|
Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (one-shot scan only, the watcher does not flip exit code based on per-batch issues), 2 on operational error.
|
|
@@ -599,17 +609,17 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
599
609
|
| `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. |
|
|
600
610
|
| `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`. |
|
|
601
611
|
| `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`), merged effective config (defaults → user → user-local → project → project-local → override). |
|
|
602
|
-
| `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', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, locked?: boolean }> }`. The
|
|
603
|
-
| `PATCH /api/plugins/:id` | implemented | **Bundle macro endpoint**: fans the toggle out across every extension inside the
|
|
604
|
-
| `PATCH /api/plugins/:
|
|
605
|
-
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare
|
|
612
|
+
| `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', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, locked?: boolean }> }`. The plugin row itself has no granular toggle axis; the row's `status` aggregates the children (`'enabled'` when at least one extension is enabled, `'disabled'` otherwise). The `description` field on the plugin item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInPlugin`; 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 whenever the plugin declares any extension AND the plugin loaded successfully. Each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). The optional `locked: true` flag is stamped when the plugin 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, every extension of the plugin was disabled in `config_plugins` / `settings.json` at `sm serve` boot, so the handlers were never bucketed into the runtime. The SPA renders a per-row hint when this flag is set AND the user re-enables at least one of the plugin's extensions in the buffered state, since re-enabling them requires `sm serve` restart (the rest of the toggle pipeline applies live). The flag is omitted when false. |
|
|
613
|
+
| `PATCH /api/plugins/:id` | implemented | **Bundle (aggregate) macro endpoint**: fans the toggle out across every extension inside the plugin. `:id` MUST be a top-level plugin id (no slash); qualified-id form is the sibling route below. Body `{ enabled: boolean }` (JSON). Persists one `config_plugins` row per child extension (`<plugin>/<ext>`) inside a single transaction; locked children are silently dropped, mirroring the CLI's bulk-mode lock semantics. Response is the canonical `RestEnvelope` (`kind: 'plugins'`) reflecting the post-write state. **Lock**, rejected with 403 `locked` when the plugin id itself is in the host lock-list (`src/server/locked-plugins.ts`). **Apply window**, the override applies on the next scan; 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 each disabled extension immediately 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. |
|
|
614
|
+
| `PATCH /api/plugins/:pluginId/extensions/:extensionId` | implemented | Canonical per-extension toggle. Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:pluginId` or `:extensionId`). 404 `not-found` when the plugin id is unknown or the extension id does not belong to that plugin. **Lock**, rejected with 403 `locked` when either the plugin id or the qualified `pluginId/extensionId` appears in the host lock-list. Same persistence + apply-window semantics as the bundle (aggregate) macro form (including the `startsAsDisabled` exception). The SPA's buffered Settings modal posts here for every per-row flip. |
|
|
615
|
+
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare plugin id (bundle cascade macro, same semantics as `PATCH /api/plugins/:id`) or a qualified `<plugin>/<extension>` id. 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` / `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**: bare plugin ids expand to their child qualified ids before persistence; `IConfigPluginsPort.set` is called per resulting key, then one grouped `scan_contributions` purge per disabled extension (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. |
|
|
606
616
|
| `ALL /api/*` (other) | reserved | structured 404 envelope (see below); future endpoints land in subsequent sub-steps. |
|
|
607
617
|
| `GET /ws` | implemented (v14.4.a) | accepts WebSocket upgrade and registers the client with the BFF broadcaster. Server-push only, the server fans `scan.*` (and forthcoming `issue.*`) events to every connected client. See **WebSocket protocol** below. |
|
|
608
618
|
| `GET *` | implemented | static asset from the resolved UI bundle, falling back to `index.html` for SPA deep links. |
|
|
609
619
|
|
|
610
620
|
List endpoints conform to [`schemas/api/rest-envelope.schema.json`](schemas/api/rest-envelope.schema.json). The `/api/scan` and `/api/health` responses carry their underlying `ScanResult` / `IHealthResponse` shapes directly (no envelope wrap). The `/api/graph` response carries the formatter's native textual output.
|
|
611
621
|
|
|
612
|
-
**`kindRegistry` envelope field.** Every payload-bearing variant of the REST envelope (`nodes` / `links` / `issues` / `plugins` lists, the `node` single, the `config` value envelope) embeds a required `kindRegistry: { [kindName]: { providerId, label, color, colorDark?, emoji?, icon? } }` field. Sentinel envelopes (`health`, `scan`, `graph`) are exempt, they carry no payload at the wire level. The BFF assembles the registry once at boot from EVERY built-in Provider's `kinds[*].ui` block (regardless of the boot-time enabled verdict, their module code is statically imported by `built-
|
|
622
|
+
**`kindRegistry` envelope field.** Every payload-bearing variant of the REST envelope (`nodes` / `links` / `issues` / `plugins` lists, the `node` single, the `config` value envelope) embeds a required `kindRegistry: { [kindName]: { providerId, label, color, colorDark?, emoji?, icon? } }` field. Sentinel envelopes (`health`, `scan`, `graph`) are exempt, they carry no payload at the wire level. The BFF assembles the registry once at boot from EVERY built-in Provider's `kinds[*].ui` block (regardless of the boot-time enabled verdict, their module code is statically imported by `built-ins.ts` and always in memory) PLUS every drop-in user Provider that loaded successfully at boot (see [`architecture.md` §Provider · `ui` presentation](architecture.md#provider--ui-presentation)). The registry is then attached to every applicable response. Built-ins are listed unconditionally because a user re-enabling one mid-session expects its kinds to render on the next scan; the runtime enabled/disabled axis is enforced at SCAN-TIME by `composeScanExtensions` reading the fresh resolver, not by hiding kinds from the registry. Drop-ins that loaded as `disabled` carry `startsAsDisabled: true` on `GET /api/plugins` and need `sm serve` restart to register, their module code was never imported. The UI consumes `kindRegistry` directly to render kind palettes, list rows, and inspector headers, built-in and user-plugin kinds render identically. A kind appearing in a response payload (e.g. `node.kind`) without a matching `kindRegistry` entry is a contract violation; the kernel rejects Providers without a `ui` block at load time so the registry is always complete for whatever kinds appear in the response.
|
|
613
623
|
|
|
614
624
|
**`providerRegistry` envelope field.** The same payload-bearing envelopes also embed a required `providerRegistry: { [providerId]: { label, color, colorDark?, emoji?, icon?, hideChip? } }` field (sibling of `kindRegistry`). Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes (`annotations.registered`, `contributions.registered`) are exempt. Same boot-time assembly discipline as `kindRegistry`: assembled once from EVERY built-in Provider's top-level `presentation` block (regardless of enabled verdict) PLUS every drop-in user Provider that loaded at boot. The UI consumes `providerRegistry` to render the active-lens dropdown, the topbar lens chip, and the per-node provider chip from the real registered-Provider set, never a hardcoded list; `hideChip: true` (the universal `markdown` fallback) suppresses only the per-card chip. This is the static boot catalog of Provider identity; the dynamic active lens (current value + filesystem-detected candidates) is served separately by `GET /api/active-provider`.
|
|
615
625
|
|
|
@@ -635,10 +645,10 @@ Error code sources at v14.2:
|
|
|
635
645
|
- `not-found` (404), unknown `/api/*` path; missing node on `/api/nodes/:pathB64`; malformed `pathB64` (treated as "no such node" so the client UX is uniform).
|
|
636
646
|
- `bad-query` (400), `ExportQueryError` from `parseExportQuery`; pagination beyond `limit ≤ 1000`; non-integer / negative `limit` / `offset`; unknown formatter on `/api/graph`; `?fresh=1` when the server started with `--no-built-ins` or `--no-plugins`.
|
|
637
647
|
- `internal` (500), uncaught error during a request (e.g. config-load failure, DB corruption surfacing through `loadScanResult`).
|
|
638
|
-
- `db-missing` (500), emitted by mutation endpoints (`PATCH /api/plugins/:id`, `PATCH /api/plugins/:
|
|
639
|
-
- `not-found` (404) on `PATCH /api/plugins/:id`, unknown plugin id (no built-in
|
|
640
|
-
- `bad-query` (400) on `PATCH /api/plugins/:id`, malformed body (missing `enabled`, wrong type), or `:id` contains a slash (the qualified-id sibling is `PATCH /api/plugins/:
|
|
641
|
-
- `locked` (403) on `PATCH /api/plugins/:id` and the qualified-id sibling, the target
|
|
648
|
+
- `db-missing` (500), emitted by mutation endpoints (`PATCH /api/plugins/:id`, `PATCH /api/plugins/:pluginId/extensions/:extensionId`, `PATCH /api/plugins`) when the project DB is absent. Read-side routes uniformly degrade to the empty shape (`/api/scan`) or zero items (list endpoints) so they do not emit this code; mutation endpoints cannot persist without a DB so they fail fast instead of silently dropping the write.
|
|
649
|
+
- `not-found` (404) on `PATCH /api/plugins/:id`, unknown plugin id (no built-in plugin, no discovered drop-in matches). The qualified-id form returns the same code when either segment misses. The bulk `PATCH /api/plugins` returns the same code with `error.details.id` set to the first offending id; the batch is rejected before any DB write.
|
|
650
|
+
- `bad-query` (400) on `PATCH /api/plugins/:id`, malformed body (missing `enabled`, wrong type), or `:id` contains a slash (the qualified-id sibling is `PATCH /api/plugins/:pluginId/extensions/:extensionId`). The qualified-id sibling returns 404 `not-found` for an unknown plugin or extension id. The bulk `PATCH /api/plugins` returns 400 for malformed `changes` array or missing/typeless `enabled`, with `error.details.id` set to the first offending entry's id.
|
|
651
|
+
- `locked` (403) on `PATCH /api/plugins/:id` and the qualified-id sibling, the target plugin id or qualified extension id is in the host lock-list (`src/server/locked-plugins.ts`). The list is hardcoded, host-only, and not user-editable; `GET /api/plugins` mirrors the same analyzer by stamping `locked: true` on the affected items. The bulk `PATCH /api/plugins` returns the same code with `error.details.id` set to the first locked entry; the batch is rejected before any DB write.
|
|
642
652
|
- `bad-query` (400) on `POST /api/scan`, the server was started with `--no-built-ins` or `--no-plugins` (partial pipeline would persist a misleading DB).
|
|
643
653
|
- `scan-busy` (409) on `POST /api/scan`, another scan (a watcher batch or another POST) is already in flight. Retry once the in-flight scan resolves; the WS `scan.completed` envelope is the unambiguous "now safe" signal.
|
|
644
654
|
- `host-not-allowed` / `origin-not-allowed` (403) on every endpoint: first-stage loopback gate rejected the request because the `Host` or `Origin` header hostname is not loopback (`127.0.0.1`, `localhost`, `::1`). Closes DNS rebinding (Host) and cross-origin abuse (Origin). The gate is always-on; the envelope `details` is `null` so the response is opaque to probes.
|
package/db-schema.md
CHANGED
|
@@ -18,7 +18,7 @@ One scope. Skill-map operates on the project scope only (`<cwd>/.skill-map/`). T
|
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| `project` | `<cwd>/.skill-map/skill-map.db` | The current repository, plus any paths the user added to `scan.extraFolders`. |
|
|
20
20
|
|
|
21
|
-
The project DB is gitignored by default. Teams MAY opt in to sharing it by
|
|
21
|
+
The project DB is gitignored by default (`sm init` adds the entry). Teams MAY opt in to sharing it by removing that `.gitignore` entry, the file is then committed and the execution log becomes a team artifact.
|
|
22
22
|
|
|
23
23
|
The `--db <path>` CLI flag overrides the DB location as an escape hatch (debugging, custom layouts).
|
|
24
24
|
|
|
@@ -232,7 +232,7 @@ Primary key: `(plugin_id, extension_id, node_path, contribution_id)`. Indexes: `
|
|
|
232
232
|
**Persistence, orphan + catalog + per-tuple sweep + upsert (NOT pure replace-all).** The watcher's cached pass leaves the contributions buffer empty for cached nodes, the orchestrator skips `extract()` when the per-(node, extractor) cache hits, so no `emitContribution` fires. A naive wipe-all would silently drop the prior valid rows on every watcher boot. The persist runs four passes inside the same tx as the rest of the scan zone:
|
|
233
233
|
|
|
234
234
|
1. **Orphan sweep**, drops every row whose `node_path` is NOT in the current live node set (`livePaths` derived from `result.nodes`). Disappeared nodes lose their contributions automatically.
|
|
235
|
-
2. **Catalog sweep**, drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (`registeredContributionKeys` collected via `collectRegisteredContributionKeys(composed)`). Uninstalled-on-disk plugins and removed contributions lose their rows on the next scan. Disabled
|
|
235
|
+
2. **Catalog sweep**, drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (`registeredContributionKeys` collected via `collectRegisteredContributionKeys(composed)`). Uninstalled-on-disk plugins and removed contributions lose their rows on the next scan. Disabled plugins are normally purged eagerly by `sm plugins disable` (see `purgeByPlugin` below), so the catalog sweep here is the fallback for the rare "config flipped between scans without going through the CLI" case.
|
|
236
236
|
3. **Per-tuple sweep**, for every `(pluginId, extensionId, node_path)` tuple in `freshlyRunTuples` (extension actually ran against that node this scan: extractor cache miss, OR analyzer), drop any row carrying that triple whose `contribution_id` is NOT refreshed by the buffer. Catches the "extractor used to emit, now does not" case without touching cached-extractor rows. Tuple format: `<pluginId>/<extensionId>/<nodePath>`.
|
|
237
237
|
4. **Upsert**, `INSERT ... ON CONFLICT DO UPDATE SET payload_json = excluded.payload_json, slot = excluded.slot` for every row in the buffer. PK conflict refreshes `payload_json` + `slot` + `emitted_at`.
|
|
238
238
|
|
|
@@ -242,7 +242,7 @@ Cached nodes' rows survive untouched, they're neither orphaned (still in the liv
|
|
|
242
242
|
|
|
243
243
|
NOT analogous to `state_plugin_kvs` (which is plugin-managed). Belongs to the `scan_*` family, sweep semantics replace pure replace-all but the data is still scan-derived.
|
|
244
244
|
|
|
245
|
-
**Eager purge on disable.** `sm plugins disable <id>` calls `StoragePort.contributions.purgeByPlugin(pluginId, extensionId)` immediately after persisting `config_plugins[<id>].enabled = false`. Every persisted toggle key is the qualified `<
|
|
245
|
+
**Eager purge on disable.** `sm plugins disable <id>` calls `StoragePort.contributions.purgeByPlugin(pluginId, extensionId)` immediately after persisting `config_plugins[<id>].enabled = false`. Every persisted toggle key is the qualified `<plugin>/<ext>` shape (the CLI's bundle macro form and the BFF's cascade endpoint expand bare plugin ids before persistence), so the purge always receives both segments. The eager purge avoids the "I disabled the extension but its chips are still rendered in the UI until I re-scan" gap. Re-enabling (`sm plugins enable <id>`) does NOT restore the rows, the next scan re-emits them, same as a cold start. Contributions are scan-derived, so this is cheap; for plugin-managed state (`state_plugin_kvs`, dedicated tables) the opposite policy holds, see `plugin-kv-api.md` § "disable does not drop data".
|
|
246
246
|
|
|
247
247
|
### `scan_node_tags`
|
|
248
248
|
|
|
@@ -415,7 +415,7 @@ Persists user-toggled enable/disable overrides. Discovery is still filesystem-ba
|
|
|
415
415
|
|
|
416
416
|
**Effective enable/disable resolution.** A plugin is enabled iff the highest-precedence layer that mentions it says so. Order from highest to lowest:
|
|
417
417
|
|
|
418
|
-
1. `config_plugins.enabled` for the row whose `plugin_id` matches, written by `sm plugins enable/disable`. Local-machine user override; never committed (the DB is gitignored unless `
|
|
418
|
+
1. `config_plugins.enabled` for the row whose `plugin_id` matches, written by `sm plugins enable/disable`. Local-machine user override; never committed (the DB is gitignored unless the team removes the `.gitignore` entry).
|
|
419
419
|
2. `.skill-map/settings.json#/plugins/<id>/enabled`, committed team-shared baseline.
|
|
420
420
|
3. Installed default, every discovered plugin is enabled until told otherwise.
|
|
421
421
|
|
|
@@ -456,7 +456,7 @@ The kernel ALSO maintains `PRAGMA user_version` (or the engine equivalent) as a
|
|
|
456
456
|
- **Location**: kernel migrations in `src/migrations/` (reference impl); plugin migrations in `<plugin-dir>/migrations/`.
|
|
457
457
|
- **Wrapping**: the kernel wraps each file in `BEGIN; ... ; COMMIT;`. Files contain DDL only.
|
|
458
458
|
- **Strict versioning**: no idempotency is required. `CREATE TABLE IF NOT EXISTS` is DISCOURAGED in kernel migrations (but permitted in plugin migrations, at the plugin author's discretion).
|
|
459
|
-
- **Auto-apply**: on startup
|
|
459
|
+
- **Auto-apply**: on startup. A backup is written to `.skill-map/backups/skill-map-pre-migrate-v<N>.db` before applying. The `sm db migrate` / `sm db backup` verbs open the DB with auto-apply suppressed so the operator drives migrations manually.
|
|
460
460
|
- **Plugin migration order**: plugins are migrated after kernel migrations and in stable alphabetical order by plugin id. A failing plugin migration disables only that plugin; other plugins and the kernel continue.
|
|
461
461
|
|
|
462
462
|
`sm db migrate` controls migration flow manually: `--dry-run`, `--status`, `--to <n>`, `--kernel-only`, `--plugin <id>`, `--no-backup`.
|
package/index.json
CHANGED
|
@@ -174,14 +174,14 @@
|
|
|
174
174
|
}
|
|
175
175
|
]
|
|
176
176
|
},
|
|
177
|
-
"specPackageVersion": "0.
|
|
177
|
+
"specPackageVersion": "0.43.0",
|
|
178
178
|
"integrity": {
|
|
179
179
|
"algorithm": "sha256",
|
|
180
180
|
"files": {
|
|
181
|
-
"CHANGELOG.md": "
|
|
182
|
-
"README.md": "
|
|
183
|
-
"architecture.md": "
|
|
184
|
-
"cli-contract.md": "
|
|
181
|
+
"CHANGELOG.md": "f96ccf5116826cd2860c6149049b208b827cf9b925a4dcfa3fa9442497827c74",
|
|
182
|
+
"README.md": "a7505a7b0672c39a8b011e3c5e7d41826306476ee63768249bba4bdb3c03d4d1",
|
|
183
|
+
"architecture.md": "49644c727384f8e12061be834bc97e57d47459dae7bea096f94330b74f568a93",
|
|
184
|
+
"cli-contract.md": "76ef201ca99b789914d0530c96744396e96b2938d661fb7ff88583596b193f12",
|
|
185
185
|
"conformance/README.md": "0c69bd9becf511ada9175b1e428ba183e31d1c8a49ff09eedf4c950bb831ec4d",
|
|
186
186
|
"conformance/cases/extractor-emits-signal.json": "0115c7bb62a7a705f72e9d8048b3f0396e5caaeb3d04dea204415e279e58479d",
|
|
187
187
|
"conformance/cases/kernel-empty-boot.json": "9b51b85ff62479cd0eee37cad260245208d94f6d79644f7ee40945a934960913",
|
|
@@ -208,11 +208,11 @@
|
|
|
208
208
|
"conformance/fixtures/signal-ir-collision/.claude/agents/architect.md": "acc46b5b2dff73d98a354e4d53b5041164595deae466a4e2ce41d7c5a72f28fb",
|
|
209
209
|
"conformance/fixtures/signal-ir-single-signal/source.md": "1eda417b4c6eed372b66870e385c8d8cd631372b77cab7e996bb711e22218f89",
|
|
210
210
|
"conformance/fixtures/signal-ir-single-signal/target.md": "527137f2b4f46c0034b0edc8932cf8613d2bf22ffaaf78f01085c82a3baaebe3",
|
|
211
|
-
"db-schema.md": "
|
|
211
|
+
"db-schema.md": "21d0d789163ee38975b50f4d71f64e7328ffc19a0e8dd1402b86a35323f3b37b",
|
|
212
212
|
"interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
|
|
213
213
|
"job-events.md": "9d5b35d4c451a7f8eef9915d85316d924ac52f1c026a316cdda5f1099d496854",
|
|
214
214
|
"job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
|
|
215
|
-
"plugin-author-guide.md": "
|
|
215
|
+
"plugin-author-guide.md": "6af14aa45d2778a57a485b3fbf66411355a578581b326468efbaaf10dffdde40",
|
|
216
216
|
"plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
|
|
217
217
|
"prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
|
|
218
218
|
"schemas/annotations.schema.json": "8c639b149cad675fdd4e7d6be2b47e920cfdd24087b41361d6e1b8280f646322",
|
|
@@ -223,12 +223,12 @@
|
|
|
223
223
|
"schemas/execution-record.schema.json": "db0eb16153493ad9f13ea0ecede44191e4a8536979adffd17ca278ddf8786c77",
|
|
224
224
|
"schemas/extensions/action.schema.json": "dc4f52d23c163c6239a487fa1c1ad9c09685cf38833d3962c604d5872716cff9",
|
|
225
225
|
"schemas/extensions/analyzer.schema.json": "8def4a5ca4934197c34abde97da70704b2751041a443c859eddd4b783e2fe1db",
|
|
226
|
-
"schemas/extensions/base.schema.json": "
|
|
226
|
+
"schemas/extensions/base.schema.json": "49baa06a4ce8a6ce75fec52b650d9bf3566e5de0b1053b06f73a71ce103e4fdf",
|
|
227
227
|
"schemas/extensions/extractor.schema.json": "ee44bf562b19318c93116c574a811857cdef1f4119326a9a604fa408889dd230",
|
|
228
228
|
"schemas/extensions/formatter.schema.json": "880dc379ad545a62404403533a01eda5171edba0390561fc46ec6e986e0b9bd3",
|
|
229
229
|
"schemas/extensions/hook.schema.json": "f56aef59e9986ffdf7d86aa2e048dccccf217000a358b8c64737cbd911c48dad",
|
|
230
230
|
"schemas/extensions/provider-kind.schema.json": "499b2418bbe6d8a84a1608e26c56b52c2652a30ce314bc2989094418797dc1e6",
|
|
231
|
-
"schemas/extensions/provider.schema.json": "
|
|
231
|
+
"schemas/extensions/provider.schema.json": "75a565b8be6f1a08f0dbfec34e10c5d4d7c990489842bf338519a7d4b97dfe8f",
|
|
232
232
|
"schemas/frontmatter/base.schema.json": "cff81510ed94824dfd12ab8b30ce9fbac65e42d61ae0edf3fbb6bbb6bb8bcb8c",
|
|
233
233
|
"schemas/history-stats.schema.json": "436aa0ffe744bdb699000447e86b45724fbd2cc4642781074eb1527522b9058c",
|
|
234
234
|
"schemas/input-types.schema.json": "cc9739aedcfdc72c1fa1285547e1392f9582169d578d72737918099acc721de0",
|
|
@@ -236,15 +236,15 @@
|
|
|
236
236
|
"schemas/job.schema.json": "dbcedf137de03fde38f74686f594e600c627bf808f2aad23511a26617a663a02",
|
|
237
237
|
"schemas/link.schema.json": "10f700feb3e23d89453d4d11cbe559bcc0b31f6edf08fffbe9e6773e58120512",
|
|
238
238
|
"schemas/node.schema.json": "14ed2e4c44d01e3f662e240219819895cca06dead374a5cadccfd423c520ed69",
|
|
239
|
-
"schemas/plugins-doctor.schema.json": "
|
|
239
|
+
"schemas/plugins-doctor.schema.json": "2238266f31402a446b313af16f933e395a02eca70128e39ab99a11de90a4735f",
|
|
240
240
|
"schemas/plugins-registry.schema.json": "6d850d06cdf70e233f20d0d7968bb0c34306f11f30ce2505cec173cd9fa784e5",
|
|
241
|
-
"schemas/project-config.schema.json": "
|
|
241
|
+
"schemas/project-config.schema.json": "6bbec6252c215bcd7b86f5c7c267c8104d1c78633ede2e7fc1dabfd3fdcaa638",
|
|
242
242
|
"schemas/refresh-report.schema.json": "47184d4f6b15e9b7671dc178b3b3886a64422da198898508ecdb2cb27876db04",
|
|
243
243
|
"schemas/report-base-deterministic.schema.json": "59785fe6f3ceb34814bbbd03d10fa7336a32835ce598946f2923d469b32aa32a",
|
|
244
244
|
"schemas/report-base.schema.json": "e4d25f055e24f18ae0f77c24661c1bddc87ff2e43b001b6a827fcb14f9753f44",
|
|
245
|
-
"schemas/scan-result.schema.json": "
|
|
245
|
+
"schemas/scan-result.schema.json": "f3caf433a2db79ce44adf09993e0f8032b7230d53eb4472ba2913a39c524d045",
|
|
246
246
|
"schemas/sidecar.schema.json": "f23dfe3ba7f71a1af2cc5cd26b57d5e057e56438655f750c1895d35061efe80a",
|
|
247
|
-
"schemas/signal.schema.json": "
|
|
247
|
+
"schemas/signal.schema.json": "57baf52e55fc9a6f122fb9b33395b5a2790e7f5b7d461cf576099b68a8a17159",
|
|
248
248
|
"schemas/summaries/agent.schema.json": "5b26b95fb082b73d302c8aa6489ab09488a155ccfbb8943dfc47079509d35122",
|
|
249
249
|
"schemas/summaries/command.schema.json": "7f522c682d0fdf5a40172c7fc8fcd23e60a0ab0253354146525bd3a3d417f1f8",
|
|
250
250
|
"schemas/summaries/hook.schema.json": "6a1ceecda7a7173dfcd8b5f705d84be1792c4bb5a2269ff666088128c02c888a",
|
package/package.json
CHANGED
package/plugin-author-guide.md
CHANGED
|
@@ -12,7 +12,7 @@ This guide is **descriptive prose, not the normative contract**. The normative p
|
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
14
|
my-plugin/
|
|
15
|
-
├── plugin.json ←
|
|
15
|
+
├── plugin.json ← plugin metadata (required)
|
|
16
16
|
└── extractors/ ← one folder per extension kind
|
|
17
17
|
└── my-extractor/
|
|
18
18
|
├── index.js ← extension entry (required)
|
|
@@ -23,7 +23,7 @@ my-plugin/
|
|
|
23
23
|
The kernel auto-discovers extensions by walking
|
|
24
24
|
`<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}` for each known kind
|
|
25
25
|
(`providers`, `extractors`, `analyzers`, `actions`, `formatters`,
|
|
26
|
-
`hooks`). **The folder layout IS the source of truth**: the
|
|
26
|
+
`hooks`). **The folder layout IS the source of truth**: the plugin id comes from the
|
|
27
27
|
top-level dir, the kind from the subfolder name, the extension id from the
|
|
28
28
|
extension folder name. The manifest does NOT declare an
|
|
29
29
|
`extensions[]` array, and an extension file does NOT declare its own `id` or `kind`
|
|
@@ -105,7 +105,7 @@ The plugin `id` is the **directory name** (`<root>/<id>/plugin.json`), not a man
|
|
|
105
105
|
|
|
106
106
|
Every extension is identified in the registry, and in any cross-extension reference, by its **qualified id** `<plugin-id>/<extension-id>`. The plugin id (the directory name) is therefore also the **namespace** for every extension the plugin ships.
|
|
107
107
|
|
|
108
|
-
Concrete examples for the reference impl's
|
|
108
|
+
Concrete examples for the reference impl's built-in extensions:
|
|
109
109
|
|
|
110
110
|
| Extension | Short id (folder name) | Qualified id (in the registry) |
|
|
111
111
|
|---|---|---|
|
|
@@ -121,7 +121,7 @@ Concrete examples for the reference impl's bundled extensions:
|
|
|
121
121
|
Built-ins split between two namespaces:
|
|
122
122
|
|
|
123
123
|
- **`core/`**, kernel-internal primitives, platform-agnostic: every built-in analyzer, the ASCII formatter, the cross-vendor extractors (`annotations`, `markdown-link`, `external-url-counter`), the universal `markdown` Provider fallback, and the `update-check` hook.
|
|
124
|
-
- **`claude/`**, the Claude Code Provider
|
|
124
|
+
- **`claude/`**, the Claude Code Provider plugin: the Provider plus the Claude-flavoured extractors (`slash-command`, `at-directive`). Other vendor plugins (`antigravity`, `openai`, `agent-skills`) follow the same shape (Provider only).
|
|
125
125
|
|
|
126
126
|
### Extension id shape
|
|
127
127
|
|
|
@@ -131,18 +131,18 @@ Authors are not required to follow this, but it makes `sm plugins list` self-gro
|
|
|
131
131
|
|
|
132
132
|
### Toggle model
|
|
133
133
|
|
|
134
|
-
Every extension is independently toggle-able by its qualified id `<
|
|
134
|
+
Every extension is independently toggle-able by its qualified id `<plugin>/<ext-id>` (e.g. `claude/at-directive`, `core/node-superseded`). The **plugin row is a presentational grouping**, not the granular toggle target: the user sees a row per plugin in `sm plugins list` and the Settings UI, with each extension listed underneath with its own enabled / disabled state.
|
|
135
135
|
|
|
136
136
|
Two id shapes resolve at the toggle surface:
|
|
137
137
|
|
|
138
|
-
- **Qualified id** (`<
|
|
139
|
-
- **Bare
|
|
140
|
-
- Single-extension
|
|
141
|
-
- Multi-extension
|
|
138
|
+
- **Qualified id** (`<plugin>/<ext-id>`): flips exactly that extension. No prompt.
|
|
139
|
+
- **Bare plugin id** (`claude`, `core`): the **bundle (aggregate) macro form**, fans the toggle across every extension inside the plugin.
|
|
140
|
+
- Single-extension plugin (`openai`, `antigravity`, `agent-skills`): applies directly, no prompt.
|
|
141
|
+
- Multi-extension plugin (`claude`, `core`): requires `--yes` OR an interactive TTY confirm. CI / pipe contexts must pass `--yes`.
|
|
142
142
|
|
|
143
|
-
`--all` is the cascade variant: it expands to every extension in every discovered
|
|
143
|
+
`--all` is the cascade variant: it expands to every extension in every discovered plugin and applies the same `--yes` / TTY-confirm gate.
|
|
144
144
|
|
|
145
|
-
Resolution order per id: DB override (`config_plugins`) > `settings.json#/plugins/<id>/enabled` > installed default (`true`). Persisted toggle keys are always qualified `<
|
|
145
|
+
Resolution order per id: DB override (`config_plugins`) > `settings.json#/plugins/<id>/enabled` > installed default (`true`). Persisted toggle keys are always qualified `<plugin>/<ext>` ids (the bundle macro path expands at write time).
|
|
146
146
|
|
|
147
147
|
There is no `granularity` manifest field; per-extension toggling is the only model.
|
|
148
148
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"version": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"description": "Extension semver. REQUIRED for external (user-authored) plugins, the AJV check at load time rejects manifests missing it. Bumped independently from the plugin's
|
|
11
|
+
"description": "Extension semver. REQUIRED for external (user-authored) plugins, the AJV check at load time rejects manifests missing it. Bumped independently from the plugin's own version; frozen into `state_executions.extension_version` on every run for reproducibility. The reference CLI's built-in extensions under `src/plugins/` use a different authoring path (type `IBuiltInManifest<I<Kind>>` = `Omit<I<Kind>, 'version'>`); the codegen at `scripts/generate-built-ins.js` stamps the CLI version onto every built-in at build time so the runtime shape still satisfies the full interface."
|
|
12
12
|
},
|
|
13
13
|
"description": {
|
|
14
14
|
"type": "string",
|
|
@@ -93,6 +93,26 @@
|
|
|
93
93
|
"description": "Path globs (relative to scope root) that this Provider claims. **Enforcement-grade since structure-as-truth refactor**: a Provider declaring `roots` only receives files that match at least one entry of the array; a Provider without `roots` acts as a fallback and receives files unmatched by every other Provider's roots. Two Providers whose `roots` both match the same file produce a `provider-ambiguous` issue and the file stays unclassified. `sm plugins doctor` warns when no file matched a specific Provider's roots in the latest scan.",
|
|
94
94
|
"items": { "type": "string" }
|
|
95
95
|
},
|
|
96
|
+
"scaffold": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"required": ["skillDir"],
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"description": "Authoring targets for verbs that MATERIALISE files into this Provider's on-disk territory (today only `sm tutorial`, which drops a skill folder where the Provider's runtime will discover it). Distinct from `detect` (which READS markers to suggest a lens) and from `classify` (which READS paths during a scan): `scaffold` is the WRITE side, the directory a generator drops new content into so the target runtime picks it up. Optional: a Provider with no `scaffold` block is never offered as a destination by a materialising verb (e.g. `openai` until Codex skills land, `antigravity` whose skills live under the open-standard `agent-skills` territory, `core/markdown` which owns no authoring convention). The skill-folder convention is uniform across hosts (`<skillDir>/<name>/SKILL.md`), so a single `skillDir` is enough today; a future verb that scaffolds agents or commands adds a sibling field (`agentDir`, `commandDir`) without breaking this one.",
|
|
101
|
+
"properties": {
|
|
102
|
+
"skillDir": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"minLength": 1,
|
|
105
|
+
"pattern": "^\\.?[A-Za-z0-9][A-Za-z0-9._/-]*$",
|
|
106
|
+
"description": "Directory (relative to the scope root) under which a materialising verb writes a skill folder, e.g. `.claude/skills` for Claude, `.agents/skills` for the open standard. The verb appends `/<skillName>/SKILL.md`. Relative, no leading slash and no `..` traversal (the pattern forbids both); the consuming verb joins it onto the cwd."
|
|
107
|
+
},
|
|
108
|
+
"aka": {
|
|
109
|
+
"type": "array",
|
|
110
|
+
"minItems": 1,
|
|
111
|
+
"description": "Display-only hints naming the agents that consume this Provider's scaffold territory, shown in parentheses next to the Provider label in the `sm tutorial` destination prompt (e.g. the open-standard `.agents/skills` is read by Antigravity and OpenAI Codex, so its `agent-skills` Provider lists them here). Purely presentational: these strings are NOT matched by `--for` (only registered Provider ids are) and have no runtime effect. Optional; absent means the prompt shows the bare Provider label.",
|
|
112
|
+
"items": { "type": "string", "minLength": 1 }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
96
116
|
"gatedByActiveLens": {
|
|
97
117
|
"type": "boolean",
|
|
98
118
|
"description": "Lens gating flag for vendor providers. When `true`, this Provider's `classify()` only runs (and the walker only iterates its territory) if `provider.id === activeProvider` (the project's active lens). When `false` or omitted (default), the Provider is universal and classifies unconditionally. Vendor providers (`claude`, `openai`, `antigravity`) MUST set this to `true`: the actual runtimes never read each other's on-disk formats (Claude Code does not consume `.codex/`; Codex CLI does not consume `.claude/`), and offering every file to every provider fabricates cross-vendor graph edges the runtimes themselves reject. Universal providers (open-standard `agent-skills`, markdown fallback `core/markdown`, any future format-based fallback) keep this `false` so their territory is consumed by every vendor and they run on every scan. When `activeProvider === null` (no lens resolved), the walker bypasses the gate entirely and every gated Provider runs, mirroring the permissive extractor-side fallback for unlensed projects. Affects classification ONLY; extractors continue to filter via their own `precondition.provider` allowlist."
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"id": {
|
|
44
44
|
"type": "string",
|
|
45
45
|
"minLength": 1,
|
|
46
|
-
"description": "Qualified extension id `<
|
|
46
|
+
"description": "Qualified extension id `<plugin>/<ext>`. The bare plugin id form is reserved for the bundle (aggregate) macro path in CLI / BFF requests and never appears in persisted state."
|
|
47
47
|
},
|
|
48
48
|
"status": {
|
|
49
49
|
"type": "string",
|
|
@@ -11,22 +11,13 @@
|
|
|
11
11
|
"const": 1,
|
|
12
12
|
"description": "Config file shape version. Bumped on breaking changes to this schema."
|
|
13
13
|
},
|
|
14
|
-
"autoMigrate": {
|
|
15
|
-
"type": "boolean",
|
|
16
|
-
"description": "Apply pending kernel and plugin migrations automatically at startup, after auto-backing-up the DB. Default true. When false, startup fails with exit 2 if migrations are pending; the user runs `sm db migrate` manually."
|
|
17
|
-
},
|
|
18
14
|
"tokenizer": {
|
|
19
15
|
"type": "string",
|
|
20
16
|
"description": "Name of the offline tokenizer used to compute per-node token counts during scan. Default `cl100k_base`. Stored alongside each token count in `scan_nodes` so consumers know which encoder produced the numbers. Changing this invalidates prior counts on next scan."
|
|
21
17
|
},
|
|
22
|
-
"providers": {
|
|
23
|
-
"type": "array",
|
|
24
|
-
"description": "Provider ids to enable, in priority order when multiple match. Empty/absent = use all registered. Note: the `activeProvider` field selects ONE of these as the project's active lens; the rest stay enabled-but-inactive until the user switches.",
|
|
25
|
-
"items": { "type": "string" }
|
|
26
|
-
},
|
|
27
18
|
"activeProvider": {
|
|
28
19
|
"type": "string",
|
|
29
|
-
"description": "The active provider lens for this project. Exactly one provider id (from the
|
|
20
|
+
"description": "The active provider lens for this project. Exactly one provider id (from the registered providers) sees the project at any time. All extractors, classifiers, and resolution rules belonging to other providers are skipped during scan. Changing this triggers an atomic drop of the `scan_*` DB zone followed by a fresh scan under the new lens; `state_*` and `config_*` zones survive the switch. When absent on a fresh project, the kernel auto-detects from filesystem (presence of `.claude/`, `.codex/`, AGENTS.md, `.cursor/`, etc.) and prompts via the CLI / UI if the heuristic is ambiguous. Google's Antigravity CLI has no vendor-specific marker and is selected manually. Stability: experimental."
|
|
30
21
|
},
|
|
31
22
|
"activeProviderMarkers": {
|
|
32
23
|
"type": "array",
|
|
@@ -49,11 +40,10 @@
|
|
|
49
40
|
"properties": {
|
|
50
41
|
"tokenize": { "type": "boolean", "description": "Whether to compute token counts. Default true." },
|
|
51
42
|
"strict": { "type": "boolean", "description": "Promote frontmatter warnings to errors. Default false." },
|
|
52
|
-
"followSymlinks": { "type": "boolean", "description": "Default false." },
|
|
53
43
|
"maxFileSizeBytes": {
|
|
54
44
|
"type": "integer",
|
|
55
45
|
"minimum": 1,
|
|
56
|
-
"description": "Files larger than this are skipped
|
|
46
|
+
"description": "Files larger than this are skipped before they are read and surfaced at WARN level. Default 1048576 (1 MiB). Protects against scanning accidental binary drops or generated artefacts. Every skipped file is reported in `ScanResult.oversizedFiles` (root-relative path + byte size), counted in `ScanResult.stats.filesOversized`, printed as a terminal warning on `sm scan` / `sm watch` / `sm serve`, and raised as a UI banner. Trim the offending paths via `.skillmapignore` or raise this limit to include them."
|
|
57
47
|
},
|
|
58
48
|
"maxNodes": {
|
|
59
49
|
"type": "integer",
|
|
@@ -86,29 +76,7 @@
|
|
|
86
76
|
"type": "object",
|
|
87
77
|
"additionalProperties": false,
|
|
88
78
|
"properties": {
|
|
89
|
-
"enabled": { "type": "boolean" }
|
|
90
|
-
"config": { "type": "object", "description": "Plugin-specific config passed to extensions at load time. Shape defined by the plugin.", "additionalProperties": true },
|
|
91
|
-
"extensions": {
|
|
92
|
-
"type": "object",
|
|
93
|
-
"description": "Per-extension enable/disable overrides within this plugin. Keys are extension ids (the `id` declared by the extractor / analyzer / etc. inside the plugin). Absent = use the extension's manifest default (enabled). Allows a user to keep a provider plugin active but disable an individual extractor whose output is noisy in their project, without disabling the whole bundle.",
|
|
94
|
-
"additionalProperties": {
|
|
95
|
-
"type": "object",
|
|
96
|
-
"additionalProperties": false,
|
|
97
|
-
"properties": {
|
|
98
|
-
"enabled": { "type": "boolean" }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
"history": {
|
|
106
|
-
"type": "object",
|
|
107
|
-
"additionalProperties": false,
|
|
108
|
-
"properties": {
|
|
109
|
-
"share": {
|
|
110
|
-
"type": "boolean",
|
|
111
|
-
"description": "When true, `./.skill-map/skill-map.db` is expected to be committed, teams remove it from `.gitignore` so the execution log becomes a shared artefact. Stability: experimental. Default false."
|
|
79
|
+
"enabled": { "type": "boolean" }
|
|
112
80
|
}
|
|
113
81
|
}
|
|
114
82
|
},
|
|
@@ -152,14 +120,6 @@
|
|
|
152
120
|
}
|
|
153
121
|
}
|
|
154
122
|
},
|
|
155
|
-
"i18n": {
|
|
156
|
-
"type": "object",
|
|
157
|
-
"description": "Stability: experimental.",
|
|
158
|
-
"additionalProperties": false,
|
|
159
|
-
"properties": {
|
|
160
|
-
"locale": { "type": "string", "description": "BCP-47 tag. Default `en`." }
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
123
|
"allowEditSmFiles": {
|
|
164
124
|
"type": "boolean",
|
|
165
125
|
"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."
|
|
@@ -47,6 +47,26 @@
|
|
|
47
47
|
"minimum": 1,
|
|
48
48
|
"description": "Override applied via `--max-nodes <N>` on the verb that ran the scan (`sm scan`, `sm refresh`, `sm watch`), or `null` when no override was passed and the value above came from the setting. The override is bidirectional: it can raise the cap above the recommended limit (the UI banner stays visible until a re-scan lands below the recommended limit) or lower it (the banner also fires if `filesWalked` reaches the lowered override). Absent on synthetic fixtures."
|
|
49
49
|
},
|
|
50
|
+
"oversizedFiles": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"description": "Files the walker skipped because their on-disk size exceeded `scan.maxFileSizeBytes` (default 1 MiB). Reported so the CLI / serve terminal can warn and the UI can raise a banner. Each entry is the root-relative, forward-slash path (same form as `node.path`) plus the file's byte size. Skipped files are never read, parsed, or indexed as nodes. Empty when no file exceeded the limit; defaults to `[]`.",
|
|
53
|
+
"items": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"required": ["path", "bytes"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"path": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Root-relative, forward-slash path of the skipped file (same form as `node.path`)."
|
|
61
|
+
},
|
|
62
|
+
"bytes": {
|
|
63
|
+
"type": "integer",
|
|
64
|
+
"minimum": 0,
|
|
65
|
+
"description": "On-disk size of the skipped file, in bytes."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
50
70
|
"nodes": {
|
|
51
71
|
"type": "array",
|
|
52
72
|
"items": { "$ref": "node.schema.json" }
|
|
@@ -66,6 +86,7 @@
|
|
|
66
86
|
"properties": {
|
|
67
87
|
"filesWalked": { "type": "integer", "minimum": 0 },
|
|
68
88
|
"filesSkipped": { "type": "integer", "minimum": 0, "description": "Files walked but not classified by any Provider." },
|
|
89
|
+
"filesOversized": { "type": "integer", "minimum": 0, "description": "Files skipped before reading because their on-disk size exceeded `scan.maxFileSizeBytes`. Equals `oversizedFiles.length`. Absent on synthetic fixtures that bypass the walker." },
|
|
69
90
|
"nodesCount": { "type": "integer", "minimum": 0 },
|
|
70
91
|
"linksCount": { "type": "integer", "minimum": 0 },
|
|
71
92
|
"issuesCount": { "type": "integer", "minimum": 0 },
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
},
|
|
136
136
|
"extractorDisabled": {
|
|
137
137
|
"type": "object",
|
|
138
|
-
"description": "Reserved for Phase 4+: populated when every candidate of this Signal came from an extractor
|
|
138
|
+
"description": "Reserved for Phase 4+: populated when every candidate of this Signal came from an extractor the operator has disabled. The config surface that toggles individual extensions is not defined yet (the earlier `plugins.<id>.extensions.<extId>.enabled` placeholder was removed); it will be specified when the filter lands. Today the resolver never sets this; the field is documented so the analyzer / UI surface can be built once the filter lands.",
|
|
139
139
|
"required": ["extractorId"],
|
|
140
140
|
"additionalProperties": false,
|
|
141
141
|
"properties": {
|