bmad-module-skill-forge 2.2.0 → 3.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 +135 -18
- package/docs/verifying-a-skill.md +83 -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 +2 -2
- 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 +32 -12
- 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 +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- 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 +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- 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 +3 -2
- 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 +1 -1
- 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 +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -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 +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -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 +1 -1
- 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 +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- 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 +128 -13
- 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 +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- 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 +4 -1
- 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 +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- 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 +27 -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/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- 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 +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- 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 +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- 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 +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- 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 +107 -29
- 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 +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- 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 +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- 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 +79 -146
- 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 +118 -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 +217 -254
- 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 +163 -88
- 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 +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- 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 +287 -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 +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -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 +127 -52
- 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 +233 -222
- 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 +250 -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 +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- 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,7 +60,12 @@ 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:
|
|
@@ -66,7 +84,9 @@ Version precedence (resolved into the rendered YAML's `version` field):
|
|
|
66
84
|
1. version_resolved if explicitly supplied (caller already ran the
|
|
67
85
|
precedence rule). Used by step 5 when it has confirmed values.
|
|
68
86
|
2. Otherwise: target_version if non-null.
|
|
69
|
-
3. Otherwise: detected_version
|
|
87
|
+
3. Otherwise: detected_version when it is full X.Y.Z semver (SEMVER_RE);
|
|
88
|
+
a detected version of another shape (`0.1`, `2.0.0rc1`) is passed as
|
|
89
|
+
found and skipped here, with a warning.
|
|
70
90
|
4. Otherwise: "1.0.0".
|
|
71
91
|
|
|
72
92
|
When target_version is set, the rendered YAML includes a `target_version`
|
|
@@ -98,6 +118,9 @@ Flat input form (`--from-flat`):
|
|
|
98
118
|
"scope_notes": "",
|
|
99
119
|
"scope_tier_a_include": null | ["code/core/src/**"],
|
|
100
120
|
"scope_amendments": null | [{...}],
|
|
121
|
+
"scope_registry_path": null | "registry/index.ts",
|
|
122
|
+
"scope_ui_variants": null | [{"name": "...", "package": "..."}],
|
|
123
|
+
"scope_demo_patterns": null | ["**/demo/**"],
|
|
101
124
|
"doc_urls": null | [{"url": "...", "label": "...", "source": "..."}],
|
|
102
125
|
"scripts_intent": null | "detect" | "none" | "...",
|
|
103
126
|
"assets_intent": null | "detect" | "none" | "...",
|
|
@@ -106,6 +129,21 @@ Flat input form (`--from-flat`):
|
|
|
106
129
|
"source_ref": null | "v0.5.0"
|
|
107
130
|
}
|
|
108
131
|
|
|
132
|
+
Base brief form (`write --base-brief <file> [--doc-urls-file <file>]
|
|
133
|
+
[--patch-file <file>]`):
|
|
134
|
+
|
|
135
|
+
The context is the brief in --base-brief (a skill-brief.yaml), so a
|
|
136
|
+
caller that rewrites a brief never retypes it and no field it holds is
|
|
137
|
+
dropped. --doc-urls-file replaces its doc_urls with the `doc_urls` of
|
|
138
|
+
that file (the output of skf-merge-doc-urls.py). --patch-file lays a
|
|
139
|
+
JSON object of changes over it: an object merges key by key, null
|
|
140
|
+
removes the key, and any other value (a string, a list) replaces it.
|
|
141
|
+
version_resolved is the patch's version_resolved, else the result's
|
|
142
|
+
`version`, so the version the brief holds (or the one the patch sets)
|
|
143
|
+
is written; a brief with a target_version needs the patch to change
|
|
144
|
+
both. stdin is not read, and --from-flat does not apply. A date the
|
|
145
|
+
YAML holds unquoted is read as its ISO text.
|
|
146
|
+
|
|
109
147
|
Output (success):
|
|
110
148
|
|
|
111
149
|
{
|
|
@@ -116,6 +154,37 @@ Output (success):
|
|
|
116
154
|
"warnings": ["string", ...]
|
|
117
155
|
}
|
|
118
156
|
|
|
157
|
+
Answers payload (consumed by `amend`), each key optional, one at least:
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
"registry_path": "registry/index.ts",
|
|
161
|
+
"demo_patterns": ["**/examples/**"],
|
|
162
|
+
"include": ["docs/llms.txt"],
|
|
163
|
+
"amendments": [{"path": "...", "action": "...", "category": "...",
|
|
164
|
+
"reason": "...", "evidence": "...",
|
|
165
|
+
"date": "2026-10-01", "workflow": "..."}]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Each amendment needs a non-empty path, action, reason and workflow and
|
|
169
|
+
an ISO date; category, when given, is a non-empty string.
|
|
170
|
+
|
|
171
|
+
`include` holds literal paths, each appended to scope.include unless the
|
|
172
|
+
list already holds it. An empty scope.include already covers every file,
|
|
173
|
+
so it stays empty: appending one path would narrow the scope to that
|
|
174
|
+
file. The output's `included` lists the paths appended.
|
|
175
|
+
|
|
176
|
+
Output of `amend` (success):
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
"status": "ok",
|
|
180
|
+
"brief_path": "/abs/path/skill-brief.yaml",
|
|
181
|
+
"backup": "/abs/path/skill-brief.yaml.bak",
|
|
182
|
+
"set": ["registry_path", "demo_patterns"], # the fields given
|
|
183
|
+
"included": ["docs/llms.txt"], # paths appended to scope.include
|
|
184
|
+
"appended": <integer>, # amendment entries
|
|
185
|
+
"bytes": <integer>
|
|
186
|
+
}
|
|
187
|
+
|
|
119
188
|
Errors emit `{"status": "error", "message": "...", "field": "..."|null}`
|
|
120
189
|
to stderr and exit non-zero.
|
|
121
190
|
|
|
@@ -123,7 +192,11 @@ Exit codes:
|
|
|
123
192
|
0 — success
|
|
124
193
|
1 — validation failure (bad context, schema violation, invariant
|
|
125
194
|
violation, version-precedence underflow with no fallback path)
|
|
195
|
+
or, for amend, a bad payload, a brief with no scope mapping or
|
|
196
|
+
an amendment that adds a skill-brief.v1.json error
|
|
126
197
|
2 — I/O failure (atomic write failed, parent directory not writable)
|
|
198
|
+
or, for amend, a brief that cannot be read or a backup that
|
|
199
|
+
cannot be written
|
|
127
200
|
|
|
128
201
|
Cross-platform: pure stdlib + PyYAML. Atomic write via temp + fsync +
|
|
129
202
|
rename, mirroring skf-atomic-write.py and the helper in
|
|
@@ -133,6 +206,8 @@ skf-forge-tier-rw.py.
|
|
|
133
206
|
from __future__ import annotations
|
|
134
207
|
|
|
135
208
|
import argparse
|
|
209
|
+
import datetime
|
|
210
|
+
import importlib.util
|
|
136
211
|
import json
|
|
137
212
|
import os
|
|
138
213
|
import re
|
|
@@ -176,6 +251,9 @@ def resolve_version(ctx: dict[str, Any]) -> str:
|
|
|
176
251
|
Uses `is not None` checks (not truthiness) so an explicitly-supplied
|
|
177
252
|
empty string surfaces as a SEMVER_RE validation failure downstream
|
|
178
253
|
rather than silently falling through to the next precedence level.
|
|
254
|
+
The detected version is the exception: callers pass it as the manifest
|
|
255
|
+
gives it, so one that is not full X.Y.Z semver falls through to the
|
|
256
|
+
default, as validate_context's warning says.
|
|
179
257
|
"""
|
|
180
258
|
vr = ctx.get("version_resolved")
|
|
181
259
|
if vr is not None:
|
|
@@ -184,7 +262,7 @@ def resolve_version(ctx: dict[str, Any]) -> str:
|
|
|
184
262
|
if tv is not None:
|
|
185
263
|
return tv
|
|
186
264
|
dv = ctx.get("detected_version")
|
|
187
|
-
if dv
|
|
265
|
+
if isinstance(dv, str) and SEMVER_RE.match(dv):
|
|
188
266
|
return dv
|
|
189
267
|
return "1.0.0"
|
|
190
268
|
|
|
@@ -333,6 +411,8 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
|
|
|
333
411
|
if not isinstance(entry, dict):
|
|
334
412
|
_die(f"scope.amendments[{i}] must be an object", field="scope.amendments")
|
|
335
413
|
|
|
414
|
+
_check_component_fields(scope)
|
|
415
|
+
|
|
336
416
|
# target_ref / source_ref — optional git refs (top-level). target_ref is a
|
|
337
417
|
# remote-monorepo tag escape hatch; source_ref is the auto-resolved ref.
|
|
338
418
|
# Both must round-trip on a ratify/re-write rather than being dropped.
|
|
@@ -361,6 +441,36 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
|
|
|
361
441
|
return warnings
|
|
362
442
|
|
|
363
443
|
|
|
444
|
+
def _check_component_fields(scope: dict[str, Any]) -> None:
|
|
445
|
+
"""scope.registry_path / scope.ui_variants / scope.demo_patterns: optional
|
|
446
|
+
component-library fields. A brief can author them, and create-skill
|
|
447
|
+
step 3d writes a confirmed registry path and demo patterns back (amend),
|
|
448
|
+
so a ratify/re-write preserves them verbatim. Light checks mirror the
|
|
449
|
+
schema."""
|
|
450
|
+
registry_path = scope.get("registry_path")
|
|
451
|
+
if registry_path is not None and (not isinstance(registry_path, str) or not registry_path):
|
|
452
|
+
_die("scope.registry_path must be a non-empty string when present", field="scope.registry_path")
|
|
453
|
+
demo_patterns = scope.get("demo_patterns")
|
|
454
|
+
if demo_patterns is not None:
|
|
455
|
+
if not isinstance(demo_patterns, list):
|
|
456
|
+
_die("scope.demo_patterns must be an array of glob strings", field="scope.demo_patterns")
|
|
457
|
+
for i, pat in enumerate(demo_patterns):
|
|
458
|
+
if not isinstance(pat, str) or not pat:
|
|
459
|
+
_die(f"scope.demo_patterns[{i}] must be a non-empty glob string", field="scope.demo_patterns")
|
|
460
|
+
ui_variants = scope.get("ui_variants")
|
|
461
|
+
if ui_variants is not None:
|
|
462
|
+
if not isinstance(ui_variants, list):
|
|
463
|
+
_die("scope.ui_variants must be an array of variant objects", field="scope.ui_variants")
|
|
464
|
+
for i, variant in enumerate(ui_variants):
|
|
465
|
+
if not isinstance(variant, dict):
|
|
466
|
+
_die(f"scope.ui_variants[{i}] must be an object", field="scope.ui_variants")
|
|
467
|
+
for key in ("name", "package"):
|
|
468
|
+
value = variant.get(key)
|
|
469
|
+
if value is not None and (not isinstance(value, str) or not value):
|
|
470
|
+
_die(f"scope.ui_variants[{i}].{key} must be a non-empty string when present",
|
|
471
|
+
field="scope.ui_variants")
|
|
472
|
+
|
|
473
|
+
|
|
364
474
|
def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]:
|
|
365
475
|
"""Build the final brief dict that will be YAML-dumped, in canonical key order."""
|
|
366
476
|
source_type = ctx.get("source_type", "source")
|
|
@@ -416,6 +526,13 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
|
|
|
416
526
|
if scope_amendments is not None:
|
|
417
527
|
brief["scope"]["amendments"] = scope_amendments
|
|
418
528
|
|
|
529
|
+
# Conditional: the component-library fields, verbatim, after amendments
|
|
530
|
+
# and in the schema doc's order. Absent -> keys omitted.
|
|
531
|
+
for field in ("registry_path", "ui_variants", "demo_patterns"):
|
|
532
|
+
value = ctx["scope"].get(field)
|
|
533
|
+
if value is not None:
|
|
534
|
+
brief["scope"][field] = value
|
|
535
|
+
|
|
419
536
|
# Conditional: target_version (must equal version)
|
|
420
537
|
tv = ctx.get("target_version")
|
|
421
538
|
if tv is not None:
|
|
@@ -528,6 +645,9 @@ _FLAT_SCOPE_KEYS = (
|
|
|
528
645
|
"scope_rationale",
|
|
529
646
|
"scope_tier_a_include",
|
|
530
647
|
"scope_amendments",
|
|
648
|
+
"scope_registry_path",
|
|
649
|
+
"scope_ui_variants",
|
|
650
|
+
"scope_demo_patterns",
|
|
531
651
|
)
|
|
532
652
|
|
|
533
653
|
|
|
@@ -589,23 +709,98 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
|
|
|
589
709
|
scope["tier_a_include"] = flat["scope_tier_a_include"]
|
|
590
710
|
if "scope_amendments" in flat and flat["scope_amendments"] is not None:
|
|
591
711
|
scope["amendments"] = flat["scope_amendments"]
|
|
712
|
+
# Optional component-library fields: the same null-drop semantics.
|
|
713
|
+
for field in ("registry_path", "ui_variants", "demo_patterns"):
|
|
714
|
+
if flat.get(f"scope_{field}") is not None:
|
|
715
|
+
scope[field] = flat[f"scope_{field}"]
|
|
592
716
|
nested["scope"] = scope
|
|
593
717
|
return nested
|
|
594
718
|
|
|
595
719
|
|
|
596
|
-
def
|
|
597
|
-
|
|
598
|
-
if
|
|
599
|
-
|
|
720
|
+
def _plain(value: Any) -> Any:
|
|
721
|
+
"""A YAML-loaded value with each unquoted date turned into its ISO text."""
|
|
722
|
+
if isinstance(value, dict):
|
|
723
|
+
return {key: _plain(item) for key, item in value.items()}
|
|
724
|
+
if isinstance(value, list):
|
|
725
|
+
return [_plain(item) for item in value]
|
|
726
|
+
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
727
|
+
return value.isoformat()
|
|
728
|
+
return value
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _overlay(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
732
|
+
"""`patch` laid over `base`: an object merges key by key, null removes the key, any other value replaces."""
|
|
733
|
+
merged = dict(base)
|
|
734
|
+
for key, value in patch.items():
|
|
735
|
+
if value is None:
|
|
736
|
+
merged.pop(key, None)
|
|
737
|
+
elif isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
738
|
+
merged[key] = _overlay(merged[key], value)
|
|
739
|
+
else:
|
|
740
|
+
merged[key] = value
|
|
741
|
+
return merged
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _read_json_object(path: Path, flag: str) -> dict[str, Any]:
|
|
600
745
|
try:
|
|
601
|
-
|
|
602
|
-
except
|
|
603
|
-
_die(f"write:
|
|
604
|
-
|
|
605
|
-
|
|
746
|
+
raw = path.read_bytes()
|
|
747
|
+
except OSError as e:
|
|
748
|
+
_die(f"write: cannot read the {flag} file {path}: {e}", code=2)
|
|
749
|
+
try:
|
|
750
|
+
data = json.loads(raw.decode("utf-8-sig"))
|
|
751
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
752
|
+
_die(f"write: the {flag} file {path} is not JSON: {e}")
|
|
753
|
+
if not isinstance(data, dict):
|
|
754
|
+
_die(f"write: the {flag} file {path} must hold a JSON object")
|
|
755
|
+
return data
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def base_context(base_brief: Path, doc_urls_file: Path | None, patch_file: Path | None) -> dict[str, Any]:
|
|
759
|
+
"""The write context of the base brief form (see the module docstring)."""
|
|
760
|
+
try:
|
|
761
|
+
original = base_brief.read_bytes()
|
|
762
|
+
except OSError as e:
|
|
763
|
+
_die(f"write: cannot read the base brief {base_brief}: {e}", code=2)
|
|
764
|
+
try:
|
|
765
|
+
brief = yaml.safe_load(original.decode("utf-8-sig"))
|
|
766
|
+
except (UnicodeDecodeError, yaml.YAMLError) as e:
|
|
767
|
+
_die(f"write: the base brief {base_brief} is not YAML: {e}")
|
|
768
|
+
if not isinstance(brief, dict):
|
|
769
|
+
_die(f"write: the base brief {base_brief} holds no brief mapping")
|
|
770
|
+
ctx = _plain(brief)
|
|
771
|
+
if doc_urls_file is not None:
|
|
772
|
+
merged = _read_json_object(doc_urls_file, "--doc-urls-file")
|
|
773
|
+
if "doc_urls" not in merged:
|
|
774
|
+
_die(f"write: the --doc-urls-file file {doc_urls_file} holds no doc_urls", field="doc_urls")
|
|
775
|
+
ctx["doc_urls"] = merged["doc_urls"]
|
|
776
|
+
if patch_file is not None:
|
|
777
|
+
ctx = _overlay(ctx, _read_json_object(patch_file, "--patch-file"))
|
|
778
|
+
if ctx.get("version_resolved") is None:
|
|
779
|
+
ctx["version_resolved"] = ctx.get("version")
|
|
780
|
+
return ctx
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def cmd_write(target: Path, from_flat: bool = False, base_brief: Path | None = None,
|
|
784
|
+
doc_urls_file: Path | None = None, patch_file: Path | None = None) -> int:
|
|
785
|
+
if base_brief is not None:
|
|
786
|
+
if from_flat:
|
|
787
|
+
_die("write: --from-flat and --base-brief exclude each other")
|
|
788
|
+
ctx = base_context(base_brief, doc_urls_file, patch_file)
|
|
789
|
+
elif doc_urls_file is not None or patch_file is not None:
|
|
790
|
+
_die("write: --doc-urls-file and --patch-file change a --base-brief")
|
|
791
|
+
else:
|
|
792
|
+
raw = sys.stdin.read()
|
|
793
|
+
if not raw or not raw.strip():
|
|
794
|
+
_die("write: empty stdin (expected JSON brief context)")
|
|
795
|
+
try:
|
|
796
|
+
ctx = json.loads(raw)
|
|
797
|
+
except json.JSONDecodeError as e:
|
|
798
|
+
_die(f"write: invalid JSON on stdin: {e}")
|
|
799
|
+
if not isinstance(ctx, dict):
|
|
800
|
+
_die("write: context payload must be a JSON object")
|
|
606
801
|
|
|
607
|
-
|
|
608
|
-
|
|
802
|
+
if from_flat:
|
|
803
|
+
ctx = flat_to_nested(ctx)
|
|
609
804
|
|
|
610
805
|
warnings = validate_context(ctx)
|
|
611
806
|
resolved_version = resolve_version(ctx)
|
|
@@ -631,6 +826,121 @@ def cmd_write(target: Path, from_flat: bool = False) -> int:
|
|
|
631
826
|
return 0
|
|
632
827
|
|
|
633
828
|
|
|
829
|
+
AMEND_FIELDS = ("registry_path", "demo_patterns", "include", "amendments")
|
|
830
|
+
BRIEF_VALIDATOR = Path(__file__).resolve().parent / "skf-validate-brief-schema.py"
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _brief_errors(brief: dict[str, Any]) -> list[dict[str, Any]]:
|
|
834
|
+
"""What skf-validate-brief-schema.py reports for `brief` as it would read it on disk."""
|
|
835
|
+
spec = importlib.util.spec_from_file_location("skf_validate_brief_schema", BRIEF_VALIDATOR)
|
|
836
|
+
if spec is None or spec.loader is None:
|
|
837
|
+
_die(f"amend: cannot load {BRIEF_VALIDATOR.name}", code=2)
|
|
838
|
+
validator = importlib.util.module_from_spec(spec)
|
|
839
|
+
spec.loader.exec_module(validator)
|
|
840
|
+
return validator.validate_brief(yaml.safe_load(render_yaml(brief)))["errors"]
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _added_errors(before: dict[str, Any], after: dict[str, Any]) -> list[dict[str, Any]]:
|
|
844
|
+
"""The schema errors `after` has that `before` did not: an amend refuses
|
|
845
|
+
only what it breaks, never a brief for an error it already held."""
|
|
846
|
+
known = {json.dumps(e, sort_keys=True) for e in _brief_errors(before)}
|
|
847
|
+
return [e for e in _brief_errors(after) if json.dumps(e, sort_keys=True) not in known]
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _check_include(paths: Any) -> list[str]:
|
|
851
|
+
"""The literal paths of an amend payload's `include`."""
|
|
852
|
+
if not isinstance(paths, list) or not all(isinstance(p, str) and p.strip() for p in paths):
|
|
853
|
+
_die("amend: include must be an array of non-empty paths", field="scope.include")
|
|
854
|
+
return paths
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _check_amendments(entries: Any) -> list[dict[str, Any]]:
|
|
858
|
+
"""The amendment entries of an amend payload, checked (see the docstring)."""
|
|
859
|
+
if not isinstance(entries, list):
|
|
860
|
+
_die("amend: amendments must be an array of amendment objects", field="scope.amendments")
|
|
861
|
+
for i, entry in enumerate(entries):
|
|
862
|
+
if not isinstance(entry, dict):
|
|
863
|
+
_die(f"amend: amendments[{i}] must be an object", field="scope.amendments")
|
|
864
|
+
for key in ("path", "action", "reason", "workflow"):
|
|
865
|
+
if not isinstance(entry.get(key), str) or not entry[key]:
|
|
866
|
+
_die(f"amend: amendments[{i}].{key} must be a non-empty string", field="scope.amendments")
|
|
867
|
+
if not isinstance(entry.get("date"), str) or not ISO_DATE_RE.match(entry["date"]):
|
|
868
|
+
_die(f"amend: amendments[{i}].date must be an ISO date (YYYY-MM-DD)", field="scope.amendments")
|
|
869
|
+
category = entry.get("category")
|
|
870
|
+
if category is not None and (not isinstance(category, str) or not category):
|
|
871
|
+
_die(f"amend: amendments[{i}].category must be a non-empty string when present",
|
|
872
|
+
field="scope.amendments")
|
|
873
|
+
return entries
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def cmd_amend(target: Path) -> int:
|
|
877
|
+
raw = sys.stdin.read()
|
|
878
|
+
try:
|
|
879
|
+
payload = json.loads(raw) if raw.strip() else None
|
|
880
|
+
except json.JSONDecodeError as e:
|
|
881
|
+
_die(f"amend: invalid JSON on stdin: {e}")
|
|
882
|
+
if not isinstance(payload, dict):
|
|
883
|
+
_die("amend: stdin must be a JSON object holding registry_path, demo_patterns, include or amendments")
|
|
884
|
+
unknown = sorted(set(payload) - set(AMEND_FIELDS))
|
|
885
|
+
if unknown:
|
|
886
|
+
_die(f"amend: unknown field(s) {unknown}; expected some of {list(AMEND_FIELDS)}")
|
|
887
|
+
given = [field for field in AMEND_FIELDS if payload.get(field) not in (None, [])]
|
|
888
|
+
if not given:
|
|
889
|
+
_die("amend: nothing to amend")
|
|
890
|
+
entries = _check_amendments(payload["amendments"]) if "amendments" in given else []
|
|
891
|
+
include = _check_include(payload["include"]) if "include" in given else []
|
|
892
|
+
|
|
893
|
+
try:
|
|
894
|
+
original = target.read_bytes()
|
|
895
|
+
except OSError as e:
|
|
896
|
+
_die(f"amend: cannot read {target}: {e}", code=2)
|
|
897
|
+
try:
|
|
898
|
+
brief = yaml.safe_load(original.decode("utf-8-sig"))
|
|
899
|
+
except (UnicodeDecodeError, yaml.YAMLError) as e:
|
|
900
|
+
_die(f"amend: {target} is not a YAML brief: {e}")
|
|
901
|
+
if not isinstance(brief, dict) or not isinstance(brief.get("scope"), dict):
|
|
902
|
+
_die(f"amend: {target} holds no scope mapping", field="scope")
|
|
903
|
+
before = yaml.safe_load(render_yaml(brief))
|
|
904
|
+
scope = brief["scope"]
|
|
905
|
+
if entries:
|
|
906
|
+
existing = scope.get("amendments")
|
|
907
|
+
if existing is not None and not isinstance(existing, list):
|
|
908
|
+
_die("amend: the brief's scope.amendments is not an array", field="scope.amendments")
|
|
909
|
+
scope["amendments"] = [*(existing or []), *entries]
|
|
910
|
+
for field in ("registry_path", "demo_patterns"):
|
|
911
|
+
if field in given:
|
|
912
|
+
scope[field] = payload[field]
|
|
913
|
+
_check_component_fields(scope)
|
|
914
|
+
included: list[str] = []
|
|
915
|
+
current = scope.get("include")
|
|
916
|
+
if include and isinstance(current, list) and current:
|
|
917
|
+
for path in include:
|
|
918
|
+
if path not in current and path not in included:
|
|
919
|
+
included.append(path)
|
|
920
|
+
scope["include"] = [*current, *included]
|
|
921
|
+
errors = _added_errors(before, brief)
|
|
922
|
+
if errors:
|
|
923
|
+
_die(f"amend: the amendment breaks skill-brief.v1.json: {errors[0].get('message')}",
|
|
924
|
+
field=errors[0].get("field"))
|
|
925
|
+
|
|
926
|
+
backup = target.with_name(target.name + ".bak")
|
|
927
|
+
try:
|
|
928
|
+
backup.write_bytes(original)
|
|
929
|
+
except OSError as e:
|
|
930
|
+
_die(f"amend: cannot write {backup}: {e}", code=2)
|
|
931
|
+
bytes_written = atomic_write(target, render_yaml(brief))
|
|
932
|
+
print(json.dumps({
|
|
933
|
+
"status": "ok",
|
|
934
|
+
"brief_path": str(target.resolve()),
|
|
935
|
+
"backup": str(backup.resolve()),
|
|
936
|
+
"set": [field for field in given if field in ("registry_path", "demo_patterns")],
|
|
937
|
+
"included": included,
|
|
938
|
+
"appended": len(entries),
|
|
939
|
+
"bytes": bytes_written,
|
|
940
|
+
}))
|
|
941
|
+
return 0
|
|
942
|
+
|
|
943
|
+
|
|
634
944
|
def main() -> int:
|
|
635
945
|
parser = argparse.ArgumentParser(
|
|
636
946
|
prog="skf-write-skill-brief",
|
|
@@ -650,12 +960,50 @@ def main() -> int:
|
|
|
650
960
|
"assembly site in step 5."
|
|
651
961
|
),
|
|
652
962
|
)
|
|
963
|
+
p_write.add_argument("--base-brief", type=Path, metavar="FILE",
|
|
964
|
+
help="start from the brief in FILE (a skill-brief.yaml) instead of reading stdin")
|
|
965
|
+
p_write.add_argument("--doc-urls-file", type=Path, metavar="FILE",
|
|
966
|
+
help="with --base-brief: replace doc_urls with the doc_urls of FILE "
|
|
967
|
+
"(skf-merge-doc-urls.py output)")
|
|
968
|
+
p_write.add_argument("--patch-file", type=Path, metavar="FILE",
|
|
969
|
+
help="with --base-brief: lay the JSON object in FILE over the brief "
|
|
970
|
+
"(objects merge, null removes, other values replace)")
|
|
971
|
+
|
|
972
|
+
p_amend = sub.add_parser(
|
|
973
|
+
"amend",
|
|
974
|
+
help=(
|
|
975
|
+
"Apply answers (JSON on stdin) to an existing brief: set scope.registry_path and "
|
|
976
|
+
"scope.demo_patterns, append paths to scope.include and entries to scope.amendments, "
|
|
977
|
+
"check the brief schema, keep a .bak copy, atomic write"
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
p_amend.add_argument("--target", type=Path, required=True, help="Path to the skill-brief.yaml to amend")
|
|
653
981
|
|
|
654
982
|
args = parser.parse_args()
|
|
655
983
|
if args.cmd == "write":
|
|
656
|
-
return cmd_write(args.target, from_flat=args.from_flat
|
|
984
|
+
return cmd_write(args.target, from_flat=args.from_flat, base_brief=args.base_brief,
|
|
985
|
+
doc_urls_file=args.doc_urls_file, patch_file=args.patch_file)
|
|
986
|
+
if args.cmd == "amend":
|
|
987
|
+
return cmd_amend(args.target)
|
|
657
988
|
return 2
|
|
658
989
|
|
|
659
990
|
|
|
991
|
+
def _force_utf8(*streams) -> None:
|
|
992
|
+
"""Reconfigure stdin, stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
993
|
+
|
|
994
|
+
A Windows console pipes them as cp1252, which would garble a payload's
|
|
995
|
+
non-ASCII text (a description, an amendment's reason) read from stdin
|
|
996
|
+
(reconfigured here, before the first read).
|
|
997
|
+
"""
|
|
998
|
+
for stream in streams:
|
|
999
|
+
if hasattr(stream, "reconfigure"):
|
|
1000
|
+
errors = getattr(stream, "errors", None)
|
|
1001
|
+
if errors is None:
|
|
1002
|
+
stream.reconfigure(encoding="utf-8")
|
|
1003
|
+
else:
|
|
1004
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
1005
|
+
|
|
1006
|
+
|
|
660
1007
|
if __name__ == "__main__":
|
|
1008
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
661
1009
|
sys.exit(main())
|