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
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Extraction Inventory: write, extend and read create-skill's extraction inventory on disk.
|
|
6
|
+
|
|
7
|
+
skf-create-skill step 3 keeps its extraction inventory as JSON beside the
|
|
8
|
+
staging folder (`_bmad-output/.skf-stage/{skill-name}.inventory.json`), so
|
|
9
|
+
the exports, their citations and the extraction rules survive a context
|
|
10
|
+
compaction between extraction and compile. `init` seeds it from the recipe
|
|
11
|
+
runner's JSON and the script/asset detector's JSON, so the model never
|
|
12
|
+
types a record those tools already wrote; the model then sends only what it
|
|
13
|
+
produced itself (`patch`, `add`, `set`). Step 3c adds the T3 items it
|
|
14
|
+
extracts from fetched documentation, step 4 the T2 annotations it finds in
|
|
15
|
+
QMD, and step 7 writes `extraction-rules.yaml` from it (`rules`). Every
|
|
16
|
+
write goes through a temporary file and one rename.
|
|
17
|
+
|
|
18
|
+
Subcommands:
|
|
19
|
+
|
|
20
|
+
init --inventory <file> --skill <name>
|
|
21
|
+
--mode source|docs-only|component-library
|
|
22
|
+
--tier Quick|Forge|Forge+|Deep
|
|
23
|
+
[--extraction <runner JSON>] [--detected <detector JSON>]
|
|
24
|
+
Write a new inventory, replacing any file at --inventory. From
|
|
25
|
+
--extraction (skf-extract-public-api.py --mode full): every
|
|
26
|
+
export as the runner recorded it (confidence T1, extraction_method
|
|
27
|
+
ast-grep), with `internal: true` on each one its
|
|
28
|
+
entry_point_diff.internal lists; files_scanned (its
|
|
29
|
+
files_in_scope); aggregates, counts and arms; extraction_rules
|
|
30
|
+
(recipe_set, the recipes ids, scope, ast_grep_version); and the
|
|
31
|
+
runner warnings as lines (the head cap, each errors[] item, each
|
|
32
|
+
file_issues[] file, each files_without_recipes extension, each
|
|
33
|
+
warnings[] entry). From --detected (skf-detect-scripts-assets.py
|
|
34
|
+
detect): scripts_inventory and assets_inventory.
|
|
35
|
+
|
|
36
|
+
add --inventory <file> --field exports|t2_annotations|t3_items|warnings
|
|
37
|
+
Read a JSON list on stdin (objects; strings for warnings) and
|
|
38
|
+
append it to that list. An entry already in the list (equal
|
|
39
|
+
JSON; for exports, the same export_name and source_file) is not
|
|
40
|
+
added twice and counts in `duplicates`. An exports entry needs an
|
|
41
|
+
export_name; a missing confidence, extraction_method,
|
|
42
|
+
ast_node_type or ast_recipe defaults to T1-low, source-read,
|
|
43
|
+
null and null (an export read by eye). A t3_items entry whose
|
|
44
|
+
export_name names an export the inventory already holds is
|
|
45
|
+
dropped, not added: T3 content never overrides a T1, T1-low or
|
|
46
|
+
T2 entry for the same export.
|
|
47
|
+
|
|
48
|
+
patch --inventory <file>
|
|
49
|
+
Read a JSON list of objects on stdin, each an export_name, an
|
|
50
|
+
optional source_file, and any of signature, params and
|
|
51
|
+
return_type, and set those fields on the export it names (by
|
|
52
|
+
export_name and source_file; by export_name alone when no
|
|
53
|
+
source_file is given). A name that matches no export is listed
|
|
54
|
+
in `unmatched`, one that matches several in `ambiguous`; neither
|
|
55
|
+
changes anything. params lists each parameter as the runner
|
|
56
|
+
records one, {name, type, default, optional}, or as a typed
|
|
57
|
+
string ('userId: string').
|
|
58
|
+
|
|
59
|
+
set --inventory <file>
|
|
60
|
+
Read a JSON object on stdin and set each of its keys: top_exports,
|
|
61
|
+
co_imports, promoted_docs, component_catalog (lists),
|
|
62
|
+
authoritative_files_scan (an object or null), files_scanned (an
|
|
63
|
+
integer), counts and arms (objects, merged into what is there)
|
|
64
|
+
and intent_mapping. intent_mapping maps `scripts` and `assets`
|
|
65
|
+
to {"intent": "<free text>", "kept": [source_file, ...]}: the
|
|
66
|
+
inventory keeps only the kept entries of that list and records
|
|
67
|
+
the others in the mapping's `left_out`.
|
|
68
|
+
|
|
69
|
+
provenance --inventory <file> --target <provenance-map.json> [--add-entries]
|
|
70
|
+
Write the provenance map to --target, so compile never types an
|
|
71
|
+
entry the inventory holds. stdin holds the map's header, a JSON
|
|
72
|
+
object: source_repo and source_commit (required, null allowed),
|
|
73
|
+
and source_ref (default null), provenance_version (default
|
|
74
|
+
"2.0"), skill_name (default the inventory's), skill_type
|
|
75
|
+
(default "single") and generated_at (default now, ISO-8601
|
|
76
|
+
UTC); any other key is refused. entries[] holds the entry of
|
|
77
|
+
every export of the inventory, in its order: export_name,
|
|
78
|
+
export_type, params, return_type, source_file, source_line,
|
|
79
|
+
confidence, extraction_method, ast_node_type and
|
|
80
|
+
signature_source, labeled by the tool that found it. An export
|
|
81
|
+
an ast-grep rule matched is T1 (extraction_method ast-grep, the
|
|
82
|
+
ast_node_type its recipe declares, signature_source T1); any
|
|
83
|
+
other was read by eye and is T1-low (extraction_method
|
|
84
|
+
source-read, ast_node_type null, signature_source T1-low).
|
|
85
|
+
params holds the map's typed strings, one per parameter the
|
|
86
|
+
runner (or a patch) recorded: `name: type`, `name?: type` for a
|
|
87
|
+
JS/TS parameter that is optional and has no default,
|
|
88
|
+
` = default` after a default, and the type alone for a
|
|
89
|
+
parameter with no name; a string a patch or an add sent is kept
|
|
90
|
+
as it is. params and return_type are null on an export whose
|
|
91
|
+
params is null: any export but a function, or a function nobody
|
|
92
|
+
read the signature of, which `unsigned` lists (patch them and
|
|
93
|
+
run provenance again). The inventory is not changed.
|
|
94
|
+
With --add-entries, stdin holds instead a JSON object with an
|
|
95
|
+
`entries` list, a `file_entries` list or both, appended to the
|
|
96
|
+
map at --target: the entries the inventory does not hold (T2 and
|
|
97
|
+
T3) and the file entries. An entry whose export_name and
|
|
98
|
+
source_file the map already holds, or a file entry whose
|
|
99
|
+
file_name it holds, is not added and counts in `duplicates`, so
|
|
100
|
+
an inventory export's entry is never replaced.
|
|
101
|
+
|
|
102
|
+
rules --inventory <file> --language <language> --target <file>
|
|
103
|
+
Write extraction-rules.yaml (the skill, its language, tier and
|
|
104
|
+
extraction mode, and the inventory's extraction_rules) to
|
|
105
|
+
--target. The inventory is not changed.
|
|
106
|
+
|
|
107
|
+
summary --inventory <file>
|
|
108
|
+
Print the inventory's counts without changing it.
|
|
109
|
+
|
|
110
|
+
Inventory shape (the keys this helper reads or writes):
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
"skill_name": "...", "extraction_mode": "...", "tier": "...",
|
|
114
|
+
"files_scanned": N | null,
|
|
115
|
+
"exports": [{"export_name": "...", "source_file": "...", ...}, ...],
|
|
116
|
+
"top_exports": ["name", ...],
|
|
117
|
+
"aggregates": {...}, "counts": {...}, "arms": {...},
|
|
118
|
+
"scripts_inventory": [...], "assets_inventory": [...],
|
|
119
|
+
"intent_mapping": {"scripts": {"intent", "kept", "left_out"}, ...},
|
|
120
|
+
"co_imports": [...], "promoted_docs": [...],
|
|
121
|
+
"authoritative_files_scan": {...} | null,
|
|
122
|
+
"extraction_rules": {"recipe_set", "recipes", "scope", "ast_grep_version"},
|
|
123
|
+
"t2_annotations": [...], "t3_items": [...], "warnings": ["..."]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Output (stdout, exit 0), for every subcommand but rules and provenance:
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
"status": "ok",
|
|
130
|
+
"inventory": "<path, / separators>",
|
|
131
|
+
"extraction_mode": "...",
|
|
132
|
+
"counts": {"files_scanned": N | null, "exports": N, "t1": N,
|
|
133
|
+
"t1_low": N, "by_type": {"<export_type>": N, ...},
|
|
134
|
+
"top_exports": N, "co_imports": N, "scripts": N,
|
|
135
|
+
"assets": N, "t2_annotations": N, "t2_past": N,
|
|
136
|
+
"t2_future": N, "functions_enriched": N, "t3_items": N,
|
|
137
|
+
"items": N},
|
|
138
|
+
"added": N, "duplicates": N, "dropped": ["name"], # add
|
|
139
|
+
"patched": N, "unmatched": [...], "ambiguous": [...], # patch
|
|
140
|
+
"set": ["key", ...] # set
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
`items` is exports plus t3_items: what compile can document. t1 and t1_low
|
|
144
|
+
count the exports by their `confidence`, by_type by their `export_type`.
|
|
145
|
+
t2_past and t2_future count the T2 annotations by their `temporal`, and
|
|
146
|
+
functions_enriched the distinct export_name values among them. rules
|
|
147
|
+
prints {"status": "ok", "target": "<path>", "bytes": N}, provenance
|
|
148
|
+
{"status": "ok", "target": "<path>", "entries": N, "unsigned": ["name
|
|
149
|
+
(file)", ...]}, and provenance --add-entries {"status": "ok", "target":
|
|
150
|
+
"<path>", "entries": N, "file_entries": N, "added": N, "duplicates": N},
|
|
151
|
+
the counts of the map after the merge.
|
|
152
|
+
|
|
153
|
+
Exit codes:
|
|
154
|
+
0 success
|
|
155
|
+
1 input error, nothing changed: an inventory that is missing, not JSON
|
|
156
|
+
or has no exports list, an --extraction or --detected file that
|
|
157
|
+
cannot be read, a provenance map at --target that --add-entries
|
|
158
|
+
cannot read, a stdin payload of the wrong shape, an unknown key or a
|
|
159
|
+
kept file the inventory does not list, or a write that failed
|
|
160
|
+
2 usage error (argparse)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
from __future__ import annotations
|
|
164
|
+
|
|
165
|
+
import argparse
|
|
166
|
+
import json
|
|
167
|
+
import os
|
|
168
|
+
import re
|
|
169
|
+
import sys
|
|
170
|
+
import time
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
|
|
173
|
+
FIELDS = ("exports", "t2_annotations", "t3_items", "warnings")
|
|
174
|
+
MODES = ("source", "docs-only", "component-library")
|
|
175
|
+
TIERS = ("Quick", "Forge", "Forge+", "Deep")
|
|
176
|
+
LISTS = ("top_exports", "co_imports", "promoted_docs", "component_catalog", "scripts_inventory",
|
|
177
|
+
"assets_inventory", "t2_annotations", "t3_items", "warnings")
|
|
178
|
+
PATCHABLE = ("signature", "params", "return_type")
|
|
179
|
+
# set: the key, the JSON types it takes, and whether an object merges into the one there.
|
|
180
|
+
SETTABLE = {
|
|
181
|
+
"top_exports": ((list,), False),
|
|
182
|
+
"co_imports": ((list,), False),
|
|
183
|
+
"promoted_docs": ((list,), False),
|
|
184
|
+
"component_catalog": ((list,), False),
|
|
185
|
+
"authoritative_files_scan": ((dict, type(None)), False),
|
|
186
|
+
"files_scanned": ((int,), False),
|
|
187
|
+
"counts": ((dict,), True),
|
|
188
|
+
"arms": ((dict,), True),
|
|
189
|
+
"intent_mapping": ((dict,), False),
|
|
190
|
+
}
|
|
191
|
+
# intent_mapping kinds and the inventory list each one filters.
|
|
192
|
+
INTENT_LISTS = {"scripts": "scripts_inventory", "assets": "assets_inventory"}
|
|
193
|
+
BY_EYE_DEFAULTS = {"confidence": "T1-low", "extraction_method": "source-read", "ast_node_type": None,
|
|
194
|
+
"ast_recipe": None}
|
|
195
|
+
YAML_KEY_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class InventoryError(Exception):
|
|
199
|
+
"""Exit 1."""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load(path: Path) -> dict:
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
205
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
206
|
+
raise InventoryError(f"cannot read the inventory {path.as_posix()}: {exc}") from exc
|
|
207
|
+
if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
|
|
208
|
+
raise InventoryError(f"the inventory {path.as_posix()} is not an object with an exports list")
|
|
209
|
+
for key in LISTS:
|
|
210
|
+
if key in data and not isinstance(data[key], list):
|
|
211
|
+
raise InventoryError(f"the inventory's {key} is not a list")
|
|
212
|
+
return data
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _read_json(path: Path, label: str):
|
|
216
|
+
try:
|
|
217
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
218
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
219
|
+
raise InventoryError(f"cannot read the {label} {path.as_posix()}: {exc}") from exc
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def counts(data: dict) -> dict:
|
|
223
|
+
exports = [e for e in data["exports"] if isinstance(e, dict)]
|
|
224
|
+
notes = [n for n in data.get("t2_annotations") or [] if isinstance(n, dict)]
|
|
225
|
+
|
|
226
|
+
def _length(key: str) -> int:
|
|
227
|
+
value = data.get(key)
|
|
228
|
+
return len(value) if isinstance(value, list) else 0
|
|
229
|
+
|
|
230
|
+
by_type: dict[str, int] = {}
|
|
231
|
+
for export in exports:
|
|
232
|
+
kind = export.get("export_type")
|
|
233
|
+
if isinstance(kind, str) and kind:
|
|
234
|
+
by_type[kind] = by_type.get(kind, 0) + 1
|
|
235
|
+
files = data.get("files_scanned")
|
|
236
|
+
return {
|
|
237
|
+
"files_scanned": files if isinstance(files, int) else None,
|
|
238
|
+
"exports": len(data["exports"]),
|
|
239
|
+
"t1": sum(1 for e in exports if e.get("confidence") == "T1"),
|
|
240
|
+
"t1_low": sum(1 for e in exports if e.get("confidence") == "T1-low"),
|
|
241
|
+
"by_type": dict(sorted(by_type.items())),
|
|
242
|
+
"top_exports": _length("top_exports"),
|
|
243
|
+
"co_imports": _length("co_imports"),
|
|
244
|
+
"scripts": _length("scripts_inventory"),
|
|
245
|
+
"assets": _length("assets_inventory"),
|
|
246
|
+
"t2_annotations": _length("t2_annotations"),
|
|
247
|
+
"t2_past": sum(1 for n in notes if n.get("temporal") == "T2-past"),
|
|
248
|
+
"t2_future": sum(1 for n in notes if n.get("temporal") == "T2-future"),
|
|
249
|
+
"functions_enriched": len({n["export_name"] for n in notes if isinstance(n.get("export_name"), str)}),
|
|
250
|
+
"t3_items": _length("t3_items"),
|
|
251
|
+
"items": len(data["exports"]) + _length("t3_items"),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def runner_warnings(extraction: dict) -> list[str]:
|
|
256
|
+
"""The recipe runner's anomalies, one line each, for Gate 2 and the evidence report."""
|
|
257
|
+
lines: list[str] = []
|
|
258
|
+
recipes = [r for r in extraction.get("recipes") or [] if isinstance(r, dict)]
|
|
259
|
+
if extraction.get("truncated"):
|
|
260
|
+
capped = ", ".join(f"`{r.get('id')}`" for r in recipes if r.get("truncated")) or "a recipe"
|
|
261
|
+
lines.append(f"Extraction hit the head cap: {capped} matched more than {extraction.get('head_cap')} "
|
|
262
|
+
"exports, so some exports were read by eye or left out. Narrow `scope.include` in the "
|
|
263
|
+
"brief until no recipe reaches the cap.")
|
|
264
|
+
for error in extraction.get("errors") or []:
|
|
265
|
+
if isinstance(error, dict):
|
|
266
|
+
lines.append(f"ast-grep did not finish on {error.get('files')} files from `{error.get('first_file')}` "
|
|
267
|
+
f"({error.get('detail')}): exports in them may be missing")
|
|
268
|
+
for issue in extraction.get("file_issues") or []:
|
|
269
|
+
if isinstance(issue, dict):
|
|
270
|
+
lines.append(f"`{issue.get('file')}` ({issue.get('issue')}): its exports are read by eye")
|
|
271
|
+
without = extraction.get("files_without_recipes")
|
|
272
|
+
if isinstance(without, dict):
|
|
273
|
+
for ext, number in sorted(without.items()):
|
|
274
|
+
lines.append(f"{number} `{ext}` files in scope are in a language no recipe reads")
|
|
275
|
+
lines.extend(w for w in extraction.get("warnings") or [] if isinstance(w, str) and w)
|
|
276
|
+
return lines
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def init(skill: str, mode: str, tier: str, extraction: dict | None = None, detected: dict | None = None) -> dict:
|
|
280
|
+
"""A new inventory, seeded from the runner's JSON and the detector's JSON."""
|
|
281
|
+
data: dict = {
|
|
282
|
+
"skill_name": skill, "extraction_mode": mode, "tier": tier, "files_scanned": None,
|
|
283
|
+
"exports": [], "top_exports": [], "aggregates": {}, "counts": {}, "arms": {},
|
|
284
|
+
"scripts_inventory": [], "assets_inventory": [], "intent_mapping": {}, "co_imports": [],
|
|
285
|
+
"promoted_docs": [], "authoritative_files_scan": None, "extraction_rules": {},
|
|
286
|
+
"t2_annotations": [], "t3_items": [], "warnings": [],
|
|
287
|
+
}
|
|
288
|
+
if extraction is not None:
|
|
289
|
+
if not isinstance(extraction, dict):
|
|
290
|
+
raise InventoryError("the --extraction file is not a JSON object")
|
|
291
|
+
diff = extraction.get("entry_point_diff")
|
|
292
|
+
internal = {(i.get("name"), i.get("source_file"))
|
|
293
|
+
for i in (diff.get("internal") or [] if isinstance(diff, dict) else []) if isinstance(i, dict)}
|
|
294
|
+
for record in extraction.get("exports") or []:
|
|
295
|
+
if not isinstance(record, dict):
|
|
296
|
+
continue
|
|
297
|
+
export = dict(record)
|
|
298
|
+
export.setdefault("confidence", "T1")
|
|
299
|
+
export.setdefault("extraction_method", "ast-grep")
|
|
300
|
+
if (export.get("export_name"), export.get("source_file")) in internal:
|
|
301
|
+
export["internal"] = True
|
|
302
|
+
data["exports"].append(export)
|
|
303
|
+
files = extraction.get("files_in_scope")
|
|
304
|
+
data["files_scanned"] = files if isinstance(files, int) else None
|
|
305
|
+
for key in ("aggregates", "counts", "arms"):
|
|
306
|
+
value = extraction.get(key)
|
|
307
|
+
data[key] = dict(value) if isinstance(value, dict) else {}
|
|
308
|
+
ast_grep = extraction.get("ast_grep")
|
|
309
|
+
data["extraction_rules"] = {
|
|
310
|
+
"recipe_set": extraction.get("recipe_set"),
|
|
311
|
+
"recipes": [r.get("id") for r in extraction.get("recipes") or [] if isinstance(r, dict)],
|
|
312
|
+
"scope": extraction.get("scope") if isinstance(extraction.get("scope"), dict) else {},
|
|
313
|
+
"ast_grep_version": ast_grep.get("version") if isinstance(ast_grep, dict) else None,
|
|
314
|
+
}
|
|
315
|
+
data["warnings"] = runner_warnings(extraction)
|
|
316
|
+
if detected is not None:
|
|
317
|
+
if not isinstance(detected, dict):
|
|
318
|
+
raise InventoryError("the --detected file is not a JSON object")
|
|
319
|
+
for key in ("scripts_inventory", "assets_inventory"):
|
|
320
|
+
value = detected.get(key) or []
|
|
321
|
+
if not isinstance(value, list) or not all(isinstance(e, dict) for e in value):
|
|
322
|
+
raise InventoryError(f"the --detected file's {key} is not a list of objects")
|
|
323
|
+
data[key] = [dict(e) for e in value]
|
|
324
|
+
return data
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _same(a, b) -> bool:
|
|
328
|
+
return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def add(data: dict, field: str, entries: list) -> tuple[int, int, list[str]]:
|
|
332
|
+
"""Append `entries` to data[field]; return (added, duplicates, dropped export names)."""
|
|
333
|
+
if field not in FIELDS:
|
|
334
|
+
raise InventoryError(f"--field must be one of {', '.join(FIELDS)}")
|
|
335
|
+
kind = str if field == "warnings" else dict
|
|
336
|
+
if not isinstance(entries, list) or not all(isinstance(e, kind) for e in entries):
|
|
337
|
+
raise InventoryError(f"stdin must hold a JSON list of {'strings' if kind is str else 'objects'}")
|
|
338
|
+
if field == "exports":
|
|
339
|
+
if not all(isinstance(e.get("export_name"), str) and e["export_name"] for e in entries):
|
|
340
|
+
raise InventoryError("every exports entry needs an export_name")
|
|
341
|
+
entries = [{**BY_EYE_DEFAULTS, **e} for e in entries]
|
|
342
|
+
target = data.setdefault(field, [])
|
|
343
|
+
known = {e.get("export_name") for e in data["exports"] if isinstance(e, dict)} - {None}
|
|
344
|
+
added = duplicates = 0
|
|
345
|
+
dropped: list[str] = []
|
|
346
|
+
for entry in entries:
|
|
347
|
+
if field == "t3_items" and entry.get("export_name") in known:
|
|
348
|
+
dropped.append(entry["export_name"])
|
|
349
|
+
continue
|
|
350
|
+
if field == "exports":
|
|
351
|
+
key = (entry["export_name"], entry.get("source_file"))
|
|
352
|
+
repeat = any(isinstance(e, dict) and (e.get("export_name"), e.get("source_file")) == key
|
|
353
|
+
for e in target)
|
|
354
|
+
else:
|
|
355
|
+
repeat = any(_same(entry, e) for e in target)
|
|
356
|
+
if repeat:
|
|
357
|
+
duplicates += 1
|
|
358
|
+
continue
|
|
359
|
+
target.append(entry)
|
|
360
|
+
added += 1
|
|
361
|
+
return added, duplicates, dropped
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def patch(data: dict, entries: list) -> tuple[int, list[str], list[str]]:
|
|
365
|
+
"""Set signature, params and return_type on the exports named; return (patched, unmatched, ambiguous)."""
|
|
366
|
+
if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
|
|
367
|
+
raise InventoryError("stdin must hold a JSON list of objects")
|
|
368
|
+
for entry in entries:
|
|
369
|
+
if not isinstance(entry.get("export_name"), str) or not entry["export_name"]:
|
|
370
|
+
raise InventoryError("every patch entry needs an export_name")
|
|
371
|
+
extra = sorted(set(entry) - {"export_name", "source_file", *PATCHABLE})
|
|
372
|
+
if extra:
|
|
373
|
+
raise InventoryError(f"patch sets only {', '.join(PATCHABLE)}; {entry['export_name']} also has "
|
|
374
|
+
f"{', '.join(extra)}")
|
|
375
|
+
patched = 0
|
|
376
|
+
unmatched: list[str] = []
|
|
377
|
+
ambiguous: list[str] = []
|
|
378
|
+
for entry in entries:
|
|
379
|
+
name, source = entry["export_name"], entry.get("source_file")
|
|
380
|
+
found = [e for e in data["exports"] if isinstance(e, dict) and e.get("export_name") == name
|
|
381
|
+
and (source is None or e.get("source_file") == source)]
|
|
382
|
+
label = name if source is None else f"{name} ({source})"
|
|
383
|
+
if not found:
|
|
384
|
+
unmatched.append(label)
|
|
385
|
+
elif len(found) > 1:
|
|
386
|
+
ambiguous.append(label)
|
|
387
|
+
else:
|
|
388
|
+
found[0].update({k: entry[k] for k in PATCHABLE if k in entry})
|
|
389
|
+
patched += 1
|
|
390
|
+
return patched, unmatched, ambiguous
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def set_fields(data: dict, values: dict) -> list[str]:
|
|
394
|
+
"""Set the keys of `values` on the inventory (see the module docstring); return the keys set."""
|
|
395
|
+
if not isinstance(values, dict):
|
|
396
|
+
raise InventoryError("stdin must hold a JSON object")
|
|
397
|
+
unknown = sorted(set(values) - set(SETTABLE))
|
|
398
|
+
if unknown:
|
|
399
|
+
raise InventoryError(f"set takes only {', '.join(SETTABLE)}; not {', '.join(unknown)}")
|
|
400
|
+
for key, value in values.items():
|
|
401
|
+
types, _ = SETTABLE[key]
|
|
402
|
+
if not isinstance(value, types) or (key == "files_scanned" and isinstance(value, bool)):
|
|
403
|
+
names = " or ".join("null" if t is type(None) else t.__name__ for t in types)
|
|
404
|
+
raise InventoryError(f"{key} must be {names}")
|
|
405
|
+
mapping = _intent_mapping(data, values["intent_mapping"]) if "intent_mapping" in values else {}
|
|
406
|
+
for key, value in values.items():
|
|
407
|
+
if key == "intent_mapping":
|
|
408
|
+
data["intent_mapping"] = {**(data.get("intent_mapping") or {}), **mapping}
|
|
409
|
+
elif SETTABLE[key][1]:
|
|
410
|
+
data[key] = {**(data.get(key) if isinstance(data.get(key), dict) else {}), **value}
|
|
411
|
+
else:
|
|
412
|
+
data[key] = value
|
|
413
|
+
return list(values)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _intent_mapping(data: dict, mapping: dict) -> dict:
|
|
417
|
+
"""Keep only the kept entries of each list a free-text intent names; return the recorded mapping."""
|
|
418
|
+
unknown = sorted(set(mapping) - set(INTENT_LISTS))
|
|
419
|
+
if unknown:
|
|
420
|
+
raise InventoryError(f"intent_mapping takes only {', '.join(INTENT_LISTS)}; not {', '.join(unknown)}")
|
|
421
|
+
recorded: dict = {}
|
|
422
|
+
filtered: dict[str, list] = {}
|
|
423
|
+
for kind, spec in mapping.items():
|
|
424
|
+
kept_files = spec.get("kept") if isinstance(spec, dict) else None
|
|
425
|
+
if not isinstance(spec, dict) or not isinstance(spec.get("intent"), str) or not isinstance(kept_files, list) \
|
|
426
|
+
or not all(isinstance(f, str) for f in kept_files):
|
|
427
|
+
raise InventoryError(f'intent_mapping.{kind} must be {{"intent": "<text>", "kept": [source_file, ...]}}')
|
|
428
|
+
entries = [e for e in data.get(INTENT_LISTS[kind]) or [] if isinstance(e, dict)]
|
|
429
|
+
listed = {e.get("source_file") for e in entries}
|
|
430
|
+
missing = [f for f in spec["kept"] if f not in listed]
|
|
431
|
+
if missing:
|
|
432
|
+
raise InventoryError(f"{INTENT_LISTS[kind]} lists no {', '.join(missing)}")
|
|
433
|
+
kept = set(spec["kept"])
|
|
434
|
+
filtered[kind] = [e for e in entries if e.get("source_file") in kept]
|
|
435
|
+
earlier = (data.get("intent_mapping") or {}).get(kind)
|
|
436
|
+
left_out = {e.get("source_file") for e in entries if e.get("source_file") not in kept}
|
|
437
|
+
if isinstance(earlier, dict) and earlier.get("intent") == spec["intent"]:
|
|
438
|
+
left_out |= set(earlier.get("left_out") or [])
|
|
439
|
+
recorded[kind] = {"intent": spec["intent"], "kept": sorted(kept), "left_out": sorted(left_out - kept)}
|
|
440
|
+
for kind, entries in filtered.items():
|
|
441
|
+
data[INTENT_LISTS[kind]] = entries
|
|
442
|
+
return recorded
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
JS_LANGUAGES = frozenset({"typescript", "tsx", "javascript"})
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def typed_param(param, language: str | None) -> str | None:
|
|
449
|
+
"""One parameter as the provenance map writes it ('userId: string',
|
|
450
|
+
'options?: TokenOptions', 'retries: int = 3'); a string is kept."""
|
|
451
|
+
if isinstance(param, str):
|
|
452
|
+
return param
|
|
453
|
+
if not isinstance(param, dict):
|
|
454
|
+
return None
|
|
455
|
+
name, type_, default = param.get("name"), param.get("type"), param.get("default")
|
|
456
|
+
if not name:
|
|
457
|
+
return type_ or None
|
|
458
|
+
text = str(name)
|
|
459
|
+
if param.get("optional") and default is None and language in JS_LANGUAGES and not text.startswith("..."):
|
|
460
|
+
text += "?"
|
|
461
|
+
if type_:
|
|
462
|
+
text += f": {type_}"
|
|
463
|
+
if default is not None:
|
|
464
|
+
text += f" = {default}"
|
|
465
|
+
return text
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# provenance: the header keys stdin may give, and the default of each (REQUIRED: no default).
|
|
469
|
+
REQUIRED = object()
|
|
470
|
+
MAP_HEADER = {
|
|
471
|
+
"provenance_version": "2.0",
|
|
472
|
+
"skill_name": None, # the inventory's
|
|
473
|
+
"skill_type": "single",
|
|
474
|
+
"source_repo": REQUIRED,
|
|
475
|
+
"source_commit": REQUIRED,
|
|
476
|
+
"source_ref": None,
|
|
477
|
+
"generated_at": None, # now
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _labels(export: dict) -> dict:
|
|
482
|
+
"""The labels the tool that found `export` gives its entry (compile.md §6)."""
|
|
483
|
+
if export.get("extraction_method") == "ast-grep":
|
|
484
|
+
return {"confidence": "T1", "extraction_method": "ast-grep", "ast_node_type": export.get("ast_node_type"),
|
|
485
|
+
"signature_source": "T1"}
|
|
486
|
+
return {"confidence": "T1-low", "extraction_method": "source-read", "ast_node_type": None,
|
|
487
|
+
"signature_source": "T1-low"}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def provenance_entries(data: dict) -> tuple[list[dict], list[str]]:
|
|
491
|
+
"""(the provenance map entry of every export, the functions among them with no params)."""
|
|
492
|
+
entries: list[dict] = []
|
|
493
|
+
unsigned: list[str] = []
|
|
494
|
+
for export in data.get("exports") or []:
|
|
495
|
+
if not isinstance(export, dict):
|
|
496
|
+
continue
|
|
497
|
+
raw = export.get("params")
|
|
498
|
+
params = None
|
|
499
|
+
if isinstance(raw, list):
|
|
500
|
+
params = [s for s in (typed_param(p, export.get("language")) for p in raw) if s is not None]
|
|
501
|
+
elif export.get("export_type") == "function":
|
|
502
|
+
unsigned.append(f"{export.get('export_name')} ({export.get('source_file')})")
|
|
503
|
+
entries.append({
|
|
504
|
+
"export_name": export.get("export_name"),
|
|
505
|
+
"export_type": export.get("export_type"),
|
|
506
|
+
"params": params,
|
|
507
|
+
"return_type": export.get("return_type") if params is not None else None,
|
|
508
|
+
"source_file": export.get("source_file"),
|
|
509
|
+
"source_line": export.get("source_line"),
|
|
510
|
+
**_labels(export),
|
|
511
|
+
})
|
|
512
|
+
return entries, unsigned
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def map_header(data: dict, given) -> dict:
|
|
516
|
+
"""The provenance map's header: stdin's keys over the defaults."""
|
|
517
|
+
if not isinstance(given, dict):
|
|
518
|
+
raise InventoryError("stdin must hold the map's header, a JSON object with source_repo and source_commit")
|
|
519
|
+
unknown = sorted(set(given) - set(MAP_HEADER))
|
|
520
|
+
if unknown:
|
|
521
|
+
raise InventoryError(f"the map's header takes only {', '.join(MAP_HEADER)}; not {', '.join(unknown)}")
|
|
522
|
+
missing = [key for key, default in MAP_HEADER.items() if default is REQUIRED and key not in given]
|
|
523
|
+
if missing:
|
|
524
|
+
raise InventoryError(f"the map's header needs {', '.join(missing)} (null when unknown)")
|
|
525
|
+
defaults = {"skill_name": data.get("skill_name"),
|
|
526
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
|
527
|
+
return {key: given[key] if key in given else defaults.get(key, default) for key, default in MAP_HEADER.items()}
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def provenance_map(data: dict, header) -> tuple[dict, list[str]]:
|
|
531
|
+
"""(the provenance map of the inventory, the functions with no params)."""
|
|
532
|
+
entries, unsigned = provenance_entries(data)
|
|
533
|
+
return {**map_header(data, header), "entries": entries}, unsigned
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def add_map_entries(provenance: dict, payload) -> tuple[int, int]:
|
|
537
|
+
"""Append stdin's `entries` and `file_entries` to the map; return (added, duplicates)."""
|
|
538
|
+
if not isinstance(payload, dict) or not payload or set(payload) - {"entries", "file_entries"}:
|
|
539
|
+
raise InventoryError('stdin must hold {"entries": [...], "file_entries": [...]} (either list, or both)')
|
|
540
|
+
keys = {"entries": ("export_name", "source_file"), "file_entries": ("file_name",)}
|
|
541
|
+
added = duplicates = 0
|
|
542
|
+
for field, items in payload.items():
|
|
543
|
+
if not isinstance(items, list) or not all(isinstance(e, dict) for e in items):
|
|
544
|
+
raise InventoryError(f"{field} must be a list of objects")
|
|
545
|
+
need = keys[field][0]
|
|
546
|
+
if not all(isinstance(e.get(need), str) and e[need] for e in items):
|
|
547
|
+
raise InventoryError(f"every {field} item needs a {need}")
|
|
548
|
+
target = provenance.setdefault(field, [])
|
|
549
|
+
if not isinstance(target, list):
|
|
550
|
+
raise InventoryError(f"the map's {field} is not a list")
|
|
551
|
+
held = {tuple(e.get(k) for k in keys[field]) for e in target if isinstance(e, dict)}
|
|
552
|
+
for item in items:
|
|
553
|
+
key = tuple(item.get(k) for k in keys[field])
|
|
554
|
+
if key in held:
|
|
555
|
+
duplicates += 1
|
|
556
|
+
continue
|
|
557
|
+
held.add(key)
|
|
558
|
+
target.append(item)
|
|
559
|
+
added += 1
|
|
560
|
+
return added, duplicates
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def rules_yaml(data: dict, language: str) -> str:
|
|
564
|
+
"""extraction-rules.yaml: how the skill was extracted, for a later run to reproduce it."""
|
|
565
|
+
head = {"skill_name": data.get("skill_name"), "language": language, "tier": data.get("tier"),
|
|
566
|
+
"extraction_mode": data.get("extraction_mode")}
|
|
567
|
+
rules = data.get("extraction_rules") if isinstance(data.get("extraction_rules"), dict) else {}
|
|
568
|
+
lines = ["# Generated by skf-extraction-inventory.py rules from the extraction inventory."]
|
|
569
|
+
lines += _yaml_lines({**head, **rules})
|
|
570
|
+
return "\n".join(lines) + "\n"
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _yaml_scalar(value) -> str:
|
|
574
|
+
if isinstance(value, dict):
|
|
575
|
+
return "{}"
|
|
576
|
+
if isinstance(value, list):
|
|
577
|
+
return "[]"
|
|
578
|
+
return json.dumps(value, ensure_ascii=False) # JSON scalars are YAML scalars
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _yaml_key(key) -> str:
|
|
582
|
+
text = str(key)
|
|
583
|
+
return text if YAML_KEY_RE.fullmatch(text) else json.dumps(text, ensure_ascii=False)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _yaml_lines(value, indent: int = 0) -> list[str]:
|
|
587
|
+
pad = " " * indent
|
|
588
|
+
lines: list[str] = []
|
|
589
|
+
items = value.items() if isinstance(value, dict) else ((None, v) for v in value)
|
|
590
|
+
for key, item in items:
|
|
591
|
+
lead = f"{pad}{_yaml_key(key)}:" if isinstance(value, dict) else f"{pad}-"
|
|
592
|
+
if isinstance(item, (dict, list)) and item:
|
|
593
|
+
nested = _yaml_lines(item, indent + 1)
|
|
594
|
+
if isinstance(value, list):
|
|
595
|
+
lines.append(f"{lead} {nested[0].lstrip()}")
|
|
596
|
+
lines.extend(nested[1:])
|
|
597
|
+
else:
|
|
598
|
+
lines.append(lead)
|
|
599
|
+
lines.extend(nested)
|
|
600
|
+
else:
|
|
601
|
+
lines.append(f"{lead} {_yaml_scalar(item)}")
|
|
602
|
+
return lines
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def write(path: Path, data: dict | str) -> None:
|
|
606
|
+
"""Write `data` (JSON, or text as it is) to `path` through a temporary file and one rename."""
|
|
607
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
608
|
+
try:
|
|
609
|
+
with open(tmp, "w", encoding="utf-8", newline="\n") as fh:
|
|
610
|
+
if isinstance(data, str):
|
|
611
|
+
fh.write(data)
|
|
612
|
+
else:
|
|
613
|
+
json.dump(data, fh, indent=2, ensure_ascii=False)
|
|
614
|
+
fh.write("\n")
|
|
615
|
+
fh.flush()
|
|
616
|
+
os.fsync(fh.fileno())
|
|
617
|
+
os.replace(tmp, path)
|
|
618
|
+
except OSError as exc:
|
|
619
|
+
try:
|
|
620
|
+
tmp.unlink()
|
|
621
|
+
except OSError:
|
|
622
|
+
pass
|
|
623
|
+
raise InventoryError(f"cannot write {path.as_posix()}: {exc}") from exc
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _result(path: Path, data: dict, **extra) -> dict:
|
|
627
|
+
return {"status": "ok", "inventory": path.as_posix(), "extraction_mode": data.get("extraction_mode"),
|
|
628
|
+
"counts": counts(data), **extra}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _stdin_json():
|
|
632
|
+
raw = sys.stdin.read()
|
|
633
|
+
try:
|
|
634
|
+
return json.loads(raw) if raw.strip() else None
|
|
635
|
+
except ValueError as exc:
|
|
636
|
+
raise InventoryError(f"stdin is not JSON: {exc}") from exc
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
640
|
+
parser = argparse.ArgumentParser(
|
|
641
|
+
prog="skf-extraction-inventory.py",
|
|
642
|
+
description=__doc__,
|
|
643
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
644
|
+
)
|
|
645
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
646
|
+
inventory_help = "the extraction inventory JSON"
|
|
647
|
+
p_init = sub.add_parser("init", help="write a new inventory from the runner's and the detector's JSON")
|
|
648
|
+
p_init.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
649
|
+
p_init.add_argument("--skill", required=True, help="the skill name")
|
|
650
|
+
p_init.add_argument("--mode", required=True, choices=MODES, help="the extraction mode")
|
|
651
|
+
p_init.add_argument("--tier", required=True, choices=TIERS, help="the forge tier")
|
|
652
|
+
p_init.add_argument("--extraction", type=Path, default=None,
|
|
653
|
+
help="the recipe runner's JSON (skf-extract-public-api.py --mode full -o)")
|
|
654
|
+
p_init.add_argument("--detected", type=Path, default=None,
|
|
655
|
+
help="the script and asset detector's JSON (skf-detect-scripts-assets.py detect)")
|
|
656
|
+
p_add = sub.add_parser("add", help="append a JSON list from stdin to one of the inventory's lists")
|
|
657
|
+
p_add.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
658
|
+
p_add.add_argument("--field", required=True, choices=FIELDS, help="the list to append to")
|
|
659
|
+
p_patch = sub.add_parser("patch", help="set signature, params and return_type on exports from stdin")
|
|
660
|
+
p_patch.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
661
|
+
p_set = sub.add_parser("set", help="set the keys of a JSON object from stdin")
|
|
662
|
+
p_set.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
663
|
+
p_rules = sub.add_parser("rules", help="write extraction-rules.yaml from the inventory")
|
|
664
|
+
p_rules.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
665
|
+
p_rules.add_argument("--language", required=True, help="the brief's language")
|
|
666
|
+
p_rules.add_argument("--target", required=True, type=Path, help="the extraction-rules.yaml to write")
|
|
667
|
+
p_prov = sub.add_parser("provenance", help="write the provenance map from the inventory, the header on stdin")
|
|
668
|
+
p_prov.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
669
|
+
p_prov.add_argument("--target", required=True, type=Path, help="the provenance-map.json to write")
|
|
670
|
+
p_prov.add_argument("--add-entries", action="store_true",
|
|
671
|
+
help="append stdin's entries and file_entries to the map at --target instead")
|
|
672
|
+
p_summary = sub.add_parser("summary", help="print the inventory's counts")
|
|
673
|
+
p_summary.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
674
|
+
return parser
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def main(argv: list[str] | None = None) -> int:
|
|
678
|
+
args = _build_parser().parse_args(argv)
|
|
679
|
+
path = args.inventory
|
|
680
|
+
try:
|
|
681
|
+
if args.command == "init":
|
|
682
|
+
extraction = _read_json(args.extraction, "runner JSON") if args.extraction else None
|
|
683
|
+
detected = _read_json(args.detected, "detector JSON") if args.detected else None
|
|
684
|
+
data = init(args.skill, args.mode, args.tier, extraction, detected)
|
|
685
|
+
write(path, data)
|
|
686
|
+
result = _result(path, data)
|
|
687
|
+
elif args.command == "provenance" and args.add_entries:
|
|
688
|
+
load(path)
|
|
689
|
+
provenance = _read_json(args.target, "provenance map")
|
|
690
|
+
if not isinstance(provenance, dict) or not isinstance(provenance.get("entries"), list):
|
|
691
|
+
raise InventoryError(f"the provenance map {args.target.as_posix()} has no entries list: "
|
|
692
|
+
"run provenance without --add-entries first")
|
|
693
|
+
added, duplicates = add_map_entries(provenance, _stdin_json())
|
|
694
|
+
write(args.target, provenance)
|
|
695
|
+
result = {"status": "ok", "target": args.target.as_posix(), "entries": len(provenance["entries"]),
|
|
696
|
+
"file_entries": len(provenance.get("file_entries") or []), "added": added,
|
|
697
|
+
"duplicates": duplicates}
|
|
698
|
+
elif args.command == "provenance":
|
|
699
|
+
provenance, unsigned = provenance_map(load(path), _stdin_json())
|
|
700
|
+
write(args.target, provenance)
|
|
701
|
+
result = {"status": "ok", "target": args.target.as_posix(), "entries": len(provenance["entries"]),
|
|
702
|
+
"unsigned": unsigned}
|
|
703
|
+
elif args.command == "rules":
|
|
704
|
+
text = rules_yaml(load(path), args.language)
|
|
705
|
+
write(args.target, text)
|
|
706
|
+
result = {"status": "ok", "target": args.target.as_posix(), "bytes": len(text.encode("utf-8"))}
|
|
707
|
+
else:
|
|
708
|
+
data = load(path)
|
|
709
|
+
if args.command == "summary":
|
|
710
|
+
result = _result(path, data)
|
|
711
|
+
elif args.command == "add":
|
|
712
|
+
payload = _stdin_json()
|
|
713
|
+
added, duplicates, dropped = add(data, args.field, [] if payload is None else payload)
|
|
714
|
+
write(path, data)
|
|
715
|
+
result = _result(path, data, added=added, duplicates=duplicates, dropped=dropped)
|
|
716
|
+
elif args.command == "patch":
|
|
717
|
+
payload = _stdin_json()
|
|
718
|
+
patched, unmatched, ambiguous = patch(data, [] if payload is None else payload)
|
|
719
|
+
write(path, data)
|
|
720
|
+
result = _result(path, data, patched=patched, unmatched=unmatched, ambiguous=ambiguous)
|
|
721
|
+
else:
|
|
722
|
+
payload = _stdin_json()
|
|
723
|
+
keys = set_fields(data, {} if payload is None else payload)
|
|
724
|
+
write(path, data)
|
|
725
|
+
result = _result(path, data, set=keys)
|
|
726
|
+
except InventoryError as exc:
|
|
727
|
+
print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
|
|
728
|
+
return 1
|
|
729
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
730
|
+
return 0
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _force_utf8(*streams) -> None:
|
|
734
|
+
"""Reconfigure stdin and the JSON streams to UTF-8 (a Windows console uses cp1252)."""
|
|
735
|
+
for stream in streams:
|
|
736
|
+
if hasattr(stream, "reconfigure"):
|
|
737
|
+
errors = getattr(stream, "errors", None)
|
|
738
|
+
if errors is None:
|
|
739
|
+
stream.reconfigure(encoding="utf-8")
|
|
740
|
+
else:
|
|
741
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
if __name__ == "__main__":
|
|
745
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
746
|
+
sys.exit(main())
|