Bulk-edit gate reference
When a mission's meta.json declares change_mode: bulk_edit, the planning
phase MUST author kitty-specs/<slug>/occurrence_map.yaml. The bulk-edit
gate validates that file before /spec-kitty.implement can claim the first
WP workspace.
This document is the schema reference for that file. For a tour of the
workflow and the human-side decisions, see the
spk-practice-bulk-edit skill.
Required schema
schema_version: "1.0"
mission: <mission-slug>
target:
term: "<the literal string being renamed>"
operation: <rename | remove>
replacement: "<the new literal string, or null if operation=remove>"
categories:
code_symbols:
action: <do_not_change | manual_review | rename | rename_if_user_visible>
# optional per-category fields...
import_paths:
action: ...
filesystem_paths:
action: ...
serialized_keys:
action: ...
cli_commands:
action: ...
user_facing_strings:
action: ...
tests_fixtures:
action: ...
logs_telemetry:
action: ...
# Optional. Narrows a category verdict for specific files, or for a single
# field inside them.
exceptions:
- path: "<file path or glob>"
action: <do_not_change | manual_review | rename | rename_if_user_visible>
reason: "<why this path deviates from its category verdict>"
# Optional. Dot-separated YAML field path, e.g. "specialization-context.languages".
field_path: "<field>"
All eight categories MUST be present. Omitting a category fails the gate; it is not a default-deny.
exceptions (optional)
Each entry requires path and action; reason and field_path are
optional. No other keys are accepted.
field_path scopes an exception to one field inside the files matching
path, instead of overriding the whole file's verdict. It exists for the case
where a governed file carries both fields that must not change and fields that
must — for example an agent profile whose directive-references must stay
fixed while specialization-context.languages in the same file migrates.
When field_path is present the file's category-level action still governs the
whole file; the reviewer-facing diff check additionally records that this one
field is pinned to action. It is a review-surface annotation, not an
enforcement mechanism — mission B2 in particular must not read a
do_not_change here as enforcement.
Omitting exceptions entirely, or omitting field_path within an entry,
leaves legacy single-term maps validating unchanged.
Top-level target: block
The target: block names the literal string being changed. It is required
when operation: rename; for operation: remove the replacement field
must be null. Both term and replacement are matched as exact
strings; word-boundary or case-insensitive matching is the agent's
responsibility per-category.
| Field | Type | Meaning |
|---|---|---|
term |
string | Old literal string. Required. |
operation |
enum | rename (term → replacement) or remove (term goes away). |
replacement |
string | null | New literal string. null only when operation: remove. |
Allowed action values
The bulk-edit gate recognises exactly four values for action per
category. Anything else fails the gate with unknown_action.
| Action | Meaning |
|---|---|
do_not_change |
Preserve as-is (historical text, dead-code-after-rename, snapshot artefacts, etc.). Files matched by this category MUST NOT be touched by the implementer. |
manual_review |
Implementer must inspect per-occurrence (e.g., dead-code that should be deleted, not renamed). The runtime does not auto-rename; the WP must add an explicit per-file decision and evidence. |
rename |
Mechanical s/old/new/ across the matched files. The implementer applies the substitution; the review gate verifies no instances of term survive in renamed files. |
rename_if_user_visible |
Rename only on public CLI surfaces / JSON keys / docs; preserve in internal text. The category-level rule typically encodes which sub-shapes count as "user visible". |
Category cookbook
The eight categories cover the standard surfaces touched by a rename. The
table below is the canonical mapping; deviating from it requires an explicit
per-mission exception with rationale captured in plan.md.
| Category | Typical action | Why |
|---|---|---|
code_symbols |
rename |
Class / function / variable identifiers — purely internal. |
import_paths |
rename |
Follows from code_symbols. |
filesystem_paths |
rename |
Module/package paths follow code_symbols. |
serialized_keys |
rename_if_user_visible |
JSON/YAML keys leaving the process boundary need renaming; in-memory dict keys often do not. |
cli_commands |
rename_if_user_visible |
User-typed surface — rename, but keep deprecated aliases per the agent's policy. |
user_facing_strings |
rename_if_user_visible |
Docs, help text, error messages. |
tests_fixtures |
manual_review |
Fixtures may encode the OLD name on purpose (regression captures) — review per-occurrence. |
logs_telemetry |
do_not_change |
Renaming log keys breaks downstream dashboards; keep the wire format stable unless explicitly versioned. |
Failure modes the gate detects
change_mode: bulk_editset, butoccurrence_map.yamlmissing.occurrence_map.yamlpresent, buttarget:block missing or malformed.target.operation: renamebutreplacementisnull.target.operation: removebutreplacementis non-null.- Fewer than eight categories declared.
- Any category with an
actionoutside the four-value vocabulary. - (review phase) A file the diff modified, deleted or renamed matches a
category whose action is
do_not_change, or matches no category at all (FR-008), with no per-file exception. A file the diff created is not judged against the map — see the next section.
Each failure prints a structured error starting with
Bulk Edit Gate: BLOCKED: (planning gate) or
Bulk Edit Review: Diff Compliance: (post-implementation gate). The triggering message also activates the
spk-practice-bulk-edit skill, which walks the agent through
remediation.
What the review gate exempts
The review-phase check reads git diff --name-status, so it knows whether a
path was created, modified, deleted or renamed. An occurrence map governs
occurrences in content that already exists, which is why these cases pass
without an exception. Every exemption is reported as a note, never silently.
| Case | Verdict | Why |
|---|---|---|
File created by the diff (A) |
pass, listed as created by this diff | No pre-existing occurrence for the map to protect. Deletions and renames are still judged; an unknown status is treated as a modification. Creating a file under another mission's kitty-specs/<slug>/ still blocks (C-004). |
The mission's own runtime state (status.json, status.events.jsonl, lifecycle logs) |
pass, listed as own bookkeeping | Written by spec-kitty itself during the review. Anchored on the canonical kitty-specs/<slug> whatever worktree the mission's feature_dir resolves to, and tested before the map's exceptions, so kitty-specs/**: do_not_change cannot pre-empt it. |
Files that cannot carry a textual occurrence: webfonts, raster images, audio/video, compiled objects, generated lockfiles the diff creates (Gemfile.lock, uv.lock, yarn.lock, …), .keep/.gitkeep |
pass, listed as no occurrences | Not text, or resolver output regenerated rather than edited. A lockfile the diff edits is not exempt, because an edit can rename a package. .svg is not exempt either: it is XML text and carries ids and classes. Opaque data files (.dat, .bin, .db, .sqlite, .pkl, archives) are deliberately not exempt: they can carry serialized keys, so they block as unclassified until the map names them. |
An explicit exceptions: entry always wins over the last row: a reviewer who
names a binary keeps whatever action they chose.
Two behaviours are deliberate, so plan your map around them:
- Creating a file is exempt even under a protected path. A file the work
package creates passes even when an explicit
do_not_changecategory, path or exception covers it. This gate cannot forbid creating files in a protected tree; it only judges edits, deletions and renames there. - Own bookkeeping outranks the map's exceptions. The mission's own
notes.mdandreview-cycle-*.mdfiles, under the mission's own directory, are exempt before the map's exceptions are consulted. An exception such askitty-specs/**: do_not_changedoes not block them.
Templates, stylesheets and build DSLs (.erb, .haml, .slim, .heex,
.vue, .svelte, .css, .scss, .rake, Rakefile, Gemfile,
.gemspec) classify as code_symbols; container recipes (Dockerfile,
Containerfile, compose files) classify as filesystem_paths. A template
under docs/ still reads as user_facing_strings.
When the check fails, the table lists the rows that block or ask for manual review and summarises the rest by the reason they passed. When it passes with notes, the notes are grouped by kind, judgement-first: paths needing manual review and structural-target bypasses are listed in full; created files, non-occurrence files and own bookkeeping are shown as a count plus a sample.
See also
- The skill
spk-practice-bulk-edit(loaded automatically whenmeta.jsondeclareschange_mode: bulk_edit). - Existing missions that exercised this gate:
kitty-specs/charter-ux-and-org-pack-vocabulary-01KSAF14/occurrence_map.yaml,kitty-specs/release-3-2-0a5-tranche-1-01KQ7YXH/occurrence_map.yaml.