bmad-module-skill-forge 1.9.0 → 2.0.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 +10 -5
- package/docs/_data/pinned.yaml +2 -2
- package/docs/agents.md +11 -2
- package/docs/architecture.md +5 -4
- package/docs/bmad-synergy.md +30 -7
- package/docs/campaign.md +172 -0
- package/docs/examples.md +7 -3
- package/docs/forge-auto.md +90 -0
- package/docs/getting-started.md +9 -0
- package/docs/how-it-works.md +6 -4
- package/docs/index.md +4 -3
- package/docs/skill-model.md +1 -1
- package/docs/troubleshooting.md +17 -1
- package/docs/verifying-a-skill.md +2 -2
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +64 -24
- package/package.json +2 -2
- package/src/module-help.csv +2 -1
- package/src/shared/data/language-corpora.json +32 -0
- package/src/shared/references/pipeline-contracts.md +6 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +8 -3
- package/src/shared/scripts/skf-detect-language.py +5 -3
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- package/src/shared/scripts/skf-language-corpora.py +100 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-shape-detect.py +576 -25
- package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
- package/src/shared/scripts/skf-validate-pins.py +2 -2
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +1 -1
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/step-auto-scope.md +196 -44
- package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
- package/src/skf-audit-skill/references/init.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/references/analyze-target.md +7 -4
- package/src/skf-brief-skill/references/confirm-brief.md +0 -1
- package/src/skf-brief-skill/references/gather-intent.md +8 -2
- package/src/skf-brief-skill/references/scope-definition.md +2 -1
- package/src/skf-brief-skill/references/step-auto-brief.md +21 -4
- package/src/skf-brief-skill/references/step-auto-validate.md +2 -2
- package/src/skf-brief-skill/references/write-brief.md +2 -3
- package/src/skf-campaign/SKILL.md +190 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +73 -0
- package/src/skf-campaign/manifest.yaml +11 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +122 -0
- package/src/skf-campaign/references/step-02-strategy.md +97 -0
- package/src/skf-campaign/references/step-03-pins.md +56 -0
- package/src/skf-campaign/references/step-04-provenance.md +63 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
- package/src/skf-campaign/references/step-06-batch.md +87 -0
- package/src/skf-campaign/references/step-07-capstone.md +63 -0
- package/src/skf-campaign/references/step-08-verify.md +75 -0
- package/src/skf-campaign/references/step-09-refine.md +83 -0
- package/src/skf-campaign/references/step-10-export.md +106 -0
- package/src/skf-campaign/references/step-11-maintenance.md +84 -0
- package/src/skf-campaign/references/step-resume.md +114 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +235 -0
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
- package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
- package/src/skf-campaign/scripts/campaign-report.py +249 -0
- package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
- package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
- package/src/skf-campaign/templates/campaign-report-template.md +54 -0
- package/src/skf-campaign/templates/kickoff-template.md +48 -0
- package/src/skf-create-skill/SKILL.md +1 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
- package/src/skf-create-skill/references/compile.md +4 -1
- package/src/skf-create-skill/references/extract.md +9 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -1
- package/src/skf-create-skill/references/generate-artifacts.md +11 -2
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/report.md +17 -1
- package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
- package/src/skf-create-skill/references/step-doc-rot.md +4 -4
- package/src/skf-create-skill/references/step-doc-sources.md +11 -2
- package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
- package/src/skf-create-skill/references/validate.md +12 -3
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-drop-skill/references/execute.md +20 -9
- package/src/skf-drop-skill/references/report.md +2 -0
- package/src/skf-drop-skill/references/select.md +7 -2
- package/src/skf-forger/SKILL.md +12 -7
- package/src/skf-refine-architecture/SKILL.md +2 -1
- package/src/skf-refine-architecture/references/compile.md +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-rename-skill/references/execute.md +5 -4
- package/src/skf-rename-skill/references/rebuild-context.md +2 -2
- package/src/skf-rename-skill/references/select.md +3 -3
- package/src/skf-setup/SKILL.md +1 -1
- package/src/skf-setup/references/auto-index.md +3 -1
- package/src/skf-setup/references/detect-and-tier.md +4 -0
- package/src/skf-setup/references/report.md +4 -1
- package/src/skf-setup/references/tier-rules.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +10 -0
- package/src/skf-test-skill/references/init.md +1 -1
- package/src/skf-test-skill/references/source-access-protocol.md +13 -3
- package/src/skf-test-skill/scripts/compute-score.py +4 -3
- package/src/skf-update-skill/customize.toml +0 -9
- package/src/skf-update-skill/references/detect-changes.md +33 -3
- package/src/skf-update-skill/references/health-check.md +2 -2
- package/src/skf-update-skill/references/init.md +1 -0
- package/src/skf-update-skill/references/re-extract.md +15 -1
- package/src/skf-verify-stack/references/report.md +1 -1
- package/tools/cli/lib/ui.js +3 -2
- package/docs/deepwiki.md +0 -89
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"""SKF Validate Brief Schema — schema check for a skill-brief.yaml on disk.
|
|
6
6
|
|
|
7
7
|
Loads `skill-brief.yaml` from a path (or from stdin/inline-YAML), validates
|
|
8
|
-
it against `
|
|
8
|
+
it against `schemas/skill-brief.v1.json` (sibling of this script), and applies the
|
|
9
9
|
two conditional rules from `skf-create-skill/references/load-brief.md §3`
|
|
10
10
|
that the JSON schema doesn't express today:
|
|
11
11
|
|
|
@@ -56,10 +56,7 @@ import yaml
|
|
|
56
56
|
from jsonschema import Draft202012Validator
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
SCHEMA_PATH = (
|
|
61
|
-
REPO_ROOT / "src" / "shared" / "scripts" / "schemas" / "skill-brief.v1.json"
|
|
62
|
-
)
|
|
59
|
+
SCHEMA_PATH = Path(__file__).resolve().parent / "schemas" / "skill-brief.v1.json"
|
|
63
60
|
|
|
64
61
|
|
|
65
62
|
# --------------------------------------------------------------------------
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"""SKF Validate Pins — resolve and validate version pins for a GitHub repository.
|
|
6
6
|
|
|
7
7
|
Shared pin validation script consumed by skf-analyze-source (AN auto-scope,
|
|
8
|
-
|
|
8
|
+
forge-auto --pin) and campaign workflows. Validates that a user-supplied --pin
|
|
9
9
|
resolves to an existing tag or branch, or auto-resolves the latest release tag
|
|
10
10
|
when no --pin is provided.
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ Tag matching priority mirrors source-resolution-protocols.md:
|
|
|
17
17
|
4. Crate prefix: {name}/v{pin}, {name}/{pin}, {name}-v{pin}
|
|
18
18
|
|
|
19
19
|
CLI:
|
|
20
|
-
uv run
|
|
20
|
+
uv run src/shared/scripts/skf-validate-pins.py \\
|
|
21
21
|
--repo-url <url> [--pin <version>] [--format tag|branch|any]
|
|
22
22
|
|
|
23
23
|
Input:
|
|
@@ -51,7 +51,10 @@ Context payload shape (consumed by `write`):
|
|
|
51
51
|
},
|
|
52
52
|
|
|
53
53
|
# Conditionally present:
|
|
54
|
-
"doc_urls": [{"url": "...", "label": "..."}],
|
|
54
|
+
"doc_urls": [{"url": "...", "label": "...", "source": "..."}],
|
|
55
|
+
# `source` (optional per #432): language-registry | readme-detection |
|
|
56
|
+
# homepage | pages-api | docs-folder. Threaded through when present;
|
|
57
|
+
# absent → entry renders as {url, label} only (no false drift).
|
|
55
58
|
"scripts_intent": "detect" | "none" | free-text,
|
|
56
59
|
"assets_intent": "detect" | "none" | free-text,
|
|
57
60
|
"source_authority": "official" | "community" | "internal",
|
|
@@ -95,7 +98,7 @@ Flat input form (`--from-flat`):
|
|
|
95
98
|
"scope_notes": "",
|
|
96
99
|
"scope_tier_a_include": null | ["code/core/src/**"],
|
|
97
100
|
"scope_amendments": null | [{...}],
|
|
98
|
-
"doc_urls": null | [...],
|
|
101
|
+
"doc_urls": null | [{"url": "...", "label": "...", "source": "..."}],
|
|
99
102
|
"scripts_intent": null | "detect" | "none" | "...",
|
|
100
103
|
"assets_intent": null | "detect" | "none" | "...",
|
|
101
104
|
"source_authority": null | "official" | "community" | "internal",
|
|
@@ -433,11 +436,24 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
|
|
|
433
436
|
brief[ref_field] = ref_val
|
|
434
437
|
|
|
435
438
|
# Conditional: doc_urls (always emitted when present)
|
|
439
|
+
#
|
|
440
|
+
# Each entry carries the {url, label} contract plus an OPTIONAL `source`
|
|
441
|
+
# provenance field (issue #432: language-registry | readme-detection |
|
|
442
|
+
# homepage | pages-api | docs-folder). `source` is emitted ONLY when present
|
|
443
|
+
# on the input entry — mirroring the source_authority null-drop discipline
|
|
444
|
+
# below (lines ~448-454): a legacy/hand-authored entry without `source`
|
|
445
|
+
# round-trips to exactly {url, label}, so a ratify/re-write of an old brief
|
|
446
|
+
# stays byte-identical. (An explicit empty-string source is treated as
|
|
447
|
+
# absent here; the schema enum is the real gate and rejects "" upstream.)
|
|
436
448
|
doc_urls = ctx.get("doc_urls")
|
|
437
449
|
if doc_urls:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
450
|
+
rendered_doc_urls: list[dict[str, Any]] = []
|
|
451
|
+
for e in doc_urls:
|
|
452
|
+
entry = {"url": e["url"], "label": e.get("label", "")}
|
|
453
|
+
if e.get("source"):
|
|
454
|
+
entry["source"] = e["source"]
|
|
455
|
+
rendered_doc_urls.append(entry)
|
|
456
|
+
brief["doc_urls"] = rendered_doc_urls
|
|
441
457
|
|
|
442
458
|
# Conditional: scripts_intent / assets_intent — emit when explicitly non-detect
|
|
443
459
|
for intent_field in ("scripts_intent", "assets_intent"):
|
|
@@ -43,7 +43,7 @@ These rules apply to every step in this workflow:
|
|
|
43
43
|
| 6 | Generate Briefs | references/generate-briefs.md | Yes | Interactive mode only |
|
|
44
44
|
| 7 | Workflow Health Check | references/health-check.md | Yes | Always |
|
|
45
45
|
|
|
46
|
-
**Auto mode path:** When `[auto]` flag is present, init (step 1) routes directly to step 1a, which performs manifest scan → shape detection → scope generation → brief write → health check, bypassing steps 2–6. After URL type detection (§0), pin resolution (§0b) validates the `--pin` argument (or resolves the latest release tag) and stores the resolved ref for downstream brief writes. Coexistence detection (§0c) then checks for existing skills matching the target. If found, the user chooses alongside/merge/skip. Headless mode auto-selects alongside. Auto-scope may produce N > 1 confirmed units when
|
|
46
|
+
**Auto mode path:** When `[auto]` flag is present, init (step 1) routes directly to step 1a, which performs manifest scan → shape detection → scope generation → brief write → health check, bypassing steps 2–6. After URL type detection (§0), pin resolution (§0b) validates the `--pin` argument (or resolves the latest release tag) and stores the resolved ref for downstream brief writes. Coexistence detection (§0c) then checks for existing skills matching the target. If found, the user chooses alongside/merge/skip. Headless mode auto-selects alongside. Auto-scope may produce N > 1 confirmed units when a monorepo crosses the decomposition threshold (`package_count > 3`) **and** the §3b cohesion check decides to split, resulting in N briefs and N `brief_paths` in the envelope; a cohesive monorepo merges to one skill instead. A single large library is never decomposed — its size is handled by `skf-create-skill`'s auto-shard. When the target is a documentation URL (not a GitHub repo or local path), auto-scope detects the docs-only input at §0, validates URL reachability, writes a docs-only brief, and emits the envelope without performing source analysis.
|
|
47
47
|
|
|
48
48
|
**Shape detection reference:** `references/step-shape-detect.md` — loaded by step 1a as a reference doc (not a chained step).
|
|
49
49
|
|
|
@@ -23,7 +23,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
|
|
|
23
23
|
| Field | Type | Constraint | Description |
|
|
24
24
|
|--------------------|--------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
25
25
|
| source_type | string | `source` or `docs-only` | Default `source`. When `docs-only`: `source_repo` optional, `doc_urls` required |
|
|
26
|
-
| doc_urls | array | `{url, label}` objects
|
|
26
|
+
| doc_urls | array | `{url, label, source?}` objects | Documentation URLs for T3 content. Required when `source_type: "docs-only"`. Optional `source` provenance enum: `language-registry` (registry-guaranteed corpus) \| `readme-detection` \| `homepage` \| `pages-api` \| `docs-folder` |
|
|
27
27
|
| `scripts_intent` | string | `detect` / `none` / free-text | Describes whether scripts should be extracted. Values: `detect` (auto-detect from source — default when absent), `none` (skip scripts), or a free-text description of expected scripts (e.g., "CLI validation tools in bin/"). |
|
|
28
28
|
| `assets_intent` | string | `detect` / `none` / free-text | Describes whether assets should be extracted. Values: `detect` (auto-detect from source — default when absent), `none` (skip assets), or a free-text description of expected assets (e.g., "JSON schemas in schemas/"). |
|
|
29
29
|
| `target_version` | string | Semantic version (`X.Y.Z` or `X.Y.Z-prerelease`) | User-specified target version. When present, overrides auto-detection and becomes the skill's version. Recommended for docs-only skills where auto-detection is unavailable. |
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'health-check.md'
|
|
3
3
|
outputFile: '{forge_data_folder}/analyze-source-report-{project_name}.md'
|
|
4
|
-
|
|
4
|
+
shapeDetectProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-shape-detect.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-shape-detect.py'
|
|
7
|
+
validatePinsProbeOrder:
|
|
8
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-pins.py'
|
|
9
|
+
- '{project-root}/src/shared/scripts/skf-validate-pins.py'
|
|
10
|
+
skillInventoryProbeOrder:
|
|
11
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-skill-inventory.py'
|
|
12
|
+
- '{project-root}/src/shared/scripts/skf-skill-inventory.py'
|
|
13
|
+
scanManifestsProbeOrder:
|
|
14
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-scan-manifests.py'
|
|
15
|
+
- '{project-root}/src/shared/scripts/skf-scan-manifests.py'
|
|
16
|
+
detectLanguageProbeOrder:
|
|
17
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-detect-language.py'
|
|
18
|
+
- '{project-root}/src/shared/scripts/skf-detect-language.py'
|
|
19
|
+
languageCorporaProbeOrder:
|
|
20
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-language-corpora.py'
|
|
21
|
+
- '{project-root}/src/shared/scripts/skf-language-corpora.py'
|
|
22
|
+
writeSkillBriefProbeOrder:
|
|
23
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
|
|
24
|
+
- '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
|
|
5
25
|
---
|
|
6
26
|
|
|
7
27
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -48,8 +68,10 @@ This section validates and resolves version pins. It runs for repo URLs and loca
|
|
|
48
68
|
|
|
49
69
|
**For repo URLs when `--pin` is provided:**
|
|
50
70
|
|
|
71
|
+
**Resolve `{validatePinsHelper}`** from `{validatePinsProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
72
|
+
|
|
51
73
|
```bash
|
|
52
|
-
uv run
|
|
74
|
+
uv run {validatePinsHelper} --repo-url {project_path} --pin {pin_value}
|
|
53
75
|
```
|
|
54
76
|
|
|
55
77
|
Handle exit codes:
|
|
@@ -63,8 +85,10 @@ Handle exit codes:
|
|
|
63
85
|
|
|
64
86
|
**For repo URLs when `--pin` is NOT provided (default):**
|
|
65
87
|
|
|
88
|
+
Using the same `{validatePinsHelper}` resolved above:
|
|
89
|
+
|
|
66
90
|
```bash
|
|
67
|
-
uv run
|
|
91
|
+
uv run {validatePinsHelper} --repo-url {project_path}
|
|
68
92
|
```
|
|
69
93
|
|
|
70
94
|
Handle exit codes:
|
|
@@ -79,8 +103,10 @@ This section checks for existing skills matching the target before proceeding. I
|
|
|
79
103
|
|
|
80
104
|
**1. Load skill inventory:**
|
|
81
105
|
|
|
106
|
+
**Resolve `{skillInventoryHelper}`** from `{skillInventoryProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
107
|
+
|
|
82
108
|
```bash
|
|
83
|
-
uv run
|
|
109
|
+
uv run {skillInventoryHelper} {skills_output_folder}
|
|
84
110
|
```
|
|
85
111
|
|
|
86
112
|
Parse the JSON output. If the exit code is non-zero or the `skills` array is empty, skip coexistence detection silently (no existing skills to conflict with) and continue to the next section: §0a for documentation URLs, §1 for all other input types.
|
|
@@ -183,9 +209,11 @@ confirmed_units:
|
|
|
183
209
|
|
|
184
210
|
**4. Write skill brief via canonical writer:**
|
|
185
211
|
|
|
212
|
+
**Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
213
|
+
|
|
186
214
|
Create directory `{forge_data_folder}/{skill_name}/` if it does not exist.
|
|
187
215
|
|
|
188
|
-
|
|
216
|
+
Pipe the flat context JSON below into the resolved writer with the `--from-flat` flag:
|
|
189
217
|
|
|
190
218
|
```json
|
|
191
219
|
{
|
|
@@ -194,7 +222,7 @@ Write `{forge_data_folder}/{skill_name}/skill-brief.yaml` through the canonical
|
|
|
194
222
|
"detected_version": null,
|
|
195
223
|
"source_type": "docs-only",
|
|
196
224
|
"source_repo": "{url}",
|
|
197
|
-
"language": "",
|
|
225
|
+
"language": "documentation",
|
|
198
226
|
"description": "Skill created from documentation at {url}",
|
|
199
227
|
"forge_tier": "{forge_tier}",
|
|
200
228
|
"created": "{current_date}",
|
|
@@ -216,6 +244,10 @@ Write `{forge_data_folder}/{skill_name}/skill-brief.yaml` through the canonical
|
|
|
216
244
|
}
|
|
217
245
|
```
|
|
218
246
|
|
|
247
|
+
```bash
|
|
248
|
+
echo '<context-json>' | uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --from-flat
|
|
249
|
+
```
|
|
250
|
+
|
|
219
251
|
**5. Emit success envelope:**
|
|
220
252
|
|
|
221
253
|
```
|
|
@@ -245,27 +277,79 @@ Load `references/step-shape-detect.md` as reference for shape detection invocati
|
|
|
245
277
|
|
|
246
278
|
### 2. Manifest Scan
|
|
247
279
|
|
|
248
|
-
|
|
280
|
+
Enumerate package manifests **deterministically** via `{scanManifestsHelper}` (the same helper the interactive `scan-project.md` uses) — do not hand-scan. Resolve `{scanManifestsHelper}` as the first path in `{scanManifestsProbeOrder}` that exists. The scanner reads a **local directory**, so how you point it at the target depends on the input form classified in §0:
|
|
249
281
|
|
|
250
282
|
**For each path in `project_paths[]`:**
|
|
251
283
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
284
|
+
- **Local filesystem path** (starts with `/`, `./`, `~/`, `~`, or is an existing directory) — scan it directly:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
uv run {scanManifestsHelper} scan {path}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- **Remote git URL** (e.g. `github.com/{owner}/{repo}`) — auto-scope has no working tree yet and the scanner cannot read a URL. Fetch **just the manifests** first (blobless + sparse + depth-1 — no source blobs, typically KB–MB even for large monorepos), then scan that tree:
|
|
255
291
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
292
|
+
```bash
|
|
293
|
+
tmp="$(mktemp -d)"
|
|
294
|
+
git clone --filter=blob:none --no-checkout --depth 1 {pinned_branch_flag} {path} "$tmp"
|
|
295
|
+
git -C "$tmp" sparse-checkout set --no-cone '**/package.json' '**/Cargo.toml' '**/pyproject.toml' '**/go.mod' '**/pom.xml' '**/build.gradle' '**/build.gradle.kts' '**/Package.swift' 'pnpm-workspace.yaml' '**/pnpm-workspace.yaml'
|
|
296
|
+
git -C "$tmp" checkout
|
|
297
|
+
uv run {scanManifestsHelper} scan "$tmp"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
where `{pinned_branch_flag}` is `--branch {pinned_ref}` when a pin was resolved in §0b (so manifests match the target version), otherwise omitted. **Retain `"$tmp"` through §3** — shape detection reads the discovered manifest files from it — then it may be discarded.
|
|
301
|
+
|
|
302
|
+
Parse the JSON envelope: `{manifests: [{path, ecosystem, ...}], total_unique, monorepo, warnings?}`. The scanner discovers the project root plus monorepo workspace members (npm/pnpm/yarn `workspaces`, Cargo `[workspace]`, and other ecosystems) and sets the `monorepo` flag — so members are found without hand-listing each workspace convention, for both local trees and remote fetches.
|
|
303
|
+
|
|
304
|
+
From the envelope, record:
|
|
305
|
+
|
|
306
|
+
1. **Supported manifest paths** — filter `manifests[].path` to the types `skf-shape-detect.py` accepts (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `build.gradle.kts`, `Package.swift`). Each `manifests[].path` is **relative to the scan root**, so resolve them against that root (`{path}` for a local scan, `"$tmp"` for a remote fetch) before use. This filtered, comma-joined list of resolved paths is fed to shape detection in §3. For a monorepo, it includes each workspace member's manifest, so the package surface is classified accurately rather than from a bare (and often export-less) repo root. The scanner may discover ecosystems shape detection does not yet classify; those are excluded here, so a repo with no supported manifest falls back to interactive at the next check rather than auto-scoping.
|
|
307
|
+
2. **`monorepo` flag** and the count of discovered supported packages — carried forward as a signal for the decomposition decision in §3a.
|
|
308
|
+
|
|
309
|
+
**Harvest tree-level language signals.** A whole-language repo may declare no parser-generator dependency (a hand-written compiler such as rustc, TypeScript, or the Go toolchain) or carry no supported manifest at all (CPython, Ruby). From the **same** fetched tree — no second clone and no blobs, since tree objects are already present in the blobless clone — collect two signals for shape detection. These are pure path listings (`git ls-tree` reads tree objects; no checkout, no blob download):
|
|
310
|
+
|
|
311
|
+
- **Remote fetch** (`"$tmp"`), or a **local path** that is a git repo (`git -C {path}`):
|
|
312
|
+
```bash
|
|
313
|
+
files="$(git -C "$tmp" ls-tree -r --name-only HEAD)" # every file path
|
|
314
|
+
dirs="$( git -C "$tmp" ls-tree -r -d --name-only HEAD)" # every directory
|
|
315
|
+
# Grammar files (depth-capped to skip deep vendored fixtures, hard-capped):
|
|
316
|
+
grammar_matches="$(printf '%s\n' "$files" \
|
|
317
|
+
| grep -Ei '\.(g4|pest|lalrpop|y|gram|lark|ebnf|peg|ungram)$|/grammar\.(js|json)$' \
|
|
318
|
+
| awk -F/ 'NF<=4' | head -n 50 | paste -sd, -)"
|
|
319
|
+
# Directory signals (trailing /) + depth-capped file basenames, narrowed to
|
|
320
|
+
# compiler-relevant paths so the argument stays bounded on huge repos. The
|
|
321
|
+
# filter is a loose superset of shape detection's gates — the script does the
|
|
322
|
+
# precise matching; this only keeps the list small.
|
|
323
|
+
tree_paths="$({ printf '%s\n' "$dirs" | sed 's#$#/#'; \
|
|
324
|
+
printf '%s\n' "$files" | awk -F/ 'NF<=5'; } \
|
|
325
|
+
| grep -Ei '(^|/)(compiler|compile|syntax|scanner|lexer|tokeniz|parser|parse|ast|binder|checker|codegen|ssagen|interpreter|vm|eval|rustc_[a-z]+)' \
|
|
326
|
+
| head -n 400 | paste -sd, -)"
|
|
327
|
+
```
|
|
328
|
+
- **Local path that is not a git repo** (`{path}`): list the tree with `find` instead, then derive `grammar_matches` / `tree_paths` the same way:
|
|
329
|
+
```bash
|
|
330
|
+
files="$(cd {path} && find . -type f -not -path '*/.git/*' | sed 's#^\./##')"
|
|
331
|
+
dirs="$( cd {path} && find . -type d -not -path '*/.git/*' | sed 's#^\./##')"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Record `<grammar_matches>` and `<tree_paths>` (each a comma-joined list, possibly empty) for §3.
|
|
335
|
+
|
|
336
|
+
**IF no supported manifests are found** (the filtered list is empty):
|
|
337
|
+
- **AND** `<grammar_matches>` is empty **AND** `<tree_paths>` shows no compiler directory (none of `compiler/`, `src/compiler/`, `cmd/compile/`, `internal/syntax/`, or a `Parser/`): the repo carries no language signal — emit fallback message: "**Auto-scope could not find any supported package manifests — switching to interactive mode.**" Load, read fully, then execute `references/scan-project.md`. **STOP HERE.**
|
|
338
|
+
- **Otherwise** (a grammar file or a compiler directory is present) the repo is a manifest-less language toolchain (CPython, Ruby): proceed to §3 with an **empty** `--manifests` and the harvested `--grammar-files` / `--tree-paths`.
|
|
259
339
|
|
|
260
340
|
### 3. Invoke Shape Detection
|
|
261
341
|
|
|
262
|
-
|
|
342
|
+
**Resolve `{shapeDetectHelper}`** from `{shapeDetectProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
343
|
+
|
|
344
|
+
Invoke the shape detection script with the discovered manifests and the harvested tree-level signals:
|
|
263
345
|
|
|
264
346
|
```
|
|
265
|
-
uv run
|
|
347
|
+
uv run {shapeDetectHelper} --repo-url <project_path_or_url> \
|
|
348
|
+
--manifests <comma_separated_manifest_paths> \
|
|
349
|
+
--grammar-files <grammar_matches> --tree-paths <tree_paths>
|
|
266
350
|
```
|
|
267
351
|
|
|
268
|
-
Parse the JSON output: `{shape, signals, confidence, export_count, package_count}`
|
|
352
|
+
`<comma_separated_manifest_paths>` may be empty for a manifest-less language repo, provided `<grammar_matches>` or `<tree_paths>` carries the signal. Parse the JSON output: `{shape, signals, confidence, export_count, package_count}`
|
|
269
353
|
|
|
270
354
|
**Handle exit codes:**
|
|
271
355
|
|
|
@@ -278,19 +362,41 @@ Parse the JSON output: `{shape, signals, confidence, export_count, package_count
|
|
|
278
362
|
|
|
279
363
|
### 3a. Check Decomposition Thresholds
|
|
280
364
|
|
|
281
|
-
Evaluate the shape detection output to determine whether this
|
|
365
|
+
Evaluate the shape detection output to determine whether this **monorepo** should be decomposed into multiple skills.
|
|
282
366
|
|
|
283
|
-
**Threshold
|
|
367
|
+
**Threshold condition:**
|
|
284
368
|
|
|
285
369
|
| Threshold | Condition | Signal |
|
|
286
370
|
|-----------|-----------|--------|
|
|
287
|
-
|
|
|
288
|
-
|
|
371
|
+
| Monorepo / multi-package | `package_count > 3` | 4+ packages — a decomposition **candidate** (empirically validated: fires on real 15-, 38-, and 442-package workspaces) |
|
|
372
|
+
|
|
373
|
+
> A *single* package with a large API surface is **not** a decomposition trigger — it produces one cohesive skill that `skf-create-skill`'s auto-shard step splits into `references/` shards at the 400-line ceiling (a single library is one install / one import namespace; fragmenting it by source directory loses cohesion). Decomposition is for genuinely multi-package monorepos only.
|
|
289
374
|
|
|
290
375
|
**Decision:**
|
|
291
376
|
|
|
292
|
-
- **
|
|
293
|
-
- **
|
|
377
|
+
- **Threshold not met** (`package_count ≤ 3`) → Continue to §4 (single-scope flow, entirely unchanged).
|
|
378
|
+
- **Threshold met** (`package_count > 3`) → this repo is a **decomposition candidate**. A threshold firing means the repo *could* decompose, not that it *should* — continue to §3b to decide merge-vs-split. Log: "Auto-decomposition candidate: package_threshold ({value} packages exceeds 3)".
|
|
379
|
+
|
|
380
|
+
### 3b. Cohesion Check — Merge to One Skill vs Split into N
|
|
381
|
+
|
|
382
|
+
Reached only when §3a flagged a decomposition candidate. Most published monorepos are **cohesive** and produce a better single skill than a pile of fragments — empirically, 5/5 real monorepos (animato 15 crates, trpc, react 38 packages, aws-sdk-js-v3 442 packages, plus zod) were best served as one cohesive skill or a curated few, not one-skill-per-package. Decide deliberately:
|
|
383
|
+
|
|
384
|
+
**Merge into ONE cohesive skill** (override the threshold → continue to §4 single-scope) when **any** of these hold:
|
|
385
|
+
|
|
386
|
+
- **Umbrella facade** — one package re-exports the members: a root or named package whose dependencies include the other workspace members, or which `pub use` / `export *`s them. The facade *is* the public surface (e.g. animato's `crates/animato` re-exporting its 15 sub-crates).
|
|
387
|
+
- **Shared runtime contract** — the members are consumed together through one entry point, and teaching the shared invariant covers them (e.g. tRPC's adapters around `@trpc/server`; aws-sdk's `new XClient(...) → client.send(new YCommand(...))` shared by every `@aws-sdk/client-*`).
|
|
388
|
+
- **Internal building blocks** — the members are private/internal pieces of one product, not independently meaningful to a consumer.
|
|
389
|
+
|
|
390
|
+
**Split into N skills** (→ §4a) when:
|
|
391
|
+
|
|
392
|
+
- The members are **independently published with distinct public surfaces serving different concerns**, **and no umbrella re-exports them** — e.g. `react-dom` and `react-server-dom-*` are separate installs with separate jobs, or a federated SDK where a consumer only ever wants one service. Each genuinely-distinct facet earns its own skill.
|
|
393
|
+
|
|
394
|
+
If genuinely unsure, **prefer merge** — a too-broad single skill is recoverable with `US`; N fragmented skills are not.
|
|
395
|
+
|
|
396
|
+
**Facet-coverage guard (merged facet-diverse repos only).** When you merge a repo whose members have genuinely distinct surfaces and you scope to only some of them, record the decision explicitly — never drop a facet silently:
|
|
397
|
+
|
|
398
|
+
- In `scope.notes`, name the in-scope facets **and** the excluded major facets, e.g. _"Scoped to react + react-dom core; excludes react-server-dom-\* (RSC), the specialized renderers (react-art/native/test), and the compiler — forge a separate skill for those."_
|
|
399
|
+
- Surface the excluded facets in the analysis report (§7) so the operator can re-scope or forge a companion skill.
|
|
294
400
|
|
|
295
401
|
### 4. Map Shape to Scope
|
|
296
402
|
|
|
@@ -302,17 +408,20 @@ Apply the shape→scope.type mapping:
|
|
|
302
408
|
| `library-API` | `public-api` | export_count > 200 |
|
|
303
409
|
| `reference-app` | `reference-app` | — |
|
|
304
410
|
| `language-reference` | `full-library` | — |
|
|
305
|
-
| `stack-compose` | `full-library` |
|
|
411
|
+
| `stack-compose` | `full-library` | Decomposition candidate when `package_count > 3` — cohesion-checked at §3b |
|
|
306
412
|
|
|
307
413
|
### 5. Generate Include/Exclude Patterns
|
|
308
414
|
|
|
309
415
|
Generate `scope.include` and `scope.exclude` arrays from the detected language and project structure.
|
|
310
416
|
|
|
311
|
-
**Detect primary language** from manifest type:
|
|
417
|
+
**Detect primary language** from manifest type (the same set shape detection classifies):
|
|
312
418
|
- `package.json` → TypeScript/JavaScript
|
|
313
419
|
- `pyproject.toml` → Python
|
|
314
420
|
- `Cargo.toml` → Rust
|
|
315
421
|
- `go.mod` → Go
|
|
422
|
+
- `pom.xml` → Java
|
|
423
|
+
- `build.gradle` / `build.gradle.kts` → Java or Kotlin (Kotlin when a `src/main/kotlin/` tree exists)
|
|
424
|
+
- `Package.swift` → Swift
|
|
316
425
|
|
|
317
426
|
**Default patterns (adjust based on actual project structure):**
|
|
318
427
|
|
|
@@ -321,6 +430,10 @@ Generate `scope.include` and `scope.exclude` arrays from the detected language a
|
|
|
321
430
|
| TypeScript/JavaScript | `['src/**/*.ts', 'src/**/*.tsx']` | `['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**']` |
|
|
322
431
|
| Python | `['src/**/*.py']` or `['{package_name}/**/*.py']` | `['**/*_test.py', '**/test_*.py', '**/tests/**']` |
|
|
323
432
|
| Rust | `['src/**/*.rs']` | `['**/tests/**', '**/benches/**']` |
|
|
433
|
+
| Go | `['**/*.go']` | `['**/*_test.go', '**/vendor/**']` |
|
|
434
|
+
| Java | `['src/main/java/**/*.java']` | `['**/src/test/**']` |
|
|
435
|
+
| Kotlin | `['src/main/kotlin/**/*.kt']` | `['**/src/test/**']` |
|
|
436
|
+
| Swift | `['Sources/**/*.swift']` | `['**/Tests/**']` |
|
|
324
437
|
|
|
325
438
|
**Adjust for actual layout:** If the project uses a non-standard layout (e.g., `lib/` instead of `src/`, or a named package directory for Python), detect and use the actual paths. Check for the existence of common source directories (`src/`, `lib/`, `pkg/`, the package name directory) and prefer the one that exists.
|
|
326
439
|
|
|
@@ -332,7 +445,7 @@ scope:
|
|
|
332
445
|
type: '{mapped_scope_type}'
|
|
333
446
|
include: ['{generated_include_patterns}']
|
|
334
447
|
exclude: ['{generated_exclude_patterns}']
|
|
335
|
-
notes: 'Auto-scoped from shape detection (shape: {shape}, confidence: {confidence})'
|
|
448
|
+
notes: 'Auto-scoped from shape detection (shape: {shape}, confidence: {confidence}).{corpus_caveat}'
|
|
336
449
|
```
|
|
337
450
|
|
|
338
451
|
Determine the skill name from the project name or package name (kebab-case, lowercase). Use the manifest `name` field if available, otherwise derive from the project directory name. If `{coexistence_suffix}` is non-empty, append it to the skill name.
|
|
@@ -341,32 +454,51 @@ Detect the primary language from the manifest ecosystem:
|
|
|
341
454
|
- `npm` → `typescript` (or `javascript` if no `.ts` files in includes)
|
|
342
455
|
- `python` → `python`
|
|
343
456
|
- `rust` → `rust`
|
|
457
|
+
- `go` → `go`
|
|
458
|
+
- `maven` → `java`
|
|
459
|
+
- `gradle` → `java` (or `kotlin` when a `src/main/kotlin/` tree exists)
|
|
460
|
+
- `swift` → `swift`
|
|
461
|
+
|
|
462
|
+
### 6b. Seed Companion Corpora (whole-language references only)
|
|
463
|
+
|
|
464
|
+
Runs only when §3 classified the repo as `language-reference` **via a whole-language signal** — the `signals` array contains a `grammar_file:` or `tree_triad:` entry (a compiler / interpreter / grammar repo such as rust-lang/rust, TypeScript, CPython). **Skip** when `language-reference` fired only from `parser_producer:` / `parser_dep:` signals (a parser *library* such as pest or lalrpop): there the code **is** the product, so no companion prose is needed and the §6/§7 caveat below does not apply.
|
|
465
|
+
|
|
466
|
+
A whole-language skill's value is in the language's **prose** — the guide/Book, the standard/library API docs, idioms — not the compiler internals. Seed those canonical corpora so the forged skill teaches the language rather than its implementation.
|
|
467
|
+
|
|
468
|
+
**Resolve `{detectLanguageHelper}`** from `{detectLanguageProbeOrder}` and **`{languageCorporaHelper}`** from `{languageCorporaProbeOrder}` (first existing path wins).
|
|
469
|
+
|
|
470
|
+
1. **Derive the corpus language key `{corpus_language}`.** Prefer the §6 manifest language when non-empty. Otherwise — a manifest-less toolchain such as CPython or Ruby — resolve it from the file paths harvested in §2:
|
|
471
|
+
```bash
|
|
472
|
+
echo '{"tree": [<harvested §2 file paths>]}' | uv run {detectLanguageHelper}
|
|
473
|
+
```
|
|
474
|
+
Use its `language` field. (`.c`/`.h`/`.y` are not in the detector's extension map, so a C-hosted language resolves by its real sources — Ruby via `.rb`.)
|
|
475
|
+
2. **Look up canonical corpora:**
|
|
476
|
+
```bash
|
|
477
|
+
uv run {languageCorporaHelper} --language {corpus_language}
|
|
478
|
+
```
|
|
479
|
+
- exit 0 → parse the `[{url, label, source}]` array (each seed carries `source: language-registry`) → these are `{corpus_seeds}`.
|
|
480
|
+
- exit 1 → no registry entry (long-tail language) → `{corpus_seeds}` is empty (README detection in brief-skill remains the only source).
|
|
481
|
+
- exit 2 → log a warning and treat as empty (best-effort; never halt).
|
|
482
|
+
3. Record `{N}` = number of seeds and `{corpus_labels}` = comma-joined labels, carried into the brief `doc_urls` (§8) and the honest caveat (§6/§7).
|
|
483
|
+
4. Build `{corpus_caveat}` (appended to `scope.notes` in §6/§8 and surfaced in §7) so the operator knows a code-only whole-language skill is low-value:
|
|
484
|
+
- `{N}` ≥ 1: `" LANGUAGE-REFERENCE CAVEAT: this skill's value is the {corpus_language} prose (guide/Book + std/library docs), not compiler internals. Seeded {N} corpus URL(s): {corpus_labels}. create-skill foregrounds this registry prose as the skill's Language Guide and demotes compiler-internal signatures to a reference-only section — review the forged skill if compiler internals still dominate."`
|
|
485
|
+
- `{N}` == 0: `" LANGUAGE-REFERENCE CAVEAT: no canonical corpora were found for {corpus_language} (README detection and the registry both came up empty). This skill is LOW-VALUE as code-only — attach the {corpus_language} guide + std/library docs manually (re-run with a doc URL, or enrich via US) before forging."`
|
|
486
|
+
|
|
487
|
+
For a parser-library `language-reference` (skipped above) and every other shape, `{corpus_caveat}` is empty.
|
|
344
488
|
|
|
345
489
|
### 4a. Multi-Scope Decomposition
|
|
346
490
|
|
|
347
|
-
This section is reached only from §
|
|
348
|
-
|
|
349
|
-
**Determine decomposition path:**
|
|
491
|
+
This section is reached only from §3b when the cohesion check decided to **split** a monorepo (members are independently published with distinct surfaces and no umbrella re-exports them). It replaces §4→§5→§6 for repos that will produce N > 1 skills.
|
|
350
492
|
|
|
351
|
-
|
|
352
|
-
- **Large-export path** (`export_count > 500`, single package): Group by top-level source directory modules (e.g., `src/auth/`, `src/core/`, `src/api/`). Each directory subtree with a meaningful export surface becomes a separate skill boundary. Candidate boundaries with fewer than ~50 exports `[PENDING VALIDATION]` should be merged into an "other" catch-all skill rather than becoming standalone skills. Name each skill as `{project_name}-{module_name}` (kebab-case); if `{coexistence_suffix}` is non-empty, append it. If no clear module structure exists (flat `src/` with all files at root level), **do not force decomposition** — fall back to single-scope flow at §4.
|
|
353
|
-
- **Combined path** (both thresholds met): Use the monorepo path. Package boundaries are explicit and take priority over export-count grouping (which is heuristic).
|
|
493
|
+
**Decompose by workspace package:** Use workspace package discovery from §2 manifest scan results. Each workspace package with its own manifest becomes a separate skill boundary. Name each skill as `{project_name}-{package_name}` (kebab-case); if `{coexistence_suffix}` is non-empty, append it. Trivial workspace members (no source files, no exports) are excluded.
|
|
354
494
|
|
|
355
495
|
**Per-boundary shape→scope mapping:**
|
|
356
496
|
|
|
357
|
-
For each decomposed boundary, apply the shape→scope mapping from §4 independently — the
|
|
358
|
-
|
|
359
|
-
| Decomposition Type | Per-Boundary Shape Mapping |
|
|
360
|
-
|--------------------|---------------------------|
|
|
361
|
-
| Monorepo (`package_count > 3`) | Re-run the shape→scope heuristic ladder from `step-shape-detect.md` per package using each package's own manifest data. Packages may have different shapes (e.g., a `library-API` core + a `reference-app` CLI). |
|
|
362
|
-
| Large-export (`export_count > 500`) | All boundaries inherit the parent shape. scope.type varies by per-boundary export count (e.g., ≤200 → `full-library`, >200 → `public-api`). |
|
|
497
|
+
For each decomposed boundary, apply the shape→scope mapping from §4 independently — re-run the shape→scope heuristic ladder from `step-shape-detect.md` per package using each package's own manifest data. Packages may have different shapes (e.g., a `library-API` core + a `reference-app` CLI).
|
|
363
498
|
|
|
364
499
|
### 5a. Generate Multi-Scope Patterns
|
|
365
500
|
|
|
366
|
-
For each decomposed boundary, generate include/exclude patterns using the same language-aware rules as §5, but scoped to the boundary's source paths.
|
|
367
|
-
|
|
368
|
-
- Monorepo boundaries: patterns are rooted at the package path (e.g., `packages/auth/src/**/*.ts` instead of `src/**/*.ts`)
|
|
369
|
-
- Large-export boundaries: patterns are rooted at the module directory (e.g., `src/auth/**/*.ts`)
|
|
501
|
+
For each decomposed boundary, generate include/exclude patterns using the same language-aware rules as §5, but scoped to the boundary's source paths. Monorepo boundaries are rooted at the package path (e.g., `packages/auth/src/**/*.ts` instead of `src/**/*.ts`).
|
|
370
502
|
|
|
371
503
|
### 6a. Build Multi-Scope
|
|
372
504
|
|
|
@@ -404,11 +536,11 @@ Add `decomposition` to frontmatter:
|
|
|
404
536
|
```yaml
|
|
405
537
|
decomposition:
|
|
406
538
|
triggered: true
|
|
407
|
-
reason: '
|
|
539
|
+
reason: 'package_threshold'
|
|
408
540
|
boundary_count: N
|
|
409
541
|
```
|
|
410
542
|
|
|
411
|
-
Each `confirmed_units` entry includes `boundary_path` — the relative path to the boundary's root (e.g., `packages/core`
|
|
543
|
+
Each `confirmed_units` entry includes `boundary_path` — the relative path to the boundary's root (e.g., `packages/core`). Omit the `decomposition` key entirely when single-scope (N = 1).
|
|
412
544
|
|
|
413
545
|
**When single-scope (N = 1):** No `decomposition` key. `confirmed_units` contains a single entry (existing behavior).
|
|
414
546
|
|
|
@@ -428,6 +560,17 @@ For single-scope (unchanged):
|
|
|
428
560
|
**Exclude Patterns:** {exclude patterns}
|
|
429
561
|
```
|
|
430
562
|
|
|
563
|
+
**When the shape is a whole-language `language-reference`** (§6b ran — a `grammar_file:`/`tree_triad:` signal), append a Companion Corpora subsection so the operator sees whether the skill has the prose that makes it useful. The status is computed from the **final** brief `doc_urls` (the entries that will actually be fetched), not the seed count alone:
|
|
564
|
+
|
|
565
|
+
```markdown
|
|
566
|
+
## Companion Corpora (language-reference)
|
|
567
|
+
|
|
568
|
+
**Why:** A whole-language skill's value is its prose (guide/Book, std/library docs, idioms), not compiler internals.
|
|
569
|
+
**Corpora in brief doc_urls:** {final_doc_urls_count}
|
|
570
|
+
- {label}: {url} # one line per doc_urls entry
|
|
571
|
+
**Status:** {ATTACHED — canonical corpora present | DEGRADED — code-only, no canonical corpora; attach the {corpus_language} guide + std/library docs before forging}
|
|
572
|
+
```
|
|
573
|
+
|
|
431
574
|
For multi-scope (N > 1):
|
|
432
575
|
```markdown
|
|
433
576
|
## Auto-Scope Analysis — Decomposition ({N} skills)
|
|
@@ -468,13 +611,22 @@ scope:
|
|
|
468
611
|
- '{include_patterns}'
|
|
469
612
|
exclude:
|
|
470
613
|
- '{exclude_patterns}'
|
|
471
|
-
notes: 'Auto-scoped from shape detection (shape: {shape}, confidence: {confidence})'
|
|
614
|
+
notes: 'Auto-scoped from shape detection (shape: {shape}, confidence: {confidence}).{corpus_caveat}'
|
|
472
615
|
description: '{1-3 sentence description based on shape, language, and manifest name}'
|
|
473
616
|
forge_tier: '{forge_tier}'
|
|
474
617
|
created: '{current_date}'
|
|
475
618
|
created_by: '{user_name}'
|
|
476
619
|
```
|
|
477
620
|
|
|
621
|
+
**Companion corpora (whole-language references).** When §6b produced `{corpus_seeds}` (`{N}` ≥ 1), add them as the brief's `doc_urls` so the language's prose is fetched and assembled alongside the code:
|
|
622
|
+
|
|
623
|
+
```yaml
|
|
624
|
+
doc_urls:
|
|
625
|
+
- { url: '{seed.url}', label: '{seed.label}', source: '{seed.source}' } # one entry per §6b seed; source is 'language-registry'
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
These are the brief's *existing* `doc_urls`; brief-skill's README detection then merges additional discovered docs on top (existing entries win). **When `{N}` is 0, omit the `doc_urls` key entirely** — the schema requires at least one entry when the key is present.
|
|
629
|
+
|
|
478
630
|
**When decomposition is active (N > 1 units):**
|
|
479
631
|
|
|
480
632
|
Loop over all N boundaries. For each boundary:
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
shapeDetectProbeOrder:
|
|
3
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-shape-detect.py'
|
|
4
|
+
- '{project-root}/src/shared/scripts/skf-shape-detect.py'
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# Shape Detection Reference
|
|
2
8
|
|
|
3
9
|
Reference document for invoking `skf-shape-detect.py` — the shared shape classification module. Loaded by `step-auto-scope.md` for auto-scope analysis.
|
|
4
10
|
|
|
5
11
|
## Invocation Contract
|
|
6
12
|
|
|
7
|
-
**
|
|
13
|
+
**Resolve `{shapeDetectHelper}`** from `{shapeDetectProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
8
14
|
|
|
9
15
|
**Command:**
|
|
10
16
|
```
|
|
11
|
-
uv run
|
|
17
|
+
uv run {shapeDetectHelper} --repo-url <url> \
|
|
18
|
+
--manifests <path1,path2,...> \
|
|
19
|
+
--grammar-files <g1,g2,...> --tree-paths <d1/,d2/,file,...>
|
|
12
20
|
```
|
|
13
21
|
|
|
14
22
|
**Arguments:**
|
|
@@ -16,9 +24,11 @@ uv run python src/shared/scripts/skf-shape-detect.py --repo-url <url> --manifest
|
|
|
16
24
|
| Arg | Required | Description |
|
|
17
25
|
|-----|----------|-------------|
|
|
18
26
|
| `--repo-url` | Yes | Repository URL (context only — no cloning performed) |
|
|
19
|
-
| `--manifests` | Yes | Comma-separated local file paths to manifest files |
|
|
27
|
+
| `--manifests` | Yes | Comma-separated local file paths to manifest files (may be empty when a tree-level signal carries the classification) |
|
|
28
|
+
| `--grammar-files` | No | Comma-separated repo-relative grammar files (`*.y`, `*.g4`, `*.pest`, `Grammar/python.gram`, ...) — a whole-language signal |
|
|
29
|
+
| `--tree-paths` | No | Comma-separated repo-relative directory (trailing `/`) and structural file signals harvested from the clone (a `compiler/` dir, a lexer+parser+ast triad) |
|
|
20
30
|
|
|
21
|
-
**Supported manifests:** `package.json`, `pyproject.toml`, `Cargo.toml`
|
|
31
|
+
**Supported manifests:** `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `build.gradle.kts`, `Package.swift`
|
|
22
32
|
|
|
23
33
|
## Output Schema
|
|
24
34
|
|
|
@@ -49,22 +59,19 @@ On exit code 2, error details are written to stderr as JSON: `{"error": "message
|
|
|
49
59
|
| `library-API` | `full-library` | export_count ≤ 200 |
|
|
50
60
|
| `library-API` | `public-api` | export_count > 200 (surface too large for full coverage) |
|
|
51
61
|
| `reference-app` | `reference-app` | Direct mapping — apps, CLIs, demos |
|
|
52
|
-
| `language-reference` | `full-library` | Language tools/parsers are library-shaped from a skill perspective |
|
|
53
|
-
| `stack-compose` | `full-library` |
|
|
62
|
+
| `language-reference` | `full-library` | Language tools/parsers are library-shaped from a skill perspective. **Corpora-dependent** for a *whole-language* reference (a `grammar_file:`/`tree_triad:` signal — a compiler/interpreter): its value is the language's prose (guide/Book + std/library docs), not compiler internals, so step-auto-scope.md §6b seeds companion corpora and §6/§7 record an honest DEGRADED caveat when none are found (mirrors the §3b facet-coverage guard). A parser *library* (`parser_producer:`/`parser_dep:`) is exempt — its code is the product. |
|
|
63
|
+
| `stack-compose` | `full-library` | Decomposition candidate when `package_count > 3` — cohesion-checked in step-auto-scope.md §3b |
|
|
54
64
|
| `unknown` | N/A | Triggers fallback to interactive mode |
|
|
55
65
|
|
|
56
66
|
## Decomposition Thresholds
|
|
57
67
|
|
|
58
|
-
When auto-scope detects a
|
|
68
|
+
When auto-scope detects a multi-package monorepo, it may recommend multi-skill decomposition instead of producing one unwieldy skill. The threshold is evaluated in step-auto-scope.md §3a.
|
|
59
69
|
|
|
60
70
|
| Threshold | Value | Signal | Decomposition Path |
|
|
61
71
|
|-----------|-------|--------|-------------------|
|
|
62
|
-
|
|
|
63
|
-
| Multi-package / monorepo | `package_count > 3` `[PENDING VALIDATION]` | Shape detection `package_count` | One skill per workspace package |
|
|
64
|
-
|
|
65
|
-
Both thresholds are marked `[PENDING VALIDATION]` — no empirical data exists yet for real-world repos. Expected tuning: after running deepwiki against 5–10 real repos, adjust thresholds based on whether decomposition produces useful skill boundaries or noise.
|
|
72
|
+
| Multi-package / monorepo | `package_count > 3` | Shape detection `package_count` | Cohesion check (§3b): merge to one skill or split per package |
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
`package_count > 3` makes a **monorepo** a decomposition candidate; step-auto-scope.md §3b then decides merge-vs-split. It is empirically validated (fires on real 15-, 38-, and 442-package workspaces). A *single* package with a large API surface is **not** decomposed — it produces one cohesive skill that `skf-create-skill`'s auto-shard splits into `references/` shards at the 400-line ceiling.
|
|
68
75
|
|
|
69
76
|
When neither threshold is met, the single-scope flow proceeds unchanged.
|
|
70
77
|
|
|
@@ -72,7 +79,7 @@ When neither threshold is met, the single-scope flow proceeds unchanged.
|
|
|
72
79
|
|
|
73
80
|
The five-shape heuristic ladder applies in order (first match wins):
|
|
74
81
|
|
|
75
|
-
1. **language-reference** — parser/grammar/language-toolchain project. Signals: parser-
|
|
82
|
+
1. **language-reference** — parser/grammar/language-toolchain project. Signals, strongest first: a hand-written-compiler tree structure (a dedicated `compiler/` directory with a lexer+parser+ast triad plus a codegen/VM/type-checker member — catches rustc, TypeScript, Go); a declared grammar file (`Grammar/python.gram`, a root `parse.y`, a `*.g4` — catches CPython, Ruby); the repo's own name being a known parser/grammar tool (pest, lalrpop, lark — the producer); or a parser-generator dependency (a DSL built on antlr4/lalrpop — the consumer). Delegating consumers (formatters, linters, bundlers that depend on a parser) and markup/DSL parsers (CSS, markdown, GraphQL) are excluded.
|
|
76
83
|
2. **stack-compose** — multi-ecosystem composite project. Signals: manifests from 2+ distinct ecosystems
|
|
77
84
|
3. **reference-app** — application, CLI, or demo project. Signals: npm `bin` field, Rust `[[bin]]`, framework deps (next, fastapi, axum, etc.)
|
|
78
85
|
4. **library-API** — library exposing a programmatic API. Signals: `main`/`module`/`exports` fields, `[lib]` target, export count
|
|
@@ -252,7 +252,7 @@ When skipping, log the reason, then set the audit-ref context variables to basel
|
|
|
252
252
|
- **[D]:** Read the prior report's findings_list (parse the Structural/Semantic/Severity sections, or the appended findings tables) and stash as `prior_findings` in workflow context. Run the new audit normally. In step 6 (report.md), after the Remediation Suggestions section, emit a `## Diff Against Prior Report` subsection summarizing added / removed / changed findings vs `prior_findings`.
|
|
253
253
|
- **[R]:** Load the prior report's frontmatter (`stepsCompleted`, `drift_score`, any intermediate state). Set `{outputFile}` to the prior report path (do NOT create a new one). Determine the next un-completed step from `stepsCompleted` and skip forward to it; downstream steps append to the existing report.
|
|
254
254
|
|
|
255
|
-
> **Note (resumability):** the existing template frontmatter captures `stepsCompleted` and `drift_score` but does not currently persist intermediate findings_list between stages. If [R] is selected and stepsCompleted indicates the prior run halted after structural-diff or semantic-diff, the appended sections in the report body (`## Structural Drift`, `## Semantic Drift`, `## Severity Classification`) serve as the implicit intermediate state — re-parse them on resume rather than re-running completed stages. **
|
|
255
|
+
> **Note (resumability):** the existing template frontmatter captures `stepsCompleted` and `drift_score` but does not currently persist intermediate findings_list between stages. If [R] is selected and stepsCompleted indicates the prior run halted after structural-diff or semantic-diff, the appended sections in the report body (`## Structural Drift`, `## Semantic Drift`, `## Severity Classification`) serve as the implicit intermediate state — re-parse them on resume rather than re-running completed stages. **Design note:** explicit mid-stage resume state is intentionally not persisted today — if resumability proves unreliable in practice, a future enhancement is to extend the report frontmatter to carry an explicit `intermediate_findings` block.
|
|
256
256
|
|
|
257
257
|
- **Other input:** help user, redisplay the gate.
|
|
258
258
|
|