bmad-module-skill-forge 2.0.1 → 2.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/docs/404.md +4 -4
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/RELEASING.md +7 -4
- package/docs/agents.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/bmad-synergy.md +13 -13
- package/docs/campaign.md +3 -3
- package/docs/concepts.md +3 -3
- package/docs/examples.md +9 -9
- package/docs/forge-auto.md +6 -6
- package/docs/getting-started.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/index.md +7 -1
- package/docs/skill-model.md +2 -2
- package/docs/troubleshooting.md +7 -7
- package/docs/why-skf.md +8 -8
- package/docs/workflows.md +6 -6
- package/package.json +7 -3
- package/src/shared/health-check.md +40 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -0
- package/src/shared/scripts/skf-detect-language.py +20 -0
- package/src/shared/scripts/skf-detect-tools.py +34 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +20 -0
- package/src/shared/scripts/skf-language-corpora.py +20 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +20 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +44 -2
- package/src/shared/scripts/skf-recommend-scope-type.py +20 -0
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-shape-detect.py +20 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +2 -1
- package/src/skf-analyze-source/references/recommend.md +1 -1
- package/src/skf-audit-skill/references/re-index.md +1 -1
- package/src/skf-brief-skill/assets/description-voice-examples.md +5 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/analyze-target.md +2 -2
- package/src/skf-brief-skill/references/gather-intent.md +4 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +3 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -2
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/compile.md +12 -3
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +5 -3
- package/src/skf-create-skill/references/step-doc-rot.md +23 -6
- package/src/skf-create-skill/scripts/scan-doc-rot.py +191 -27
- package/src/skf-create-stack-skill/references/detect-integrations.md +1 -1
- package/src/skf-setup/references/ccc-index.md +2 -2
- package/src/skf-setup/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +11 -5
- package/src/skf-test-skill/references/score.md +7 -1
- package/src/skf-test-skill/scripts/aggregate-coherence.py +13 -3
- package/src/skf-test-skill/scripts/compute-score.py +17 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +36 -9
- package/tools/cli/lib/installer.js +31 -1
- package/tools/validate-docs-links.js +665 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| source_repo | string | GitHub URL or local path | Repository or project root (optional when `source_type: "docs-only"`) |
|
|
10
10
|
| language | string | Recognized language | Primary programming language |
|
|
11
11
|
| scope | object | See Scope Object below | Boundary definition |
|
|
12
|
-
| description | string | 1-3 sentences | What the skill covers
|
|
12
|
+
| description | string | 1-3 sentences | What the skill covers. Must contain a literal `Use when` clause — validators test for that exact phrase. |
|
|
13
13
|
| forge_tier | string | `Quick` / `Forge` / `Forge+` / `Deep` | Inherited from forge-tier.yaml (Title Case) |
|
|
14
14
|
| created | string | ISO date `YYYY-MM-DD` | Generation date |
|
|
15
15
|
| created_by | string | user_name from config | Who generated the brief |
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| `target_ref` | string | Git ref (tag or branch) | Optional. Explicit git ref used verbatim as the resolved `source_ref`, bypassing version-to-tag matching. Escape hatch for monorepo crate tags whose prefix differs from the skill name (e.g. tag `livekit/v0.7.42` for skill `livekit-rust`). Remote sources only. |
|
|
27
27
|
| `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
|
|
28
28
|
| `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
|
|
29
|
-
| `scope.tier_a_include` | array | Glob patterns
|
|
29
|
+
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional. Narrower tier-A include list for a curated-subset skill — stratified monorepo packages, single-crate `specific-modules` scopes, and multi-entry exports-map packages. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`, so the denominator reflects the authoring surface rather than incidentally-matched internal infrastructure. **It also exempts the brief from the denominator deflation guard**, so supply it only when it genuinely narrows the surface — never as an empty list, and never as a way to widen (listing files outside `scope.include` inverts its meaning). See `skf-test-skill/references/source-access-protocol.md` stratified-scope resolution. |
|
|
30
30
|
|
|
31
31
|
When `source_type: "docs-only"`:
|
|
32
32
|
- `source_repo` becomes optional (set to doc site URL for reference)
|
|
@@ -206,9 +206,9 @@ Fall back to ad-hoc inspection — `Gemfile` / `*.csproj` / `*.sln` / `Package.s
|
|
|
206
206
|
If `tools.ccc` is true in forge-tier.yaml, supplement the module listing with a semantic discovery pass:
|
|
207
207
|
|
|
208
208
|
**CCC Semantic Discovery:**
|
|
209
|
-
- **Claude Code:** Use `/ccc search "{repo_name} public API exports modules" {source_path}`
|
|
209
|
+
- **Claude Code:** Use `/ccc search "{repo_name} public API exports modules"` from `{source_path}` — the query is variadic, so a trailing path is swallowed into the search string rather than selecting a project
|
|
210
210
|
- **Cursor:** Use `ccc` MCP server `search` tool with query `"{repo_name} public API exports modules"` and path `{source_path}`
|
|
211
|
-
- **CLI fallback:** `ccc search "{repo_name} public API exports modules"
|
|
211
|
+
- **CLI fallback:** `cd {source_path} && ccc search --limit 10 "{repo_name} public API exports modules"` — `ccc search` reads the index in the current working directory and has no project-selector flag (`--path` is a file-path glob filter *within* the index)
|
|
212
212
|
|
|
213
213
|
See `knowledge/tool-resolution.md` for full bridge-to-tool mapping.
|
|
214
214
|
|
|
@@ -352,7 +352,9 @@ The schema's `description` field is 1-3 sentences and surfaces in skill registri
|
|
|
352
352
|
|
|
353
353
|
Compose a candidate 1-3 sentence description from the gathered material. **Write like a human library maintainer would** — what does an agent get from this skill, and when should it route here? Two facts must come through (what the skill is, when to use it); everything else is voice. Resist filling in the same skeleton every time.
|
|
354
354
|
|
|
355
|
-
Load `{descriptionVoiceExamplesPath}` for the five voice examples (range of acceptable leads
|
|
355
|
+
Load `{descriptionVoiceExamplesPath}` for the five voice examples (range of acceptable leads and structures) and the "do not template-stamp" guidance, then compose in that spirit. The asset documents what "in that spirit" means; the gathered material to draw on is the target repo, the user's intent, the version if set, and any scope hints.
|
|
356
|
+
|
|
357
|
+
**Whatever lead you choose, the description must contain a literal `Use when` clause somewhere** — validators test for that exact phrase, so alternatives like "Triggers on…" or "Reach for this when…" do not satisfy it on their own. The clause need not lead: a descriptive opener followed by `Use when …` is both good voice and validator-clean.
|
|
356
358
|
|
|
357
359
|
Present:
|
|
358
360
|
|
|
@@ -360,7 +362,7 @@ Present:
|
|
|
360
362
|
|
|
361
363
|
> {synthesized description}
|
|
362
364
|
|
|
363
|
-
This is the text agents read when deciding whether to route to your skill — it sits in the registry row alongside dozens of other skills.
|
|
365
|
+
This is the text agents read when deciding whether to route to your skill — it sits in the registry row alongside dozens of other skills. A specific 'use when…' trigger helps agents match real user requests; generic descriptions blend in and get skipped. Edit, replace, or accept as-is."
|
|
364
366
|
|
|
365
367
|
Wait for user confirmation or alternative.
|
|
366
368
|
|
|
@@ -81,7 +81,9 @@ For each ready Tier A skill:
|
|
|
81
81
|
- `{{directive_content}}` — raw content of the file at `campaign.directive_path`, or "No directive configured" if unset/missing.
|
|
82
82
|
Present the completed kickoff message as the context for the skill's pipeline run.
|
|
83
83
|
- **AN → BS → CS → TS** — standard forge pipeline for this skill. When BS (Brief Synthesis) produces the skill's brief, set `skills[current].brief_path` to the brief path from the BS result envelope so the field the schema declares is populated and available for resume and reporting.
|
|
84
|
-
- **Doc-rot check** —
|
|
84
|
+
- **Doc-rot check** — **record** what CS step 5c already produced; do not re-derive it. Because the pipeline body runs inline (see the §5 note above), that step's context is directly readable here: `corrections_added`, `correction_matches` (the enriched match records — `source`, `pattern`, `category`, `context_line`, `affected`), `corrections_deduped` and `corrections_capped`. Append one `[doc-rot]`-prefixed entry per `correction_matches` record to the skill's `workarounds_applied` array so they survive state write and are available for §6 propagation. When step 5c took its skip branch (`doc_rot_triggered: false`, `corrections_added: 0`), there is nothing to record — append nothing and move on.
|
|
85
|
+
|
|
86
|
+
**Do not hand-grep the feeder artifacts here, and do not re-run the scan.** `skf-create-skill/references/step-doc-rot.md` §2 requires the scan to run in its helper precisely so identical feeders yield identical matches; a second pass in this loop is free to disagree with the corrections CS actually wrote. It would also be self-contaminating: step 5c has already written `## CORRECTION` blocks into the compiled SKILL.md, and the helper's exclusion windows cover only that file's frontmatter and its Migration & Deprecation Warnings section — so re-scanning it would re-match the blocks 5c itself authored and record them as fresh findings.
|
|
85
87
|
3. **Record results:**
|
|
86
88
|
- On success: set `status` to `"completed"`, set `completed_at` to current ISO-8601 with timezone, record `quality_score`. Backup and write state.
|
|
87
89
|
- On failure: set `status` to `"failed"`. Log the failure reason (the sub-skill's `halt_reason`/exit code, or "unparseable result envelope") to the decision log so `campaign status` and the report surface *why* a skill failed without the operator opening sub-skill logs. Backup and write state. Downstream skills whose `depends_on` does NOT include the failed skill continue processing normally; those that DO depend on it are blocked at §4's dependency gate.
|
|
@@ -15,7 +15,7 @@ description: >
|
|
|
15
15
|
**Frontmatter rules:**
|
|
16
16
|
|
|
17
17
|
- `name`: lowercase alphanumeric + hyphens only, must match the skill output directory name. Prefer gerund form (`processing-pdfs`, `analyzing-spreadsheets`) for clarity.
|
|
18
|
-
- `description`: non-empty, max 1024 chars, optimized for agent discovery. Use third-person voice ("Processes Excel files..." not "I can help you..." or "You can use this to...") and include a trigger phrase so agents know when to match
|
|
18
|
+
- `description`: non-empty, max 1024 chars, optimized for agent discovery. Use third-person voice ("Processes Excel files..." not "I can help you..." or "You can use this to...") and include a trigger phrase so agents know when to match. **The description must contain a literal `Use when` clause somewhere** — `skill-check`'s `description.use_when_phrase` rule tests for that exact phrase anywhere in the string, so `Triggers on …` or `Reach for this when …` on their own score below 100 with a `description should contain "Use when" phrasing` warning. The lead is still free: a descriptive opener followed by a `Use when …` clause (`Charts and visualizations powered by D3.js. Use when plotting data …`) passes cleanly, and the other two phrases are fine as *additional* text. When using `Use when `, follow with a **gerund** (`Use when building/processing/analyzing X…`) or a noun-phrase clause (`Use when the user requests to "X"`); never a bare indicative verb like `builds`/`processes`, because `skill-check --fix` will prepend a literal `Use when ` to a description missing the trigger phrase, producing ungrammatical output (`Use when builds X…`). Inconsistent point-of-view or a missing trigger phrase causes discovery problems since the description is injected into the system prompt. See `skf-brief-skill/assets/description-voice-examples.md` for the canonical voice palette.
|
|
19
19
|
- **The `description` cannot contain angle brackets** — neither standalone placeholders like `<name>`, `<component>`, `<path>`, nor inline generics like `` `Array<T>` `` or `` `Meta<typeof X>` ``. Both `skill-check`'s `description_field` validator and `tessl`'s deterministic description check parse the frontmatter description as a raw string and reject any `<` or `>`, regardless of markdown context (backticks do not protect content here). A rejected description fails the review with 0% score. When the natural phrasing would use angle brackets:
|
|
20
20
|
- Prefer the curly-brace form in prose: `{name}`, `{component-id}`, `{path}` — readable and tessl-safe.
|
|
21
21
|
- Uppercase placeholders are also acceptable: `NAME`, `COMPONENT_ID`.
|
|
@@ -62,6 +62,7 @@ These sections form the essential standalone body. Target: **under 300 lines tot
|
|
|
62
62
|
- Provenance citation per function
|
|
63
63
|
|
|
64
64
|
**Section 4b — Migration & Deprecation Warnings (~10 lines, Deep tier only):**
|
|
65
|
+
- Emit the heading verbatim as `## Migration & Deprecation Warnings`. Downstream gates match that exact text, so a naturally-retitled section (`## Deprecations & pre-1.0 hazards`, `## v2 → v3 breaking changes`) is reported as *missing* while its bullets are re-emitted as duplicate `## CORRECTION` blocks.
|
|
65
66
|
- Only populated when step 4 enrichment produced **T2-future** annotations (deprecation warnings, breaking changes, planned renames)
|
|
66
67
|
- List each warning as a single-line bullet: function name, what changed or will change, source citation
|
|
67
68
|
- Max 10 lines — just the actionable warnings, not full context
|
|
@@ -268,7 +269,7 @@ Replace per-function subsections with `references/pattern-*.md` groupings: one r
|
|
|
268
269
|
- `stats.exports_documented` MAY be set to the Pattern Surface row count as a proxy, but its semantics change: it counts authored pattern surfaces, not public exports. Emit an adjacent `stats.pattern_surfaces_documented` with the same integer so downstream consumers (test-skill, feasibility, discovery) can discriminate.
|
|
269
270
|
- `exports_public_api` / `exports_internal` / `exports_total` are not meaningful for a reference app — omit them, or set all three to the Pattern Surface count with a note in `stats.notes`: `"reference-app: export counts are pattern-surface proxies, not library exports"`.
|
|
270
271
|
- Do not fabricate signature / type-coverage data from pattern surfaces. skf-test-skill will skip those categories when metadata flags a reference-app (same skip path as `stackSkill` once that flag is wired — see scoring-rules.md).
|
|
271
|
-
- Do not emit `effective_denominator` for reference-app scope, even when the source is a monorepo. `effective_denominator` counts public exports from the authoring-surface barrel — a library concept that `pattern_surfaces_documented` already replaces here. A reference-app-in-monorepo literally satisfies `compile.md` §4's emit-conditions (
|
|
272
|
+
- Do not emit `effective_denominator` for reference-app scope, even when the source is a monorepo. `effective_denominator` counts public exports from the authoring-surface barrel — a library concept that `pattern_surfaces_documented` already replaces here. A reference-app-in-monorepo literally satisfies `compile.md` §4's emit-conditions (condition 1 arm (a) + non-`full-library` + stratified `scope.notes`), so this carve-out takes precedence: pattern-surface coverage is the reference-app basis, and skf-test-skill skips library-export coverage for it.
|
|
272
273
|
|
|
273
274
|
**Language / spec-reference sub-shape:** A reference app that documents an engine- or spec-versioned **language** — a query language, grammar, or DSL (e.g. SurrealQL @ SurrealDB) whose value is construct idioms rather than wiring or exports — is a recognized reference-app sub-shape. Keep `scope.type: "reference-app"` (there is no separate enum value), so every carve-out above applies unchanged (`pattern_surfaces_documented` proxy, no `effective_denominator`, test-skill signature/type skip). Adapt the three overrides to the language's surface instead of app wiring:
|
|
274
275
|
|
|
@@ -134,7 +134,7 @@ Indexed pipe-delimited format for CLAUDE.md managed section (~80-120 tokens per
|
|
|
134
134
|
"total_coverage": 0.0,
|
|
135
135
|
"scripts_count": 0,
|
|
136
136
|
"assets_count": 0
|
|
137
|
-
// "effective_denominator": 0 // optional — emitted for
|
|
137
|
+
// "effective_denominator": 0 // optional — emitted for the curated-subset shapes in compile.md §4 condition 1; test-skill uses this as the coverage denominator when present. See compile.md §4 emit rules.
|
|
138
138
|
},
|
|
139
139
|
// scripts[] and assets[] — include ONLY when inventories are non-empty; omit entirely otherwise
|
|
140
140
|
// "scripts": [{ "file": "scripts/{name}", "purpose": "{description}", "source_file": "{source-path}", "confidence": "T1-low" }],
|
|
@@ -111,12 +111,21 @@ Following the structure from the skill-sections data file:
|
|
|
111
111
|
- `exports_public_api`: count of exports from public entry points (`__init__.py`, `index.ts`, `lib.rs`, or equivalent) — derive this from step 3's entry-point validation (section 4b), not from the provenance-map entry count (which may be incomplete if extraction patterns missed some export types)
|
|
112
112
|
- `exports_internal`: count of all other non-underscore-prefixed exports (internal modules, helpers, adapters)
|
|
113
113
|
- `scripts` / `assets`: the `scripts_inventory` / `assets_inventory` arrays (or `[]` when empty) — the helper sets `stats.scripts_count` / `stats.assets_count` from their lengths
|
|
114
|
-
- `effective_denominator` (**optional** — include in the helper's judgment payload only for
|
|
115
|
-
1. The
|
|
114
|
+
- `effective_denominator` (**optional** — include in the helper's judgment payload only for the curated-subset shapes enumerated in condition 1 below): the count of public exports from files matched by the brief's authoring-surface globs, filtered by `scope.exclude`, resolved against `source_path`. **Prefer `scope.tier_a_include` when the brief supplies it** — that narrow list represents the authoring surface the brief intends to document; resolve its globs across `source_path` and count the union of **named exports** — items reachable from the language's public entry-point barrel (`lib.rs` `pub use`, `index.ts` / `index.js` re-exports, `__init__.py` exports), where a type counts **once** with its methods and impl-block members rolling up under it (count a `pub fn` only when it is a free function reachable from the barrel, never a method on an already-counted type). **Otherwise use `scope.include`** — the coarse list. This counting unit matches what `skf-test-skill` re-derives on the consumer side (`coverage-check.md` §2c excludes `kind: "method"` from `documented_set`), so the producer- and test-side denominators stay aligned; without it, a type-heavy API (a few handle types carrying hundreds of methods) inflates the denominator by an order of magnitude and auto-fails the coverage gate. This is the coverage denominator `skf-test-skill` uses when the brief documents a curated subset rather than a package's whole barrel, so it must match the brief's authoring intent. Compute when ALL of the following hold:
|
|
115
|
+
1. The skill's coverage denominator is resolved by a `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition clause that actually consumes `effective_denominator` — **any one** of:
|
|
116
|
+
- **(a) stratified-scope:** the source is a monorepo (detected via `packages/` layout, `workspaces` field in root `package.json`, `lerna.json`, `rush.json`, `nx.json`, or Cargo `[workspace]`);
|
|
117
|
+
- **(b) single-crate curated subset:** `scope.type` is `specific-modules` on a non-monorepo repo;
|
|
118
|
+
- **(c) multi-entry (exports-map):** the in-scope `package.json` declares an `exports` map with more than one non-root subpath, and the repo carries no monorepo markers.
|
|
119
|
+
|
|
120
|
+
Arms (b) and (c) are not a widening for its own sake — both consumer clauses already say "prefer `metadata.json.stats.effective_denominator`", and a monorepo-only emit condition makes those two rungs unreachable, so a non-monorepo curated scope has no way to lock its denominator.
|
|
116
121
|
2. `scope.type` is not `full-library` (and not `reference-app` — see carve-out below), AND the resolved include list (`tier_a_include` if present, else `scope.include`) lists a curated file/directory subset rather than the full workspace.
|
|
117
122
|
3. `scope.notes` is present and documents the stratification strategy (e.g., a tiered A/B/C plan) — this serves as the intent marker confirming the subset is by design.
|
|
118
123
|
|
|
119
|
-
Otherwise omit it from the payload entirely — when absent, `skf-test-skill` falls back to `exports_public_api`. See `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition ("Stratified-scope monorepo packages") for the test-side consumption rules.
|
|
124
|
+
Otherwise omit it from the payload entirely — when absent, `skf-test-skill` falls back to `exports_public_api`. See `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition ("Stratified-scope monorepo packages", "Single-crate curated subset", "Multi-entry (exports-map) packages") for the test-side consumption rules.
|
|
125
|
+
|
|
126
|
+
**A `public-api` scope on a single-package repo with no multi-subpath `exports` map matches none of the three arms** — that shape falls to the standard root-barrel rule, which honors neither `effective_denominator` nor `tier_a_include`. Emitting the field there has no effect on the test-side denominator, so omit it.
|
|
127
|
+
|
|
128
|
+
**That shape has no brief-side denominator lever, and `scope.exclude` is not one.** The root-barrel rule counts the named exports of a single entry-point file (`index.ts` / `__init__.py` / `lib.rs`), so excluding *files* cannot remove exports from the barrel's own export list — the denominator is unchanged. When the brief genuinely documents a curated subset of a single package's surface (citation-only files alongside a narrow `tier_a_include`), express that with `scope.type: "specific-modules"` rather than `public-api`: the single-crate curated-subset clause in `skf-test-skill` `references/source-access-protocol.md` filters the barrel by `scope.include` / `scope.exclude` **and** honors `effective_denominator`, so condition 1 arm (b) applies and the denominator matches the authoring intent. Reserve `public-api` for the case where the whole root barrel is the intended surface.
|
|
120
129
|
|
|
121
130
|
**Reference-app carve-out:** never emit `effective_denominator` for `scope.type: "reference-app"`, even when the three conditions above are literally satisfied (a reference-app-in-monorepo matches all of them). A reference app's coverage basis is `pattern_surfaces_documented`, not library exports — see the Reference-App stats semantics in `assets/compile-assembly-rules.md`.
|
|
122
131
|
- **Shape:** pass `--shape reference-app` or `--shape stack` when the matching assembly-rules override applies (default `library`). For a reference app, also put `pattern_surfaces_documented` (the Pattern Surface row count) in the payload — the helper uses it as `exports_documented`, emits `stats.pattern_surfaces_documented`, and refuses to emit `effective_denominator`; the distribution then sums to the per-citation count, not to `exports_documented`. For a stack, put the own-barrel `exports_documented` (usually `0`) in the payload; the distribution sums to the cited-constituent count.
|
|
@@ -101,7 +101,7 @@ Then run CCC indexing and discovery on the resolved clone (workspace or ephemera
|
|
|
101
101
|
node_modules/, dist/, build/, .git/, vendor/, __pycache__/, .cache/, .next/, .nuxt/, target/, out/, .venv/, .tox/
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Read `settings.yml`, append any patterns not already present to the `exclude_patterns` array, write back. **Reuse check:** if an existing `.cocoindex_code/settings.yml` was already present (workspace hit), read its `exclude_patterns` first and diff against the standard-exclusion list above. If ANY standard entry is missing from the existing list, append only the missing entries (preserving any user-added patterns) AND force a re-index by running `ccc
|
|
104
|
+
Read `settings.yml`, append any patterns not already present to the `exclude_patterns` array, write back. **Reuse check:** if an existing `.cocoindex_code/settings.yml` was already present (workspace hit), read its `exclude_patterns` first and diff against the standard-exclusion list above. If ANY standard entry is missing from the existing list, append only the missing entries (preserving any user-added patterns) AND force a re-index by running `ccc reset --force` (discards the stale databases) followed by `ccc index` — `ccc index` itself takes no rebuild flag, so re-running it alone would reuse the existing index. If every standard entry is already present, skip the write and skip the forced re-index — the existing index is valid. Record `ccc_exclusions_augmented: {count}` in context for the evidence report.
|
|
105
105
|
|
|
106
106
|
**Note:** Brief-specific `include_patterns` and `exclude_patterns` are not written to `settings.yml`. The CCC index is general-purpose — it indexes everything (minus standard artifacts). Brief-specific filtering happens at search result time, not index time. This allows a single workspace CCC index to serve multiple briefs with different scope filters.
|
|
107
107
|
|
|
@@ -194,11 +194,13 @@ Ensure the source path used for extraction is indexed by ccc and registered in t
|
|
|
194
194
|
|
|
195
195
|
**Index verification:**
|
|
196
196
|
|
|
197
|
-
`ccc index`
|
|
197
|
+
**Working-directory contract:** `ccc init`, `ccc index`, and `ccc status` take **no positional path argument** — each operates on the current working directory, and passing a path makes them exit non-zero with `Got unexpected extra argument`. Run them from `{source_root}` (`cd {source_root} && ccc …`, the same form `extract.md` step 2b and `knowledge/ccc-bridge.md` use), then return to `{project-root}` before the registry update below, which resolves `{forgeTierConfig}` relative to the project.
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
`ccc index` requires the directory to be initialized first. Run `cd {source_root} && ccc init` (idempotent — a no-op once initialized) before `cd {source_root} && ccc index`, or use the ccc MCP tool. `ccc_bridge.ensure_index` is a conceptual interface, not a callable function. Indexing is a no-op if the source was already indexed during setup or step 2b.
|
|
200
200
|
|
|
201
|
-
**
|
|
201
|
+
**Nested project marker:** when `{source_root}` is a subtree of a repo that already carries a project marker (e.g. a cloned source tree under `.forge-sources/`, or a `.cocoindex_code` / VCS marker in a parent), `ccc init` exits non-zero with `A parent directory has a project marker`. This is expected — re-run as `cd {source_root} && ccc init -f` to initialize at the subtree anyway, then `ccc index`. Do not treat the parent-marker warning as fatal.
|
|
202
|
+
|
|
203
|
+
**Verify the index is not degraded:** after `ccc index`, run `cd {source_root} && ccc status` and read the `Languages:` breakdown — a non-zero `Chunks`/`Files` total is not sufficient. Confirm the source's primary language (`{brief.language}`) reports a non-trivial chunk count. An index dominated by `markdown`/config chunks with the source language absent or near-zero means `ccc index` ran against degraded settings (a stale `.cocoindex_code/` inherited from a parent project, or a no-op `ccc init` over a prior partial index that returned `Project already initialized.`) — the source code was never indexed, which silently cripples later `skf-audit-skill` / `skf-update-skill` searches with no error surfaced. When the source language is absent, rebuild from a clean init (`cd {source_root} && ccc init -f` then `ccc index`) and re-check the breakdown. Note that `-f` only skips the parent-marker warning — it does not clear a stale `.cocoindex_code/`; when the degraded settings survive the forced re-init, `ccc reset` (run from `{source_root}`) is what discards the existing databases. If the source language is still missing after a forced re-init, log it and continue per the error handling below — a degraded index is not workflow-fatal, but it must be recorded so the gap is visible.
|
|
202
204
|
|
|
203
205
|
**Registry update:**
|
|
204
206
|
|
|
@@ -29,7 +29,7 @@ Identify the feeder artifacts in the **staging directory** for the current skill
|
|
|
29
29
|
1. **Evidence report:** `_bmad-output/{skill-name}/evidence-report.md`
|
|
30
30
|
2. **Provenance map:** `_bmad-output/{skill-name}/provenance-map.json` — focus on T2/T3 entries with temporal annotations
|
|
31
31
|
3. **Temporal context:** changelogs, migration guides, and issue/PR data fetched by step 3b and enriched by step 4 (available in workflow context)
|
|
32
|
-
4. **Compiled SKILL.md:** the staged `_bmad-output/{skill-name}/SKILL.md` itself — check for `[QMD:...]` or `[DOC:...]` annotations referencing corrections. **Do not treat its own `## Migration & Deprecation Warnings` section (§4b)
|
|
32
|
+
4. **Compiled SKILL.md:** the staged `_bmad-output/{skill-name}/SKILL.md` itself — check for `[QMD:...]` or `[DOC:...]` annotations referencing corrections. **Do not treat its own self-authored regions as correction sources:** compile already wrote the `## Migration & Deprecation Warnings` section (step 5 §4b) and the frontmatter `description` (step 5 §2) from the same T2-future annotations, so both restate already-surfaced corrections — §2 discards matches that land in either.
|
|
33
33
|
|
|
34
34
|
For each artifact, attempt to load its content. If an artifact does not exist or is empty, skip it — this is not an error.
|
|
35
35
|
|
|
@@ -42,6 +42,7 @@ The scan is deterministic plumbing — a fixed-table substring grep with one cor
|
|
|
42
42
|
```bash
|
|
43
43
|
uv run {scanDocRotHelper} \
|
|
44
44
|
--skill-md _bmad-output/{skill-name}/SKILL.md \
|
|
45
|
+
--max-corrections 10 \
|
|
45
46
|
_bmad-output/{skill-name}/evidence-report.md \
|
|
46
47
|
_bmad-output/{skill-name}/provenance-map.json \
|
|
47
48
|
_bmad-output/{skill-name}-temporal/*.md
|
|
@@ -49,13 +50,15 @@ uv run {scanDocRotHelper} \
|
|
|
49
50
|
|
|
50
51
|
(Pass whichever of the §1 feeder paths actually exist — the script skips any that are missing or empty, and reports the loaded set in `scanned`.)
|
|
51
52
|
|
|
52
|
-
The script emits `{scanned: [...], matches: [...], match_count, excluded_count}` on stdout. Each entry in `matches` is a match record with the deterministic fields the script owns:
|
|
53
|
+
The script emits `{scanned: [...], matches: [...], match_count, excluded_count, deduped_count, capped_count, cap}` on stdout. Each entry in `matches` is a match record with the deterministic fields the script owns:
|
|
53
54
|
|
|
54
55
|
- `source`: the feeder artifact path where the match was found
|
|
55
56
|
- `pattern`: the specific pattern string that matched
|
|
56
57
|
- `category`: the category label
|
|
57
58
|
- `context_line`: the line containing the match
|
|
58
|
-
- `line_number`: the 1-indexed line the match sits on
|
|
59
|
+
- `line_number`: the 1-indexed line the match sits on — a representative occurrence when duplicates were collapsed
|
|
60
|
+
- `occurrences`: how many feeder lines collapsed into this record (1 when it is unique)
|
|
61
|
+
- `duplicate_of`: the `{source, line_number}` of every collapsed occurrence (empty when it is unique)
|
|
59
62
|
|
|
60
63
|
Read `matches` into `correction_matches: [{match records}]`. Then add the one judgment field the script cannot infer:
|
|
61
64
|
|
|
@@ -79,7 +82,19 @@ Read `matches` into `correction_matches: [{match records}]`. Then add the one ju
|
|
|
79
82
|
| `migration required` | Migration |
|
|
80
83
|
| `signature changed` | Signature change |
|
|
81
84
|
|
|
82
|
-
**Exclusion — drop
|
|
85
|
+
**Exclusion — drop the compiled SKILL.md's self-authored content (deterministic, no AI judgment, applied inside the script before it emits):** the script discards any match whose `source` is the compiled SKILL.md (feeder #4) **and** whose `line_number` sits in either of two positional windows. `excluded_count` reports the combined total.
|
|
86
|
+
|
|
87
|
+
1. **Its own `## Migration & Deprecation Warnings` section** — a line at or after the heading and before the next `##` heading. Compile (step 5 §4b) authored that section from the same T2-future annotations, so re-emitting its bullets as `## CORRECTION` blocks in §3 would duplicate already-surfaced content verbatim.
|
|
88
|
+
2. **Its own YAML frontmatter** — a line at or inside the leading `---` … `---` fences. Compile (step 5 §2) writes the frontmatter `description` from the same annotations, and it routinely restates the release's headline breaking change, so a match there is already-surfaced too. The frontmatter is a closed key set of pipeline scalars, so it can never carry an upstream correction the body has missed — and §3 forbids annotating it in any case, which would leave such a match with nowhere legitimate to go.
|
|
89
|
+
|
|
90
|
+
Both are positional boundary checks on text already loaded, not semantic assessments. The heading search starts after the frontmatter, so an indented restatement of the heading inside a folded `description:` cannot anchor the window in the wrong place. Set `feeder_artifacts_scanned` from the script's `scanned` list.
|
|
91
|
+
|
|
92
|
+
**Bounding — collapse duplicates, then cap (deterministic, applied inside the script after the exclusions):** the temporal feeder above is a verbatim upstream changelog, so a single run can match hundreds of "breaking"/"deprecated" lines drawn from years of release history. Two bounds keep §3's output proportionate:
|
|
93
|
+
|
|
94
|
+
- **Duplicate collapse.** Matches sharing a `category` and the same whitespace-normalized, lowercased `context_line` collapse into their first occurrence, which carries `occurrences` and `duplicate_of`. `deduped_count` reports how many collapsed. A changelog that restates one deprecation across many releases yields one block, not one per release.
|
|
95
|
+
- **Cap.** At most `--max-corrections` matches survive (default 10; pass `0` for unlimited). `capped_count` reports how many were dropped and `cap` the limit in force. Selection prefers the compiled SKILL.md's own annotations over other feeders, then scan order. Ten blocks at ~7 lines each fits step 5b's 400-line body budget with headroom.
|
|
96
|
+
|
|
97
|
+
Neither bound is a judgment call, and neither is silent — §4 logs both counts.
|
|
83
98
|
|
|
84
99
|
**IF `correction_matches` is empty** (the script returned `match_count: 0`, or the run had no feeder files to pass)**:**
|
|
85
100
|
- Log: `"doc-rot: skipped (no correction indicators found in feeder artifacts)"`
|
|
@@ -90,7 +105,7 @@ Read `matches` into `correction_matches: [{match records}]`. Then add the one ju
|
|
|
90
105
|
|
|
91
106
|
### §3. Annotate SKILL.md with Correction Blocks
|
|
92
107
|
|
|
93
|
-
For each entry in `correction_matches`, insert a `## CORRECTION` block into the compiled SKILL.md.
|
|
108
|
+
For each entry in `correction_matches`, insert a `## CORRECTION` block into the compiled SKILL.md. The list is already collapsed and capped by §2's bounding, so this loop is bounded by construction — do not re-expand it from `duplicate_of`, and do not write the drop counts into the artifact (they belong in the §4 log, not in a third-party skill).
|
|
94
109
|
|
|
95
110
|
**Block format:**
|
|
96
111
|
|
|
@@ -114,11 +129,13 @@ Store: `corrections_added: {count of blocks inserted}`
|
|
|
114
129
|
|
|
115
130
|
### §4. Log Results
|
|
116
131
|
|
|
117
|
-
Log: `"doc-rot: {corrections_added} correction blocks added from {feeder_artifacts_scanned_count} feeder artifacts"`
|
|
132
|
+
Log: `"doc-rot: {corrections_added} correction blocks added from {feeder_artifacts_scanned_count} feeder artifacts ({deduped_count} duplicates collapsed, {capped_count} dropped over cap {cap})"`
|
|
118
133
|
|
|
119
134
|
Set context:
|
|
120
135
|
- `doc_rot_triggered: true`
|
|
121
136
|
- `corrections_added: {count}`
|
|
137
|
+
- `corrections_deduped: {deduped_count}`
|
|
138
|
+
- `corrections_capped: {capped_count}`
|
|
122
139
|
- `feeder_artifacts_scanned: [{list}]`
|
|
123
140
|
- `correction_matches: [{match records}]`
|
|
124
141
|
|
|
@@ -8,13 +8,25 @@ step 5c self-declares its correction scan "grep-based and deterministic — no A
|
|
|
8
8
|
judgment is used for detection." This helper *is* that grep: it walks the
|
|
9
9
|
resolved feeder artifacts, matches every line against the fixed 13-row
|
|
10
10
|
correction-pattern table with case-insensitive substring containment (no regex,
|
|
11
|
-
no semantics), and emits the matches as JSON. It applies the
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
no semantics), and emits the matches as JSON. It applies the positional filters
|
|
12
|
+
the step documents — dropping matches that land inside the compiled SKILL.md's
|
|
13
|
+
own YAML frontmatter or its own `## Migration & Deprecation Warnings` section
|
|
14
|
+
(both are self-authored: compile §2 wrote the frontmatter `description` and
|
|
15
|
+
compile §4b wrote the migration bullets, so re-emitting either would be
|
|
16
|
+
circular) — and then bounds what survives, before it returns. Running the scan
|
|
17
|
+
here (instead of in-prompt) makes the "deterministic, identical input →
|
|
18
|
+
identical output" promise actually hold: the model no longer hand-greps
|
|
19
|
+
multi-KB artifacts.
|
|
20
|
+
|
|
21
|
+
Bounding matters because §2's own example command passes the raw temporal
|
|
22
|
+
changelog as a feeder. That artifact is a verbatim upstream dump (thousands of
|
|
23
|
+
lines of release history), so an unbounded run turns years of "breaking" /
|
|
24
|
+
"deprecated" lines into hundreds of `## CORRECTION` blocks and blows the
|
|
25
|
+
compiled body's line budget. Two deterministic bounds run after the exclusions:
|
|
26
|
+
duplicate collapse (same category + same normalized line text) and a hard cap
|
|
27
|
+
on how many blocks a single run may propose. Nothing is silently destroyed —
|
|
28
|
+
a collapsed record carries `occurrences` and `duplicate_of`, and the counts of
|
|
29
|
+
what was collapsed and capped are reported so the step can log them.
|
|
18
30
|
|
|
19
31
|
The genuine judgment the step keeps in-prompt is untouched by this script:
|
|
20
32
|
enriching each match's `affected` symbol from surrounding context (§2) and
|
|
@@ -29,11 +41,15 @@ CLI usage:
|
|
|
29
41
|
uv run scan-doc-rot.py --skill-md <staged SKILL.md> [FEEDER ...]
|
|
30
42
|
uv run scan-doc-rot.py --feeder evidence-report.md --feeder provenance-map.json
|
|
31
43
|
|
|
32
|
-
--skill-md
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
--skill-md the compiled/staged SKILL.md feeder (feeder #4); matches
|
|
45
|
+
inside its YAML frontmatter or its `## Migration &
|
|
46
|
+
Deprecation Warnings` section are excluded. It is also
|
|
47
|
+
scanned like any other feeder.
|
|
48
|
+
FEEDER any other feeder artifact (evidence-report.md,
|
|
49
|
+
provenance-map.json, temporal-context files). Repeatable
|
|
50
|
+
positionally or via --feeder.
|
|
51
|
+
--max-corrections cap on emitted matches (default 10). 0 or negative means
|
|
52
|
+
unlimited.
|
|
37
53
|
|
|
38
54
|
Missing or empty files are skipped silently (not an error), matching §1's
|
|
39
55
|
"attempt to load; if it does not exist or is empty, skip it."
|
|
@@ -43,11 +59,16 @@ Output (stdout, one object):
|
|
|
43
59
|
"scanned": ["<path>", ...], # feeders that existed and were non-empty
|
|
44
60
|
"matches": [ # correction_matches[] (affected added in-prompt)
|
|
45
61
|
{"source": "<path>", "pattern": "deprecated", "category": "Deprecation",
|
|
46
|
-
"context_line": "<line text>", "line_number": <1-indexed int
|
|
62
|
+
"context_line": "<line text>", "line_number": <1-indexed int>,
|
|
63
|
+
"occurrences": <int>, # 1 unless duplicates collapsed into this record
|
|
64
|
+
"duplicate_of": [{"source": "<path>", "line_number": <int>}, ...]},
|
|
47
65
|
...
|
|
48
66
|
],
|
|
49
|
-
"match_count": <int>,
|
|
50
|
-
"excluded_count": <int
|
|
67
|
+
"match_count": <int>, # len(matches), after exclusions/collapse/cap
|
|
68
|
+
"excluded_count": <int>, # SKILL.md matches dropped (frontmatter + §4b)
|
|
69
|
+
"deduped_count": <int>, # matches collapsed into a surviving record
|
|
70
|
+
"capped_count": <int>, # matches dropped because the cap was reached
|
|
71
|
+
"cap": <int> # effective cap (0 = unlimited)
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
Exit codes:
|
|
@@ -85,6 +106,11 @@ PATTERN_TABLE: list[tuple[str, str]] = [
|
|
|
85
106
|
|
|
86
107
|
_MIGRATION_HEADING = re.compile(r"^\s*##\s+Migration\s*&\s*Deprecation Warnings", re.IGNORECASE)
|
|
87
108
|
|
|
109
|
+
# Cap on how many `## CORRECTION` blocks one run may propose (step-doc-rot.md §3).
|
|
110
|
+
# Step 5b budgets the compiled body at 400 lines and each block is ~7 lines, so
|
|
111
|
+
# 10 blocks stays inside the budget with headroom.
|
|
112
|
+
DEFAULT_MAX_CORRECTIONS = 10
|
|
113
|
+
|
|
88
114
|
|
|
89
115
|
def scan_text(text: str, source: str) -> list[dict]:
|
|
90
116
|
"""Case-insensitive substring scan of `text` against PATTERN_TABLE.
|
|
@@ -109,6 +135,27 @@ def scan_text(text: str, source: str) -> list[dict]:
|
|
|
109
135
|
return matches
|
|
110
136
|
|
|
111
137
|
|
|
138
|
+
def frontmatter_range(text: str) -> tuple[int, int] | None:
|
|
139
|
+
"""1-indexed [start, end] inclusive window of the leading YAML frontmatter
|
|
140
|
+
block (both `---` fences included), or None when the file has no frontmatter.
|
|
141
|
+
|
|
142
|
+
Delimiter handling mirrors the established SKF convention (see
|
|
143
|
+
`skf-shard-body.py` `split_frontmatter` and
|
|
144
|
+
`skf-validate-feasibility-report.py` `split_frontmatter`): the opening fence
|
|
145
|
+
must be the FIRST line and the closing fence is the next line that is
|
|
146
|
+
exactly `---` once surrounding whitespace is stripped. A `---` horizontal
|
|
147
|
+
rule further down the body therefore cannot open a block, and an unterminated
|
|
148
|
+
opening fence yields None rather than swallowing the file.
|
|
149
|
+
"""
|
|
150
|
+
lines = text.splitlines()
|
|
151
|
+
if not lines or lines[0].strip() != "---":
|
|
152
|
+
return None
|
|
153
|
+
for idx in range(2, len(lines) + 1):
|
|
154
|
+
if lines[idx - 1].strip() == "---":
|
|
155
|
+
return (1, idx)
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
112
159
|
def migration_section_range(skill_md_text: str) -> tuple[int, int] | None:
|
|
113
160
|
"""1-indexed [start, end) line window of the `## Migration & Deprecation
|
|
114
161
|
Warnings` section, or None if the section is absent.
|
|
@@ -116,11 +163,18 @@ def migration_section_range(skill_md_text: str) -> tuple[int, int] | None:
|
|
|
116
163
|
start = the heading line; end = the next level-2 (`## `) heading, or EOF.
|
|
117
164
|
A `### ` subsection inside the section does NOT close it (only a sibling
|
|
118
165
|
`## ` heading does), matching §2's "before the next `##` heading".
|
|
166
|
+
|
|
167
|
+
The search starts after any YAML frontmatter. `_MIGRATION_HEADING` tolerates
|
|
168
|
+
leading whitespace, so an indented restatement of the heading inside a folded
|
|
169
|
+
`description:` would otherwise anchor the window in the frontmatter and leave
|
|
170
|
+
the real body section unexcluded.
|
|
119
171
|
"""
|
|
120
172
|
lines = skill_md_text.splitlines()
|
|
173
|
+
front = frontmatter_range(skill_md_text)
|
|
174
|
+
first_body_line = front[1] + 1 if front else 1
|
|
121
175
|
start = None
|
|
122
|
-
for idx
|
|
123
|
-
if _MIGRATION_HEADING.match(
|
|
176
|
+
for idx in range(first_body_line, len(lines) + 1):
|
|
177
|
+
if _MIGRATION_HEADING.match(lines[idx - 1]):
|
|
124
178
|
start = idx
|
|
125
179
|
break
|
|
126
180
|
if start is None:
|
|
@@ -133,6 +187,38 @@ def migration_section_range(skill_md_text: str) -> tuple[int, int] | None:
|
|
|
133
187
|
return (start, end)
|
|
134
188
|
|
|
135
189
|
|
|
190
|
+
def apply_frontmatter_exclusion(
|
|
191
|
+
matches: list[dict], skill_md_source: str | None, skill_md_text: str | None
|
|
192
|
+
) -> tuple[list[dict], int]:
|
|
193
|
+
"""Drop matches whose source is the compiled SKILL.md and whose line sits
|
|
194
|
+
inside its YAML frontmatter. Returns (kept, dropped).
|
|
195
|
+
|
|
196
|
+
Same circularity argument as the §4b exclusion below: compile (step 5 §2)
|
|
197
|
+
authors the frontmatter `description` from the very annotations this scan
|
|
198
|
+
looks for, so a `breaking change` / `deprecated` phrase there is already
|
|
199
|
+
surfaced, not a new correction. The frontmatter is also a closed key set of
|
|
200
|
+
pipeline scalars (`name`, `description`), so a match in it can never be an
|
|
201
|
+
upstream correction the body has missed.
|
|
202
|
+
|
|
203
|
+
Scoped to the compiled SKILL.md deliberately. evidence-report.md also carries
|
|
204
|
+
frontmatter, but its frontmatter holds pinned counts rather than authored
|
|
205
|
+
prose, so it cannot restate an upstream correction and is left in scope.
|
|
206
|
+
"""
|
|
207
|
+
if skill_md_source is None or skill_md_text is None:
|
|
208
|
+
return matches, 0
|
|
209
|
+
window = frontmatter_range(skill_md_text)
|
|
210
|
+
if window is None:
|
|
211
|
+
return matches, 0
|
|
212
|
+
start, end = window
|
|
213
|
+
kept, dropped = [], 0
|
|
214
|
+
for m in matches:
|
|
215
|
+
if m["source"] == skill_md_source and start <= m["line_number"] <= end:
|
|
216
|
+
dropped += 1
|
|
217
|
+
continue
|
|
218
|
+
kept.append(m)
|
|
219
|
+
return kept, dropped
|
|
220
|
+
|
|
221
|
+
|
|
136
222
|
def apply_migration_exclusion(
|
|
137
223
|
matches: list[dict], skill_md_source: str | None, skill_md_text: str | None
|
|
138
224
|
) -> tuple[list[dict], int]:
|
|
@@ -153,6 +239,58 @@ def apply_migration_exclusion(
|
|
|
153
239
|
return kept, dropped
|
|
154
240
|
|
|
155
241
|
|
|
242
|
+
def collapse_duplicates(matches: list[dict]) -> tuple[list[dict], int]:
|
|
243
|
+
"""Collapse matches that repeat the same text under the same category.
|
|
244
|
+
|
|
245
|
+
Returns (kept, collapsed). The dedup key is
|
|
246
|
+
`(category, whitespace-normalized lowercased context_line)` — a temporal
|
|
247
|
+
changelog restates the same deprecation across many releases, and each
|
|
248
|
+
restatement would otherwise become its own `## CORRECTION` block saying the
|
|
249
|
+
same thing.
|
|
250
|
+
|
|
251
|
+
The first occurrence in scan order survives and gains two fields:
|
|
252
|
+
`occurrences` (how many lines collapsed into it, including itself) and
|
|
253
|
+
`duplicate_of` (the `{source, line_number}` of every later occurrence), so
|
|
254
|
+
nothing is silently destroyed. Records are copies — the inputs are untouched.
|
|
255
|
+
"""
|
|
256
|
+
seen: dict[tuple[str, str], dict] = {}
|
|
257
|
+
kept: list[dict] = []
|
|
258
|
+
for m in matches:
|
|
259
|
+
key = (m["category"], " ".join(m["context_line"].split()).lower())
|
|
260
|
+
survivor = seen.get(key)
|
|
261
|
+
if survivor is not None:
|
|
262
|
+
survivor["occurrences"] += 1
|
|
263
|
+
survivor["duplicate_of"].append(
|
|
264
|
+
{"source": m["source"], "line_number": m["line_number"]}
|
|
265
|
+
)
|
|
266
|
+
continue
|
|
267
|
+
record = dict(m, occurrences=1, duplicate_of=[])
|
|
268
|
+
seen[key] = record
|
|
269
|
+
kept.append(record)
|
|
270
|
+
return kept, len(matches) - len(kept)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def apply_cap(
|
|
274
|
+
matches: list[dict], cap: int, skill_md_source: str | None
|
|
275
|
+
) -> tuple[list[dict], int]:
|
|
276
|
+
"""Keep at most `cap` matches. Returns (kept, dropped).
|
|
277
|
+
|
|
278
|
+
A cap of 0 or less means unlimited. Selection prefers the compiled SKILL.md's
|
|
279
|
+
own annotations over other feeders — a naive head-slice would drop them first,
|
|
280
|
+
because scan order puts the skill-md feeder last — and falls back to scan
|
|
281
|
+
order within each group. The kept records are returned in scan order, so a
|
|
282
|
+
run that does not hit the cap is ordered exactly as it is today.
|
|
283
|
+
"""
|
|
284
|
+
if cap <= 0 or len(matches) <= cap:
|
|
285
|
+
return matches, 0
|
|
286
|
+
priority = sorted(
|
|
287
|
+
range(len(matches)),
|
|
288
|
+
key=lambda i: (0 if matches[i]["source"] == skill_md_source else 1, i),
|
|
289
|
+
)
|
|
290
|
+
keep = sorted(priority[:cap])
|
|
291
|
+
return [matches[i] for i in keep], len(matches) - cap
|
|
292
|
+
|
|
293
|
+
|
|
156
294
|
def _load(path: str) -> str | None:
|
|
157
295
|
"""Read a feeder file as UTF-8. Returns None for missing/empty (skip, not
|
|
158
296
|
an error). UTF-8 avoids cp1252 mojibake on Windows."""
|
|
@@ -165,9 +303,15 @@ def _load(path: str) -> str | None:
|
|
|
165
303
|
return text
|
|
166
304
|
|
|
167
305
|
|
|
168
|
-
def scan_files(
|
|
169
|
-
|
|
170
|
-
|
|
306
|
+
def scan_files(
|
|
307
|
+
feeders: list[str],
|
|
308
|
+
skill_md: str | None,
|
|
309
|
+
*,
|
|
310
|
+
max_corrections: int = DEFAULT_MAX_CORRECTIONS,
|
|
311
|
+
) -> dict:
|
|
312
|
+
"""Scan every feeder + the skill-md feeder, apply the self-authorship
|
|
313
|
+
exclusions, collapse duplicates, cap the survivors, and return the result
|
|
314
|
+
object. Deterministic: feeders scanned in the given order."""
|
|
171
315
|
scanned: list[str] = []
|
|
172
316
|
matches: list[dict] = []
|
|
173
317
|
skill_md_text = None
|
|
@@ -185,12 +329,21 @@ def scan_files(feeders: list[str], skill_md: str | None) -> dict:
|
|
|
185
329
|
skill_md_text = text
|
|
186
330
|
matches.extend(scan_text(text, path))
|
|
187
331
|
|
|
188
|
-
|
|
332
|
+
# Frontmatter first, then the §4b section — the two windows are disjoint, so
|
|
333
|
+
# the total is order-independent, but running them in file order keeps the
|
|
334
|
+
# attribution obvious when debugging a run.
|
|
335
|
+
matches, front_excluded = apply_frontmatter_exclusion(matches, skill_md, skill_md_text)
|
|
336
|
+
matches, section_excluded = apply_migration_exclusion(matches, skill_md, skill_md_text)
|
|
337
|
+
matches, deduped = collapse_duplicates(matches)
|
|
338
|
+
matches, capped = apply_cap(matches, max_corrections, skill_md)
|
|
189
339
|
return {
|
|
190
340
|
"scanned": scanned,
|
|
191
341
|
"matches": matches,
|
|
192
342
|
"match_count": len(matches),
|
|
193
|
-
"excluded_count":
|
|
343
|
+
"excluded_count": front_excluded + section_excluded,
|
|
344
|
+
"deduped_count": deduped,
|
|
345
|
+
"capped_count": capped,
|
|
346
|
+
"cap": max(max_corrections, 0),
|
|
194
347
|
}
|
|
195
348
|
|
|
196
349
|
|
|
@@ -200,8 +353,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
200
353
|
description=(
|
|
201
354
|
"Deterministic doc-rot correction-indicator scan (step-doc-rot.md §2): "
|
|
202
355
|
"case-insensitive substring match of feeder artifacts against the fixed "
|
|
203
|
-
"correction-pattern table, with the compiled SKILL.md's
|
|
204
|
-
"Deprecation Warnings section excluded
|
|
356
|
+
"correction-pattern table, with the compiled SKILL.md's frontmatter and "
|
|
357
|
+
"Migration & Deprecation Warnings section excluded, duplicates collapsed, "
|
|
358
|
+
"and the survivors capped."
|
|
205
359
|
),
|
|
206
360
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
207
361
|
)
|
|
@@ -222,8 +376,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
222
376
|
"--skill-md",
|
|
223
377
|
dest="skill_md",
|
|
224
378
|
default=None,
|
|
225
|
-
help="Compiled/staged SKILL.md feeder; its Migration &
|
|
226
|
-
"section
|
|
379
|
+
help="Compiled/staged SKILL.md feeder; its frontmatter and its Migration & "
|
|
380
|
+
"Deprecation Warnings section are excluded from matches.",
|
|
381
|
+
)
|
|
382
|
+
parser.add_argument(
|
|
383
|
+
"--max-corrections",
|
|
384
|
+
dest="max_corrections",
|
|
385
|
+
type=int,
|
|
386
|
+
default=DEFAULT_MAX_CORRECTIONS,
|
|
387
|
+
help=(
|
|
388
|
+
f"Maximum matches to emit (default {DEFAULT_MAX_CORRECTIONS}). "
|
|
389
|
+
"0 or negative means unlimited."
|
|
390
|
+
),
|
|
227
391
|
)
|
|
228
392
|
return parser
|
|
229
393
|
|
|
@@ -236,7 +400,7 @@ def main(argv=None) -> int:
|
|
|
236
400
|
parser.print_usage(file=sys.stderr)
|
|
237
401
|
print("error: supply at least one feeder path or --skill-md", file=sys.stderr)
|
|
238
402
|
return 1
|
|
239
|
-
result = scan_files(feeders, args.skill_md)
|
|
403
|
+
result = scan_files(feeders, args.skill_md, max_corrections=args.max_corrections)
|
|
240
404
|
print(json.dumps(result, indent=2))
|
|
241
405
|
return 0
|
|
242
406
|
|
|
@@ -145,7 +145,7 @@ For each library pair with **0 or 1 co-import files** (below the 2-file threshol
|
|
|
145
145
|
|
|
146
146
|
**CCC precision guard for 1-file pairs (H3):** When a CCC hit would elevate a 1-file pair to qualifying status, run a post-hoc verification on that file: re-grep the file and confirm it contains explicit import statements for **both** libraries (per the ecosystem import patterns from `{manifestPatternsPath}`). If either import is missing (e.g., one library is only name-dropped in a comment or string), drop the CCC-added file from the candidate list. Only pairs with ≥2 files that each contain explicit imports for both libraries qualify. Log rejected CCC candidates in workflow state for the evidence report.
|
|
147
147
|
|
|
148
|
-
**Tool resolution for ccc_bridge.search:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `ccc search "{libA} {libB}"
|
|
148
|
+
**Tool resolution for ccc_bridge.search:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `cd {source_root} && ccc search --limit 10 "{libA} {libB}"` (CLI). `ccc search` reads the index in the current working directory and has no project-selector flag (`--path` is a file-path glob filter *within* the index, and the result cap is `--limit`, not `--top`). See `knowledge/tool-resolution.md`.
|
|
149
149
|
|
|
150
150
|
For pairs that already qualify (2+ files), CCC is not needed for detection — but the CCC results may surface additional integration files for richer classification in section 3.
|
|
151
151
|
|