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,5 +1,5 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Hash Content — SHA-256 hashing helpers for skill workflows.
|
|
@@ -17,13 +17,14 @@ Two patterns recur across the update-skill workflow's stage prose:
|
|
|
17
17
|
content hash, compare against stored hash" and classify entries as
|
|
18
18
|
MODIFIED_FILE / DELETED_FILE / (UNCHANGED). Same hashing primitive,
|
|
19
19
|
iterated, plus a stat-and-compare. NEW_FILE detection is intentionally
|
|
20
|
-
out of scope here
|
|
20
|
+
out of scope here: that lives in `skf-detect-scripts-assets.py`,
|
|
21
|
+
piped into update-skill's `skf-new-file-diff.py`.
|
|
21
22
|
|
|
22
23
|
3. **[MANUAL]-section integrity** — the update-skill workflow's headline
|
|
23
24
|
Workflow Rule is "[MANUAL] sections survive regeneration with zero
|
|
24
25
|
content loss." init.md §5 captures a pre-write inventory of every
|
|
25
|
-
`<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block;
|
|
26
|
-
(HALT gate) and
|
|
26
|
+
`<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block; merge.md §6b,
|
|
27
|
+
write.md §1 (HALT gate) and write.md §7 verify the merged SKILL.md
|
|
27
28
|
against it. Doing that by an LLM marker-count + eyeball can silently
|
|
28
29
|
pass a block whose interior was truncated (marker count unchanged).
|
|
29
30
|
The `manual-inventory` / `manual-verify` subcommands extract each block
|
|
@@ -49,7 +50,11 @@ Subcommands:
|
|
|
49
50
|
"stats": {"total": N, "unchanged": U, "modified": M, "deleted": D}
|
|
50
51
|
}
|
|
51
52
|
The provenance file must contain `file_entries` as a top-level array
|
|
52
|
-
OR be the array itself (handles both schemas).
|
|
53
|
+
OR be the array itself (handles both schemas). A map with no
|
|
54
|
+
`file_entries` field, or a null one, tracks no file (a skill with no
|
|
55
|
+
script, asset or promoted document, or a map written before the
|
|
56
|
+
field existed): it compares no row. A `file_entries` that is neither
|
|
57
|
+
null nor an array is an error.
|
|
53
58
|
|
|
54
59
|
manual-inventory <skill-md>
|
|
55
60
|
Extract every `<!-- [MANUAL:name] --> … <!-- [/MANUAL:name] -->` block
|
|
@@ -82,17 +87,41 @@ Subcommands:
|
|
|
82
87
|
The inventory may be the object emitted by `manual-inventory` OR a bare
|
|
83
88
|
`blocks[]` array (handles both shapes).
|
|
84
89
|
|
|
90
|
+
manual-inventory-amend --inventory <inventory.json> --plan <plan.json>
|
|
91
|
+
--output <amended-inventory.json>
|
|
92
|
+
Apply the user's [MANUAL] conflict decisions to a captured inventory,
|
|
93
|
+
so the post-merge check verifies what the user approved instead of
|
|
94
|
+
the pre-merge blocks. The plan is
|
|
95
|
+
{"decisions": [
|
|
96
|
+
{"name": "...", "action": "keep" | "remove" | "edit",
|
|
97
|
+
"content_file": "<path>" | "content": "<text>"}, ...]}
|
|
98
|
+
`remove` (an [R]emove or an orphan the user removed) drops every
|
|
99
|
+
block of that name; `edit` (an [E]dit) rehashes the one block of that
|
|
100
|
+
name from the approved interior: the bytes of `content_file` (a path
|
|
101
|
+
relative to the plan's folder, or absolute), or `content` encoded as
|
|
102
|
+
UTF-8, exactly the bytes the merged file holds between the markers,
|
|
103
|
+
newlines included. `keep` and a block the plan does not name stay as
|
|
104
|
+
captured. Writes the amended inventory ({blocks[], count}, the shape
|
|
105
|
+
`manual-inventory` emits) to --output through a temporary file and a
|
|
106
|
+
rename, then prints
|
|
107
|
+
{"output": "...", "count": N, "removed": [names],
|
|
108
|
+
"edited": [names], "kept": [names]}
|
|
109
|
+
A plan with no decisions copies the inventory. A plan that names a
|
|
110
|
+
block the inventory does not hold, decides one name twice, edits a
|
|
111
|
+
name that several blocks share, or gives an edit no content (or both
|
|
112
|
+
forms) is refused with exit 1 and nothing written.
|
|
113
|
+
|
|
85
114
|
compare-constituent-hashes <provenance-map.json> [--skills-root <root>]
|
|
86
115
|
Constituent-drift detection for compose-mode stack skills. Replaces the
|
|
87
116
|
per-constituent read + SHA-256 + compare loop at
|
|
88
|
-
`src/skf-audit-skill/references/
|
|
89
|
-
the `constituents[]` array from a
|
|
90
|
-
entry carrying `skill_name`,
|
|
91
|
-
`metadata_hash`), reads each
|
|
92
|
-
`{skill_path}/active/{skill_name}/metadata.json`,
|
|
93
|
-
SHA-256 with the same `sha256:` prefix convention the
|
|
94
|
-
(`skf-enumerate-stack-skills._sha256_of_bytes`), and
|
|
95
|
-
constituent:
|
|
117
|
+
`src/skf-audit-skill/references/constituent-freshness.md` (step 1c,
|
|
118
|
+
Constituent Freshness). Reads the `constituents[]` array from a
|
|
119
|
+
compose-mode provenance map (each entry carrying `skill_name`,
|
|
120
|
+
`skill_path`, and the compile-time `metadata_hash`), reads each
|
|
121
|
+
constituent's live `{skill_path}/active/{skill_name}/metadata.json`,
|
|
122
|
+
recomputes its SHA-256 with the same `sha256:` prefix convention the
|
|
123
|
+
writer used (`skf-enumerate-stack-skills._sha256_of_bytes`), and
|
|
124
|
+
buckets each constituent:
|
|
96
125
|
{
|
|
97
126
|
"drifted": [{"skill_name","skill_path","stored_hash","current_hash"}],
|
|
98
127
|
"fresh": [{"skill_name"}],
|
|
@@ -126,6 +155,8 @@ CLI examples:
|
|
|
126
155
|
--provenance-map /path/to/provenance-map.json
|
|
127
156
|
uv run skf-hash-content.py manual-inventory SKILL.md
|
|
128
157
|
uv run skf-hash-content.py manual-verify --inventory inventory.json SKILL.md
|
|
158
|
+
uv run skf-hash-content.py manual-inventory-amend --inventory inventory.json \\
|
|
159
|
+
--plan manual-plan.json --output amended-inventory.json
|
|
129
160
|
|
|
130
161
|
Exit codes:
|
|
131
162
|
0 — operation succeeded (including: file in compare missing on disk, its
|
|
@@ -212,11 +243,14 @@ def load_file_entries(provenance_path: Path) -> list[dict]:
|
|
|
212
243
|
"""Extract the file_entries list from a provenance file.
|
|
213
244
|
|
|
214
245
|
Accepts two shapes:
|
|
215
|
-
- top-level object with a `file_entries` key (canonical)
|
|
246
|
+
- top-level object with a `file_entries` key (canonical); with no
|
|
247
|
+
such key, or a null one, the map tracks no file and the list is
|
|
248
|
+
empty
|
|
216
249
|
- top-level array of entries (already-extracted)
|
|
217
250
|
|
|
218
|
-
Returns a copy of the list. Raises ValueError on malformed JSON
|
|
219
|
-
|
|
251
|
+
Returns a copy of the list. Raises ValueError on malformed JSON, a
|
|
252
|
+
`file_entries` that is not an array, or a top level that is neither
|
|
253
|
+
an object nor an array.
|
|
220
254
|
"""
|
|
221
255
|
try:
|
|
222
256
|
data = json.loads(provenance_path.read_text(encoding="utf-8"))
|
|
@@ -228,9 +262,7 @@ def load_file_entries(provenance_path: Path) -> list[dict]:
|
|
|
228
262
|
if isinstance(data, dict):
|
|
229
263
|
entries = data.get("file_entries")
|
|
230
264
|
if entries is None:
|
|
231
|
-
|
|
232
|
-
f"provenance file {provenance_path} has no `file_entries` field"
|
|
233
|
-
)
|
|
265
|
+
return [] # no tracked file: a map without (or with a null) file_entries
|
|
234
266
|
if not isinstance(entries, list):
|
|
235
267
|
raise ValueError(
|
|
236
268
|
f"`file_entries` in {provenance_path} is not an array"
|
|
@@ -466,6 +498,102 @@ def manual_verify(inventory_path: Path, skill_md_path: Path) -> dict:
|
|
|
466
498
|
return classify_manual_blocks(inv_blocks, cur_blocks)
|
|
467
499
|
|
|
468
500
|
|
|
501
|
+
_PLAN_ACTIONS = ("keep", "remove", "edit")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _load_plan(plan_path: Path) -> list[dict]:
|
|
505
|
+
"""The decisions[] of a manual plan. Raises ValueError on malformed input."""
|
|
506
|
+
try:
|
|
507
|
+
data = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
508
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
509
|
+
raise ValueError(f"failed to read plan file {plan_path}: {exc}") from exc
|
|
510
|
+
if not isinstance(data, dict) or not isinstance(data.get("decisions"), list):
|
|
511
|
+
raise ValueError(f"plan file {plan_path} must be an object with a `decisions` array")
|
|
512
|
+
decisions = data["decisions"]
|
|
513
|
+
for d in decisions:
|
|
514
|
+
if not isinstance(d, dict) or not isinstance(d.get("name"), str) or not d["name"].strip():
|
|
515
|
+
raise ValueError(f"every decision in {plan_path} needs a non-empty `name`: {d!r}")
|
|
516
|
+
if d.get("action") not in _PLAN_ACTIONS:
|
|
517
|
+
raise ValueError(f"decision for {d['name']!r}: `action` must be one of "
|
|
518
|
+
f"{', '.join(_PLAN_ACTIONS)}, got {d.get('action')!r}")
|
|
519
|
+
return decisions
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _edited_interior(decision: dict, plan_dir: Path) -> bytes:
|
|
523
|
+
"""The approved interior bytes of an `edit` decision."""
|
|
524
|
+
has_file, has_text = "content_file" in decision, "content" in decision
|
|
525
|
+
if has_file == has_text:
|
|
526
|
+
raise ValueError(f"edit of {decision['name']!r} needs exactly one of `content_file` and `content`")
|
|
527
|
+
if has_text:
|
|
528
|
+
if not isinstance(decision["content"], str):
|
|
529
|
+
raise ValueError(f"edit of {decision['name']!r}: `content` must be a string")
|
|
530
|
+
return decision["content"].encode("utf-8")
|
|
531
|
+
raw = decision["content_file"]
|
|
532
|
+
if not isinstance(raw, str) or not raw:
|
|
533
|
+
raise ValueError(f"edit of {decision['name']!r}: `content_file` must be a path")
|
|
534
|
+
path = Path(raw)
|
|
535
|
+
if not path.is_absolute():
|
|
536
|
+
path = plan_dir / path
|
|
537
|
+
try:
|
|
538
|
+
return path.read_bytes()
|
|
539
|
+
except OSError as exc:
|
|
540
|
+
raise ValueError(f"edit of {decision['name']!r}: cannot read {path}: {exc}") from exc
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def amend_inventory(inv_blocks: list[dict], decisions: list[dict], plan_dir: Path) -> tuple[list[dict], dict]:
|
|
544
|
+
"""The inventory blocks with the plan's decisions applied, and what changed.
|
|
545
|
+
|
|
546
|
+
Pure apart from reading each edit's content file. Raises ValueError for
|
|
547
|
+
a plan the inventory cannot take (see the module docstring).
|
|
548
|
+
"""
|
|
549
|
+
names = [b.get("name") for b in inv_blocks]
|
|
550
|
+
seen: set[str] = set()
|
|
551
|
+
by_name: dict[str, dict] = {}
|
|
552
|
+
for d in decisions:
|
|
553
|
+
name = d["name"]
|
|
554
|
+
if name in seen:
|
|
555
|
+
raise ValueError(f"the plan decides {name!r} twice")
|
|
556
|
+
seen.add(name)
|
|
557
|
+
if name not in names:
|
|
558
|
+
raise ValueError(f"the plan names {name!r}, which the inventory does not hold")
|
|
559
|
+
if d["action"] == "edit" and names.count(name) > 1:
|
|
560
|
+
raise ValueError(f"the plan edits {name!r}, which {names.count(name)} blocks share")
|
|
561
|
+
by_name[name] = d
|
|
562
|
+
out: list[dict] = []
|
|
563
|
+
for block in inv_blocks:
|
|
564
|
+
decision = by_name.get(block.get("name"))
|
|
565
|
+
if decision is None or decision["action"] == "keep":
|
|
566
|
+
out.append(dict(block))
|
|
567
|
+
elif decision["action"] == "edit":
|
|
568
|
+
out.append({**block, "content_hash": sha256_of_bytes(_edited_interior(decision, plan_dir))})
|
|
569
|
+
changes = {action: [d["name"] for d in decisions if d["action"] == key]
|
|
570
|
+
for action, key in (("removed", "remove"), ("edited", "edit"), ("kept", "keep"))}
|
|
571
|
+
return out, changes
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _write_atomic(path: Path, text: str) -> None:
|
|
575
|
+
"""Write `text` to `path` through a temporary file and one rename."""
|
|
576
|
+
tmp = path.with_name(f".{path.name}.skf-tmp")
|
|
577
|
+
try:
|
|
578
|
+
tmp.write_bytes(text.encode("utf-8"))
|
|
579
|
+
tmp.replace(path)
|
|
580
|
+
except OSError:
|
|
581
|
+
try:
|
|
582
|
+
tmp.unlink()
|
|
583
|
+
except OSError:
|
|
584
|
+
pass
|
|
585
|
+
raise
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def manual_inventory_amend(inventory_path: Path, plan_path: Path, output_path: Path) -> dict:
|
|
589
|
+
"""Write the amended inventory to `output_path` and return the summary."""
|
|
590
|
+
inv_blocks = load_inventory_blocks(inventory_path)
|
|
591
|
+
decisions = _load_plan(plan_path)
|
|
592
|
+
blocks, changes = amend_inventory(inv_blocks, decisions, plan_path.parent)
|
|
593
|
+
_write_atomic(output_path, json.dumps({"blocks": blocks, "count": len(blocks)}, indent=2) + "\n")
|
|
594
|
+
return {"output": output_path.as_posix(), "count": len(blocks), **changes}
|
|
595
|
+
|
|
596
|
+
|
|
469
597
|
# --------------------------------------------------------------------------
|
|
470
598
|
# Constituent-hash comparison (compose-mode stack drift)
|
|
471
599
|
# --------------------------------------------------------------------------
|
|
@@ -516,7 +644,8 @@ def load_constituents(provenance_path: Path) -> list[dict]:
|
|
|
516
644
|
def _constituent_metadata_path(skills_root: Path, skill_path: str, skill_name: str) -> Path:
|
|
517
645
|
"""Resolve a constituent's live metadata.json path.
|
|
518
646
|
|
|
519
|
-
Mirrors
|
|
647
|
+
Mirrors the by-hand fallback of constituent-freshness.md (audit-skill
|
|
648
|
+
step 1c):
|
|
520
649
|
`{skill_path}/active/{skill_name}/metadata.json`. A relative `skill_path`
|
|
521
650
|
resolves against `skills_root`; an absolute one is used as-is.
|
|
522
651
|
"""
|
|
@@ -675,6 +804,28 @@ def _cmd_manual_verify(args: argparse.Namespace) -> int:
|
|
|
675
804
|
return 0
|
|
676
805
|
|
|
677
806
|
|
|
807
|
+
def _cmd_manual_inventory_amend(args: argparse.Namespace) -> int:
|
|
808
|
+
inventory, plan, output = Path(args.inventory), Path(args.plan), Path(args.output)
|
|
809
|
+
for label, path in (("inventory", inventory), ("plan", plan)):
|
|
810
|
+
if not path.is_file():
|
|
811
|
+
print(f"error: {label} not found: {path}", file=sys.stderr)
|
|
812
|
+
return 1
|
|
813
|
+
if not output.parent.is_dir():
|
|
814
|
+
print(f"error: output folder not found: {output.parent}", file=sys.stderr)
|
|
815
|
+
return 1
|
|
816
|
+
try:
|
|
817
|
+
result = manual_inventory_amend(inventory, plan, output)
|
|
818
|
+
except ValueError as exc:
|
|
819
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
820
|
+
return 1
|
|
821
|
+
except OSError as exc:
|
|
822
|
+
print(f"error: cannot write {output}: {exc}", file=sys.stderr)
|
|
823
|
+
return 1
|
|
824
|
+
json.dump(result, sys.stdout, indent=2)
|
|
825
|
+
sys.stdout.write("\n")
|
|
826
|
+
return 0
|
|
827
|
+
|
|
828
|
+
|
|
678
829
|
def _cmd_compare_constituents(args: argparse.Namespace) -> int:
|
|
679
830
|
provenance = Path(args.provenance_map)
|
|
680
831
|
skills_root = Path(args.skills_root)
|
|
@@ -741,6 +892,15 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
741
892
|
)
|
|
742
893
|
p_ver.set_defaults(func=_cmd_manual_verify)
|
|
743
894
|
|
|
895
|
+
p_amend = sub.add_parser(
|
|
896
|
+
"manual-inventory-amend",
|
|
897
|
+
help="apply the user's [MANUAL] remove and edit decisions to a captured inventory",
|
|
898
|
+
)
|
|
899
|
+
p_amend.add_argument("--inventory", required=True, help="the manual-inventory JSON captured before the merge")
|
|
900
|
+
p_amend.add_argument("--plan", required=True, help="the plan JSON holding the user's decisions")
|
|
901
|
+
p_amend.add_argument("--output", required=True, help="where to write the amended inventory JSON")
|
|
902
|
+
p_amend.set_defaults(func=_cmd_manual_inventory_amend)
|
|
903
|
+
|
|
744
904
|
p_con = sub.add_parser(
|
|
745
905
|
"compare-constituent-hashes",
|
|
746
906
|
help="classify compose-mode stack constituents as drifted/fresh/missing",
|
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Load Provenance — normalize provenance-map.json into deterministic projections.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
`skf-audit-skill` reads one provenance map in several steps, and each needs
|
|
8
|
+
a projection of it with one correct answer:
|
|
9
9
|
|
|
10
10
|
1. **init.md §4 Load Provenance Map + Stack Skill Detection** — extracts
|
|
11
11
|
`source_root`, `baseline_commit` (`source_commit`), `baseline_ref`
|
|
12
|
-
(`source_ref`),
|
|
13
|
-
top-level `libraries` for legacy v1 stacks)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
(`source_ref`), detects stack-skill flags (`provenance_version` and
|
|
13
|
+
top-level `libraries` for legacy v1 stacks), and takes the baseline
|
|
14
|
+
facts init.md §7 shows (export count, generation time and age).
|
|
15
|
+
|
|
16
|
+
2. **re-index.md §2 and §3**: the **bounded scan list** (the union of
|
|
17
|
+
`entries[].source_file` and `file_entries[].source_file`), which the
|
|
18
|
+
recipe runner reads, and for a stack the library of each file
|
|
19
|
+
(`source_library_by_file`), which skf-extraction-snapshot.py gives each
|
|
20
|
+
export of the snapshot it builds (it loads this script beside it).
|
|
21
|
+
|
|
22
|
+
skf-structural-diff.py derives the re-export map from the provenance map
|
|
23
|
+
itself and applies the quote-style, stdlib-prefix and re-export transforms
|
|
24
|
+
on both sides (structural-diff.md §1), so no step carries `reexport_map`
|
|
25
|
+
from here; the field stays for a caller that passes it to the diff as
|
|
26
|
+
`--reexport-map`. The diff also takes a semantic kind a by-eye read
|
|
27
|
+
records as the runner's base kind for the same declaration (its
|
|
28
|
+
export-type comparison).
|
|
25
29
|
|
|
26
30
|
The transforms here are pure projections — no I/O against the source tree,
|
|
27
|
-
no comparisons. The script reads one JSON, emits one JSON.
|
|
28
|
-
canonicalization transforms in structural-diff.md §1 (quote-style on string
|
|
29
|
-
defaults, stdlib module qualification on signature components) STAY in the
|
|
30
|
-
LLM prose because they require per-signature judgment; this script handles
|
|
31
|
-
only the deterministic projections.
|
|
31
|
+
no comparisons. The script reads one JSON, emits one JSON.
|
|
32
32
|
|
|
33
33
|
Subcommand:
|
|
34
|
-
normalize <map.json>
|
|
34
|
+
normalize <map.json> [--now <ISO-8601>]
|
|
35
35
|
Emit JSON:
|
|
36
36
|
{
|
|
37
37
|
"bounded_scan_files": ["<rel-path, forward-slash>", ...],
|
|
38
38
|
"is_stack_skill": <bool>,
|
|
39
39
|
"legacy_stack_provenance": <bool>,
|
|
40
|
+
"compose_mode_stack": <bool>,
|
|
40
41
|
"source_root": "<path or null>",
|
|
41
42
|
"baseline_commit": "<sha or null>",
|
|
42
43
|
"baseline_ref": "<ref or null>",
|
|
43
|
-
"reexport_map": {"<from-internal>": "<to-public>"}
|
|
44
|
+
"reexport_map": {"<from-internal>": "<to-public>"},
|
|
45
|
+
"source_library_by_file": {"<rel-path>": "<library>"},
|
|
46
|
+
"export_count": <int>,
|
|
47
|
+
"generated_at": "<YYYY-MM-DDTHH:MM:SSZ or null>",
|
|
48
|
+
"age_days": <int or null>,
|
|
49
|
+
"age_source": "generated_at" | "mtime" | null
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
Bounded scan = union of `entries[].source_file` and
|
|
@@ -55,6 +61,17 @@ Subcommand:
|
|
|
55
61
|
Note: detecting `is_stack_skill` for a v1 map by the presence of
|
|
56
62
|
`libraries` alone (the documented behavior in init.md) is also
|
|
57
63
|
honored — see condition handling in `detect_stack_flags`.
|
|
64
|
+
- `compose_mode_stack` is true for a stack whose map holds a
|
|
65
|
+
`constituents` list: the compose-mode variant create-stack-skill
|
|
66
|
+
writes, whose entries come from constituent skills rather than a
|
|
67
|
+
source tree. Audit-skill checks its constituents' freshness
|
|
68
|
+
instead of re-indexing a source.
|
|
69
|
+
|
|
70
|
+
File-to-library map: each `entries[].source_file` (forward slashes)
|
|
71
|
+
with the `source_library` of its entries, for a stack's per-library
|
|
72
|
+
diff. A file whose entries name no library is left out, and when its
|
|
73
|
+
entries name two, the first one's wins. Empty for a single skill
|
|
74
|
+
whose entries carry no `source_library`.
|
|
58
75
|
|
|
59
76
|
Re-export map: read `reexport_map` field directly from the provenance
|
|
60
77
|
map if it exists (writer-side captures __init__.py walk results there).
|
|
@@ -62,8 +79,18 @@ Subcommand:
|
|
|
62
79
|
entries — older provenance writers may have used that shape. Empty
|
|
63
80
|
object when neither is present.
|
|
64
81
|
|
|
82
|
+
Baseline facts for init.md §7 (the audit baseline summary):
|
|
83
|
+
- `export_count` is the number of `entries[]` objects.
|
|
84
|
+
- `generated_at` is the map's `generated_at` in UTC, whatever zone
|
|
85
|
+
it was written in; null when absent or not an ISO-8601 date-time.
|
|
86
|
+
- `age_days` is the whole days from `generated_at` to now (never
|
|
87
|
+
below 0), or from the file's modification time when the map has
|
|
88
|
+
no usable `generated_at` (`age_source` says which; null when
|
|
89
|
+
neither is known). `--now` fixes "now" for a reproducible run.
|
|
90
|
+
|
|
65
91
|
CLI examples:
|
|
66
92
|
uv run skf-load-provenance.py normalize /path/to/provenance-map.json
|
|
93
|
+
uv run skf-load-provenance.py normalize /path/to/provenance-map.json --now 2026-05-01T00:00:00Z
|
|
67
94
|
|
|
68
95
|
Exit codes:
|
|
69
96
|
0 — normalization succeeded (including empty / well-formed map with no
|
|
@@ -75,7 +102,9 @@ from __future__ import annotations
|
|
|
75
102
|
|
|
76
103
|
import argparse
|
|
77
104
|
import json
|
|
105
|
+
import re
|
|
78
106
|
import sys
|
|
107
|
+
from datetime import datetime, timedelta, timezone
|
|
79
108
|
from pathlib import Path
|
|
80
109
|
|
|
81
110
|
|
|
@@ -141,6 +170,25 @@ def bounded_scan_files(data: dict) -> list[str]:
|
|
|
141
170
|
return sorted(paths)
|
|
142
171
|
|
|
143
172
|
|
|
173
|
+
def source_library_by_file(data: dict) -> dict[str, str]:
|
|
174
|
+
"""{source_file: source_library} from `entries[]`, paths forward-slash.
|
|
175
|
+
|
|
176
|
+
The first library named for a file wins; entries without a string
|
|
177
|
+
`source_library` add nothing.
|
|
178
|
+
"""
|
|
179
|
+
out: dict[str, str] = {}
|
|
180
|
+
entries = data.get("entries")
|
|
181
|
+
if not isinstance(entries, list):
|
|
182
|
+
return out
|
|
183
|
+
for entry in entries:
|
|
184
|
+
if not isinstance(entry, dict):
|
|
185
|
+
continue
|
|
186
|
+
sf, library = entry.get("source_file"), entry.get("source_library")
|
|
187
|
+
if isinstance(sf, str) and sf and isinstance(library, str) and library:
|
|
188
|
+
out.setdefault(_posix(sf), library)
|
|
189
|
+
return dict(sorted(out.items()))
|
|
190
|
+
|
|
191
|
+
|
|
144
192
|
def _normalize_version(v: object) -> tuple[int, ...] | None:
|
|
145
193
|
"""Parse a provenance_version string like "2.0" / "1.5" into a tuple.
|
|
146
194
|
|
|
@@ -226,7 +274,77 @@ def extract_reexport_map(data: dict) -> dict[str, str]:
|
|
|
226
274
|
return out
|
|
227
275
|
|
|
228
276
|
|
|
229
|
-
|
|
277
|
+
ISO_TIME_RE = re.compile(
|
|
278
|
+
r"(\d{4})-(\d{2})-(\d{2})"
|
|
279
|
+
r"(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d+))?)?)?"
|
|
280
|
+
r"\s*([Zz]|[+-]\d{2}(?::?\d{2})?)?")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Keep identical to _parse_iso_utc in skf-skill-inventory.py
|
|
284
|
+
# (test/test-skf-skill-inventory.py pins the copies).
|
|
285
|
+
def _parse_iso_utc(value):
|
|
286
|
+
"""An ISO-8601 date or date-time as an aware UTC datetime, else None.
|
|
287
|
+
|
|
288
|
+
A time without a zone is read as UTC, a date alone as its midnight UTC.
|
|
289
|
+
"""
|
|
290
|
+
if not isinstance(value, str):
|
|
291
|
+
return None
|
|
292
|
+
m = ISO_TIME_RE.fullmatch(value.strip())
|
|
293
|
+
if not m:
|
|
294
|
+
return None
|
|
295
|
+
year, month, day, hour, minute, second, fraction, zone = m.groups()
|
|
296
|
+
offset = timedelta(0)
|
|
297
|
+
if zone and zone not in ("Z", "z"):
|
|
298
|
+
digits = zone[1:].replace(":", "")
|
|
299
|
+
offset = timedelta(hours=int(digits[:2]), minutes=int(digits[2:] or 0))
|
|
300
|
+
if zone[0] == "-":
|
|
301
|
+
offset = -offset
|
|
302
|
+
try:
|
|
303
|
+
moment = datetime(int(year), int(month), int(day), int(hour or 0), int(minute or 0),
|
|
304
|
+
int(second or 0), int((fraction or "0")[:6].ljust(6, "0")))
|
|
305
|
+
return (moment - offset).replace(tzinfo=timezone.utc)
|
|
306
|
+
except (ValueError, OverflowError): # no such date, or out of datetime's range
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def export_count(data: dict) -> int:
|
|
311
|
+
"""The number of `entries[]` objects (the exports the map records)."""
|
|
312
|
+
entries = data.get("entries")
|
|
313
|
+
if not isinstance(entries, list):
|
|
314
|
+
return 0
|
|
315
|
+
return sum(1 for entry in entries if isinstance(entry, dict))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def provenance_age(
|
|
319
|
+
data: dict, mtime: datetime | None = None, now: datetime | None = None
|
|
320
|
+
) -> dict:
|
|
321
|
+
"""{generated_at, age_days, age_source} for the audit baseline.
|
|
322
|
+
|
|
323
|
+
`generated_at` is the map's field in UTC (null when absent or not
|
|
324
|
+
ISO-8601). The age counts whole days from it to `now` (the current time
|
|
325
|
+
when None), else from `mtime` (the file's modification time), never
|
|
326
|
+
below 0.
|
|
327
|
+
"""
|
|
328
|
+
generated = _parse_iso_utc(data.get("generated_at"))
|
|
329
|
+
if generated is not None:
|
|
330
|
+
since, source = generated, "generated_at"
|
|
331
|
+
elif mtime is not None:
|
|
332
|
+
since, source = mtime, "mtime"
|
|
333
|
+
else:
|
|
334
|
+
since, source = None, None
|
|
335
|
+
age_days = None
|
|
336
|
+
if since is not None:
|
|
337
|
+
age_days = max(0, ((now or datetime.now(timezone.utc)) - since).days)
|
|
338
|
+
return {
|
|
339
|
+
"generated_at": generated.strftime("%Y-%m-%dT%H:%M:%SZ") if generated else None,
|
|
340
|
+
"age_days": age_days,
|
|
341
|
+
"age_source": source,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def normalize(
|
|
346
|
+
data: dict, mtime: datetime | None = None, now: datetime | None = None
|
|
347
|
+
) -> dict:
|
|
230
348
|
"""Build the full normalized projection record."""
|
|
231
349
|
is_stack, legacy = detect_stack_flags(data)
|
|
232
350
|
source_root = data.get("source_root") if isinstance(data.get("source_root"), str) else None
|
|
@@ -240,10 +358,14 @@ def normalize(data: dict) -> dict:
|
|
|
240
358
|
"bounded_scan_files": bounded_scan_files(data),
|
|
241
359
|
"is_stack_skill": is_stack,
|
|
242
360
|
"legacy_stack_provenance": legacy,
|
|
361
|
+
"compose_mode_stack": is_stack and isinstance(data.get("constituents"), list),
|
|
243
362
|
"source_root": source_root,
|
|
244
363
|
"baseline_commit": baseline_commit,
|
|
245
364
|
"baseline_ref": baseline_ref,
|
|
246
365
|
"reexport_map": extract_reexport_map(data),
|
|
366
|
+
"source_library_by_file": source_library_by_file(data),
|
|
367
|
+
"export_count": export_count(data),
|
|
368
|
+
**provenance_age(data, mtime, now),
|
|
247
369
|
}
|
|
248
370
|
|
|
249
371
|
|
|
@@ -257,12 +379,19 @@ def _cmd_normalize(args: argparse.Namespace) -> int:
|
|
|
257
379
|
if not path.is_file():
|
|
258
380
|
print(f"error: provenance map not found: {path}", file=sys.stderr)
|
|
259
381
|
return 1
|
|
382
|
+
now = None
|
|
383
|
+
if args.now is not None:
|
|
384
|
+
now = _parse_iso_utc(args.now)
|
|
385
|
+
if now is None:
|
|
386
|
+
print(f"error: --now is not an ISO-8601 date-time: {args.now}", file=sys.stderr)
|
|
387
|
+
return 1
|
|
260
388
|
try:
|
|
261
389
|
data = load_provenance(path)
|
|
262
|
-
|
|
390
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
|
|
391
|
+
except (ValueError, OSError) as exc:
|
|
263
392
|
print(f"error: {exc}", file=sys.stderr)
|
|
264
393
|
return 1
|
|
265
|
-
result = normalize(data)
|
|
394
|
+
result = normalize(data, mtime=mtime, now=now)
|
|
266
395
|
json.dump(result, sys.stdout, indent=2)
|
|
267
396
|
sys.stdout.write("\n")
|
|
268
397
|
return 0
|
|
@@ -273,7 +402,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
273
402
|
prog="skf-load-provenance",
|
|
274
403
|
description=(
|
|
275
404
|
"Normalize provenance-map.json into deterministic projections "
|
|
276
|
-
"(bounded scan, stack flags, source_root/commit/ref, reexport map
|
|
405
|
+
"(bounded scan, stack flags, source_root/commit/ref, reexport map, "
|
|
406
|
+
"file-to-library map, export count, generated_at and age)."
|
|
277
407
|
),
|
|
278
408
|
)
|
|
279
409
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
@@ -281,6 +411,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
281
411
|
"normalize", help="emit normalized projection JSON from a provenance map"
|
|
282
412
|
)
|
|
283
413
|
p_norm.add_argument("provenance_map", help="path to provenance-map.json")
|
|
414
|
+
p_norm.add_argument(
|
|
415
|
+
"--now",
|
|
416
|
+
help="ISO-8601 date-time that age_days counts to (default: the current time)",
|
|
417
|
+
)
|
|
284
418
|
p_norm.set_defaults(func=_cmd_normalize)
|
|
285
419
|
return parser
|
|
286
420
|
|