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