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
|
@@ -6,36 +6,76 @@
|
|
|
6
6
|
"""Structural Diff — Deterministic comparison of skill export inventories.
|
|
7
7
|
|
|
8
8
|
Compares a baseline export inventory against a current export inventory and
|
|
9
|
-
produces a structured JSON diff showing added, removed, and
|
|
10
|
-
Used by audit-skill and update-skill to
|
|
9
|
+
produces a structured JSON diff showing added, removed, changed, and moved
|
|
10
|
+
entries. Used by audit-skill (structural-diff step) and update-skill to
|
|
11
|
+
replace LLM-based, token-spending inventory comparison — the LLM can silently
|
|
12
|
+
drop or mis-match entries when diffing dozens/hundreds of exports by hand.
|
|
11
13
|
|
|
12
14
|
CLI:
|
|
13
15
|
python3 skf-structural-diff.py baseline.json current.json
|
|
14
16
|
python3 skf-structural-diff.py baseline.json current.json -o diff-result.json
|
|
17
|
+
python3 skf-structural-diff.py provenance-map.json snapshot.json --reexport-map map.json
|
|
15
18
|
|
|
16
19
|
Input:
|
|
17
|
-
Two JSON files. Each file
|
|
18
|
-
- An object with an "exports" array
|
|
20
|
+
Two JSON files. Each file may be either:
|
|
21
|
+
- An object with an "exports" array (extraction-snapshot format)
|
|
22
|
+
- An object with an "entries" array (provenance-map.json format)
|
|
19
23
|
- A plain array of export entries
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
- file
|
|
28
|
-
- line
|
|
29
|
-
-
|
|
24
|
+
- A name-keyed object of entry objects
|
|
25
|
+
|
|
26
|
+
Field-name aliasing (provenance-map entries[] vs extraction-snapshot
|
|
27
|
+
exports[]) is handled transparently, so the two sides may use different
|
|
28
|
+
shapes:
|
|
29
|
+
- name <- name | export_name
|
|
30
|
+
- type <- type | export_type
|
|
31
|
+
- file <- file | source_file
|
|
32
|
+
- line <- line | source_line
|
|
33
|
+
- signature <- signature
|
|
34
|
+
- confidence <- confidence
|
|
35
|
+
|
|
36
|
+
"name" is the primary match key. Nameless entries are skipped.
|
|
37
|
+
|
|
38
|
+
Canonicalization (applied symmetrically to BOTH sides before matching):
|
|
39
|
+
The baseline extractor (skf-create-skill) and the re-extractor (audit step 2)
|
|
40
|
+
can differ in cosmetic detail that would otherwise surface as false-positive
|
|
41
|
+
"Changed"/"Removed"/"Added" entries. These deterministic transforms collapse
|
|
42
|
+
the cosmetic differences:
|
|
43
|
+
- quote-style : normalize string-literal quotes in signatures
|
|
44
|
+
(double -> single), so `= "Hnsw"` == `= 'Hnsw'`.
|
|
45
|
+
- stdlib-prefix : strip module prefixes on well-known stdlib helpers
|
|
46
|
+
(typing., dataclasses., collections[.abc]., enum.),
|
|
47
|
+
so `typing.Optional[int]` == `Optional[int]`.
|
|
48
|
+
User-defined namespaces (e.g. `pkg.typing.X`) are
|
|
49
|
+
NOT collapsed.
|
|
50
|
+
- reexport-resolution : rewrite internal symbol names to their public
|
|
51
|
+
re-export name via the reexport map, so a renamed
|
|
52
|
+
public re-export (`_Impl` -> `Public`) matches the
|
|
53
|
+
baseline entry instead of showing up as
|
|
54
|
+
Removed `_Impl` + Added `Public`.
|
|
55
|
+
|
|
56
|
+
The reexport map is taken from --reexport-map when provided; otherwise it is
|
|
57
|
+
derived from the baseline provenance map itself (top-level `reexport_map`
|
|
58
|
+
plus any per-entry `reexported_as`) — the same projection produced by
|
|
59
|
+
`skf-load-provenance.py normalize`.
|
|
60
|
+
|
|
61
|
+
Change detection:
|
|
62
|
+
A field is only compared when present (non-null) on BOTH sides — a field
|
|
63
|
+
absent on one side means "insufficient data", never an asserted change. This
|
|
64
|
+
avoids false positives when the two inventories carry different metadata.
|
|
30
65
|
|
|
31
66
|
Output:
|
|
32
67
|
JSON object:
|
|
33
|
-
summary:
|
|
34
|
-
added:
|
|
35
|
-
removed:
|
|
36
|
-
changed:
|
|
37
|
-
moved:
|
|
38
|
-
unchanged_count:
|
|
68
|
+
summary: { added, removed, changed, moved, unchanged }
|
|
69
|
+
added: list of entries present in current but not baseline
|
|
70
|
+
removed: list of entries present in baseline but not current
|
|
71
|
+
changed: list of { name, field, baseline_value, current_value }
|
|
72
|
+
moved: list of { name, previous_file, current_file }
|
|
73
|
+
unchanged_count: number of entries that matched with no field change
|
|
74
|
+
applied_transforms: list of { transform, count } — which canonicalization
|
|
75
|
+
transforms actually fired and how many values each
|
|
76
|
+
touched (empty when none fired). Surfaced by audit
|
|
77
|
+
step 6's Provenance section so a reviewer can tell
|
|
78
|
+
which differences the diff collapsed.
|
|
39
79
|
|
|
40
80
|
Exit codes:
|
|
41
81
|
0 — inventories are identical (no diff)
|
|
@@ -45,7 +85,9 @@ Exit codes:
|
|
|
45
85
|
from __future__ import annotations
|
|
46
86
|
|
|
47
87
|
import argparse
|
|
88
|
+
import collections
|
|
48
89
|
import json
|
|
90
|
+
import re
|
|
49
91
|
import sys
|
|
50
92
|
from pathlib import Path
|
|
51
93
|
|
|
@@ -55,68 +97,220 @@ from pathlib import Path
|
|
|
55
97
|
# "file" is excluded — file moves are tracked separately in the "moved" list.
|
|
56
98
|
DIFF_FIELDS = ["type", "signature", "line", "confidence"]
|
|
57
99
|
|
|
100
|
+
# Well-known stdlib module prefixes whose unqualified form is importable at
|
|
101
|
+
# the call site. Longer prefixes must precede their own containing prefix
|
|
102
|
+
# (collections.abc before collections) so the alternation strips the longest.
|
|
103
|
+
# The negative lookbehind `(?<![\w.])` ensures we only strip a top-level module
|
|
104
|
+
# reference — never a user-defined namespace such as `pkg.typing.Foo`.
|
|
105
|
+
_STDLIB_PREFIX_RE = re.compile(
|
|
106
|
+
r"(?<![\w.])(?:typing|dataclasses|collections\.abc|collections|enum)\."
|
|
107
|
+
)
|
|
58
108
|
|
|
59
|
-
def load_inventory(path: Path) -> tuple[list[dict], str | None]:
|
|
60
|
-
"""Load an export inventory from a JSON file.
|
|
61
109
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
# Loading
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
65
113
|
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
|
|
115
|
+
def load_json(path: Path) -> tuple[object, str | None]:
|
|
116
|
+
"""Read and parse a JSON file. Returns (data, error_message)."""
|
|
68
117
|
try:
|
|
69
118
|
text = path.read_text(encoding="utf-8")
|
|
70
119
|
except OSError as exc:
|
|
71
|
-
return
|
|
72
|
-
|
|
120
|
+
return None, f"Cannot read file '{path}': {exc}"
|
|
73
121
|
try:
|
|
74
|
-
|
|
122
|
+
return json.loads(text), None
|
|
75
123
|
except json.JSONDecodeError as exc:
|
|
76
|
-
return
|
|
124
|
+
return None, f"Invalid JSON in '{path}': {exc}"
|
|
77
125
|
|
|
126
|
+
|
|
127
|
+
def entries_from_data(data: object, source: str = "") -> tuple[list[dict], str | None]:
|
|
128
|
+
"""Extract the export-entry list from an already-parsed inventory.
|
|
129
|
+
|
|
130
|
+
Accepts:
|
|
131
|
+
- [...] (plain array)
|
|
132
|
+
- {"exports": [...]} (extraction-snapshot format)
|
|
133
|
+
- {"entries": [...]} (provenance-map.json format)
|
|
134
|
+
- {name: {...}, ...} (name-keyed object of entry objects)
|
|
135
|
+
|
|
136
|
+
Returns (entries, error_message).
|
|
137
|
+
"""
|
|
138
|
+
where = f" in '{source}'" if source else ""
|
|
78
139
|
if isinstance(data, list):
|
|
79
140
|
return data, None
|
|
80
141
|
if isinstance(data, dict):
|
|
81
|
-
if
|
|
142
|
+
if isinstance(data.get("exports"), list):
|
|
82
143
|
return data["exports"], None
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
144
|
+
if isinstance(data.get("entries"), list):
|
|
145
|
+
return data["entries"], None
|
|
146
|
+
# A plain name-keyed object of entries (some inventory formats).
|
|
147
|
+
values = list(data.values())
|
|
148
|
+
if values and all(isinstance(v, dict) for v in values):
|
|
149
|
+
return values, None
|
|
150
|
+
return [], (
|
|
151
|
+
f"Unrecognised inventory format{where}: expected array or object "
|
|
152
|
+
f"with an 'exports' or 'entries' key"
|
|
153
|
+
)
|
|
154
|
+
return [], f"Unrecognised inventory format{where}: top-level value must be array or object"
|
|
155
|
+
|
|
88
156
|
|
|
89
|
-
|
|
157
|
+
def load_inventory(path: Path) -> tuple[list[dict], str | None]:
|
|
158
|
+
"""Load an export inventory from a JSON file (thin wrapper, back-compat).
|
|
159
|
+
|
|
160
|
+
Returns (exports, error_message).
|
|
161
|
+
"""
|
|
162
|
+
data, err = load_json(path)
|
|
163
|
+
if err:
|
|
164
|
+
return [], err
|
|
165
|
+
return entries_from_data(data, str(path))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
# Re-export map
|
|
170
|
+
# --------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def extract_reexport_map(data: object) -> dict[str, str]:
|
|
174
|
+
"""Derive the {internal -> public} re-export map from a provenance map.
|
|
175
|
+
|
|
176
|
+
Mirrors skf-load-provenance.extract_reexport_map:
|
|
177
|
+
1. top-level `reexport_map` object, plus
|
|
178
|
+
2. per-entry `reexported_as` on `entries[]` items
|
|
179
|
+
({"export_name": "_Impl", "reexported_as": "Public"}).
|
|
180
|
+
Non-string keys/values are skipped. Returns {} for non-dict input.
|
|
181
|
+
"""
|
|
182
|
+
out: dict[str, str] = {}
|
|
183
|
+
if not isinstance(data, dict):
|
|
184
|
+
return out
|
|
185
|
+
top = data.get("reexport_map")
|
|
186
|
+
if isinstance(top, dict):
|
|
187
|
+
for k, v in top.items():
|
|
188
|
+
if isinstance(k, str) and isinstance(v, str):
|
|
189
|
+
out[k] = v
|
|
190
|
+
entries = data.get("entries")
|
|
191
|
+
if isinstance(entries, list):
|
|
192
|
+
for entry in entries:
|
|
193
|
+
if not isinstance(entry, dict):
|
|
194
|
+
continue
|
|
195
|
+
internal = entry.get("export_name")
|
|
196
|
+
public = entry.get("reexported_as")
|
|
197
|
+
if isinstance(internal, str) and isinstance(public, str):
|
|
198
|
+
out.setdefault(internal, public)
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_reexport_map(path: Path) -> tuple[dict[str, str], str | None]:
|
|
203
|
+
"""Load an explicit re-export map file.
|
|
204
|
+
|
|
205
|
+
Accepts either a plain {internal: public} object or the full
|
|
206
|
+
`skf-load-provenance.py normalize` output (which nests it under the
|
|
207
|
+
`reexport_map` key). Non-string pairs are skipped.
|
|
208
|
+
"""
|
|
209
|
+
data, err = load_json(path)
|
|
210
|
+
if err:
|
|
211
|
+
return {}, err
|
|
212
|
+
if not isinstance(data, dict):
|
|
213
|
+
return {}, f"reexport map '{path}' must be a JSON object"
|
|
214
|
+
src = data["reexport_map"] if isinstance(data.get("reexport_map"), dict) else data
|
|
215
|
+
return {k: v for k, v in src.items() if isinstance(k, str) and isinstance(v, str)}, None
|
|
90
216
|
|
|
91
217
|
|
|
92
|
-
|
|
93
|
-
|
|
218
|
+
# --------------------------------------------------------------------------
|
|
219
|
+
# Canonicalization
|
|
220
|
+
# --------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _first(entry: dict, *keys: str) -> object:
|
|
224
|
+
"""First non-null value among the given keys (field-name aliasing)."""
|
|
225
|
+
for k in keys:
|
|
226
|
+
v = entry.get(k)
|
|
227
|
+
if v is not None:
|
|
228
|
+
return v
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def canon_signature(sig: object) -> tuple[object, set[str]]:
|
|
233
|
+
"""Canonicalize a signature string. Returns (canonical, transforms_fired).
|
|
234
|
+
|
|
235
|
+
Non-string input passes through unchanged (empty transform set).
|
|
236
|
+
"""
|
|
237
|
+
if not isinstance(sig, str):
|
|
238
|
+
return sig, set()
|
|
239
|
+
applied: set[str] = set()
|
|
240
|
+
out = sig
|
|
241
|
+
if '"' in out:
|
|
242
|
+
out = out.replace('"', "'")
|
|
243
|
+
applied.add("quote-style")
|
|
244
|
+
stripped = _STDLIB_PREFIX_RE.sub("", out)
|
|
245
|
+
if stripped != out:
|
|
246
|
+
applied.add("stdlib-prefix")
|
|
247
|
+
out = stripped
|
|
248
|
+
return out, applied
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _normalize_entries(
|
|
252
|
+
entries: list[dict],
|
|
253
|
+
reexport_map: dict[str, str],
|
|
254
|
+
transform_counts: collections.Counter,
|
|
255
|
+
) -> dict[str, dict]:
|
|
256
|
+
"""Build a name-keyed dict of canonicalized records.
|
|
257
|
+
|
|
258
|
+
Applies field-name aliasing, signature canonicalization, and re-export
|
|
259
|
+
name resolution. Accumulates fired-transform counts into transform_counts.
|
|
260
|
+
Nameless entries are skipped. On duplicate resolved names, the last entry
|
|
261
|
+
wins (consistent with prior behaviour).
|
|
262
|
+
"""
|
|
94
263
|
result: dict[str, dict] = {}
|
|
95
264
|
for entry in entries:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
265
|
+
if not isinstance(entry, dict):
|
|
266
|
+
continue
|
|
267
|
+
raw_name = _first(entry, "name", "export_name")
|
|
268
|
+
if not isinstance(raw_name, str) or not raw_name.strip():
|
|
269
|
+
continue
|
|
270
|
+
name = raw_name.strip()
|
|
271
|
+
resolved = reexport_map.get(name, name)
|
|
272
|
+
if resolved != name:
|
|
273
|
+
transform_counts["reexport-resolution"] += 1
|
|
274
|
+
name = resolved
|
|
275
|
+
|
|
276
|
+
canon_sig, sig_transforms = canon_signature(entry.get("signature"))
|
|
277
|
+
for t in sig_transforms:
|
|
278
|
+
transform_counts[t] += 1
|
|
279
|
+
|
|
280
|
+
# Normalized record — also the public entry shape emitted in
|
|
281
|
+
# added[]/removed[], so both sides render into one consistent table
|
|
282
|
+
# regardless of the input shape they came from.
|
|
283
|
+
result[name] = {
|
|
284
|
+
"name": name,
|
|
285
|
+
"type": _first(entry, "type", "export_type"),
|
|
286
|
+
"signature": canon_sig,
|
|
287
|
+
"file": _first(entry, "file", "source_file"),
|
|
288
|
+
"line": _first(entry, "line", "source_line"),
|
|
289
|
+
"confidence": entry.get("confidence"),
|
|
290
|
+
}
|
|
99
291
|
return result
|
|
100
292
|
|
|
101
293
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if base.get(field) != curr.get(field):
|
|
106
|
-
return False
|
|
107
|
-
return True
|
|
294
|
+
# --------------------------------------------------------------------------
|
|
295
|
+
# Diff
|
|
296
|
+
# --------------------------------------------------------------------------
|
|
108
297
|
|
|
109
298
|
|
|
110
299
|
def diff_inventories(
|
|
111
300
|
baseline_entries: list[dict],
|
|
112
301
|
current_entries: list[dict],
|
|
302
|
+
reexport_map: dict[str, str] | None = None,
|
|
113
303
|
) -> dict:
|
|
114
304
|
"""Compute the structural diff between two export inventories.
|
|
115
305
|
|
|
116
|
-
Returns a dict with keys: summary, added, removed, changed,
|
|
306
|
+
Returns a dict with keys: summary, added, removed, changed, moved,
|
|
307
|
+
unchanged_count, applied_transforms.
|
|
117
308
|
"""
|
|
118
|
-
|
|
119
|
-
|
|
309
|
+
reexport_map = reexport_map or {}
|
|
310
|
+
transform_counts: collections.Counter = collections.Counter()
|
|
311
|
+
|
|
312
|
+
baseline = _normalize_entries(baseline_entries, reexport_map, transform_counts)
|
|
313
|
+
current = _normalize_entries(current_entries, reexport_map, transform_counts)
|
|
120
314
|
|
|
121
315
|
baseline_names = set(baseline.keys())
|
|
122
316
|
current_names = set(current.keys())
|
|
@@ -125,6 +319,9 @@ def diff_inventories(
|
|
|
125
319
|
removed_names = baseline_names - current_names
|
|
126
320
|
common_names = baseline_names & current_names
|
|
127
321
|
|
|
322
|
+
# Emit normalized entries (uniform name/type/signature/file/line/confidence
|
|
323
|
+
# shape) so added[] and removed[] render into the same report table even
|
|
324
|
+
# though they originate from the snapshot and provenance-map shapes.
|
|
128
325
|
added = [current[n] for n in sorted(added_names)]
|
|
129
326
|
removed = [baseline[n] for n in sorted(removed_names)]
|
|
130
327
|
|
|
@@ -133,12 +330,12 @@ def diff_inventories(
|
|
|
133
330
|
unchanged_count = 0
|
|
134
331
|
|
|
135
332
|
for name in sorted(common_names):
|
|
136
|
-
|
|
137
|
-
|
|
333
|
+
base_rec = baseline[name]
|
|
334
|
+
curr_rec = current[name]
|
|
138
335
|
|
|
139
|
-
#
|
|
140
|
-
base_file =
|
|
141
|
-
curr_file =
|
|
336
|
+
# File moves are tracked separately from field changes.
|
|
337
|
+
base_file = base_rec.get("file")
|
|
338
|
+
curr_file = curr_rec.get("file")
|
|
142
339
|
if base_file and curr_file and base_file != curr_file:
|
|
143
340
|
moved.append({
|
|
144
341
|
"name": name,
|
|
@@ -148,8 +345,12 @@ def diff_inventories(
|
|
|
148
345
|
|
|
149
346
|
entry_changed = False
|
|
150
347
|
for field in DIFF_FIELDS:
|
|
151
|
-
base_val =
|
|
152
|
-
curr_val =
|
|
348
|
+
base_val = base_rec.get(field)
|
|
349
|
+
curr_val = curr_rec.get(field)
|
|
350
|
+
# Compare only when data is present on both sides; a missing value
|
|
351
|
+
# on either side is insufficient evidence to assert a change.
|
|
352
|
+
if base_val is None or curr_val is None:
|
|
353
|
+
continue
|
|
153
354
|
if base_val != curr_val:
|
|
154
355
|
changed.append({
|
|
155
356
|
"name": name,
|
|
@@ -162,9 +363,14 @@ def diff_inventories(
|
|
|
162
363
|
if not entry_changed:
|
|
163
364
|
unchanged_count += 1
|
|
164
365
|
|
|
165
|
-
# Count distinct names that have at least one field change
|
|
166
366
|
changed_names = len({c["name"] for c in changed})
|
|
167
367
|
|
|
368
|
+
applied_transforms = [
|
|
369
|
+
{"transform": name, "count": transform_counts[name]}
|
|
370
|
+
for name in sorted(transform_counts)
|
|
371
|
+
if transform_counts[name] > 0
|
|
372
|
+
]
|
|
373
|
+
|
|
168
374
|
return {
|
|
169
375
|
"summary": {
|
|
170
376
|
"added": len(added),
|
|
@@ -178,18 +384,26 @@ def diff_inventories(
|
|
|
178
384
|
"changed": changed,
|
|
179
385
|
"moved": moved,
|
|
180
386
|
"unchanged_count": unchanged_count,
|
|
387
|
+
"applied_transforms": applied_transforms,
|
|
181
388
|
}
|
|
182
389
|
|
|
183
390
|
|
|
391
|
+
# --------------------------------------------------------------------------
|
|
392
|
+
# CLI
|
|
393
|
+
# --------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
|
|
184
396
|
def main() -> int:
|
|
185
397
|
parser = argparse.ArgumentParser(
|
|
186
398
|
prog="skf-structural-diff.py",
|
|
187
399
|
description=(
|
|
188
400
|
"Compare two JSON export inventories and produce a structured diff.\n"
|
|
189
401
|
"\n"
|
|
190
|
-
"Each inventory
|
|
191
|
-
"
|
|
192
|
-
"
|
|
402
|
+
"Each inventory may be a JSON array of export entries, an object with\n"
|
|
403
|
+
"an 'exports' key (extraction-snapshot), or an object with an 'entries'\n"
|
|
404
|
+
"key (provenance-map.json). Field names are aliased across the two\n"
|
|
405
|
+
"shapes, and signatures are canonicalized (quote style, stdlib module\n"
|
|
406
|
+
"prefixes, public re-export names) symmetrically before matching.\n"
|
|
193
407
|
"\n"
|
|
194
408
|
"Exit code 0 means no differences were found.\n"
|
|
195
409
|
"Exit code 1 means differences were found (or an error occurred)."
|
|
@@ -197,8 +411,9 @@ def main() -> int:
|
|
|
197
411
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
198
412
|
epilog=(
|
|
199
413
|
"examples:\n"
|
|
200
|
-
" python3 skf-structural-diff.py provenance-map.json
|
|
414
|
+
" python3 skf-structural-diff.py provenance-map.json extraction-snapshot.json\n"
|
|
201
415
|
" python3 skf-structural-diff.py baseline.json current.json -o diff.json\n"
|
|
416
|
+
" python3 skf-structural-diff.py provenance-map.json snapshot.json --reexport-map map.json\n"
|
|
202
417
|
),
|
|
203
418
|
)
|
|
204
419
|
parser.add_argument(
|
|
@@ -211,6 +426,15 @@ def main() -> int:
|
|
|
211
426
|
metavar="current.json",
|
|
212
427
|
help="path to the current export inventory (e.g. extraction-snapshot.json)",
|
|
213
428
|
)
|
|
429
|
+
parser.add_argument(
|
|
430
|
+
"--reexport-map",
|
|
431
|
+
metavar="FILE",
|
|
432
|
+
help=(
|
|
433
|
+
"path to a JSON re-export map ({internal: public}) or a "
|
|
434
|
+
"skf-load-provenance normalize output. When omitted, the map is "
|
|
435
|
+
"derived from the baseline provenance map itself."
|
|
436
|
+
),
|
|
437
|
+
)
|
|
214
438
|
parser.add_argument(
|
|
215
439
|
"-o",
|
|
216
440
|
"--output",
|
|
@@ -223,17 +447,33 @@ def main() -> int:
|
|
|
223
447
|
baseline_path = Path(args.baseline)
|
|
224
448
|
current_path = Path(args.current)
|
|
225
449
|
|
|
226
|
-
|
|
450
|
+
baseline_data, err = load_json(baseline_path)
|
|
451
|
+
if err:
|
|
452
|
+
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
453
|
+
return 1
|
|
454
|
+
current_data, err = load_json(current_path)
|
|
227
455
|
if err:
|
|
228
456
|
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
229
457
|
return 1
|
|
230
458
|
|
|
231
|
-
|
|
459
|
+
baseline_entries, err = entries_from_data(baseline_data, str(baseline_path))
|
|
232
460
|
if err:
|
|
233
461
|
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
234
462
|
return 1
|
|
463
|
+
current_entries, err = entries_from_data(current_data, str(current_path))
|
|
464
|
+
if err:
|
|
465
|
+
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
466
|
+
return 1
|
|
467
|
+
|
|
468
|
+
if args.reexport_map:
|
|
469
|
+
reexport_map, err = load_reexport_map(Path(args.reexport_map))
|
|
470
|
+
if err:
|
|
471
|
+
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
472
|
+
return 1
|
|
473
|
+
else:
|
|
474
|
+
reexport_map = extract_reexport_map(baseline_data)
|
|
235
475
|
|
|
236
|
-
result = diff_inventories(baseline_entries, current_entries)
|
|
476
|
+
result = diff_inventories(baseline_entries, current_entries, reexport_map)
|
|
237
477
|
output_text = json.dumps(result, indent=2)
|
|
238
478
|
|
|
239
479
|
if args.output:
|
|
@@ -5,8 +5,8 @@
|
|
|
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 `
|
|
9
|
-
|
|
8
|
+
it against `schemas/skill-brief.v1.json` (sibling of this script), and applies the
|
|
9
|
+
conditional rules from `skf-create-skill/references/load-brief.md §3`
|
|
10
10
|
that the JSON schema doesn't express today:
|
|
11
11
|
|
|
12
12
|
- When `source_type == "docs-only"`: `doc_urls` must have ≥1 entry, and
|
|
@@ -18,6 +18,11 @@ that the JSON schema doesn't express today:
|
|
|
18
18
|
only contains whitespace would slip past a naive presence check; we
|
|
19
19
|
catch that case explicitly so the downstream directory-resolution code
|
|
20
20
|
doesn't produce `{name}//` paths.
|
|
21
|
+
- `scope.include` must contain ≥1 glob pattern for every scope type except
|
|
22
|
+
`docs-only`. The schema requires the `scope.include` key but types it as
|
|
23
|
+
an array without a `minItems` floor; this rule enforces the non-empty
|
|
24
|
+
constraint conditional on `scope.type`, matching brief-skill's
|
|
25
|
+
consumption-time validation.
|
|
21
26
|
|
|
22
27
|
The script produces skill-friendly error messages — translating raw
|
|
23
28
|
jsonschema diagnostics into the "Brief validation failed: ..." form the
|
|
@@ -56,10 +61,7 @@ import yaml
|
|
|
56
61
|
from jsonschema import Draft202012Validator
|
|
57
62
|
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
SCHEMA_PATH = (
|
|
61
|
-
REPO_ROOT / "src" / "shared" / "scripts" / "schemas" / "skill-brief.v1.json"
|
|
62
|
-
)
|
|
64
|
+
SCHEMA_PATH = Path(__file__).resolve().parent / "schemas" / "skill-brief.v1.json"
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
# --------------------------------------------------------------------------
|
|
@@ -265,6 +267,38 @@ def _version_non_empty_rule(brief: dict) -> list[dict]:
|
|
|
265
267
|
return []
|
|
266
268
|
|
|
267
269
|
|
|
270
|
+
def _scope_include_non_empty_rule(brief: dict) -> list[dict]:
|
|
271
|
+
"""`scope.include` must carry ≥1 glob pattern, except for docs-only scope.
|
|
272
|
+
|
|
273
|
+
The JSON schema requires the `scope.include` key and types it as an array of
|
|
274
|
+
strings, but cannot express the non-empty constraint conditional on
|
|
275
|
+
`scope.type`. A `docs-only` skill has no source globs to include, so it is
|
|
276
|
+
exempt; every other scope type must name at least one pattern or the
|
|
277
|
+
downstream extraction has nothing to walk. Surfacing it here keeps the
|
|
278
|
+
deterministic gate consistent with brief-skill's consumption-time
|
|
279
|
+
validation instead of paying the model to re-check structure each run.
|
|
280
|
+
"""
|
|
281
|
+
scope = brief.get("scope")
|
|
282
|
+
if not isinstance(scope, dict):
|
|
283
|
+
# missing/malformed scope is the schema's `required`/`type` job
|
|
284
|
+
return []
|
|
285
|
+
if scope.get("type") == "docs-only":
|
|
286
|
+
return []
|
|
287
|
+
include = scope.get("include")
|
|
288
|
+
if not (isinstance(include, list) and len(include) >= 1):
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
"field": "scope.include",
|
|
292
|
+
"message": (
|
|
293
|
+
"Brief validation failed: `scope.include` must contain at "
|
|
294
|
+
"least one glob pattern (docs-only scope excepted). "
|
|
295
|
+
"Update your skill-brief.yaml and re-run."
|
|
296
|
+
),
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
|
|
268
302
|
# --------------------------------------------------------------------------
|
|
269
303
|
# Main validate
|
|
270
304
|
# --------------------------------------------------------------------------
|
|
@@ -287,6 +321,7 @@ def validate_brief(brief: dict) -> dict:
|
|
|
287
321
|
|
|
288
322
|
errors.extend(_version_non_empty_rule(brief))
|
|
289
323
|
errors.extend(_target_version_matches_version_rule(brief))
|
|
324
|
+
errors.extend(_scope_include_non_empty_rule(brief))
|
|
290
325
|
|
|
291
326
|
return {
|
|
292
327
|
"valid": not errors,
|