bmad-module-skill-forge 2.0.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/docs/_data/pinned.yaml +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- 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-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- 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-manifest-ops.py +67 -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-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 +39 -1
- 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 +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- 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 +42 -180
- 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/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- 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 +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- 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 +34 -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 +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- 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 +59 -28
- 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 +51 -31
- 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 +42 -10
- package/src/skf-drop-skill/references/select.md +88 -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 +27 -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 +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- 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 +13 -10
- 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 +107 -78
- 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 +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- 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 +109 -75
- 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 +38 -20
- 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 +6 -6
- 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 +131 -1
- 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 +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -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 +12 -24
- 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/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -7,9 +7,41 @@
|
|
|
7
7
|
Validates SKILL.md frontmatter, context-snippet.md format, and metadata.json
|
|
8
8
|
schema against agentskills.io specification. Outputs JSON validation results.
|
|
9
9
|
|
|
10
|
+
Two package shapes are supported via --skill-type (default: individual):
|
|
11
|
+
individual — the single-skill schema: SKILL.md body sections
|
|
12
|
+
(Overview/Description/Key Exports/Usage) and metadata.json
|
|
13
|
+
fields (source_repo, stats.*). This is the legacy default and
|
|
14
|
+
is unchanged for existing callers (quick-skill, create-skill).
|
|
15
|
+
stack — the capstone stack schema: skips the individual body/metadata
|
|
16
|
+
checks and instead validates the stack count-equalities
|
|
17
|
+
(library_count vs per-library reference files; integration_count
|
|
18
|
+
vs integration pair files; confidence_distribution sum vs
|
|
19
|
+
library_count), emitted under validation.stack_counts.
|
|
20
|
+
|
|
21
|
+
The --export-gate flag selects a third, self-contained mode used by
|
|
22
|
+
skf-export-skill's publishing gate (load-skill.md §2 + package.md §1-3). It is
|
|
23
|
+
additive and orthogonal to --skill-type: it does NOT touch the individual/stack
|
|
24
|
+
code path above, so existing callers keep byte-identical output. Under the flag
|
|
25
|
+
the script emits the deterministic verdict the export prompt previously derived
|
|
26
|
+
by hand each run:
|
|
27
|
+
- metadata.json required-field presence for the full agentskills.io set
|
|
28
|
+
(name, version, skill_type, source_authority, exports, generation_date,
|
|
29
|
+
confidence_tier) as high-severity issues under validation.metadata.issues;
|
|
30
|
+
- enum-membership checks (skill_type, source_authority, confidence_tier) as
|
|
31
|
+
high-severity issues under validation.metadata.enum_issues;
|
|
32
|
+
- SKILL.md Section 7b (Scripts & Assets) cross-reference against on-disk
|
|
33
|
+
scripts/ and assets/ files, under validation.crossref_7b.{missing,orphans}
|
|
34
|
+
(a §7b-named file absent on disk is high; an on-disk file not named in §7b
|
|
35
|
+
is a low orphan warning);
|
|
36
|
+
- a deterministic export_status ∈ READY / WARNINGS / NOT_READY alongside the
|
|
37
|
+
existing PASS/FAIL result (NOT_READY on any high issue, WARNINGS when only
|
|
38
|
+
medium/low issues remain, READY when clean).
|
|
39
|
+
|
|
10
40
|
CLI: python3 skf-validate-output.py <skill-package-dir>
|
|
11
41
|
python3 skf-validate-output.py <skill-package-dir> --generated-by quick-skill
|
|
12
42
|
python3 skf-validate-output.py <skill-package-dir> --skip-frontmatter
|
|
43
|
+
python3 skf-validate-output.py <skill-package-dir> --skill-type stack
|
|
44
|
+
python3 skf-validate-output.py <skill-package-dir> --export-gate
|
|
13
45
|
"""
|
|
14
46
|
|
|
15
47
|
from __future__ import annotations
|
|
@@ -156,13 +188,270 @@ def validate_metadata_json(data, generated_by=None):
|
|
|
156
188
|
return issues
|
|
157
189
|
|
|
158
190
|
|
|
159
|
-
|
|
191
|
+
# --- Export-gate mode (skf-export-skill publishing gate) -------------------
|
|
192
|
+
#
|
|
193
|
+
# These helpers back the --export-gate flag only. They are additive: nothing
|
|
194
|
+
# above (individual / stack modes) calls them, so existing callers of
|
|
195
|
+
# validate_skill_package() keep byte-identical output.
|
|
196
|
+
|
|
197
|
+
_EXPORT_GATE_ENUMS = {
|
|
198
|
+
"skill_type": ("single", "stack"),
|
|
199
|
+
"source_authority": ("official", "internal", "community"),
|
|
200
|
+
"confidence_tier": ("Quick", "Forge", "Forge+", "Deep"),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Matches a scripts/… or assets/… path token (e.g. `scripts/run.py`). The
|
|
204
|
+
# leading filename char must be alphanumeric/dot/underscore so a bare
|
|
205
|
+
# `scripts/` (the §7b directory note with nothing after the slash) is not
|
|
206
|
+
# captured as a file reference.
|
|
207
|
+
_SECTION_7B_PATH_RE = re.compile(r"(?:scripts|assets)/[A-Za-z0-9._][A-Za-z0-9._/\-]*")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def validate_metadata_export_gate(data):
|
|
211
|
+
"""agentskills.io export-gate metadata validation.
|
|
212
|
+
|
|
213
|
+
Returns (required_issues, enum_issues). Required-field presence for the full
|
|
214
|
+
agentskills.io set is high-severity; enum-membership mismatches are
|
|
215
|
+
high-severity. An empty `exports` array is a low warning (matching
|
|
216
|
+
load-skill.md §2's "warn if empty — graceful handling"), not a hard halt.
|
|
217
|
+
"""
|
|
218
|
+
required_issues = []
|
|
219
|
+
enum_issues = []
|
|
220
|
+
|
|
221
|
+
# String required fields — high-severity presence checks.
|
|
222
|
+
for field in ("name", "version", "skill_type", "source_authority",
|
|
223
|
+
"generation_date", "confidence_tier"):
|
|
224
|
+
val = data.get(field)
|
|
225
|
+
if not val or not isinstance(val, str):
|
|
226
|
+
required_issues.append({
|
|
227
|
+
"severity": "high",
|
|
228
|
+
"field": field,
|
|
229
|
+
"message": f"{field} missing or not a non-empty string",
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
# exports — must be present as an array (high if absent/not-a-list).
|
|
233
|
+
exports = data.get("exports")
|
|
234
|
+
if not isinstance(exports, list):
|
|
235
|
+
required_issues.append({
|
|
236
|
+
"severity": "high",
|
|
237
|
+
"field": "exports",
|
|
238
|
+
"message": "exports missing or not an array",
|
|
239
|
+
})
|
|
240
|
+
elif len(exports) == 0:
|
|
241
|
+
required_issues.append({
|
|
242
|
+
"severity": "low",
|
|
243
|
+
"field": "exports",
|
|
244
|
+
"message": "exports array is empty",
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
# Enum membership — only when the value is present as a non-empty string
|
|
248
|
+
# (otherwise the required-field check above already fired for it).
|
|
249
|
+
for field, allowed in _EXPORT_GATE_ENUMS.items():
|
|
250
|
+
val = data.get(field)
|
|
251
|
+
if isinstance(val, str) and val and val not in allowed:
|
|
252
|
+
enum_issues.append({
|
|
253
|
+
"severity": "high",
|
|
254
|
+
"field": field,
|
|
255
|
+
"message": f"{field} '{val}' not in {list(allowed)}",
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return required_issues, enum_issues
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _extract_section_7b_refs(skill_md_text):
|
|
262
|
+
"""Extract scripts/… and assets/… paths named in SKILL.md's Section 7b
|
|
263
|
+
(Scripts & Assets). Returns a set of posix path strings.
|
|
264
|
+
|
|
265
|
+
Section 7b is optional (create-skill emits it only when scripts or assets
|
|
266
|
+
are detected), so an absent section yields an empty set. The section is
|
|
267
|
+
located by its heading — a markdown heading whose text mentions both
|
|
268
|
+
"script" and "asset" (the "Scripts & Assets" title) — and the region runs
|
|
269
|
+
until the next heading of the same or higher level.
|
|
270
|
+
"""
|
|
271
|
+
if not skill_md_text:
|
|
272
|
+
return set()
|
|
273
|
+
|
|
274
|
+
lines = skill_md_text.split("\n")
|
|
275
|
+
start = None
|
|
276
|
+
heading_level = None
|
|
277
|
+
for i, line in enumerate(lines):
|
|
278
|
+
m = re.match(r"^(#{1,6})\s+(.*)$", line)
|
|
279
|
+
if m:
|
|
280
|
+
text = m.group(2).lower()
|
|
281
|
+
if "script" in text and "asset" in text:
|
|
282
|
+
start = i + 1
|
|
283
|
+
heading_level = len(m.group(1))
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
if start is None:
|
|
287
|
+
return set()
|
|
288
|
+
|
|
289
|
+
end = len(lines)
|
|
290
|
+
for j in range(start, len(lines)):
|
|
291
|
+
m = re.match(r"^(#{1,6})\s+", lines[j])
|
|
292
|
+
if m and len(m.group(1)) <= heading_level:
|
|
293
|
+
end = j
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
region = "\n".join(lines[start:end])
|
|
297
|
+
refs = set()
|
|
298
|
+
for match in _SECTION_7B_PATH_RE.findall(region):
|
|
299
|
+
refs.add(match.rstrip("./-"))
|
|
300
|
+
return refs
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def crossref_section_7b(skill_md_text, pkg_dir):
|
|
304
|
+
"""Cross-reference SKILL.md Section 7b against on-disk scripts/ and assets/.
|
|
305
|
+
|
|
306
|
+
Returns (missing, orphans) — both sorted lists of posix relative paths
|
|
307
|
+
(e.g. "scripts/run.py"). `missing` = paths named under Section 7b that do
|
|
308
|
+
not exist on disk (a broken manifest — the caller treats these as high).
|
|
309
|
+
`orphans` = scripts/assets files present on disk but not named under
|
|
310
|
+
Section 7b (a low warning). Deterministic: same inputs → same sorted lists.
|
|
311
|
+
"""
|
|
312
|
+
pkg_dir = Path(pkg_dir)
|
|
313
|
+
referenced = _extract_section_7b_refs(skill_md_text)
|
|
314
|
+
|
|
315
|
+
on_disk = set()
|
|
316
|
+
for sub in ("scripts", "assets"):
|
|
317
|
+
d = pkg_dir / sub
|
|
318
|
+
if d.is_dir():
|
|
319
|
+
for p in d.rglob("*"):
|
|
320
|
+
if p.is_file():
|
|
321
|
+
on_disk.add(p.relative_to(pkg_dir).as_posix())
|
|
322
|
+
|
|
323
|
+
missing = sorted(referenced - on_disk)
|
|
324
|
+
orphans = sorted(on_disk - referenced)
|
|
325
|
+
return missing, orphans
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def validate_stack_counts(skill_dir, meta):
|
|
329
|
+
"""Validate stack-package count equalities against on-disk reference files.
|
|
330
|
+
|
|
331
|
+
Deterministic integer arithmetic the validate.md prompt previously did by
|
|
332
|
+
hand each run:
|
|
333
|
+
- library_count (metadata) == number of per-library reference files
|
|
334
|
+
(references/*.md, excluding the integrations/ subdir and stack-catalog.md)
|
|
335
|
+
- integration_count (metadata) == number of integration pair files
|
|
336
|
+
(references/integrations/*.md)
|
|
337
|
+
- sum(confidence_distribution over t1, t1_low, t2, t3) == library_count
|
|
338
|
+
|
|
339
|
+
Returns (issues, observed) where issues use the same {severity, field,
|
|
340
|
+
message} shape as the other validators and observed carries the exact
|
|
341
|
+
counts so the prompt can echo numbers without recomputing them.
|
|
342
|
+
"""
|
|
343
|
+
skill_dir = Path(skill_dir)
|
|
344
|
+
references = skill_dir / "references"
|
|
345
|
+
|
|
346
|
+
# Per-library reference files: top-level references/*.md.
|
|
347
|
+
# glob("*.md") is non-recursive, so references/integrations/*.md is
|
|
348
|
+
# excluded automatically; stack-catalog.md is excluded by name.
|
|
349
|
+
ref_file_count = 0
|
|
350
|
+
if references.is_dir():
|
|
351
|
+
for p in sorted(references.glob("*.md")):
|
|
352
|
+
if p.name == "stack-catalog.md":
|
|
353
|
+
continue
|
|
354
|
+
ref_file_count += 1
|
|
355
|
+
|
|
356
|
+
# Integration pair files: references/integrations/*.md.
|
|
357
|
+
integrations_dir = references / "integrations"
|
|
358
|
+
pair_file_count = 0
|
|
359
|
+
if integrations_dir.is_dir():
|
|
360
|
+
pair_file_count = sum(1 for _ in integrations_dir.glob("*.md"))
|
|
361
|
+
|
|
362
|
+
library_count_meta = meta.get("library_count")
|
|
363
|
+
integration_count_meta = meta.get("integration_count")
|
|
364
|
+
|
|
365
|
+
dist = meta.get("confidence_distribution")
|
|
366
|
+
confidence_sum = None
|
|
367
|
+
if isinstance(dist, dict):
|
|
368
|
+
confidence_sum = 0
|
|
369
|
+
for key in ("t1", "t1_low", "t2", "t3"):
|
|
370
|
+
val = dist.get(key, 0)
|
|
371
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool):
|
|
372
|
+
confidence_sum += val
|
|
373
|
+
|
|
374
|
+
observed = {
|
|
375
|
+
"library_count_meta": library_count_meta,
|
|
376
|
+
"ref_file_count": ref_file_count,
|
|
377
|
+
"integration_count_meta": integration_count_meta,
|
|
378
|
+
"pair_file_count": pair_file_count,
|
|
379
|
+
"confidence_sum": confidence_sum,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
def _is_int(v):
|
|
383
|
+
return isinstance(v, int) and not isinstance(v, bool)
|
|
384
|
+
|
|
385
|
+
issues = []
|
|
386
|
+
|
|
387
|
+
# library_count vs per-library reference file count
|
|
388
|
+
if _is_int(library_count_meta):
|
|
389
|
+
if library_count_meta != ref_file_count:
|
|
390
|
+
issues.append({
|
|
391
|
+
"severity": "medium",
|
|
392
|
+
"field": "library_count",
|
|
393
|
+
"message": f"library_count ({library_count_meta}) does not match per-library reference file count ({ref_file_count})",
|
|
394
|
+
})
|
|
395
|
+
else:
|
|
396
|
+
issues.append({
|
|
397
|
+
"severity": "medium",
|
|
398
|
+
"field": "library_count",
|
|
399
|
+
"message": "library_count missing or not an integer",
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
# integration_count vs integration pair file count
|
|
403
|
+
if _is_int(integration_count_meta):
|
|
404
|
+
if integration_count_meta != pair_file_count:
|
|
405
|
+
issues.append({
|
|
406
|
+
"severity": "medium",
|
|
407
|
+
"field": "integration_count",
|
|
408
|
+
"message": f"integration_count ({integration_count_meta}) does not match integration pair file count ({pair_file_count})",
|
|
409
|
+
})
|
|
410
|
+
else:
|
|
411
|
+
issues.append({
|
|
412
|
+
"severity": "medium",
|
|
413
|
+
"field": "integration_count",
|
|
414
|
+
"message": "integration_count missing or not an integer",
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
# confidence_distribution sum vs library_count
|
|
418
|
+
if confidence_sum is None:
|
|
419
|
+
issues.append({
|
|
420
|
+
"severity": "medium",
|
|
421
|
+
"field": "confidence_distribution",
|
|
422
|
+
"message": "confidence_distribution missing or not an object with t1/t1_low/t2/t3 keys",
|
|
423
|
+
})
|
|
424
|
+
elif _is_int(library_count_meta) and confidence_sum != library_count_meta:
|
|
425
|
+
issues.append({
|
|
426
|
+
"severity": "medium",
|
|
427
|
+
"field": "confidence_distribution",
|
|
428
|
+
"message": f"confidence_distribution sum ({confidence_sum}) does not match library_count ({library_count_meta})",
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
return issues, observed
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False, skill_type="individual", export_gate=False):
|
|
160
435
|
"""Validate a complete skill package directory.
|
|
161
436
|
|
|
162
437
|
When `skip_frontmatter` is True, the SKILL.md frontmatter pass is omitted —
|
|
163
438
|
intended for callers that already validated frontmatter via skill-check or
|
|
164
439
|
skf-validate-frontmatter.py and only want body / snippet / metadata checks.
|
|
440
|
+
|
|
441
|
+
`skill_type` selects the package schema (default "individual", unchanged for
|
|
442
|
+
existing callers). "stack" skips the individual-skill body-structure and
|
|
443
|
+
metadata-schema passes (which assume the single-skill shape) and instead
|
|
444
|
+
runs validate_stack_counts, emitting validation.stack_counts.{issues,observed}.
|
|
445
|
+
Frontmatter and context-snippet checks are shape-agnostic and run for both.
|
|
446
|
+
|
|
447
|
+
When `export_gate` is True, a self-contained export-gate validation runs
|
|
448
|
+
instead (see _validate_export_gate). This branch is additive: the
|
|
449
|
+
individual/stack code below is untouched, so callers that never pass
|
|
450
|
+
export_gate=True get byte-identical output.
|
|
165
451
|
"""
|
|
452
|
+
if export_gate:
|
|
453
|
+
return _validate_export_gate(skill_dir)
|
|
454
|
+
|
|
166
455
|
skill_dir = Path(skill_dir)
|
|
167
456
|
skill_name = skill_dir.name
|
|
168
457
|
|
|
@@ -195,8 +484,15 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False)
|
|
|
195
484
|
else:
|
|
196
485
|
fm_issues = validate_frontmatter(content, skill_name)
|
|
197
486
|
fm_section = fm_issues
|
|
198
|
-
|
|
199
|
-
|
|
487
|
+
if skill_type == "stack":
|
|
488
|
+
# Individual-skill body sections (Overview/Description/Key Exports/
|
|
489
|
+
# Usage) do not apply to a stack capstone — checked in validate.md §4.
|
|
490
|
+
body_section = {"skipped": "stack-type: individual body-structure check not applicable"}
|
|
491
|
+
body_issues = []
|
|
492
|
+
else:
|
|
493
|
+
body_issues = validate_body_structure(content)
|
|
494
|
+
body_section = body_issues
|
|
495
|
+
result["validation"]["skill_md"] = {"frontmatter": fm_section, "body": body_section}
|
|
200
496
|
for issue in fm_issues + body_issues:
|
|
201
497
|
result["summary"]["total_issues"] += 1
|
|
202
498
|
result["summary"]["by_severity"][issue["severity"]] += 1
|
|
@@ -223,17 +519,31 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False)
|
|
|
223
519
|
try:
|
|
224
520
|
with open(meta_path, encoding="utf-8") as f:
|
|
225
521
|
meta = json.load(f)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
result["
|
|
230
|
-
|
|
522
|
+
if skill_type == "stack":
|
|
523
|
+
# Individual-skill metadata schema (source_repo, stats.*) does not
|
|
524
|
+
# apply to a stack capstone; validate the stack count-equalities.
|
|
525
|
+
result["validation"]["metadata"] = {"skipped": "stack-type: individual metadata schema not checked (see validation.stack_counts)"}
|
|
526
|
+
stack_issues, observed = validate_stack_counts(skill_dir, meta)
|
|
527
|
+
result["validation"]["stack_counts"] = {"issues": stack_issues, "observed": observed}
|
|
528
|
+
for issue in stack_issues:
|
|
529
|
+
result["summary"]["total_issues"] += 1
|
|
530
|
+
result["summary"]["by_severity"][issue["severity"]] += 1
|
|
531
|
+
else:
|
|
532
|
+
meta_issues = validate_metadata_json(meta, generated_by)
|
|
533
|
+
result["validation"]["metadata"] = {"issues": meta_issues}
|
|
534
|
+
for issue in meta_issues:
|
|
535
|
+
result["summary"]["total_issues"] += 1
|
|
536
|
+
result["summary"]["by_severity"][issue["severity"]] += 1
|
|
231
537
|
except json.JSONDecodeError as e:
|
|
232
538
|
result["validation"]["metadata"] = {"error": f"JSON parse error: {e}"}
|
|
233
539
|
result["summary"]["total_issues"] += 1
|
|
234
540
|
result["summary"]["by_severity"]["high"] += 1
|
|
541
|
+
if skill_type == "stack":
|
|
542
|
+
result["validation"]["stack_counts"] = {"skipped": f"metadata.json parse error: {e}"}
|
|
235
543
|
else:
|
|
236
544
|
result["validation"]["metadata"] = {"skipped": "metadata.json not found"}
|
|
545
|
+
if skill_type == "stack":
|
|
546
|
+
result["validation"]["stack_counts"] = {"skipped": "metadata.json not found"}
|
|
237
547
|
|
|
238
548
|
# Overall pass/fail
|
|
239
549
|
result["result"] = "PASS" if result["summary"]["by_severity"]["high"] == 0 else "FAIL"
|
|
@@ -241,16 +551,125 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False)
|
|
|
241
551
|
return result
|
|
242
552
|
|
|
243
553
|
|
|
554
|
+
def _validate_export_gate(skill_dir):
|
|
555
|
+
"""Export-gate validation for skf-export-skill's publishing gate.
|
|
556
|
+
|
|
557
|
+
Self-contained: does not run the individual/stack passes. Emits the
|
|
558
|
+
deterministic verdict load-skill.md §2 and package.md §1-3 previously
|
|
559
|
+
derived in-prompt — SKILL.md presence/non-emptiness, metadata.json valid
|
|
560
|
+
JSON + required-field presence + enum membership, and the SKILL.md
|
|
561
|
+
Section 7b <-> on-disk scripts/assets cross-reference — plus a deterministic
|
|
562
|
+
export_status ∈ READY / WARNINGS / NOT_READY.
|
|
563
|
+
"""
|
|
564
|
+
skill_dir = Path(skill_dir)
|
|
565
|
+
skill_name = skill_dir.name
|
|
566
|
+
|
|
567
|
+
result = {
|
|
568
|
+
"status": "ok",
|
|
569
|
+
"mode": "export-gate",
|
|
570
|
+
"skill_dir": str(skill_dir),
|
|
571
|
+
"skill_name": skill_name,
|
|
572
|
+
"files_found": {},
|
|
573
|
+
"validation": {},
|
|
574
|
+
"summary": {"total_issues": 0, "by_severity": {"high": 0, "medium": 0, "low": 0}},
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
def _record(issues):
|
|
578
|
+
for issue in issues:
|
|
579
|
+
result["summary"]["total_issues"] += 1
|
|
580
|
+
result["summary"]["by_severity"][issue["severity"]] += 1
|
|
581
|
+
|
|
582
|
+
skill_md_path = skill_dir / "SKILL.md"
|
|
583
|
+
meta_path = skill_dir / "metadata.json"
|
|
584
|
+
result["files_found"]["SKILL.md"] = skill_md_path.exists()
|
|
585
|
+
result["files_found"]["metadata.json"] = meta_path.exists()
|
|
586
|
+
|
|
587
|
+
# 1. SKILL.md must exist and be non-empty.
|
|
588
|
+
skill_md_text = ""
|
|
589
|
+
skill_md_issues = []
|
|
590
|
+
if not skill_md_path.exists():
|
|
591
|
+
skill_md_issues.append({"severity": "high", "field": "SKILL.md", "message": "SKILL.md not found"})
|
|
592
|
+
else:
|
|
593
|
+
skill_md_text = skill_md_path.read_text(encoding="utf-8")
|
|
594
|
+
if not skill_md_text.strip():
|
|
595
|
+
skill_md_issues.append({"severity": "high", "field": "SKILL.md", "message": "SKILL.md is empty"})
|
|
596
|
+
result["validation"]["skill_md"] = {"issues": skill_md_issues}
|
|
597
|
+
_record(skill_md_issues)
|
|
598
|
+
|
|
599
|
+
# 2. metadata.json must exist, parse as JSON, and satisfy the required-field
|
|
600
|
+
# presence + enum-membership contract.
|
|
601
|
+
if not meta_path.exists():
|
|
602
|
+
meta_issues = [{"severity": "high", "field": "metadata.json", "message": "metadata.json not found"}]
|
|
603
|
+
result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
|
|
604
|
+
_record(meta_issues)
|
|
605
|
+
else:
|
|
606
|
+
try:
|
|
607
|
+
with open(meta_path, encoding="utf-8") as f:
|
|
608
|
+
meta = json.load(f)
|
|
609
|
+
except json.JSONDecodeError as e:
|
|
610
|
+
meta_issues = [{"severity": "high", "field": "metadata.json", "message": f"JSON parse error: {e}"}]
|
|
611
|
+
result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
|
|
612
|
+
_record(meta_issues)
|
|
613
|
+
else:
|
|
614
|
+
required_issues, enum_issues = validate_metadata_export_gate(meta)
|
|
615
|
+
result["validation"]["metadata"] = {"issues": required_issues, "enum_issues": enum_issues}
|
|
616
|
+
_record(required_issues)
|
|
617
|
+
_record(enum_issues)
|
|
618
|
+
|
|
619
|
+
# 3. SKILL.md Section 7b <-> on-disk scripts/assets cross-reference.
|
|
620
|
+
missing, orphans = crossref_section_7b(skill_md_text, skill_dir)
|
|
621
|
+
crossref_issues = []
|
|
622
|
+
for path in missing:
|
|
623
|
+
crossref_issues.append({
|
|
624
|
+
"severity": "high",
|
|
625
|
+
"field": "crossref_7b",
|
|
626
|
+
"message": f"SKILL.md Section 7b references '{path}' but it is absent on disk",
|
|
627
|
+
})
|
|
628
|
+
for path in orphans:
|
|
629
|
+
crossref_issues.append({
|
|
630
|
+
"severity": "low",
|
|
631
|
+
"field": "crossref_7b",
|
|
632
|
+
"message": f"'{path}' present on disk but not referenced in SKILL.md Section 7b (orphan)",
|
|
633
|
+
})
|
|
634
|
+
result["validation"]["crossref_7b"] = {
|
|
635
|
+
"missing": missing,
|
|
636
|
+
"orphans": orphans,
|
|
637
|
+
"issues": crossref_issues,
|
|
638
|
+
}
|
|
639
|
+
_record(crossref_issues)
|
|
640
|
+
|
|
641
|
+
# Deterministic export_status + PASS/FAIL result.
|
|
642
|
+
by_sev = result["summary"]["by_severity"]
|
|
643
|
+
if by_sev["high"] > 0:
|
|
644
|
+
result["export_status"] = "NOT_READY"
|
|
645
|
+
elif by_sev["medium"] > 0 or by_sev["low"] > 0:
|
|
646
|
+
result["export_status"] = "WARNINGS"
|
|
647
|
+
else:
|
|
648
|
+
result["export_status"] = "READY"
|
|
649
|
+
result["result"] = "PASS" if by_sev["high"] == 0 else "FAIL"
|
|
650
|
+
|
|
651
|
+
return result
|
|
652
|
+
|
|
653
|
+
|
|
244
654
|
if __name__ == "__main__":
|
|
245
655
|
if len(sys.argv) < 2:
|
|
246
656
|
print(
|
|
247
657
|
"Usage: python3 skf-validate-output.py <skill-package-dir> "
|
|
248
|
-
"[--generated-by <generator>] [--skip-frontmatter]"
|
|
658
|
+
"[--generated-by <generator>] [--skip-frontmatter] "
|
|
659
|
+
"[--skill-type {individual,stack}] [--export-gate]",
|
|
249
660
|
file=sys.stderr,
|
|
250
661
|
)
|
|
251
662
|
sys.exit(1)
|
|
252
663
|
|
|
253
664
|
pkg_dir = sys.argv[1]
|
|
665
|
+
|
|
666
|
+
# --export-gate is a distinct, self-contained mode (skf-export-skill's
|
|
667
|
+
# publishing gate). It ignores the individual/stack flags below.
|
|
668
|
+
if "--export-gate" in sys.argv:
|
|
669
|
+
result = validate_skill_package(pkg_dir, export_gate=True)
|
|
670
|
+
print(json.dumps(result, indent=2))
|
|
671
|
+
sys.exit(0 if result["result"] == "PASS" else 1)
|
|
672
|
+
|
|
254
673
|
gen_by = None
|
|
255
674
|
if "--generated-by" in sys.argv:
|
|
256
675
|
idx = sys.argv.index("--generated-by")
|
|
@@ -259,6 +678,20 @@ if __name__ == "__main__":
|
|
|
259
678
|
|
|
260
679
|
skip_fm = "--skip-frontmatter" in sys.argv
|
|
261
680
|
|
|
262
|
-
|
|
681
|
+
skill_type = "individual"
|
|
682
|
+
if "--skill-type" in sys.argv:
|
|
683
|
+
idx = sys.argv.index("--skill-type")
|
|
684
|
+
if idx + 1 < len(sys.argv):
|
|
685
|
+
skill_type = sys.argv[idx + 1]
|
|
686
|
+
if skill_type not in ("individual", "stack"):
|
|
687
|
+
print(
|
|
688
|
+
f"Error: --skill-type must be 'individual' or 'stack', got: {skill_type}",
|
|
689
|
+
file=sys.stderr,
|
|
690
|
+
)
|
|
691
|
+
sys.exit(2)
|
|
692
|
+
|
|
693
|
+
result = validate_skill_package(
|
|
694
|
+
pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm, skill_type=skill_type
|
|
695
|
+
)
|
|
263
696
|
print(json.dumps(result, indent=2))
|
|
264
697
|
sys.exit(0 if result["result"] == "PASS" else 1)
|
|
@@ -51,7 +51,7 @@ import sys
|
|
|
51
51
|
from typing import Any, Dict, List, Optional, Tuple
|
|
52
52
|
|
|
53
53
|
_GITHUB_URL_RE = re.compile(
|
|
54
|
-
r"https?://(?:www\.)?github\.com/([^/\s]+)/([^/\s
|
|
54
|
+
r"https?://(?:www\.)?github\.com/([^/\s]+)/([^/\s]+?)(?:\.git)?/?$",
|
|
55
55
|
re.IGNORECASE,
|
|
56
56
|
)
|
|
57
57
|
|