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.
Files changed (118) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +10 -5
  3. package/docs/_data/pinned.yaml +2 -2
  4. package/docs/agents.md +11 -2
  5. package/docs/architecture.md +5 -4
  6. package/docs/bmad-synergy.md +30 -7
  7. package/docs/campaign.md +172 -0
  8. package/docs/examples.md +7 -3
  9. package/docs/forge-auto.md +90 -0
  10. package/docs/getting-started.md +9 -0
  11. package/docs/how-it-works.md +6 -4
  12. package/docs/index.md +4 -3
  13. package/docs/skill-model.md +1 -1
  14. package/docs/troubleshooting.md +17 -1
  15. package/docs/verifying-a-skill.md +2 -2
  16. package/docs/why-skf.md +1 -1
  17. package/docs/workflows.md +64 -24
  18. package/package.json +2 -2
  19. package/src/module-help.csv +2 -1
  20. package/src/shared/data/language-corpora.json +32 -0
  21. package/src/shared/references/pipeline-contracts.md +6 -3
  22. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  23. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  24. package/src/shared/scripts/skf-detect-docs.py +8 -3
  25. package/src/shared/scripts/skf-detect-language.py +5 -3
  26. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  27. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  28. package/src/shared/scripts/skf-language-corpora.py +100 -0
  29. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  30. package/src/shared/scripts/skf-preapply.py +1 -1
  31. package/src/shared/scripts/skf-shape-detect.py +576 -25
  32. package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
  33. package/src/shared/scripts/skf-validate-pins.py +2 -2
  34. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  35. package/src/skf-analyze-source/SKILL.md +1 -1
  36. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  37. package/src/skf-analyze-source/references/step-auto-scope.md +196 -44
  38. package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
  39. package/src/skf-audit-skill/references/init.md +1 -1
  40. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  41. package/src/skf-brief-skill/references/analyze-target.md +7 -4
  42. package/src/skf-brief-skill/references/confirm-brief.md +0 -1
  43. package/src/skf-brief-skill/references/gather-intent.md +8 -2
  44. package/src/skf-brief-skill/references/scope-definition.md +2 -1
  45. package/src/skf-brief-skill/references/step-auto-brief.md +21 -4
  46. package/src/skf-brief-skill/references/step-auto-validate.md +2 -2
  47. package/src/skf-brief-skill/references/write-brief.md +2 -3
  48. package/src/skf-campaign/SKILL.md +190 -0
  49. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  50. package/src/skf-campaign/customize.toml +73 -0
  51. package/src/skf-campaign/manifest.yaml +11 -0
  52. package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
  53. package/src/skf-campaign/references/health-check.md +35 -0
  54. package/src/skf-campaign/references/step-01-setup.md +122 -0
  55. package/src/skf-campaign/references/step-02-strategy.md +97 -0
  56. package/src/skf-campaign/references/step-03-pins.md +56 -0
  57. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  58. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  59. package/src/skf-campaign/references/step-06-batch.md +87 -0
  60. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  61. package/src/skf-campaign/references/step-08-verify.md +75 -0
  62. package/src/skf-campaign/references/step-09-refine.md +83 -0
  63. package/src/skf-campaign/references/step-10-export.md +106 -0
  64. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  65. package/src/skf-campaign/references/step-resume.md +114 -0
  66. package/src/skf-campaign/scripts/.gitkeep +0 -0
  67. package/src/skf-campaign/scripts/campaign-deps.py +235 -0
  68. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  69. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  70. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  71. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  72. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  73. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  74. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  75. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  76. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  77. package/src/skf-create-skill/SKILL.md +1 -1
  78. package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
  79. package/src/skf-create-skill/references/compile.md +4 -1
  80. package/src/skf-create-skill/references/extract.md +9 -1
  81. package/src/skf-create-skill/references/extraction-patterns.md +3 -1
  82. package/src/skf-create-skill/references/generate-artifacts.md +11 -2
  83. package/src/skf-create-skill/references/health-check.md +2 -2
  84. package/src/skf-create-skill/references/report.md +17 -1
  85. package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
  86. package/src/skf-create-skill/references/step-doc-rot.md +4 -4
  87. package/src/skf-create-skill/references/step-doc-sources.md +11 -2
  88. package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
  89. package/src/skf-create-skill/references/validate.md +12 -3
  90. package/src/skf-drop-skill/SKILL.md +2 -2
  91. package/src/skf-drop-skill/references/execute.md +20 -9
  92. package/src/skf-drop-skill/references/report.md +2 -0
  93. package/src/skf-drop-skill/references/select.md +7 -2
  94. package/src/skf-forger/SKILL.md +12 -7
  95. package/src/skf-refine-architecture/SKILL.md +2 -1
  96. package/src/skf-refine-architecture/references/compile.md +1 -1
  97. package/src/skf-refine-architecture/references/report.md +1 -1
  98. package/src/skf-rename-skill/SKILL.md +2 -2
  99. package/src/skf-rename-skill/references/execute.md +5 -4
  100. package/src/skf-rename-skill/references/rebuild-context.md +2 -2
  101. package/src/skf-rename-skill/references/select.md +3 -3
  102. package/src/skf-setup/SKILL.md +1 -1
  103. package/src/skf-setup/references/auto-index.md +3 -1
  104. package/src/skf-setup/references/detect-and-tier.md +4 -0
  105. package/src/skf-setup/references/report.md +4 -1
  106. package/src/skf-setup/references/tier-rules.md +1 -1
  107. package/src/skf-test-skill/references/coverage-check.md +10 -0
  108. package/src/skf-test-skill/references/init.md +1 -1
  109. package/src/skf-test-skill/references/source-access-protocol.md +13 -3
  110. package/src/skf-test-skill/scripts/compute-score.py +4 -3
  111. package/src/skf-update-skill/customize.toml +0 -9
  112. package/src/skf-update-skill/references/detect-changes.md +33 -3
  113. package/src/skf-update-skill/references/health-check.md +2 -2
  114. package/src/skf-update-skill/references/init.md +1 -0
  115. package/src/skf-update-skill/references/re-extract.md +15 -1
  116. package/src/skf-verify-stack/references/report.md +1 -1
  117. package/tools/cli/lib/ui.js +3 -2
  118. 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 `src/shared/scripts/schemas/skill-brief.v1.json`, and applies the
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
- REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
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
- deepwiki --pin) and campaign workflows. Validates that a user-supplied --pin
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 python src/shared/scripts/skf-validate-pins.py \\
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
- brief["doc_urls"] = [
439
- {"url": e["url"], "label": e.get("label", "")} for e in doc_urls
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 decomposition thresholds are met (`export_count > 500` `[PENDING VALIDATION]` or `package_count > 3` `[PENDING VALIDATION]`), resulting in N briefs and N `brief_paths` in the envelope. 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.
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 | Documentation URLs for T3 content. Required when `source_type: "docs-only"` |
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
- shapeDetectScript: 'src/shared/scripts/skf-shape-detect.py'
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 python src/shared/scripts/skf-validate-pins.py --repo-url {project_path} --pin {pin_value}
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 python src/shared/scripts/skf-validate-pins.py --repo-url {project_path}
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 python src/shared/scripts/skf-skill-inventory.py {skills_output_folder}
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
- Write `{forge_data_folder}/{skill_name}/skill-brief.yaml` through the canonical writer (`skf-write-skill-brief.py`) with the following context:
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
- Perform a lightweight manifest scan — find standard package manifests in the project root and workspace paths. Do NOT crawl the full directory tree.
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
- 1. Check the project root for: `package.json`, `pyproject.toml`, `Cargo.toml`
253
- 2. If a workspace configuration exists (e.g., `pnpm-workspace.yaml`, Cargo.toml `[workspace].members`), scan workspace member paths for additional manifests
254
- 3. Record each discovered manifest as `{path, type}` pairs
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
- **IF no manifests found:**
257
- - Emit fallback message: "**Auto-scope could not find any package manifests — switching to interactive mode.**"
258
- - Load, read fully, then execute `references/scan-project.md`. **STOP HERE.**
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
- Invoke the shape detection script with discovered manifests:
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 python {shapeDetectScript} --repo-url <project_path_or_url> --manifests <comma_separated_manifest_paths>
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 repo should be decomposed into multiple skills.
365
+ Evaluate the shape detection output to determine whether this **monorepo** should be decomposed into multiple skills.
282
366
 
283
- **Threshold conditions** (both constants are `[PENDING VALIDATION]` — subject to empirical tuning):
367
+ **Threshold condition:**
284
368
 
285
369
  | Threshold | Condition | Signal |
286
370
  |-----------|-----------|--------|
287
- | Large export surface | `export_count > 500` `[PENDING VALIDATION]` | Single skill covering 500+ exports produces unwieldy output |
288
- | Monorepo / multi-package | `package_count > 3` `[PENDING VALIDATION]` | 4+ packages are almost always multi-concern |
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
- - **Neither threshold met** → Continue to §4 (single-scope flow, entirely unchanged).
293
- - **Either or both thresholds met**Route to §4a (multi-scope decomposition flow). Log: "Auto-decomposition triggered: {reason} ({value} exceeds threshold {threshold} `[PENDING VALIDATION]`)" where reason is `export_threshold`, `package_threshold`, or `both`.
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` | Multi-scope via §3a when `package_count > 3` `[PENDING VALIDATION]` |
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 §3a when one or both decomposition thresholds are met. It replaces §4→§5→§6 for repos that will produce N > 1 skills.
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
- - **Monorepo path** (`package_count > 3`): 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.
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 boundary's local characteristics determine its scope.type:
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: 'export_threshold' | 'package_threshold' | 'both'
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` for monorepo, `src/auth` for large-export). Omit the `decomposition` key entirely when single-scope (N = 1).
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
- **Script:** `src/shared/scripts/skf-shape-detect.py`
13
+ **Resolve `{shapeDetectHelper}`** from `{shapeDetectProbeOrder}`; first existing path wins; HALT if neither resolves.
8
14
 
9
15
  **Command:**
10
16
  ```
11
- uv run python src/shared/scripts/skf-shape-detect.py --repo-url <url> --manifests <path1,path2,...>
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` | Multi-skill decomposition when `package_count > 3` `[PENDING VALIDATION]` see step-auto-scope.md §3a |
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 repo exceeding complexity thresholds, it recommends multi-skill decomposition instead of producing a single unwieldy skill. The thresholds are evaluated in step-auto-scope.md §3a.
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
- | Large export surface | `export_count > 500` `[PENDING VALIDATION]` | Shape detection `export_count` | Group by top-level source directory modules |
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
- When both thresholds are met simultaneously, the monorepo path takes priority (package boundaries are explicit; export-count grouping is heuristic).
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-related deps (pest, antlr4, tree-sitter, lark, etc.)
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. **TODO:** if resumability proves unreliable in practice, extend the report frontmatter to carry an explicit `intermediate_findings` block (out of scope for this PR).
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