bmad-module-skill-forge 1.0.0 → 1.1.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/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +5 -2
- package/docs/{RELEASING.md → _internal/RELEASING.md} +87 -61
- package/docs/{STABILITY.md → _internal/STABILITY.md} +2 -2
- package/package.json +1 -6
- package/release-audits/v1.0.0-launch-audit.md +313 -2
- package/src/skf-audit-skill/steps-c/step-01-init.md +60 -0
- package/src/skf-audit-skill/steps-c/step-02-re-index.md +7 -1
- package/src/skf-audit-skill/steps-c/step-03-structural-diff.md +19 -1
- package/src/skf-audit-skill/steps-c/step-04-semantic-diff.md +10 -1
- package/src/skf-audit-skill/steps-c/step-05-severity-classify.md +8 -0
- package/src/skf-audit-skill/steps-c/step-06-report.md +8 -0
- package/src/skf-brief-skill/assets/skill-brief-schema.md +36 -18
- package/src/skf-drop-skill/steps-c/step-02-execute.md +1 -1
- package/src/skf-export-skill/assets/managed-section-format.md +1 -1
- package/src/skf-export-skill/steps-c/step-04-update-context.md +4 -4
- package/src/skf-rename-skill/steps-c/step-02-execute.md +1 -1
- package/src/skf-test-skill/steps-c/step-03-coverage-check.md +1 -1
- package/src/skf-update-skill/steps-c/step-02-detect-changes.md +123 -0
- package/tools/validate-docs-drift.js +42 -2
|
@@ -67,14 +67,23 @@ scope:
|
|
|
67
67
|
# - "code/core/src/manager-api/**"
|
|
68
68
|
# - "code/core/src/preview-api/**"
|
|
69
69
|
notes: "Optional notes about scope decisions"
|
|
70
|
-
# Optional: amendment log for scope decisions made during create-skill §2a
|
|
70
|
+
# Optional: amendment log for scope decisions made during create-skill §2a,
|
|
71
|
+
# update-skill §1b (auth-doc), and update-skill §1c (scope-expansion).
|
|
71
72
|
# amendments:
|
|
72
73
|
# - path: "apps/docs/public/llms.txt"
|
|
73
|
-
# action: "promoted" # "promoted" | "skipped"
|
|
74
|
+
# action: "promoted" # "promoted" | "skipped" | "demoted-include" | "demoted-exclude"
|
|
75
|
+
# category: "auth-doc" # "auth-doc" (default for legacy entries) | "scope-expansion"
|
|
74
76
|
# reason: "authoritative AI docs — only source for canonical install command"
|
|
75
|
-
# heuristic: "llms.txt"
|
|
77
|
+
# heuristic: "llms.txt" # required for auth-doc; absent for scope-expansion
|
|
76
78
|
# date: "2026-04-11"
|
|
77
79
|
# workflow: "skf-create-skill"
|
|
80
|
+
# - path: "python/cocoindex/_internal/api.py"
|
|
81
|
+
# action: "promoted"
|
|
82
|
+
# category: "scope-expansion"
|
|
83
|
+
# reason: "out-of-scope new public API — drift report drift-report-20260424-212355.md"
|
|
84
|
+
# evidence: "~70 new exports flagged out-of-scope by audit"
|
|
85
|
+
# date: "2026-04-25"
|
|
86
|
+
# workflow: "skf-update-skill"
|
|
78
87
|
# Additional fields when scope.type is "component-library":
|
|
79
88
|
# registry_path: "path/to/registry.ts" # Optional — auto-detected if omitted
|
|
80
89
|
# ui_variants: # Optional — design system variants
|
|
@@ -87,37 +96,46 @@ scope:
|
|
|
87
96
|
|
|
88
97
|
### Scope Amendments (Optional)
|
|
89
98
|
|
|
90
|
-
`scope.amendments[]` is an additive, optional audit log of scope decisions made by workflows after the brief was first authored.
|
|
99
|
+
`scope.amendments[]` is an additive, optional audit log of scope decisions made by workflows after the brief was first authored. Two writer paths exist today:
|
|
100
|
+
|
|
101
|
+
- **Auth-doc promotions** (`category: "auth-doc"`) — `skf-create-skill` §2a and its mirror `skf-update-skill` §1b append entries when extraction discovers authoritative AI documentation files (`llms.txt`, `AGENTS.md`, etc.) that the original scope patterns excluded.
|
|
102
|
+
- **Scope-expansion promotions** (`category: "scope-expansion"`) — `skf-update-skill` §1c appends entries when an audit drift report flags out-of-scope new public API paths (typically a major-version restructure where the brief's `scope.include` no longer reflects the real surface).
|
|
91
103
|
|
|
92
104
|
**Entry fields:**
|
|
93
105
|
|
|
94
106
|
| Field | Type | Required | Description |
|
|
95
107
|
|---|---|---|---|
|
|
96
|
-
| `path` | string | yes | Relative path from source root to the file being amended.
|
|
97
|
-
| `action` | string | yes | One of: `promoted` (
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
108
|
+
| `path` | string | yes | Relative path (or glob, for `category: "scope-expansion"`) from source root to the file or tree being amended. For `promoted` actions this matches the literal entry added to `scope.include`. |
|
|
109
|
+
| `action` | string | yes | One of: `promoted` (path added to `scope.include`), `skipped` (user declined promotion; decision recorded to prevent re-prompting), `demoted-include` (path removed from `scope.include` — only valid with `category: "scope-expansion"`), `demoted-exclude` (path removed from `scope.exclude` — only valid with `category: "scope-expansion"`). |
|
|
110
|
+
| `category` | string | no | One of: `auth-doc` (default for entries without this field — the historical sole use case), `scope-expansion`. Distinguishes which workflow path wrote the entry and which writer-rules apply on re-runs. |
|
|
111
|
+
| `reason` | string | yes | Human-readable sentence explaining the decision. Either user-provided at prompt time or auto-generated. |
|
|
112
|
+
| `heuristic` | string | conditional | Required for `category: "auth-doc"` — the basename that matched (`llms.txt`, `AGENTS.md`, etc.). Omit for `category: "scope-expansion"`. |
|
|
113
|
+
| `evidence` | string | conditional | Required for `category: "scope-expansion"` — short rationale from the source signal (e.g., a drift-report finding's evidence one-liner). Omit for `category: "auth-doc"`. |
|
|
100
114
|
| `date` | string | yes | ISO date (`YYYY-MM-DD`) when the amendment was recorded. |
|
|
101
|
-
| `workflow` | string | yes | Workflow name that wrote the amendment (`skf-create-skill`, `skf-update-skill`). Identifies which workflow
|
|
115
|
+
| `workflow` | string | yes | Workflow name that wrote the amendment (`skf-create-skill`, `skf-update-skill`). Identifies which workflow made the decision. |
|
|
116
|
+
|
|
117
|
+
**Promotion write-through:** When `action: "promoted"`, the workflow also appends the literal path to `scope.include`. This is a belt-and-suspenders design: future runs read `scope.include` during scope filtering and include the file in the filtered list automatically, so the §2a/§1b/§1c discovery loop finds no candidate and does not re-prompt. The `amendments[]` entry is the human-readable audit trail of *why* the path was added.
|
|
102
118
|
|
|
103
|
-
**
|
|
119
|
+
**Skip recording:** When `action: "skipped"`, the workflow does NOT modify `scope.include` or `scope.exclude`. The amendment entry alone is enough to prevent re-prompting, because the discovery loop checks `amendments[]` before prompting.
|
|
104
120
|
|
|
105
|
-
**
|
|
121
|
+
**Demotion (scope-expansion only):** `demoted-include` removes a previously-promoted path from `scope.include` — used when a prior `[P]` decision is reversed. `demoted-exclude` removes a path from `scope.exclude` — used when a previously excluded path needs to be re-evaluated. Both write the structural change and append the amendment so future runs see the rationale. Demotion is not valid for `category: "auth-doc"`: auth-doc skips already prevent re-prompting without scope mutation.
|
|
106
122
|
|
|
107
|
-
**Backward compatibility:** `scope.amendments` is optional. Briefs without this field validate unchanged. Treat missing as an empty list.
|
|
123
|
+
**Backward compatibility:** `scope.amendments` is optional. Briefs without this field validate unchanged. Treat missing as an empty list. Existing entries without `category` are equivalent to `category: "auth-doc"` — readers must default the field when absent.
|
|
108
124
|
|
|
109
125
|
**Who reads `amendments[]`:**
|
|
110
126
|
|
|
111
|
-
- `skf-create-skill` §2a consults it to avoid re-prompting on decided files.
|
|
112
|
-
- `skf-update-skill` §1b (mirror of §2a) consults it for the same reason.
|
|
113
|
-
- `skf-
|
|
127
|
+
- `skf-create-skill` §2a consults it to avoid re-prompting on decided auth-doc files.
|
|
128
|
+
- `skf-update-skill` §1b (mirror of §2a) consults it for the same auth-doc reason.
|
|
129
|
+
- `skf-update-skill` §1c consults it to avoid re-prompting on decided scope-expansion candidates and to honor prior `demoted-*` decisions.
|
|
130
|
+
- `skf-audit-skill` may optionally report on stale promotions (promoted paths that no longer exist in source) as a future enhancement — not currently implemented.
|
|
114
131
|
- Humans reading the brief see the audit trail of non-obvious scope decisions.
|
|
115
132
|
|
|
116
133
|
**Who writes `amendments[]`:**
|
|
117
134
|
|
|
118
|
-
- `skf-create-skill` §2a (Discovered Authoritative Files Protocol)
|
|
119
|
-
- `skf-update-skill` §1b (mirror of §2a applied during change detection)
|
|
120
|
-
-
|
|
135
|
+
- `skf-create-skill` §2a (Discovered Authoritative Files Protocol) — `category: "auth-doc"`
|
|
136
|
+
- `skf-update-skill` §1b (mirror of §2a applied during change detection) — `category: "auth-doc"`
|
|
137
|
+
- `skf-update-skill` §1c (Major-Version Scope Reconciliation) — `category: "scope-expansion"`
|
|
138
|
+
- Manual edits by the brief author are permitted but should include all required fields above (and `category` when the entry is not an auth-doc decision).
|
|
121
139
|
|
|
122
140
|
## YAML Template
|
|
123
141
|
|
|
@@ -95,7 +95,7 @@ For each entry in `target_context_files`:
|
|
|
95
95
|
|
|
96
96
|
5. **Rewrite root paths for the current context file** using the generic rewrite algorithm from export-skill step-04 section 4d:
|
|
97
97
|
|
|
98
|
-
For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the
|
|
98
|
+
For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the **effective target prefix** if different. The effective target prefix is `snippet_skill_root_override` when that key is set in config.yaml — applied uniformly to every snippet so the managed section references the real on-disk location and never mixes override and per-IDE paths — otherwise the current entry's `skill_root`. See `skf-export-skill/steps-c/step-04-update-context.md` §4d for full semantics.
|
|
99
99
|
|
|
100
100
|
6. **Sort skills alphabetically by name.** Count totals (skills, stack skills).
|
|
101
101
|
|
|
@@ -71,7 +71,7 @@ All other IDEs use AGENTS.md as the context file, each with its own skill direct
|
|
|
71
71
|
**`snippet_skill_root_override` (optional):** Authoring repos where all skills live under one shared on-disk directory (e.g. `skills/`) that does not match any per-IDE skill root may set `snippet_skill_root_override: skills/` in `config.yaml`. When set:
|
|
72
72
|
|
|
73
73
|
- `export-skill/step-03` §2.7 uses the override as `{skill_root}` for snippet generation instead of the IDE-mapped value
|
|
74
|
-
- `export-skill/step-04` §4d (and its equivalents in `drop-skill/step-02` and `rename-skill/step-02`)
|
|
74
|
+
- `export-skill/step-04` §4d (and its equivalents in `drop-skill/step-02` and `rename-skill/step-02`) treat the override as the effective target prefix for the managed-section rebuild: snippets whose prefix already matches the override pass through unchanged, and any other prefix — including per-IDE prefixes carried by sibling snippets that were exported before the override was adopted, and legacy `skills/` drafts — is rewritten to the override so the rebuilt managed section uniformly references the real on-disk location
|
|
75
75
|
|
|
76
76
|
Consuming projects (the common case) omit the field and keep the default IDE-mapping behavior. The override is a narrow escape hatch for repos that author skills into `{skills_output_folder}` and never duplicate them into `.claude/skills/`-style directories.
|
|
77
77
|
|
|
@@ -166,11 +166,11 @@ For each snippet being included in the managed section:
|
|
|
166
166
|
|
|
167
167
|
1. Read the `root:` value from the snippet's first line — it has the form `root: {prefix}{skill-name}/`
|
|
168
168
|
2. Extract the current prefix by stripping the trailing `{skill-name}/` from the root value
|
|
169
|
-
3. **
|
|
170
|
-
4.
|
|
171
|
-
5. If they differ, replace the prefix with the target
|
|
169
|
+
3. **Resolve the effective target prefix:** If `snippet_skill_root_override` is set in config.yaml, use the override value as the effective target prefix for this rebuild — by the override contract every skill in the repo lives under that single shared on-disk directory, so the managed section must reference the override for *every* snippet (not just the ones that already match it). Otherwise, use the current target's `skill_root` as the effective target prefix.
|
|
170
|
+
4. Compare the extracted prefix against the effective target prefix
|
|
171
|
+
5. If they differ, replace the prefix with the effective target prefix, preserving the skill name. When the override is set, this rewrites stale per-IDE prefixes carried by sibling snippets that were exported before the override was adopted, so the rebuilt managed section uniformly references the real on-disk location instead of mixing override and per-IDE paths.
|
|
172
172
|
6. Example: if snippet has `root: .claude/skills/my-lib/` but target skill_root is `.windsurf/skills/`, rewrite to `root: .windsurf/skills/my-lib/`
|
|
173
|
-
7. Example: if
|
|
173
|
+
7. Example: if `snippet_skill_root_override: skills/` is set, the effective target prefix is `skills/` regardless of the IDE mapping. A snippet with `root: skills/my-lib/` passes through unchanged; a sibling snippet with `root: .claude/skills/my-other/` (carried over from a pre-override export) is rewritten to `root: skills/my-other/`.
|
|
174
174
|
|
|
175
175
|
This algorithm handles any IDE's skill root path — including future IDEs — without enumerating known prefixes. The legacy `skills/` prefix (no leading dot) may appear in draft snippets generated by create-skill/quick-skill before export.
|
|
176
176
|
|
|
@@ -199,7 +199,7 @@ For each entry in `target_context_files`:
|
|
|
199
199
|
|
|
200
200
|
4. **Rewrite root paths for the current context file** using the generic rewrite algorithm from export-skill step-04 section 4d:
|
|
201
201
|
|
|
202
|
-
For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the
|
|
202
|
+
For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the **effective target prefix** if different. The effective target prefix is `snippet_skill_root_override` when that key is set in config.yaml — applied uniformly to every snippet so the managed section references the real on-disk location and never mixes override and per-IDE paths — otherwise the current entry's `skill_root`. See `skf-export-skill/steps-c/step-04-update-context.md` §4d for full semantics.
|
|
203
203
|
|
|
204
204
|
5. **Sort skills alphabetically by name.** Count totals (skills, stack skills).
|
|
205
205
|
|
|
@@ -79,7 +79,7 @@ test-skill is a quality gate — it MUST NOT trust subagent output blindly. Befo
|
|
|
79
79
|
|
|
80
80
|
**Schema validation (required keys + types):**
|
|
81
81
|
|
|
82
|
-
1.
|
|
82
|
+
1. Strip wrapping markdown fences before parsing. Subagents frequently return JSON wrapped in a code fence — a line of three backticks (optionally followed by a language tag like `json`) preceding the JSON and a closing line of three backticks after it — despite prompt instructions to return raw JSON. When the first non-empty line of the response is three backticks (optionally with a language tag) and the last non-empty line is three backticks, remove those two fence lines before parsing. Then parse the remaining content as JSON. On parse failure of the inner content → HALT "coverage-check: subagent response not valid JSON".
|
|
83
83
|
2. Required keys present: `exports` (list), `cross_check_mismatches` (list — may be empty). Missing key or wrong type → HALT "coverage-check: subagent JSON schema invalid — missing/typo: {key}". Note: the parent already knows the skill name from workflow context (`{resolved_skill_package}` from step-01) — the subagent is not required to echo it back, and doing so introduces a contract-drift surface without improving verification.
|
|
84
84
|
3. Each `exports[]` entry must be a dict with at minimum `name` (non-empty string) and `kind` (one of `function|class|type|constant|hook|interface|method`). Reject entries violating this; if >0 rejections, HALT "coverage-check: subagent returned malformed export entries — {count} entries do not match schema".
|
|
85
85
|
4. `cross_check_mismatches[]` entries (when non-empty) must carry `export`, `skill_md_line`, `reference_file`, `reference_line`, `issue`. Missing fields → HALT.
|
|
@@ -131,6 +131,80 @@ Read the source directory at `{source_root}` and build a current file inventory:
|
|
|
131
131
|
|
|
132
132
|
**Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, NOT in the change manifest. But §2 Category A ("files in source but not in provenance map → ADDED") would still find the promoted doc files on disk and classify them as ADDED if nothing prevents it. The coordination mechanism is an explicit pre-filter exclusion set built in §2.0 (below) that every Category A subprocess worker receives as an input before it starts scanning. See §2.0 for the exact contract. The exclusion set is the only mechanism guaranteeing that parallel subprocesses cannot double-count `promoted_docs_new[]` paths — prose-level "skip any path" instructions cannot cross subprocess boundaries.
|
|
133
133
|
|
|
134
|
+
### 1c. Major-Version Scope Reconciliation (Pre-Detection)
|
|
135
|
+
|
|
136
|
+
**Purpose:** When upstream undergoes a paradigm shift (rebrand, package restructure, major-version rewrite), the brief's `scope.include` no longer reflects the real public API. §1b handles new authoritative-doc files; §1c handles new **code globs** that fall outside the original scope. Without it, update-skill silently misses the new surface and pays the gap cost on every future update — the cocoindex `0.3.37 → 1.0.0` and cognee `0.5.8 → 1.0.0` runs are existence proofs that this case is real and recurring.
|
|
137
|
+
|
|
138
|
+
**Skip this section entirely if:**
|
|
139
|
+
|
|
140
|
+
- `update_mode == "gap-driven"` (test-report mode — source hasn't drifted), OR
|
|
141
|
+
- `metadata.json.source_type == "docs-only"` (no source tree to scope), OR
|
|
142
|
+
- No audit drift report is available at the path computed in step 1 below.
|
|
143
|
+
|
|
144
|
+
**Procedure:**
|
|
145
|
+
|
|
146
|
+
1. **Locate the most recent audit drift report.** Search `{forge_data_folder}/{skill_name}/{baseline_version}/drift-report-*.md`, sorted by timestamp descending. Pick the latest. If none found, **skip §1c entirely** — the post-detection deletion-ratio trigger in §2.2 still catches major restructures even without an audit pass.
|
|
147
|
+
|
|
148
|
+
2. **Parse the report for an "Out-of-Scope Observations" section.** Look for either a top-level `## Out-of-Scope Observations` heading or a `### Out-of-Scope New Public API` subsection under `## Remediation Suggestions`. Each entry must expose:
|
|
149
|
+
|
|
150
|
+
- `path` — literal file path or directory glob (e.g., `python/cocoindex/_internal/api.py` or `python/cocoindex/resources/**`)
|
|
151
|
+
- `evidence` — short one-liner from the report (export count, rationale)
|
|
152
|
+
|
|
153
|
+
**Note:** This section is an optional audit-skill output. `skf-audit-skill` does not currently discover new files (per `src/skf-audit-skill/steps-c/step-02-re-index.md` — new-file detection is the responsibility of update-skill). The section is a forward-looking integration point: manual additions to the drift report or a future audit-skill enhancement populate it. If absent, no candidates from this trigger — proceed to step 6.
|
|
154
|
+
|
|
155
|
+
3. **Reconcile against existing amendments.** For each candidate, consult `brief.scope.amendments[]`:
|
|
156
|
+
|
|
157
|
+
- `action: "promoted"` AND `path` matches → already in scope, skip silently.
|
|
158
|
+
- `action: "skipped"` AND `path` matches → user previously declined, honor silently.
|
|
159
|
+
- `action: "demoted-include"` or `action: "demoted-exclude"` AND `path` matches → user previously narrowed scope on this path, do not re-prompt; record as `pre_decided`.
|
|
160
|
+
- No matching amendment → continue to user prompt.
|
|
161
|
+
|
|
162
|
+
4. **Prompt for each unresolved candidate.** Present the same menu shape as §1b:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
**Out-of-scope new public API discovered**
|
|
166
|
+
|
|
167
|
+
Path: {candidate.path}
|
|
168
|
+
Evidence: {evidence from drift report}
|
|
169
|
+
Drift report: {report relative path}
|
|
170
|
+
|
|
171
|
+
This path was not in the brief's `scope.include` when the skill was created. How should update-skill handle it?
|
|
172
|
+
|
|
173
|
+
[P] Promote — add to scope.include AND extract in this run
|
|
174
|
+
[S] Skip — leave out of scope AND record skip in amendments (no re-prompt)
|
|
175
|
+
[U] Update — halt this run and return to skf-brief-skill to refine scope
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
5. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate — record `action: "skipped"`, `category: "scope-expansion"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently expand scope.
|
|
179
|
+
|
|
180
|
+
6. **Apply decision:**
|
|
181
|
+
|
|
182
|
+
- **[P] Promote:**
|
|
183
|
+
1. Append `candidate.path` to `brief.scope.include` as a literal glob (preserve any wildcards from the drift report).
|
|
184
|
+
2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "out-of-scope new public API — drift report {report basename}"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
|
|
185
|
+
3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
|
|
186
|
+
4. Display: `"Promoted {path} — brief amended; §2 Category A will pick up matching files as ADDED."`
|
|
187
|
+
5. **No `promoted_docs_new[]` entry and no `change_detection_excludes` write** — promoted code globs flow through the standard §2 Category A → §3 extraction path, unlike §1b's promoted docs which bypass extraction.
|
|
188
|
+
|
|
189
|
+
- **[S] Skip:**
|
|
190
|
+
1. Do NOT modify `scope.include` or `scope.exclude`.
|
|
191
|
+
2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1c"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
|
|
192
|
+
3. **Write the amended brief back to disk** so neither §1c nor a future run will re-prompt.
|
|
193
|
+
4. Display: `"Skipped {path} — decision recorded in amendments."`
|
|
194
|
+
|
|
195
|
+
- **[U] Update:**
|
|
196
|
+
1. Halt the workflow immediately.
|
|
197
|
+
2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
|
|
198
|
+
3. Exit with status `halted-for-brief-refinement`. Change manifest is not yet built — no partial writes to provenance.
|
|
199
|
+
|
|
200
|
+
7. **Summary:** After all candidates are resolved (or none were found):
|
|
201
|
+
|
|
202
|
+
- `"Scope reconciliation: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments."`
|
|
203
|
+
- If N = 0 (section absent or empty): `"Scope reconciliation: no out-of-scope observations in drift report."`
|
|
204
|
+
- If §1c was skipped entirely (no drift report): omit this line; §2.2 will still run.
|
|
205
|
+
|
|
206
|
+
**Record for evidence report:** the update-skill evidence report appends `scope_reconciliation_pre: {drift_report: path, candidates: N, promoted: P, skipped: S, pre_decided: A, decisions: [{path, action, evidence}]}` (omit when §1c was skipped).
|
|
207
|
+
|
|
134
208
|
### 2. Compare Against Provenance Map
|
|
135
209
|
|
|
136
210
|
**If normal mode (provenance map available):**
|
|
@@ -183,6 +257,55 @@ Aggregate all subprocess results into a unified change manifest.
|
|
|
183
257
|
- All exports will be fully re-extracted in step 03
|
|
184
258
|
- Skip export-level comparison
|
|
185
259
|
|
|
260
|
+
#### 2.2 — Major-Version Scope Reconciliation (Post-Detection)
|
|
261
|
+
|
|
262
|
+
**Purpose:** §1c catches the major-version case when an audit drift report supplies explicit candidates. §2.2 is the safety net that fires when no audit was run (or audit emitted no out-of-scope section): it inspects the just-built Category A/B results for the deletion-ratio signature of a major-version restructure and gives the user an off-ramp before §3 commits the change manifest.
|
|
263
|
+
|
|
264
|
+
**Skip this section entirely if:**
|
|
265
|
+
|
|
266
|
+
- `update_mode == "gap-driven"` (test-report mode), OR
|
|
267
|
+
- `degraded_mode == true` (no provenance baseline to compare against — every export looks "modified", deletion ratio is meaningless), OR
|
|
268
|
+
- Provenance map's tracked export count is zero.
|
|
269
|
+
|
|
270
|
+
**Trigger:** Compute
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
deleted_export_count = (sum of exports across Category A DELETED files)
|
|
274
|
+
+ (DELETED_EXPORT count from Category B)
|
|
275
|
+
total_provenance_exports = provenance_map.entries.length
|
|
276
|
+
deletion_ratio = deleted_export_count / total_provenance_exports
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If `deletion_ratio >= 0.50`, present the prompt below. Otherwise skip §2.2 silently and continue to §3.
|
|
280
|
+
|
|
281
|
+
**Prompt:**
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
**Major-version scope shift detected**
|
|
285
|
+
|
|
286
|
+
Deleted exports: {deleted_export_count} of {total_provenance_exports} ({percent}%)
|
|
287
|
+
Deleted files: {deleted_file_count}
|
|
288
|
+
Added files (in scope): {added_in_scope_count}
|
|
289
|
+
Renamed/moved exports: {renamed_or_moved_count}
|
|
290
|
+
|
|
291
|
+
The upstream surface appears to have been substantially replaced. The brief's
|
|
292
|
+
`scope.include` patterns may no longer reflect the real public API.
|
|
293
|
+
|
|
294
|
+
[C] Continue — proceed with re-extraction; the deletion is intentional
|
|
295
|
+
[B] Brief — halt and re-run skf-brief-skill to refine scope first
|
|
296
|
+
[A] Audit — halt and run skf-audit-skill to map the new surface, then re-run update-skill
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Headless mode (`{headless_mode}` is true):** auto-select `[C] Continue`, log a WARN-level entry to the evidence report (`scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "headless-continue"}`), and surface the warning in step-07's report. A non-interactive run must not silently halt, but the user must be able to see the signal post-hoc.
|
|
300
|
+
|
|
301
|
+
**Apply decision:**
|
|
302
|
+
|
|
303
|
+
- **[C] Continue:** record `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "continue"}` and proceed to §3.
|
|
304
|
+
- **[B] Brief:** halt with status `halted-for-brief-refinement`. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` Change manifest discarded — no partial writes.
|
|
305
|
+
- **[A] Audit:** halt with status `halted-for-audit`. Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface — its drift report will feed §1c on the next update-skill run."` Change manifest discarded.
|
|
306
|
+
|
|
307
|
+
**Why both §1c and §2.2:** §1c is precise (per-path P/S/U) but requires upstream signal from audit-skill. §2.2 is coarse (single halt/continue) but self-contained — it fires even when the user runs update-skill directly without audit. Together they cover the major-version case across the two real workflows.
|
|
308
|
+
|
|
186
309
|
### 3. Build Change Manifest
|
|
187
310
|
|
|
188
311
|
Compile the change manifest with structured entries:
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* every version number, commit SHA, and library reference the docs cite.
|
|
6
6
|
*
|
|
7
7
|
* What it checks:
|
|
8
|
+
* - The anchor `skf_version` matches `package.json.version` (self-reference invariant
|
|
9
|
+
* declared in pinned.yaml's header; enforced here at CI/pre-commit time AND bumped
|
|
10
|
+
* by release.yaml on every release)
|
|
8
11
|
* - Every skill listed in `docs/_data/pinned.yaml` actually exists at the
|
|
9
12
|
* claimed version and commit in $OMS/skills/<name>/<version>/<name>/metadata.json
|
|
10
13
|
* - The metadata.json `version` field matches the anchor `version`
|
|
@@ -32,6 +35,7 @@ const yaml = require('js-yaml');
|
|
|
32
35
|
const SKF_ROOT = path.resolve(__dirname, '..');
|
|
33
36
|
const DOCS_DIR = path.join(SKF_ROOT, 'docs');
|
|
34
37
|
const ANCHORS_PATH = path.join(DOCS_DIR, '_data', 'pinned.yaml');
|
|
38
|
+
const PACKAGE_JSON_PATH = path.join(SKF_ROOT, 'package.json');
|
|
35
39
|
|
|
36
40
|
function loadAnchors() {
|
|
37
41
|
if (!fs.existsSync(ANCHORS_PATH)) {
|
|
@@ -46,6 +50,38 @@ function loadAnchors() {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function checkSkfVersionAgainstPackageJson(anchors) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (!fs.existsSync(PACKAGE_JSON_PATH)) {
|
|
56
|
+
errors.push(`CRITICAL: package.json not found at ${PACKAGE_JSON_PATH}`);
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
59
|
+
let pkg;
|
|
60
|
+
try {
|
|
61
|
+
pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
errors.push(`CRITICAL: could not parse ${PACKAGE_JSON_PATH}: ${error.message}`);
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
const anchorVersion = anchors.skf_version;
|
|
67
|
+
const packageVersion = pkg.version;
|
|
68
|
+
if (!anchorVersion) {
|
|
69
|
+
errors.push(`MISSING_ANCHOR: pinned.yaml has no skf_version key`);
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
if (!packageVersion) {
|
|
73
|
+
errors.push(`MISSING_PKG_VERSION: package.json has no version field`);
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
76
|
+
if (anchorVersion !== packageVersion) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`SKF_VERSION_DRIFT: pinned.yaml skf_version "${anchorVersion}" !== package.json version "${packageVersion}" ` +
|
|
79
|
+
`— bump pinned.yaml or check that release.yaml's "Update pinned.yaml skf_version" step fired`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
|
84
|
+
|
|
49
85
|
function resolveOmsPath(anchors) {
|
|
50
86
|
if (process.env.OMS) {
|
|
51
87
|
return path.resolve(process.env.OMS);
|
|
@@ -172,7 +208,11 @@ function main() {
|
|
|
172
208
|
const anchors = loadAnchors();
|
|
173
209
|
const omsPath = resolveOmsPath(anchors);
|
|
174
210
|
|
|
175
|
-
const errors = [
|
|
211
|
+
const errors = [
|
|
212
|
+
...checkSkfVersionAgainstPackageJson(anchors),
|
|
213
|
+
...checkCanonicalFiles(anchors, omsPath),
|
|
214
|
+
...checkDocsForStaleVersions(anchors),
|
|
215
|
+
];
|
|
176
216
|
|
|
177
217
|
if (errors.length > 0) {
|
|
178
218
|
console.error('DRIFT DETECTED:\n');
|
|
@@ -186,7 +226,7 @@ function main() {
|
|
|
186
226
|
}
|
|
187
227
|
|
|
188
228
|
const skillCount = Object.keys(anchors.skills).length;
|
|
189
|
-
console.log(`OK: ${skillCount} skills checked against ${omsPath}, no drift.`);
|
|
229
|
+
console.log(`OK: skf_version ${anchors.skf_version} matches package.json; ${skillCount} skills checked against ${omsPath}, no drift.`);
|
|
190
230
|
process.exit(0);
|
|
191
231
|
}
|
|
192
232
|
|