bmad-module-skill-forge 1.9.0 → 2.0.1
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/.nvmrc +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/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +4 -3
- 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-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +240 -4
- package/src/shared/scripts/skf-detect-language.py +82 -3
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-language-corpora.py +100 -0
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- 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-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shape-detect.py +576 -25
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +3 -3
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
- package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +13 -18
- package/src/skf-brief-skill/references/confirm-brief.md +2 -15
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +27 -24
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +6 -15
- package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
- package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +19 -13
- package/src/skf-campaign/SKILL.md +121 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +75 -0
- package/src/skf-campaign/manifest.yaml +15 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +121 -0
- package/src/skf-campaign/references/step-02-strategy.md +102 -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 +88 -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 +115 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +244 -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-batch.py +199 -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-status.py +162 -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 +15 -4
- package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +37 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +20 -27
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +10 -8
- package/src/skf-create-skill/references/generate-artifacts.md +20 -25
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +27 -14
- package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +33 -15
- package/src/skf-create-skill/references/step-doc-sources.md +13 -4
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +69 -29
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +67 -36
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +44 -10
- package/src/skf-drop-skill/references/select.md +93 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +32 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +16 -8
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +10 -8
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +14 -11
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +112 -82
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +21 -13
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +11 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +10 -8
- package/src/skf-setup/references/report.md +26 -32
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +111 -67
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +39 -21
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +15 -5
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +135 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +11 -6
- package/src/skf-update-skill/references/detect-changes.md +48 -36
- package/src/skf-update-skill/references/health-check.md +5 -11
- package/src/skf-update-skill/references/init.md +40 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +27 -25
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/tools/cli/lib/ui.js +3 -2
- package/docs/deepwiki.md +0 -89
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -19,6 +19,12 @@ Commands:
|
|
|
19
19
|
remove <skill-name> — Remove skill from manifest
|
|
20
20
|
deprecate <skill-name> [ver] — Mark skill or version as deprecated
|
|
21
21
|
rename <old-name> <new-name> — Rename a skill entry
|
|
22
|
+
affected-versions <name> — List every version of <name> that a rename must
|
|
23
|
+
touch: the union of manifest
|
|
24
|
+
exports.<name>.versions keys and on-disk
|
|
25
|
+
version dirs under <skills-folder>/<name>/
|
|
26
|
+
(excluding the `active` symlink), deduped and
|
|
27
|
+
sorted semver-descending (newest first)
|
|
22
28
|
"""
|
|
23
29
|
|
|
24
30
|
from __future__ import annotations
|
|
@@ -217,10 +223,68 @@ def cmd_rename(manifest_path, old_name, new_name):
|
|
|
217
223
|
return {"status": "ok", "action": "renamed", "from": old_name, "to": new_name}
|
|
218
224
|
|
|
219
225
|
|
|
226
|
+
def _version_sort_key(version):
|
|
227
|
+
"""Semver-descending sort key (used with reverse=True → newest first).
|
|
228
|
+
|
|
229
|
+
Splits `X.Y.Z[-prerelease][+build]` into numeric release components so
|
|
230
|
+
0.10.0 sorts above 0.9.0 (a known LLM-unreliable numeric-vs-lexical case).
|
|
231
|
+
A non-numeric component sorts below any numeric one at the same position;
|
|
232
|
+
per semver, a version WITHOUT a prerelease outranks the same version WITH
|
|
233
|
+
one (1.0.0 > 1.0.0-rc1).
|
|
234
|
+
"""
|
|
235
|
+
core = str(version).split("+", 1)[0]
|
|
236
|
+
main_part, _, pre = core.partition("-")
|
|
237
|
+
release = []
|
|
238
|
+
for part in main_part.split("."):
|
|
239
|
+
if part.isdigit():
|
|
240
|
+
release.append((1, int(part), ""))
|
|
241
|
+
else:
|
|
242
|
+
release.append((0, 0, part))
|
|
243
|
+
# pre == "" (no prerelease) must sort ABOVE a prerelease, i.e. compare higher.
|
|
244
|
+
return (release, pre == "", pre)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cmd_affected_versions(manifest_path, skills_folder, skill_name):
|
|
248
|
+
data, err = read_manifest(manifest_path)
|
|
249
|
+
if err:
|
|
250
|
+
return {"status": "error", "error": err}
|
|
251
|
+
|
|
252
|
+
manifest_versions = []
|
|
253
|
+
entry = data.get("exports", {}).get(skill_name)
|
|
254
|
+
if entry:
|
|
255
|
+
versions = entry.get("versions", {})
|
|
256
|
+
if isinstance(versions, dict):
|
|
257
|
+
manifest_versions = list(versions.keys())
|
|
258
|
+
elif isinstance(versions, list): # residual v1 safety
|
|
259
|
+
manifest_versions = list(versions)
|
|
260
|
+
|
|
261
|
+
on_disk_versions = []
|
|
262
|
+
skill_dir = Path(skills_folder) / skill_name
|
|
263
|
+
if skill_dir.is_dir():
|
|
264
|
+
for child in skill_dir.iterdir():
|
|
265
|
+
if child.name == "active":
|
|
266
|
+
continue # the active symlink is not a version
|
|
267
|
+
if child.is_dir():
|
|
268
|
+
on_disk_versions.append(child.name)
|
|
269
|
+
|
|
270
|
+
union = sorted(set(manifest_versions) | set(on_disk_versions), key=_version_sort_key, reverse=True)
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
"status": "ok",
|
|
274
|
+
"skill": skill_name,
|
|
275
|
+
"affected_versions": union,
|
|
276
|
+
"count": len(union),
|
|
277
|
+
"sources": {
|
|
278
|
+
"manifest": sorted(set(manifest_versions), key=_version_sort_key, reverse=True),
|
|
279
|
+
"on_disk": sorted(set(on_disk_versions), key=_version_sort_key, reverse=True),
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
220
284
|
def main():
|
|
221
285
|
if len(sys.argv) < 3:
|
|
222
286
|
print("Usage: python3 skf-manifest-ops.py <skills-folder> <command> [args]", file=sys.stderr)
|
|
223
|
-
print("Commands: read, get <name>, set <name> <version> [--ides a,b], remove <name>, deprecate <name> [version], rename <old> <new>", file=sys.stderr)
|
|
287
|
+
print("Commands: read, get <name>, set <name> <version> [--ides a,b], remove <name>, deprecate <name> [version], rename <old> <new>, affected-versions <name>", file=sys.stderr)
|
|
224
288
|
sys.exit(1)
|
|
225
289
|
|
|
226
290
|
skills_folder = Path(sys.argv[1])
|
|
@@ -245,6 +309,8 @@ def main():
|
|
|
245
309
|
result = cmd_deprecate(manifest_path, sys.argv[3], ver)
|
|
246
310
|
elif command == "rename" and len(sys.argv) >= 5:
|
|
247
311
|
result = cmd_rename(manifest_path, sys.argv[3], sys.argv[4])
|
|
312
|
+
elif command == "affected-versions" and len(sys.argv) >= 4:
|
|
313
|
+
result = cmd_affected_versions(manifest_path, skills_folder, sys.argv[3])
|
|
248
314
|
else:
|
|
249
315
|
result = {"status": "error", "error": f"Unknown command or missing args: {command}"}
|
|
250
316
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.9"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Merge Doc URLs — merge corpora-seeded + detected docs for a skill brief.
|
|
6
|
+
|
|
7
|
+
Deterministic merge of an upstream brief's `doc_urls` (registry-seeded
|
|
8
|
+
companion corpora, source: language-registry) with the docs freshly
|
|
9
|
+
discovered by `skf-detect-docs.py` (mapped to the brief `{url, label, source}`
|
|
10
|
+
contract). Replaces the prose dedup/suppression that lived inline in
|
|
11
|
+
`skf-brief-skill/references/step-auto-brief.md` §3 — that logic is fiddly
|
|
12
|
+
(normalized-URL collisions, non-corpus path segments, locale duplicates) and
|
|
13
|
+
must behave identically on every run, so it belongs in a tested script.
|
|
14
|
+
|
|
15
|
+
Three operations, in order:
|
|
16
|
+
|
|
17
|
+
1. Normalized-URL dedup — lowercase host, strip a trailing `/index.html`
|
|
18
|
+
and any trailing `/` before comparing, so a seeded `…/book/` and a
|
|
19
|
+
README's `…/book/index.html` collapse to one entry. Existing
|
|
20
|
+
(corpora-seeded) entries always win and are NEVER dropped.
|
|
21
|
+
|
|
22
|
+
2. Non-corpus segment suppression (whole-language references only) — when
|
|
23
|
+
the brief is a whole-language reference (scope_type == "full-library"
|
|
24
|
+
AND ≥1 existing entry carries source == "language-registry"), drop a
|
|
25
|
+
README-detected entry whose host matches a registry corpus host and
|
|
26
|
+
whose path contains a known non-corpus segment (whatsnew / contribute /
|
|
27
|
+
wiki). Path-component-anchored, never a bare substring — `/whatsnew/`
|
|
28
|
+
and `/whatsnew-2024/` drop, `/docs/whatsnewfeatures/` does not.
|
|
29
|
+
|
|
30
|
+
3. Non-primary-locale collapse (whole-language references only) — drop a
|
|
31
|
+
README-detected entry on a registry host whose leading path segment is a
|
|
32
|
+
non-primary locale (e.g. `/ja/master/`) when a locale-stripped twin is
|
|
33
|
+
already kept (e.g. a seeded `/en/master/` or `/master/`). Twin-required:
|
|
34
|
+
a `/ja/…` page with no `/en/…` counterpart is kept.
|
|
35
|
+
|
|
36
|
+
Suppression is GATED on the whole-language marker so ordinary skills pass
|
|
37
|
+
through with byte-identical merge behaviour (dedup only). N==0 whole-language
|
|
38
|
+
references (registry miss → no seeded corpora) carry no language-registry
|
|
39
|
+
entry, so the gate is inactive and their README docs are NOT suppressed — the
|
|
40
|
+
DEGRADED case is intentionally out of scope (there is no canonical corpus host
|
|
41
|
+
to anchor suppression against).
|
|
42
|
+
|
|
43
|
+
CLI:
|
|
44
|
+
echo '{"scope_type": "...", "existing": [...], "detected": [...]}' \
|
|
45
|
+
| uv run src/shared/scripts/skf-merge-doc-urls.py
|
|
46
|
+
|
|
47
|
+
Input (JSON object on stdin):
|
|
48
|
+
{
|
|
49
|
+
"scope_type": "full-library",
|
|
50
|
+
"existing": [{"url": "...", "label": "...", "source": "language-registry"}, ...],
|
|
51
|
+
"detected": [{"url": "...", "label": "...", "source": "readme-detection"}, ...]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Output (JSON object on stdout):
|
|
55
|
+
{
|
|
56
|
+
"doc_urls": [{"url": "...", "label": "...", "source": "..."}, ...],
|
|
57
|
+
"suppressed": [{"url": "...", "reason": "non-corpus-segment|non-primary-locale-dup"}, ...]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Exit codes:
|
|
61
|
+
0 success (merged list emitted)
|
|
62
|
+
2 error (bad JSON, wrong input shape)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
import json
|
|
68
|
+
import re
|
|
69
|
+
import sys
|
|
70
|
+
from typing import Any
|
|
71
|
+
from urllib.parse import urlsplit
|
|
72
|
+
|
|
73
|
+
# Path components (case-insensitive) that mark a non-corpus page on a doc host:
|
|
74
|
+
# changelogs/news, contribution guides, wikis. Matched as whole path components
|
|
75
|
+
# (or component + a separator), never as a bare substring.
|
|
76
|
+
_NON_CORPUS_SEGMENTS = ("whatsnew", "contribute", "wiki")
|
|
77
|
+
|
|
78
|
+
# Closed allowlist of plausible doc-site locale codes. A leading path segment is
|
|
79
|
+
# treated as a locale only when it is in this set (or a `ll-rr` form whose
|
|
80
|
+
# language part is). Keeps a genuine subsection like `/go/` or `/is/` from being
|
|
81
|
+
# misread as a locale.
|
|
82
|
+
_LOCALE_CODES = {
|
|
83
|
+
"en", "ja", "zh", "ko", "fr", "de", "es", "pt", "ru", "it", "nl", "pl",
|
|
84
|
+
"tr", "uk", "cs", "id", "vi", "fa", "ar", "hi", "th", "sv", "da", "fi",
|
|
85
|
+
"nb", "no", "hu", "ro", "el", "he", "bg", "hr", "sk", "sl", "et", "lv",
|
|
86
|
+
"lt", "sr", "ca",
|
|
87
|
+
}
|
|
88
|
+
_LOCALE_REGION_RE = re.compile(r"^([a-z]{2})-[a-z]{2}$")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _norm_parts(url: str) -> tuple[str, str]:
|
|
92
|
+
"""Return (lowercased host, normalized path) for dedup / host matching.
|
|
93
|
+
|
|
94
|
+
Path normalization strips a trailing `/index.html` and any trailing `/`.
|
|
95
|
+
Scheme, query, and fragment are ignored — a doc page is identified by its
|
|
96
|
+
host + path.
|
|
97
|
+
"""
|
|
98
|
+
parts = urlsplit(url.strip())
|
|
99
|
+
host = parts.netloc.lower()
|
|
100
|
+
path = parts.path
|
|
101
|
+
low = path.lower()
|
|
102
|
+
if low.endswith("/index.html"):
|
|
103
|
+
path = path[: -len("/index.html")]
|
|
104
|
+
path = path.rstrip("/")
|
|
105
|
+
return host, path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _norm_key(url: str) -> tuple[str, str]:
|
|
109
|
+
return _norm_parts(url)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_locale(seg: str) -> bool:
|
|
113
|
+
s = seg.lower()
|
|
114
|
+
if s in _LOCALE_CODES:
|
|
115
|
+
return True
|
|
116
|
+
m = _LOCALE_REGION_RE.match(s)
|
|
117
|
+
return bool(m) and m.group(1) in _LOCALE_CODES
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _is_primary_locale(seg: str) -> bool:
|
|
121
|
+
s = seg.lower()
|
|
122
|
+
return s == "en" or s.startswith("en-")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _path_segments(path: str) -> list[str]:
|
|
126
|
+
return [s for s in path.split("/") if s]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _locale_stripped_key(url: str) -> tuple[str, str, bool, bool]:
|
|
130
|
+
"""Return (host, path-without-leading-locale, has_locale, is_primary).
|
|
131
|
+
|
|
132
|
+
The third/fourth flags let the caller tell `/ja/master/` (non-primary
|
|
133
|
+
locale) apart from `/master/` (no locale) and `/en/master/` (primary).
|
|
134
|
+
"""
|
|
135
|
+
host, path = _norm_parts(url)
|
|
136
|
+
segs = _path_segments(path)
|
|
137
|
+
has_locale = bool(segs) and _is_locale(segs[0])
|
|
138
|
+
is_primary = has_locale and _is_primary_locale(segs[0])
|
|
139
|
+
if has_locale:
|
|
140
|
+
segs = segs[1:]
|
|
141
|
+
return host, "/".join(segs), has_locale, is_primary
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _matches_non_corpus_segment(path: str) -> bool:
|
|
145
|
+
for seg in _path_segments(path):
|
|
146
|
+
low = seg.lower()
|
|
147
|
+
for token in _NON_CORPUS_SEGMENTS:
|
|
148
|
+
if low == token:
|
|
149
|
+
return True
|
|
150
|
+
# component + separator (whatsnew-2024) — but NOT whatsnewfeatures
|
|
151
|
+
if low.startswith(token) and len(low) > len(token) and not low[len(token)].isalnum():
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def merge_doc_urls(
|
|
157
|
+
scope_type: str,
|
|
158
|
+
existing: list[dict[str, Any]],
|
|
159
|
+
detected: list[dict[str, Any]],
|
|
160
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
161
|
+
"""Merge existing (seeded) + detected doc_urls. Returns (doc_urls, suppressed)."""
|
|
162
|
+
kept: list[dict[str, Any]] = []
|
|
163
|
+
suppressed: list[dict[str, Any]] = []
|
|
164
|
+
seen: set[tuple[str, str]] = set()
|
|
165
|
+
|
|
166
|
+
# 1. Existing (corpora-seeded) entries win and are never dropped.
|
|
167
|
+
for e in existing:
|
|
168
|
+
if not isinstance(e, dict) or not e.get("url"):
|
|
169
|
+
continue
|
|
170
|
+
key = _norm_key(e["url"])
|
|
171
|
+
if key in seen:
|
|
172
|
+
continue
|
|
173
|
+
seen.add(key)
|
|
174
|
+
kept.append(e)
|
|
175
|
+
|
|
176
|
+
# Whole-language gate: registry-seeded corpora present on a full-library brief.
|
|
177
|
+
registry_hosts = {
|
|
178
|
+
_norm_parts(e["url"])[0]
|
|
179
|
+
for e in kept
|
|
180
|
+
if isinstance(e, dict) and e.get("url") and e.get("source") == "language-registry"
|
|
181
|
+
}
|
|
182
|
+
suppression_active = scope_type == "full-library" and bool(registry_hosts)
|
|
183
|
+
|
|
184
|
+
for d in detected:
|
|
185
|
+
if not isinstance(d, dict) or not d.get("url"):
|
|
186
|
+
continue
|
|
187
|
+
url = d["url"]
|
|
188
|
+
key = _norm_key(url)
|
|
189
|
+
if key in seen:
|
|
190
|
+
continue # dedup — a seeded/earlier entry already covers this URL
|
|
191
|
+
|
|
192
|
+
if suppression_active:
|
|
193
|
+
host, path = _norm_parts(url)
|
|
194
|
+
if host in registry_hosts:
|
|
195
|
+
if _matches_non_corpus_segment(path):
|
|
196
|
+
suppressed.append({"url": url, "reason": "non-corpus-segment"})
|
|
197
|
+
continue
|
|
198
|
+
dh, dpath, has_locale, is_primary = _locale_stripped_key(url)
|
|
199
|
+
if has_locale and not is_primary:
|
|
200
|
+
twin = any(
|
|
201
|
+
_locale_stripped_key(k["url"])[0] == dh
|
|
202
|
+
and _locale_stripped_key(k["url"])[1] == dpath
|
|
203
|
+
for k in kept
|
|
204
|
+
if k.get("url")
|
|
205
|
+
)
|
|
206
|
+
if twin:
|
|
207
|
+
suppressed.append({"url": url, "reason": "non-primary-locale-dup"})
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
seen.add(key)
|
|
211
|
+
kept.append(d)
|
|
212
|
+
|
|
213
|
+
return kept, suppressed
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main() -> int:
|
|
217
|
+
raw = sys.stdin.read()
|
|
218
|
+
if not raw or not raw.strip():
|
|
219
|
+
sys.stderr.write("skf-merge-doc-urls: empty stdin (expected JSON object)\n")
|
|
220
|
+
return 2
|
|
221
|
+
try:
|
|
222
|
+
payload = json.loads(raw)
|
|
223
|
+
except json.JSONDecodeError as exc:
|
|
224
|
+
sys.stderr.write(f"skf-merge-doc-urls: invalid JSON on stdin: {exc}\n")
|
|
225
|
+
return 2
|
|
226
|
+
if not isinstance(payload, dict):
|
|
227
|
+
sys.stderr.write("skf-merge-doc-urls: input must be a JSON object\n")
|
|
228
|
+
return 2
|
|
229
|
+
|
|
230
|
+
scope_type = payload.get("scope_type") or ""
|
|
231
|
+
existing = payload.get("existing") or []
|
|
232
|
+
detected = payload.get("detected") or []
|
|
233
|
+
if not isinstance(existing, list) or not isinstance(detected, list):
|
|
234
|
+
sys.stderr.write("skf-merge-doc-urls: 'existing' and 'detected' must be arrays\n")
|
|
235
|
+
return 2
|
|
236
|
+
|
|
237
|
+
doc_urls, suppressed = merge_doc_urls(scope_type, existing, detected)
|
|
238
|
+
json.dump({"doc_urls": doc_urls, "suppressed": suppressed}, sys.stdout, ensure_ascii=False)
|
|
239
|
+
sys.stdout.write("\n")
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
sys.exit(main())
|
|
@@ -9,7 +9,7 @@ registry, then scans target directory files for fingerprint matches and applies
|
|
|
9
9
|
the corresponding fixes.
|
|
10
10
|
|
|
11
11
|
CLI:
|
|
12
|
-
uv run
|
|
12
|
+
uv run src/shared/scripts/skf-preapply.py \
|
|
13
13
|
--target-dir <path> [--registry <path>] [--local-registry <path>] [--log-dir <path>]
|
|
14
14
|
|
|
15
15
|
Input:
|
|
@@ -16,10 +16,23 @@ Actions:
|
|
|
16
16
|
clear — Remove managed section entirely (markers + content)
|
|
17
17
|
insert — Insert managed section if not present (at end of file)
|
|
18
18
|
check — Check if managed section exists, report status
|
|
19
|
+
|
|
20
|
+
Query actions (read-only, take positional file lists instead of <file> <action>):
|
|
21
|
+
orphan-detect <context-file>... --exported-skills a,b
|
|
22
|
+
— Parse `[skill-name v...]` rows across one or more context
|
|
23
|
+
files, drop rows whose skill_name is in the exported set,
|
|
24
|
+
and report the survivors (deduped by (skill_name, version),
|
|
25
|
+
with verbatim snippet_text and sorted source_files provenance)
|
|
26
|
+
as `orphan_managed_rows`.
|
|
27
|
+
root-probe <snippet-file>... --reference-root .claude/skills/
|
|
28
|
+
— Read each snippet's first line, parse its `root:` prefix
|
|
29
|
+
(trailing `{skill-name}/` stripped), collect the unique
|
|
30
|
+
`observed_prefixes`, and flag `mismatch` against the reference.
|
|
19
31
|
"""
|
|
20
32
|
|
|
21
33
|
from __future__ import annotations
|
|
22
34
|
|
|
35
|
+
import argparse
|
|
23
36
|
import json
|
|
24
37
|
import os
|
|
25
38
|
import re
|
|
@@ -34,6 +47,16 @@ MARKER_PATTERN = re.compile(
|
|
|
34
47
|
re.DOTALL,
|
|
35
48
|
)
|
|
36
49
|
|
|
50
|
+
# A managed-section skill row header: `[skill-name v1.2.3]…`, optionally
|
|
51
|
+
# prefixed by the format's leading `|`. Requires a ` v<version>` inside the
|
|
52
|
+
# brackets so the section header `[SKF Skills]|{n} skills|{m} stack` (no ` v`)
|
|
53
|
+
# is never mistaken for a skill row.
|
|
54
|
+
ROW_HEADER_PATTERN = re.compile(
|
|
55
|
+
r"^\|?\[(?P<name>[^\]]+?) v(?P<version>[^\]\s]+)\]"
|
|
56
|
+
)
|
|
57
|
+
# The `root:` field of a snippet's first line: `…|root: {prefix}{skill-name}/`.
|
|
58
|
+
SNIPPET_ROOT_PATTERN = re.compile(r"root:\s*(?P<root>\S.*?)\s*$")
|
|
59
|
+
|
|
37
60
|
|
|
38
61
|
def read_context_file(file_path):
|
|
39
62
|
"""Read a context file. Returns (content, None) or (None, error)."""
|
|
@@ -222,7 +245,164 @@ def cmd_insert(file_path, new_content):
|
|
|
222
245
|
return {"status": "ok", "action": "inserted", "bytes_written": len(updated)}
|
|
223
246
|
|
|
224
247
|
|
|
248
|
+
def _is_row_separator(line):
|
|
249
|
+
"""A blank/separator line between snippets: bare `|` or empty."""
|
|
250
|
+
return line.strip() in ("", "|")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def parse_managed_rows(body):
|
|
254
|
+
"""Split the between-marker `body` into skill rows.
|
|
255
|
+
|
|
256
|
+
Returns a list of {skill_name, version, snippet_text} dicts, one per
|
|
257
|
+
`[skill-name v...]` header found. `snippet_text` is the header line plus its
|
|
258
|
+
continuation lines, captured verbatim (byte-preserved) up to — but not
|
|
259
|
+
including — the next bare-`|` separator, the next row header, or end of body.
|
|
260
|
+
Non-row lines (the `[SKF Skills]` header, the IMPORTANT preamble) are skipped.
|
|
261
|
+
"""
|
|
262
|
+
lines = body.split("\n")
|
|
263
|
+
rows = []
|
|
264
|
+
i = 0
|
|
265
|
+
n = len(lines)
|
|
266
|
+
while i < n:
|
|
267
|
+
m = ROW_HEADER_PATTERN.match(lines[i])
|
|
268
|
+
if not m:
|
|
269
|
+
i += 1
|
|
270
|
+
continue
|
|
271
|
+
start = i
|
|
272
|
+
i += 1
|
|
273
|
+
while i < n and not ROW_HEADER_PATTERN.match(lines[i]) and not _is_row_separator(lines[i]):
|
|
274
|
+
i += 1
|
|
275
|
+
rows.append(
|
|
276
|
+
{
|
|
277
|
+
"skill_name": m.group("name"),
|
|
278
|
+
"version": m.group("version"),
|
|
279
|
+
"snippet_text": "\n".join(lines[start:i]),
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
return rows
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_orphan_detect(context_files, exported_skills):
|
|
286
|
+
"""Detect managed-section rows absent from the exported skill set.
|
|
287
|
+
|
|
288
|
+
Scans every context file that exists and has a managed section, parses its
|
|
289
|
+
skill rows, drops those whose skill_name is in `exported_skills`, and
|
|
290
|
+
accumulates the survivors keyed by (skill_name, version). The first-seen
|
|
291
|
+
snippet_text wins; each file the row appears in is added to source_files.
|
|
292
|
+
Deterministic: rows sorted by (skill_name, version), source_files sorted.
|
|
293
|
+
"""
|
|
294
|
+
exported = {s.strip() for s in exported_skills.split(",") if s.strip()}
|
|
295
|
+
orphans = {} # (name, version) -> {skill_name, version, snippet_text, source_files:set}
|
|
296
|
+
for cf in context_files:
|
|
297
|
+
content, err = read_context_file(cf)
|
|
298
|
+
if err:
|
|
299
|
+
continue # missing file — no orphans by definition
|
|
300
|
+
match = find_managed_section(content)
|
|
301
|
+
if not match:
|
|
302
|
+
continue # no managed section — no orphans
|
|
303
|
+
for row in parse_managed_rows(match.group(2)):
|
|
304
|
+
if row["skill_name"] in exported:
|
|
305
|
+
continue # not an orphan — it is being (re)exported
|
|
306
|
+
key = (row["skill_name"], row["version"])
|
|
307
|
+
if key in orphans:
|
|
308
|
+
orphans[key]["source_files"].add(cf) # keep first-seen snippet_text
|
|
309
|
+
else:
|
|
310
|
+
orphans[key] = {
|
|
311
|
+
"skill_name": row["skill_name"],
|
|
312
|
+
"version": row["version"],
|
|
313
|
+
"snippet_text": row["snippet_text"],
|
|
314
|
+
"source_files": {cf},
|
|
315
|
+
}
|
|
316
|
+
orphan_rows = [
|
|
317
|
+
{
|
|
318
|
+
"skill_name": v["skill_name"],
|
|
319
|
+
"version": v["version"],
|
|
320
|
+
"snippet_text": v["snippet_text"],
|
|
321
|
+
"source_files": sorted(v["source_files"]),
|
|
322
|
+
}
|
|
323
|
+
for _, v in sorted(orphans.items())
|
|
324
|
+
]
|
|
325
|
+
return {"status": "ok", "orphan_managed_rows": orphan_rows}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def cmd_root_probe(snippet_files, reference_root):
|
|
329
|
+
"""Observe snippet root prefixes and flag mismatch against a reference.
|
|
330
|
+
|
|
331
|
+
Reads the first line of each snippet that exists, parses its `root:` value,
|
|
332
|
+
strips the trailing `{skill-name}/` to recover the prefix, and collects the
|
|
333
|
+
unique prefixes. `mismatch` is True when any observed prefix differs from
|
|
334
|
+
`reference_root`. Deterministic: observed_prefixes sorted.
|
|
335
|
+
"""
|
|
336
|
+
observed = set()
|
|
337
|
+
for sf in snippet_files:
|
|
338
|
+
content, err = read_context_file(sf)
|
|
339
|
+
if err:
|
|
340
|
+
continue # missing snippet — skip
|
|
341
|
+
first_line = content.split("\n", 1)[0]
|
|
342
|
+
header = ROW_HEADER_PATTERN.match(first_line)
|
|
343
|
+
root_match = SNIPPET_ROOT_PATTERN.search(first_line)
|
|
344
|
+
if not root_match:
|
|
345
|
+
continue # no root: field — skip
|
|
346
|
+
root_value = root_match.group("root")
|
|
347
|
+
prefix = root_value
|
|
348
|
+
if header:
|
|
349
|
+
suffix = header.group("name") + "/"
|
|
350
|
+
if root_value.endswith(suffix):
|
|
351
|
+
prefix = root_value[: -len(suffix)]
|
|
352
|
+
observed.add(prefix)
|
|
353
|
+
observed_prefixes = sorted(observed)
|
|
354
|
+
return {
|
|
355
|
+
"status": "ok",
|
|
356
|
+
"reference_root": reference_root,
|
|
357
|
+
"observed_prefixes": observed_prefixes,
|
|
358
|
+
"mismatch": any(p != reference_root for p in observed_prefixes),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _run_query_action(argv):
|
|
363
|
+
"""Dispatch the read-only query actions (orphan-detect / root-probe).
|
|
364
|
+
|
|
365
|
+
These take a positional file list rather than the legacy `<file> <action>`
|
|
366
|
+
shape, so they are parsed with argparse and routed here before the legacy
|
|
367
|
+
path. Returns (result_dict, exit_code).
|
|
368
|
+
"""
|
|
369
|
+
action = argv[0]
|
|
370
|
+
if action == "orphan-detect":
|
|
371
|
+
parser = argparse.ArgumentParser(
|
|
372
|
+
prog="skf-rebuild-managed-sections.py orphan-detect",
|
|
373
|
+
description="Detect managed-section rows absent from the exported skill set.",
|
|
374
|
+
)
|
|
375
|
+
parser.add_argument("context_files", nargs="+", help="Context file paths to scan")
|
|
376
|
+
parser.add_argument(
|
|
377
|
+
"--exported-skills",
|
|
378
|
+
default="",
|
|
379
|
+
help="Comma-separated skill names being exported (excluded from orphans)",
|
|
380
|
+
)
|
|
381
|
+
args = parser.parse_args(argv[1:])
|
|
382
|
+
return cmd_orphan_detect(args.context_files, args.exported_skills), 0
|
|
383
|
+
# action == "root-probe"
|
|
384
|
+
parser = argparse.ArgumentParser(
|
|
385
|
+
prog="skf-rebuild-managed-sections.py root-probe",
|
|
386
|
+
description="Observe snippet root prefixes and flag mismatch against a reference.",
|
|
387
|
+
)
|
|
388
|
+
parser.add_argument("snippet_files", nargs="+", help="Snippet file paths to probe")
|
|
389
|
+
parser.add_argument(
|
|
390
|
+
"--reference-root",
|
|
391
|
+
required=True,
|
|
392
|
+
help="Reference skill root prefix to compare observed prefixes against",
|
|
393
|
+
)
|
|
394
|
+
args = parser.parse_args(argv[1:])
|
|
395
|
+
return cmd_root_probe(args.snippet_files, args.reference_root), 0
|
|
396
|
+
|
|
397
|
+
|
|
225
398
|
def main():
|
|
399
|
+
# Query actions take a positional file list, so they are action-first
|
|
400
|
+
# (`orphan-detect <file>...`) rather than the legacy `<file> <action>` shape.
|
|
401
|
+
if len(sys.argv) >= 2 and sys.argv[1] in ("orphan-detect", "root-probe"):
|
|
402
|
+
result, code = _run_query_action(sys.argv[1:])
|
|
403
|
+
print(json.dumps(result, indent=2))
|
|
404
|
+
sys.exit(code)
|
|
405
|
+
|
|
226
406
|
if len(sys.argv) < 3:
|
|
227
407
|
print("Usage: python3 skf-rebuild-managed-sections.py <context-file> <action> [--content <text>]", file=sys.stderr)
|
|
228
408
|
print("Actions: read, replace, clear, insert, check", file=sys.stderr)
|
|
@@ -15,7 +15,7 @@ The five-rule ladder (apply in order, first match wins):
|
|
|
15
15
|
entries or `Component[]` annotation when contents available;
|
|
16
16
|
presence-only when contents not available)
|
|
17
17
|
2. reference-app — intent text mentions wiring / integration / starter
|
|
18
|
-
/
|
|
18
|
+
/ build-config keywords, OR analysis flagged as demo/example
|
|
19
19
|
3. specific-modules — intent names a specific subset ("just the X",
|
|
20
20
|
"only the Y") OR module_count >= 6
|
|
21
21
|
4. public-api — export_count <= 8 AND intent mentions "the API",
|
|
@@ -71,7 +71,6 @@ REFERENCE_APP_KEYWORDS = (
|
|
|
71
71
|
"integration example",
|
|
72
72
|
"integration sample",
|
|
73
73
|
"starter",
|
|
74
|
-
"lifecycle",
|
|
75
74
|
"build config",
|
|
76
75
|
"build-config",
|
|
77
76
|
"demo app",
|