bmad-module-skill-forge 2.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +143 -18
- package/docs/verifying-a-skill.md +87 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +3 -3
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +31 -59
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +233 -64
- package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +772 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +5247 -96
- package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +700 -0
- package/src/shared/scripts/skf-hash-content.py +180 -20
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +161 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +176 -28
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +787 -121
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +120 -67
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
- package/src/shared/scripts/skf-write-skill-brief.py +406 -25
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +149 -210
- package/src/skf-audit-skill/references/re-index.md +103 -110
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +34 -25
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +80 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
- package/src/skf-brief-skill/SKILL.md +37 -26
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +18 -25
- package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +144 -127
- package/src/skf-brief-skill/references/confirm-brief.md +11 -9
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +7 -10
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
- package/src/skf-create-skill/assets/skill-sections.md +21 -11
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +119 -70
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
- package/src/skf-create-skill/references/extract.md +204 -162
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
- package/src/skf-create-skill/references/extraction-patterns.md +588 -201
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +37 -57
- package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +83 -146
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +122 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +209 -264
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +164 -91
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +164 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +42 -155
- package/src/skf-test-skill/references/source-access-protocol.md +18 -94
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +380 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
- package/src/skf-test-skill/scripts/score-signatures.py +479 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +253 -223
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +254 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +182 -49
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +102 -35
- package/src/skf-update-skill/references/write.md +357 -155
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
3
|
-
# dependencies = ["pyyaml"]
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = ["pyyaml", "jsonschema>=4.0"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Write Skill Brief — Schema-validated atomic writer for skill-brief.yaml.
|
|
6
6
|
|
|
@@ -16,13 +16,26 @@ seams (key order, YAML formatting, conditional field inclusion rules,
|
|
|
16
16
|
atomic-write behaviour) that the LLM cannot fully close on every
|
|
17
17
|
invocation. This script is the single source of truth.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Subcommands:
|
|
20
20
|
|
|
21
21
|
write Read brief context as JSON on stdin, validate against
|
|
22
22
|
src/shared/scripts/schemas/skill-brief.v1.json, apply
|
|
23
23
|
version-precedence rules, render the canonical YAML, and
|
|
24
24
|
atomically write to --target. Emits a JSON success envelope
|
|
25
|
-
on stdout.
|
|
25
|
+
on stdout. With --base-brief, the context is a brief already
|
|
26
|
+
on disk instead, with the changes the other two flags give
|
|
27
|
+
(see "Base brief form" below).
|
|
28
|
+
|
|
29
|
+
amend Read answers as JSON on stdin and apply them to the brief
|
|
30
|
+
already at --target: set scope.registry_path and
|
|
31
|
+
scope.demo_patterns, append literal paths to scope.include,
|
|
32
|
+
append the entries to scope.amendments, refuse an amendment
|
|
33
|
+
that adds a skill-brief.v1.json error (skf-validate-brief-schema.py's
|
|
34
|
+
rules), copy the brief to <target>.bak, and write it atomically.
|
|
35
|
+
Every other field keeps its value and its place; comments are
|
|
36
|
+
not kept (the .bak copy holds them). create-skill step 3d
|
|
37
|
+
records its answers so, and step 3's authoritative-files
|
|
38
|
+
protocol its promote, skip and headless-deferral decisions.
|
|
26
39
|
|
|
27
40
|
Context payload shape (consumed by `write`):
|
|
28
41
|
|
|
@@ -47,14 +60,20 @@ Context payload shape (consumed by `write`):
|
|
|
47
60
|
"notes": "",
|
|
48
61
|
# Conditionally present (preserved verbatim on a ratify/re-write):
|
|
49
62
|
"tier_a_include": ["code/core/src/**"], # stratified-scope monorepos
|
|
50
|
-
"amendments": [{...}]
|
|
63
|
+
"amendments": [{...}], # post-authoring audit log
|
|
64
|
+
# component-library scopes (create-skill step 3d writes the first
|
|
65
|
+
# two back when the user confirms them):
|
|
66
|
+
"registry_path": "registry/index.ts",
|
|
67
|
+
"ui_variants": [{"name": "shadcnui", "package": "packages/ui"}],
|
|
68
|
+
"demo_patterns": ["**/demo/**", "**/*.stories.*"]
|
|
51
69
|
},
|
|
52
70
|
|
|
53
71
|
# Conditionally present:
|
|
54
72
|
"doc_urls": [{"url": "...", "label": "...", "source": "..."}],
|
|
55
73
|
# `source` (optional per #432): language-registry | readme-detection |
|
|
56
74
|
# homepage | pages-api | docs-folder. Threaded through when present;
|
|
57
|
-
# absent → entry renders as {url, label} only (no false drift)
|
|
75
|
+
# absent → entry renders as {url, label} only (no false drift); any
|
|
76
|
+
# other value is refused before anything is written (exit 1).
|
|
58
77
|
"scripts_intent": "detect" | "none" | free-text,
|
|
59
78
|
"assets_intent": "detect" | "none" | free-text,
|
|
60
79
|
"source_authority": "official" | "community" | "internal",
|
|
@@ -66,8 +85,12 @@ Version precedence (resolved into the rendered YAML's `version` field):
|
|
|
66
85
|
1. version_resolved if explicitly supplied (caller already ran the
|
|
67
86
|
precedence rule). Used by step 5 when it has confirmed values.
|
|
68
87
|
2. Otherwise: target_version if non-null.
|
|
69
|
-
3. Otherwise: detected_version
|
|
70
|
-
|
|
88
|
+
3. Otherwise: detected_version when it is full X.Y.Z semver (SEMVER_RE);
|
|
89
|
+
a detected version of another shape (`0.1`, `2.0.0rc1`) is passed as
|
|
90
|
+
found and skipped here, with a warning.
|
|
91
|
+
4. Otherwise: "1.0.0", with a warning when no detected_version was given
|
|
92
|
+
(a docs-only brief aside), so a headless caller tells the default
|
|
93
|
+
from a detected 1.0.0.
|
|
71
94
|
|
|
72
95
|
When target_version is set, the rendered YAML includes a `target_version`
|
|
73
96
|
field whose value MUST match `version` (the script enforces this
|
|
@@ -98,6 +121,9 @@ Flat input form (`--from-flat`):
|
|
|
98
121
|
"scope_notes": "",
|
|
99
122
|
"scope_tier_a_include": null | ["code/core/src/**"],
|
|
100
123
|
"scope_amendments": null | [{...}],
|
|
124
|
+
"scope_registry_path": null | "registry/index.ts",
|
|
125
|
+
"scope_ui_variants": null | [{"name": "...", "package": "..."}],
|
|
126
|
+
"scope_demo_patterns": null | ["**/demo/**"],
|
|
101
127
|
"doc_urls": null | [{"url": "...", "label": "...", "source": "..."}],
|
|
102
128
|
"scripts_intent": null | "detect" | "none" | "...",
|
|
103
129
|
"assets_intent": null | "detect" | "none" | "...",
|
|
@@ -106,6 +132,21 @@ Flat input form (`--from-flat`):
|
|
|
106
132
|
"source_ref": null | "v0.5.0"
|
|
107
133
|
}
|
|
108
134
|
|
|
135
|
+
Base brief form (`write --base-brief <file> [--doc-urls-file <file>]
|
|
136
|
+
[--patch-file <file>]`):
|
|
137
|
+
|
|
138
|
+
The context is the brief in --base-brief (a skill-brief.yaml), so a
|
|
139
|
+
caller that rewrites a brief never retypes it and no field it holds is
|
|
140
|
+
dropped. --doc-urls-file replaces its doc_urls with the `doc_urls` of
|
|
141
|
+
that file (the output of skf-merge-doc-urls.py). --patch-file lays a
|
|
142
|
+
JSON object of changes over it: an object merges key by key, null
|
|
143
|
+
removes the key, and any other value (a string, a list) replaces it.
|
|
144
|
+
version_resolved is the patch's version_resolved, else the result's
|
|
145
|
+
`version`, so the version the brief holds (or the one the patch sets)
|
|
146
|
+
is written; a brief with a target_version needs the patch to change
|
|
147
|
+
both. stdin is not read, and --from-flat does not apply. A date the
|
|
148
|
+
YAML holds unquoted is read as its ISO text.
|
|
149
|
+
|
|
109
150
|
Output (success):
|
|
110
151
|
|
|
111
152
|
{
|
|
@@ -116,6 +157,37 @@ Output (success):
|
|
|
116
157
|
"warnings": ["string", ...]
|
|
117
158
|
}
|
|
118
159
|
|
|
160
|
+
Answers payload (consumed by `amend`), each key optional, one at least:
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
"registry_path": "registry/index.ts",
|
|
164
|
+
"demo_patterns": ["**/examples/**"],
|
|
165
|
+
"include": ["docs/llms.txt"],
|
|
166
|
+
"amendments": [{"path": "...", "action": "...", "category": "...",
|
|
167
|
+
"reason": "...", "evidence": "...",
|
|
168
|
+
"date": "2026-10-01", "workflow": "..."}]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Each amendment needs a non-empty path, action, reason and workflow and
|
|
172
|
+
an ISO date; category, when given, is a non-empty string.
|
|
173
|
+
|
|
174
|
+
`include` holds literal paths, each appended to scope.include unless the
|
|
175
|
+
list already holds it. An empty scope.include already covers every file,
|
|
176
|
+
so it stays empty: appending one path would narrow the scope to that
|
|
177
|
+
file. The output's `included` lists the paths appended.
|
|
178
|
+
|
|
179
|
+
Output of `amend` (success):
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
"status": "ok",
|
|
183
|
+
"brief_path": "/abs/path/skill-brief.yaml",
|
|
184
|
+
"backup": "/abs/path/skill-brief.yaml.bak",
|
|
185
|
+
"set": ["registry_path", "demo_patterns"], # the fields given
|
|
186
|
+
"included": ["docs/llms.txt"], # paths appended to scope.include
|
|
187
|
+
"appended": <integer>, # amendment entries
|
|
188
|
+
"bytes": <integer>
|
|
189
|
+
}
|
|
190
|
+
|
|
119
191
|
Errors emit `{"status": "error", "message": "...", "field": "..."|null}`
|
|
120
192
|
to stderr and exit non-zero.
|
|
121
193
|
|
|
@@ -123,7 +195,11 @@ Exit codes:
|
|
|
123
195
|
0 — success
|
|
124
196
|
1 — validation failure (bad context, schema violation, invariant
|
|
125
197
|
violation, version-precedence underflow with no fallback path)
|
|
198
|
+
or, for amend, a bad payload, a brief with no scope mapping or
|
|
199
|
+
an amendment that adds a skill-brief.v1.json error
|
|
126
200
|
2 — I/O failure (atomic write failed, parent directory not writable)
|
|
201
|
+
or, for amend, a brief that cannot be read or a backup that
|
|
202
|
+
cannot be written
|
|
127
203
|
|
|
128
204
|
Cross-platform: pure stdlib + PyYAML. Atomic write via temp + fsync +
|
|
129
205
|
rename, mirroring skf-atomic-write.py and the helper in
|
|
@@ -133,6 +209,8 @@ skf-forge-tier-rw.py.
|
|
|
133
209
|
from __future__ import annotations
|
|
134
210
|
|
|
135
211
|
import argparse
|
|
212
|
+
import datetime
|
|
213
|
+
import importlib.util
|
|
136
214
|
import json
|
|
137
215
|
import os
|
|
138
216
|
import re
|
|
@@ -160,6 +238,8 @@ VALID_SCOPE_TYPES = {
|
|
|
160
238
|
"reference-app",
|
|
161
239
|
"docs-only",
|
|
162
240
|
}
|
|
241
|
+
# skill-brief.v1.json's doc_urls[].source enum (a test pins it to the schema).
|
|
242
|
+
VALID_DOC_URL_SOURCES = {"language-registry", "readme-detection", "homepage", "pages-api", "docs-folder"}
|
|
163
243
|
|
|
164
244
|
|
|
165
245
|
def _die(message: str, field: str | None = None, code: int = 1) -> None:
|
|
@@ -176,6 +256,9 @@ def resolve_version(ctx: dict[str, Any]) -> str:
|
|
|
176
256
|
Uses `is not None` checks (not truthiness) so an explicitly-supplied
|
|
177
257
|
empty string surfaces as a SEMVER_RE validation failure downstream
|
|
178
258
|
rather than silently falling through to the next precedence level.
|
|
259
|
+
The detected version is the exception: callers pass it as the manifest
|
|
260
|
+
gives it, so one that is not full X.Y.Z semver falls through to the
|
|
261
|
+
default, as validate_context's warning says.
|
|
179
262
|
"""
|
|
180
263
|
vr = ctx.get("version_resolved")
|
|
181
264
|
if vr is not None:
|
|
@@ -184,7 +267,7 @@ def resolve_version(ctx: dict[str, Any]) -> str:
|
|
|
184
267
|
if tv is not None:
|
|
185
268
|
return tv
|
|
186
269
|
dv = ctx.get("detected_version")
|
|
187
|
-
if dv
|
|
270
|
+
if isinstance(dv, str) and SEMVER_RE.match(dv):
|
|
188
271
|
return dv
|
|
189
272
|
return "1.0.0"
|
|
190
273
|
|
|
@@ -243,6 +326,10 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
|
|
|
243
326
|
url = entry.get("url")
|
|
244
327
|
if not url or not isinstance(url, str):
|
|
245
328
|
_die(f"doc_urls[{i}].url is required and must be a non-empty string", field="doc_urls")
|
|
329
|
+
source = entry.get("source")
|
|
330
|
+
if source and (not isinstance(source, str) or source not in VALID_DOC_URL_SOURCES):
|
|
331
|
+
_die(f"doc_urls[{i}].source must be one of {sorted(VALID_DOC_URL_SOURCES)}. Got: {source!r}",
|
|
332
|
+
field="doc_urls")
|
|
246
333
|
|
|
247
334
|
# source_authority (default 'community') with docs-only force rule
|
|
248
335
|
source_authority = ctx.get("source_authority", "community")
|
|
@@ -333,6 +420,8 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
|
|
|
333
420
|
if not isinstance(entry, dict):
|
|
334
421
|
_die(f"scope.amendments[{i}] must be an object", field="scope.amendments")
|
|
335
422
|
|
|
423
|
+
_check_component_fields(scope)
|
|
424
|
+
|
|
336
425
|
# target_ref / source_ref — optional git refs (top-level). target_ref is a
|
|
337
426
|
# remote-monorepo tag escape hatch; source_ref is the auto-resolved ref.
|
|
338
427
|
# Both must round-trip on a ratify/re-write rather than being dropped.
|
|
@@ -357,10 +446,46 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
|
|
|
357
446
|
warnings.append(
|
|
358
447
|
f"detected_version {detected!r} is not full X.Y.Z semver — falling through to default 1.0.0"
|
|
359
448
|
)
|
|
449
|
+
elif detected is None and tv is None and ctx.get("version_resolved") is None and source_type != "docs-only":
|
|
450
|
+
# The default would otherwise reach a headless caller as if detected.
|
|
451
|
+
warnings.append(
|
|
452
|
+
"no source version detected: version defaults to 1.0.0; for a remote source skf-create-skill "
|
|
453
|
+
"tries tag 1.0.0 or v1.0.0; set target_version in the brief to pin it"
|
|
454
|
+
)
|
|
360
455
|
|
|
361
456
|
return warnings
|
|
362
457
|
|
|
363
458
|
|
|
459
|
+
def _check_component_fields(scope: dict[str, Any]) -> None:
|
|
460
|
+
"""scope.registry_path / scope.ui_variants / scope.demo_patterns: optional
|
|
461
|
+
component-library fields. A brief can author them, and create-skill
|
|
462
|
+
step 3d writes a confirmed registry path and demo patterns back (amend),
|
|
463
|
+
so a ratify/re-write preserves them verbatim. Light checks mirror the
|
|
464
|
+
schema."""
|
|
465
|
+
registry_path = scope.get("registry_path")
|
|
466
|
+
if registry_path is not None and (not isinstance(registry_path, str) or not registry_path):
|
|
467
|
+
_die("scope.registry_path must be a non-empty string when present", field="scope.registry_path")
|
|
468
|
+
demo_patterns = scope.get("demo_patterns")
|
|
469
|
+
if demo_patterns is not None:
|
|
470
|
+
if not isinstance(demo_patterns, list):
|
|
471
|
+
_die("scope.demo_patterns must be an array of glob strings", field="scope.demo_patterns")
|
|
472
|
+
for i, pat in enumerate(demo_patterns):
|
|
473
|
+
if not isinstance(pat, str) or not pat:
|
|
474
|
+
_die(f"scope.demo_patterns[{i}] must be a non-empty glob string", field="scope.demo_patterns")
|
|
475
|
+
ui_variants = scope.get("ui_variants")
|
|
476
|
+
if ui_variants is not None:
|
|
477
|
+
if not isinstance(ui_variants, list):
|
|
478
|
+
_die("scope.ui_variants must be an array of variant objects", field="scope.ui_variants")
|
|
479
|
+
for i, variant in enumerate(ui_variants):
|
|
480
|
+
if not isinstance(variant, dict):
|
|
481
|
+
_die(f"scope.ui_variants[{i}] must be an object", field="scope.ui_variants")
|
|
482
|
+
for key in ("name", "package"):
|
|
483
|
+
value = variant.get(key)
|
|
484
|
+
if value is not None and (not isinstance(value, str) or not value):
|
|
485
|
+
_die(f"scope.ui_variants[{i}].{key} must be a non-empty string when present",
|
|
486
|
+
field="scope.ui_variants")
|
|
487
|
+
|
|
488
|
+
|
|
364
489
|
def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]:
|
|
365
490
|
"""Build the final brief dict that will be YAML-dumped, in canonical key order."""
|
|
366
491
|
source_type = ctx.get("source_type", "source")
|
|
@@ -416,6 +541,13 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
|
|
|
416
541
|
if scope_amendments is not None:
|
|
417
542
|
brief["scope"]["amendments"] = scope_amendments
|
|
418
543
|
|
|
544
|
+
# Conditional: the component-library fields, verbatim, after amendments
|
|
545
|
+
# and in the schema doc's order. Absent -> keys omitted.
|
|
546
|
+
for field in ("registry_path", "ui_variants", "demo_patterns"):
|
|
547
|
+
value = ctx["scope"].get(field)
|
|
548
|
+
if value is not None:
|
|
549
|
+
brief["scope"][field] = value
|
|
550
|
+
|
|
419
551
|
# Conditional: target_version (must equal version)
|
|
420
552
|
tv = ctx.get("target_version")
|
|
421
553
|
if tv is not None:
|
|
@@ -477,10 +609,11 @@ def render_yaml(brief: dict[str, Any]) -> str:
|
|
|
477
609
|
|
|
478
610
|
The step 5 §3 template shows leading and trailing `---` markers, but those were
|
|
479
611
|
wrapping the example YAML for documentation purposes — actual on-disk YAML uses
|
|
480
|
-
only the leading `---` (or none). A trailing `---`
|
|
481
|
-
document
|
|
482
|
-
|
|
483
|
-
|
|
612
|
+
only the leading `---` (or none). A trailing `---` starts a second, empty
|
|
613
|
+
document: SKF's own readers skip it (they parse a brief with
|
|
614
|
+
skf-resolve-authoritative-files.py's parse_brief_yaml, which reads the SKF 1.x
|
|
615
|
+
briefs that ended so), but other YAML tools that expect a single document,
|
|
616
|
+
such as `yaml.safe_load`, refuse it, so the writer leaves it out.
|
|
484
617
|
"""
|
|
485
618
|
body = yaml.safe_dump(
|
|
486
619
|
brief,
|
|
@@ -528,6 +661,9 @@ _FLAT_SCOPE_KEYS = (
|
|
|
528
661
|
"scope_rationale",
|
|
529
662
|
"scope_tier_a_include",
|
|
530
663
|
"scope_amendments",
|
|
664
|
+
"scope_registry_path",
|
|
665
|
+
"scope_ui_variants",
|
|
666
|
+
"scope_demo_patterns",
|
|
531
667
|
)
|
|
532
668
|
|
|
533
669
|
|
|
@@ -589,23 +725,115 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
|
|
|
589
725
|
scope["tier_a_include"] = flat["scope_tier_a_include"]
|
|
590
726
|
if "scope_amendments" in flat and flat["scope_amendments"] is not None:
|
|
591
727
|
scope["amendments"] = flat["scope_amendments"]
|
|
728
|
+
# Optional component-library fields: the same null-drop semantics.
|
|
729
|
+
for field in ("registry_path", "ui_variants", "demo_patterns"):
|
|
730
|
+
if flat.get(f"scope_{field}") is not None:
|
|
731
|
+
scope[field] = flat[f"scope_{field}"]
|
|
592
732
|
nested["scope"] = scope
|
|
593
733
|
return nested
|
|
594
734
|
|
|
595
735
|
|
|
596
|
-
def
|
|
597
|
-
|
|
598
|
-
if
|
|
599
|
-
|
|
736
|
+
def _plain(value: Any) -> Any:
|
|
737
|
+
"""A YAML-loaded value with each unquoted date turned into its ISO text."""
|
|
738
|
+
if isinstance(value, dict):
|
|
739
|
+
return {key: _plain(item) for key, item in value.items()}
|
|
740
|
+
if isinstance(value, list):
|
|
741
|
+
return [_plain(item) for item in value]
|
|
742
|
+
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
743
|
+
return value.isoformat()
|
|
744
|
+
return value
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _overlay(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
748
|
+
"""`patch` laid over `base`: an object merges key by key, null removes the key, any other value replaces."""
|
|
749
|
+
merged = dict(base)
|
|
750
|
+
for key, value in patch.items():
|
|
751
|
+
if value is None:
|
|
752
|
+
merged.pop(key, None)
|
|
753
|
+
elif isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
754
|
+
merged[key] = _overlay(merged[key], value)
|
|
755
|
+
else:
|
|
756
|
+
merged[key] = value
|
|
757
|
+
return merged
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _read_json_object(path: Path, flag: str) -> dict[str, Any]:
|
|
600
761
|
try:
|
|
601
|
-
|
|
602
|
-
except
|
|
603
|
-
_die(f"write:
|
|
604
|
-
|
|
605
|
-
|
|
762
|
+
raw = path.read_bytes()
|
|
763
|
+
except OSError as e:
|
|
764
|
+
_die(f"write: cannot read the {flag} file {path}: {e}", code=2)
|
|
765
|
+
try:
|
|
766
|
+
data = json.loads(raw.decode("utf-8-sig"))
|
|
767
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
768
|
+
_die(f"write: the {flag} file {path} is not JSON: {e}")
|
|
769
|
+
if not isinstance(data, dict):
|
|
770
|
+
_die(f"write: the {flag} file {path} must hold a JSON object")
|
|
771
|
+
return data
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
RESOLVER = Path(__file__).resolve().parent / "skf-resolve-authoritative-files.py"
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _parse_brief_yaml(text: str) -> Any:
|
|
778
|
+
"""A brief's YAML as skf-resolve-authoritative-files.py (from this folder)
|
|
779
|
+
reads it: an SKF 1.x brief's trailing `---` is no second document."""
|
|
780
|
+
spec = importlib.util.spec_from_file_location("skf_resolve_authoritative_files", RESOLVER)
|
|
781
|
+
if spec is None or spec.loader is None:
|
|
782
|
+
_die(f"cannot load {RESOLVER.name}", code=2)
|
|
783
|
+
resolver = importlib.util.module_from_spec(spec)
|
|
784
|
+
try:
|
|
785
|
+
spec.loader.exec_module(resolver)
|
|
786
|
+
except (OSError, ImportError, SyntaxError) as exc:
|
|
787
|
+
_die(f"cannot load {RESOLVER.name} beside {Path(__file__).name}: {exc}", code=2)
|
|
788
|
+
return resolver.parse_brief_yaml(text)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def base_context(base_brief: Path, doc_urls_file: Path | None, patch_file: Path | None) -> dict[str, Any]:
|
|
792
|
+
"""The write context of the base brief form (see the module docstring)."""
|
|
793
|
+
try:
|
|
794
|
+
original = base_brief.read_bytes()
|
|
795
|
+
except OSError as e:
|
|
796
|
+
_die(f"write: cannot read the base brief {base_brief}: {e}", code=2)
|
|
797
|
+
try:
|
|
798
|
+
brief = _parse_brief_yaml(original.decode("utf-8-sig"))
|
|
799
|
+
except (UnicodeDecodeError, yaml.YAMLError) as e:
|
|
800
|
+
_die(f"write: the base brief {base_brief} is not YAML: {e}")
|
|
801
|
+
if not isinstance(brief, dict):
|
|
802
|
+
_die(f"write: the base brief {base_brief} holds no brief mapping")
|
|
803
|
+
ctx = _plain(brief)
|
|
804
|
+
if doc_urls_file is not None:
|
|
805
|
+
merged = _read_json_object(doc_urls_file, "--doc-urls-file")
|
|
806
|
+
if "doc_urls" not in merged:
|
|
807
|
+
_die(f"write: the --doc-urls-file file {doc_urls_file} holds no doc_urls", field="doc_urls")
|
|
808
|
+
ctx["doc_urls"] = merged["doc_urls"]
|
|
809
|
+
if patch_file is not None:
|
|
810
|
+
ctx = _overlay(ctx, _read_json_object(patch_file, "--patch-file"))
|
|
811
|
+
if ctx.get("version_resolved") is None:
|
|
812
|
+
ctx["version_resolved"] = ctx.get("version")
|
|
813
|
+
return ctx
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def cmd_write(target: Path, from_flat: bool = False, base_brief: Path | None = None,
|
|
817
|
+
doc_urls_file: Path | None = None, patch_file: Path | None = None) -> int:
|
|
818
|
+
if base_brief is not None:
|
|
819
|
+
if from_flat:
|
|
820
|
+
_die("write: --from-flat and --base-brief exclude each other")
|
|
821
|
+
ctx = base_context(base_brief, doc_urls_file, patch_file)
|
|
822
|
+
elif doc_urls_file is not None or patch_file is not None:
|
|
823
|
+
_die("write: --doc-urls-file and --patch-file change a --base-brief")
|
|
824
|
+
else:
|
|
825
|
+
raw = sys.stdin.read()
|
|
826
|
+
if not raw or not raw.strip():
|
|
827
|
+
_die("write: empty stdin (expected JSON brief context)")
|
|
828
|
+
try:
|
|
829
|
+
ctx = json.loads(raw)
|
|
830
|
+
except json.JSONDecodeError as e:
|
|
831
|
+
_die(f"write: invalid JSON on stdin: {e}")
|
|
832
|
+
if not isinstance(ctx, dict):
|
|
833
|
+
_die("write: context payload must be a JSON object")
|
|
606
834
|
|
|
607
|
-
|
|
608
|
-
|
|
835
|
+
if from_flat:
|
|
836
|
+
ctx = flat_to_nested(ctx)
|
|
609
837
|
|
|
610
838
|
warnings = validate_context(ctx)
|
|
611
839
|
resolved_version = resolve_version(ctx)
|
|
@@ -631,6 +859,121 @@ def cmd_write(target: Path, from_flat: bool = False) -> int:
|
|
|
631
859
|
return 0
|
|
632
860
|
|
|
633
861
|
|
|
862
|
+
AMEND_FIELDS = ("registry_path", "demo_patterns", "include", "amendments")
|
|
863
|
+
BRIEF_VALIDATOR = Path(__file__).resolve().parent / "skf-validate-brief-schema.py"
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _brief_errors(brief: dict[str, Any]) -> list[dict[str, Any]]:
|
|
867
|
+
"""What skf-validate-brief-schema.py reports for `brief` as it would read it on disk."""
|
|
868
|
+
spec = importlib.util.spec_from_file_location("skf_validate_brief_schema", BRIEF_VALIDATOR)
|
|
869
|
+
if spec is None or spec.loader is None:
|
|
870
|
+
_die(f"amend: cannot load {BRIEF_VALIDATOR.name}", code=2)
|
|
871
|
+
validator = importlib.util.module_from_spec(spec)
|
|
872
|
+
spec.loader.exec_module(validator)
|
|
873
|
+
return validator.validate_brief(yaml.safe_load(render_yaml(brief)))["errors"]
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _added_errors(before: dict[str, Any], after: dict[str, Any]) -> list[dict[str, Any]]:
|
|
877
|
+
"""The schema errors `after` has that `before` did not: an amend refuses
|
|
878
|
+
only what it breaks, never a brief for an error it already held."""
|
|
879
|
+
known = {json.dumps(e, sort_keys=True) for e in _brief_errors(before)}
|
|
880
|
+
return [e for e in _brief_errors(after) if json.dumps(e, sort_keys=True) not in known]
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _check_include(paths: Any) -> list[str]:
|
|
884
|
+
"""The literal paths of an amend payload's `include`."""
|
|
885
|
+
if not isinstance(paths, list) or not all(isinstance(p, str) and p.strip() for p in paths):
|
|
886
|
+
_die("amend: include must be an array of non-empty paths", field="scope.include")
|
|
887
|
+
return paths
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _check_amendments(entries: Any) -> list[dict[str, Any]]:
|
|
891
|
+
"""The amendment entries of an amend payload, checked (see the docstring)."""
|
|
892
|
+
if not isinstance(entries, list):
|
|
893
|
+
_die("amend: amendments must be an array of amendment objects", field="scope.amendments")
|
|
894
|
+
for i, entry in enumerate(entries):
|
|
895
|
+
if not isinstance(entry, dict):
|
|
896
|
+
_die(f"amend: amendments[{i}] must be an object", field="scope.amendments")
|
|
897
|
+
for key in ("path", "action", "reason", "workflow"):
|
|
898
|
+
if not isinstance(entry.get(key), str) or not entry[key]:
|
|
899
|
+
_die(f"amend: amendments[{i}].{key} must be a non-empty string", field="scope.amendments")
|
|
900
|
+
if not isinstance(entry.get("date"), str) or not ISO_DATE_RE.match(entry["date"]):
|
|
901
|
+
_die(f"amend: amendments[{i}].date must be an ISO date (YYYY-MM-DD)", field="scope.amendments")
|
|
902
|
+
category = entry.get("category")
|
|
903
|
+
if category is not None and (not isinstance(category, str) or not category):
|
|
904
|
+
_die(f"amend: amendments[{i}].category must be a non-empty string when present",
|
|
905
|
+
field="scope.amendments")
|
|
906
|
+
return entries
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def cmd_amend(target: Path) -> int:
|
|
910
|
+
raw = sys.stdin.read()
|
|
911
|
+
try:
|
|
912
|
+
payload = json.loads(raw) if raw.strip() else None
|
|
913
|
+
except json.JSONDecodeError as e:
|
|
914
|
+
_die(f"amend: invalid JSON on stdin: {e}")
|
|
915
|
+
if not isinstance(payload, dict):
|
|
916
|
+
_die("amend: stdin must be a JSON object holding registry_path, demo_patterns, include or amendments")
|
|
917
|
+
unknown = sorted(set(payload) - set(AMEND_FIELDS))
|
|
918
|
+
if unknown:
|
|
919
|
+
_die(f"amend: unknown field(s) {unknown}; expected some of {list(AMEND_FIELDS)}")
|
|
920
|
+
given = [field for field in AMEND_FIELDS if payload.get(field) not in (None, [])]
|
|
921
|
+
if not given:
|
|
922
|
+
_die("amend: nothing to amend")
|
|
923
|
+
entries = _check_amendments(payload["amendments"]) if "amendments" in given else []
|
|
924
|
+
include = _check_include(payload["include"]) if "include" in given else []
|
|
925
|
+
|
|
926
|
+
try:
|
|
927
|
+
original = target.read_bytes()
|
|
928
|
+
except OSError as e:
|
|
929
|
+
_die(f"amend: cannot read {target}: {e}", code=2)
|
|
930
|
+
try:
|
|
931
|
+
brief = _parse_brief_yaml(original.decode("utf-8-sig"))
|
|
932
|
+
except (UnicodeDecodeError, yaml.YAMLError) as e:
|
|
933
|
+
_die(f"amend: {target} is not a YAML brief: {e}")
|
|
934
|
+
if not isinstance(brief, dict) or not isinstance(brief.get("scope"), dict):
|
|
935
|
+
_die(f"amend: {target} holds no scope mapping", field="scope")
|
|
936
|
+
before = yaml.safe_load(render_yaml(brief))
|
|
937
|
+
scope = brief["scope"]
|
|
938
|
+
if entries:
|
|
939
|
+
existing = scope.get("amendments")
|
|
940
|
+
if existing is not None and not isinstance(existing, list):
|
|
941
|
+
_die("amend: the brief's scope.amendments is not an array", field="scope.amendments")
|
|
942
|
+
scope["amendments"] = [*(existing or []), *entries]
|
|
943
|
+
for field in ("registry_path", "demo_patterns"):
|
|
944
|
+
if field in given:
|
|
945
|
+
scope[field] = payload[field]
|
|
946
|
+
_check_component_fields(scope)
|
|
947
|
+
included: list[str] = []
|
|
948
|
+
current = scope.get("include")
|
|
949
|
+
if include and isinstance(current, list) and current:
|
|
950
|
+
for path in include:
|
|
951
|
+
if path not in current and path not in included:
|
|
952
|
+
included.append(path)
|
|
953
|
+
scope["include"] = [*current, *included]
|
|
954
|
+
errors = _added_errors(before, brief)
|
|
955
|
+
if errors:
|
|
956
|
+
_die(f"amend: the amendment breaks skill-brief.v1.json: {errors[0].get('message')}",
|
|
957
|
+
field=errors[0].get("field"))
|
|
958
|
+
|
|
959
|
+
backup = target.with_name(target.name + ".bak")
|
|
960
|
+
try:
|
|
961
|
+
backup.write_bytes(original)
|
|
962
|
+
except OSError as e:
|
|
963
|
+
_die(f"amend: cannot write {backup}: {e}", code=2)
|
|
964
|
+
bytes_written = atomic_write(target, render_yaml(brief))
|
|
965
|
+
print(json.dumps({
|
|
966
|
+
"status": "ok",
|
|
967
|
+
"brief_path": str(target.resolve()),
|
|
968
|
+
"backup": str(backup.resolve()),
|
|
969
|
+
"set": [field for field in given if field in ("registry_path", "demo_patterns")],
|
|
970
|
+
"included": included,
|
|
971
|
+
"appended": len(entries),
|
|
972
|
+
"bytes": bytes_written,
|
|
973
|
+
}))
|
|
974
|
+
return 0
|
|
975
|
+
|
|
976
|
+
|
|
634
977
|
def main() -> int:
|
|
635
978
|
parser = argparse.ArgumentParser(
|
|
636
979
|
prog="skf-write-skill-brief",
|
|
@@ -650,12 +993,50 @@ def main() -> int:
|
|
|
650
993
|
"assembly site in step 5."
|
|
651
994
|
),
|
|
652
995
|
)
|
|
996
|
+
p_write.add_argument("--base-brief", type=Path, metavar="FILE",
|
|
997
|
+
help="start from the brief in FILE (a skill-brief.yaml) instead of reading stdin")
|
|
998
|
+
p_write.add_argument("--doc-urls-file", type=Path, metavar="FILE",
|
|
999
|
+
help="with --base-brief: replace doc_urls with the doc_urls of FILE "
|
|
1000
|
+
"(skf-merge-doc-urls.py output)")
|
|
1001
|
+
p_write.add_argument("--patch-file", type=Path, metavar="FILE",
|
|
1002
|
+
help="with --base-brief: lay the JSON object in FILE over the brief "
|
|
1003
|
+
"(objects merge, null removes, other values replace)")
|
|
1004
|
+
|
|
1005
|
+
p_amend = sub.add_parser(
|
|
1006
|
+
"amend",
|
|
1007
|
+
help=(
|
|
1008
|
+
"Apply answers (JSON on stdin) to an existing brief: set scope.registry_path and "
|
|
1009
|
+
"scope.demo_patterns, append paths to scope.include and entries to scope.amendments, "
|
|
1010
|
+
"check the brief schema, keep a .bak copy, atomic write"
|
|
1011
|
+
),
|
|
1012
|
+
)
|
|
1013
|
+
p_amend.add_argument("--target", type=Path, required=True, help="Path to the skill-brief.yaml to amend")
|
|
653
1014
|
|
|
654
1015
|
args = parser.parse_args()
|
|
655
1016
|
if args.cmd == "write":
|
|
656
|
-
return cmd_write(args.target, from_flat=args.from_flat
|
|
1017
|
+
return cmd_write(args.target, from_flat=args.from_flat, base_brief=args.base_brief,
|
|
1018
|
+
doc_urls_file=args.doc_urls_file, patch_file=args.patch_file)
|
|
1019
|
+
if args.cmd == "amend":
|
|
1020
|
+
return cmd_amend(args.target)
|
|
657
1021
|
return 2
|
|
658
1022
|
|
|
659
1023
|
|
|
1024
|
+
def _force_utf8(*streams) -> None:
|
|
1025
|
+
"""Reconfigure stdin, stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
1026
|
+
|
|
1027
|
+
A Windows console pipes them as cp1252, which would garble a payload's
|
|
1028
|
+
non-ASCII text (a description, an amendment's reason) read from stdin
|
|
1029
|
+
(reconfigured here, before the first read).
|
|
1030
|
+
"""
|
|
1031
|
+
for stream in streams:
|
|
1032
|
+
if hasattr(stream, "reconfigure"):
|
|
1033
|
+
errors = getattr(stream, "errors", None)
|
|
1034
|
+
if errors is None:
|
|
1035
|
+
stream.reconfigure(encoding="utf-8")
|
|
1036
|
+
else:
|
|
1037
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
1038
|
+
|
|
1039
|
+
|
|
660
1040
|
if __name__ == "__main__":
|
|
1041
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
661
1042
|
sys.exit(main())
|