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,92 +1,302 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
|
-
"""SKF Severity Classify
|
|
6
|
-
|
|
7
|
-
Applies severity rules
|
|
8
|
-
drift score. Used by
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
"""SKF Severity Classify: rule-based severity classification for drift findings.
|
|
6
|
+
|
|
7
|
+
Applies the severity rules of audit-skill's references/severity-rules.md to a
|
|
8
|
+
list of drift findings and computes an overall drift score. Used by
|
|
9
|
+
audit-skill (severity-classify step).
|
|
10
|
+
|
|
11
|
+
CLI:
|
|
12
|
+
python3 skf-severity-classify.py findings.json [-o result.json]
|
|
13
|
+
python3 skf-severity-classify.py - < findings.json
|
|
14
|
+
python3 skf-severity-classify.py --from-diff structural-diff.json [-o findings.json]
|
|
15
|
+
python3 skf-severity-classify.py [--from-diff DIFF] [--file-drift FILE] [--constituents FILE]
|
|
16
|
+
[--semantic FILE] [--doc-drift FILE] [-o findings.json]
|
|
17
|
+
python3 skf-severity-classify.py --rules [--format json|markdown]
|
|
18
|
+
|
|
19
|
+
Classify (the default mode):
|
|
20
|
+
FINDINGS is a file path, `-` for stdin, or the JSON array itself. Pass a
|
|
21
|
+
file: a `detail` that quotes a signature reaches the helper intact, where
|
|
22
|
+
`echo '<JSON>'` breaks on the first single quote.
|
|
23
|
+
|
|
24
|
+
Input: a JSON array of findings, each an object with:
|
|
25
|
+
- type: removed | added | changed | moved | renamed | deprecated |
|
|
26
|
+
semantic
|
|
27
|
+
- category: what the change is about. The (type, category) pair must be
|
|
28
|
+
one the rule table below accepts (--rules prints it); a pair
|
|
29
|
+
it does not accept is an error, never a default severity.
|
|
30
|
+
A Script/Asset Drift row (skf-compare-file-hashes.py compare)
|
|
31
|
+
is added/file, removed/file or changed/file.
|
|
32
|
+
- count: optional positive integer: the exports a rollup row stands
|
|
33
|
+
for (default 1)
|
|
34
|
+
- category_choices: optional list (written by --from-diff): when it is
|
|
35
|
+
present, category must be one of it
|
|
36
|
+
- every other key (detail, name, file, line, confidence, ...) is carried
|
|
37
|
+
through untouched. type, category and category_choices compare case-
|
|
38
|
+
insensitively.
|
|
39
|
+
|
|
40
|
+
The added-exports rule counts exports, not rows: the counts of every
|
|
41
|
+
added/export finding are summed, and more than 3 grade each of them HIGH,
|
|
42
|
+
3 or fewer MEDIUM. A rollup row of 15 added exports therefore grades HIGH.
|
|
43
|
+
|
|
44
|
+
Output:
|
|
45
|
+
{
|
|
46
|
+
"status": "ok",
|
|
47
|
+
"drift_score": "CLEAN" | "MINOR" | "SIGNIFICANT" | "CRITICAL",
|
|
48
|
+
"total_findings": N, # rows
|
|
49
|
+
"total_items": N, # rows weighted by count
|
|
50
|
+
"added_export_count": N, # what the added-exports rule compared
|
|
51
|
+
"by_severity": {"CRITICAL": N, "HIGH": N, "MEDIUM": N, "LOW": N},
|
|
52
|
+
"findings": [ {<finding>, "severity": "...", "rule": "<the rule>"} ]
|
|
53
|
+
}
|
|
54
|
+
by_severity counts rows weighted by count, so it sums to total_items.
|
|
55
|
+
findings are sorted CRITICAL first, input order kept within a severity.
|
|
56
|
+
drift_score follows severity-rules.md's Overall Drift Score table: CLEAN
|
|
57
|
+
(no findings), MINOR (LOW only), SIGNIFICANT (any MEDIUM or HIGH, no
|
|
58
|
+
CRITICAL), CRITICAL (any CRITICAL).
|
|
59
|
+
|
|
60
|
+
Findings that fit no rule print {status: "error", error, problems:
|
|
61
|
+
[{index, type, category, problem}]}, naming every one of them, and exit
|
|
62
|
+
1; an input that is not an array prints {status: "error", error}.
|
|
63
|
+
|
|
64
|
+
--from-diff DIFF:
|
|
65
|
+
Projects a skf-structural-diff.py result into the findings array, so the
|
|
66
|
+
diff's buckets reach the classifier without a hand-built table. Each
|
|
67
|
+
finding carries name, detail, file, line and confidence (and
|
|
68
|
+
source_library for a --group-by source_library diff), and a fixed
|
|
69
|
+
category where the bucket decides it:
|
|
70
|
+
added[] added/export
|
|
71
|
+
removed[] removed/export, category_choices [export, internal_helper]:
|
|
72
|
+
an internal helper referenced in documented patterns grades
|
|
73
|
+
HIGH, a public export CRITICAL
|
|
74
|
+
moved[] moved/export
|
|
75
|
+
changed[] one finding per export: changed/location when its line is
|
|
76
|
+
the only field that changed; otherwise changed/signature with
|
|
77
|
+
category_choices listing the categories a signature or type
|
|
78
|
+
change can have, from signature (CRITICAL, the default) down
|
|
79
|
+
to style and whitespace (LOW)
|
|
80
|
+
A removed or added finding whose name the diff lists in ambiguous_names[]
|
|
81
|
+
carries "ambiguous_name": true. A reviewer who judges a removed and an
|
|
82
|
+
added finding of one ambiguous name to be one export that moved records
|
|
83
|
+
it as the projection records a move: the added finding becomes
|
|
84
|
+
{type: moved, category: export, detail: "moved from <removed file:line>
|
|
85
|
+
to <file:line>"} without ambiguous_name, and the removed finding is
|
|
86
|
+
dropped. Every other ambiguous finding stays as projected: a finding
|
|
87
|
+
that keeps ambiguous_name with another type fits no rule.
|
|
88
|
+
label_changes[] and signature_unverified[] are never findings. Only the
|
|
89
|
+
categories with category_choices need judgment; the rest are fixed.
|
|
90
|
+
|
|
91
|
+
--file-drift FILE, --constituents FILE, --semantic FILE, --doc-drift FILE:
|
|
92
|
+
Project the other drift sources into the same findings array, after the
|
|
93
|
+
diff's findings and in this order. A hash comparison labels no export,
|
|
94
|
+
so its findings carry a null line and confidence:
|
|
95
|
+
--file-drift a skf-compare-file-hashes.py compare result: each
|
|
96
|
+
added[] path is added/file, each removed[] path
|
|
97
|
+
removed/file and each changed[] item changed/file,
|
|
98
|
+
with the path as name and file
|
|
99
|
+
--constituents a skf-hash-content.py compare-constituent-hashes
|
|
100
|
+
result: each drifted[] entry is changed/constituent
|
|
101
|
+
(detail "metadata.json <stored> -> <current>"), each
|
|
102
|
+
missing[] entry removed/constituent (its reason as the
|
|
103
|
+
detail), with skill_name as name and skill_path as
|
|
104
|
+
file; fresh[] and skipped_null_hash[] are no findings
|
|
105
|
+
--semantic the semantic diff's findings, an array of objects whose
|
|
106
|
+
type is semantic, kept as written
|
|
107
|
+
--doc-drift a skf-detect-docs.py compare-hashes result, the whole
|
|
108
|
+
drift of a docs-only skill: each changed[] entry is
|
|
109
|
+
changed/doc_source (detail "content <old> -> <new>"),
|
|
110
|
+
with its url as name and file; unchanged[],
|
|
111
|
+
fetch_failed[] and skipped_null_hash[] are no findings
|
|
112
|
+
Each may be given with or without --from-diff. A FILE of these four
|
|
113
|
+
that does not exist is skipped (the stage that saves it did not run),
|
|
114
|
+
but at least one source file must exist. The summary line's sources
|
|
115
|
+
gives the findings each flag added, or null for a skipped file.
|
|
116
|
+
|
|
117
|
+
--rules:
|
|
118
|
+
Prints the rule table: each rule's severity and text, and the types and
|
|
119
|
+
categories it accepts, as JSON
|
|
120
|
+
({status, added_export_threshold, rules: [{severity, rule, types,
|
|
121
|
+
categories, added_exports}]}) or, with --format markdown, as a table.
|
|
122
|
+
|
|
123
|
+
-o FILE writes the output to FILE and prints one summary line on stdout.
|
|
124
|
+
|
|
125
|
+
Exit codes:
|
|
126
|
+
0 output written
|
|
127
|
+
1 input error: unreadable or invalid JSON, not a findings array, a
|
|
128
|
+
finding that fits no rule, a source file that is not what its flag
|
|
129
|
+
reads, no source file that exists, or an -o FILE that cannot be
|
|
130
|
+
written (an error that starts "Cannot write output")
|
|
131
|
+
({status: "error", ...} on stdout)
|
|
132
|
+
2 usage error (argparse, usage on stderr)
|
|
19
133
|
"""
|
|
20
134
|
|
|
21
135
|
from __future__ import annotations
|
|
22
136
|
|
|
137
|
+
import argparse
|
|
23
138
|
import json
|
|
24
139
|
import sys
|
|
140
|
+
from pathlib import Path
|
|
141
|
+
|
|
142
|
+
SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW")
|
|
25
143
|
|
|
26
|
-
#
|
|
144
|
+
# More new public exports than this grade HIGH; this many or fewer MEDIUM.
|
|
145
|
+
ADDED_EXPORT_THRESHOLD = 3
|
|
27
146
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
]
|
|
147
|
+
# Categories that name a public export of any kind.
|
|
148
|
+
PUBLIC_EXPORTS = ("export", "function", "class", "type", "interface")
|
|
149
|
+
# The types a LOW bucket applies to.
|
|
150
|
+
STRUCTURAL_TYPES = ("added", "removed", "changed", "moved", "renamed")
|
|
33
151
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
]
|
|
152
|
+
LINE_ONLY_RULE = "Line-only changes (same export and file, only the line number changed)"
|
|
153
|
+
CONSTITUENT_CHANGED_RULE = "Constituent skills changed since the stack was composed (compose-mode stacks)"
|
|
154
|
+
CONSTITUENT_MISSING_RULE = "Constituent skills that can no longer be found (compose-mode stacks)"
|
|
155
|
+
SCRIPT_ASSET_RULE = "Added or changed script, asset or doc files (Script/Asset Drift)"
|
|
156
|
+
DOC_SOURCE_RULE = "Changed documentation sources of a docs-only skill (doc drift)"
|
|
40
157
|
|
|
41
|
-
MEDIUM_RULES = [
|
|
42
|
-
{"type": "changed", "categories": {"implementation", "optional_parameter", "internal_pattern"}},
|
|
43
|
-
{"type": "added", "categories": {"export"}, "threshold_max": 3}, # 1-3 new exports
|
|
44
|
-
{"type": "moved", "categories": {"export", "function"}},
|
|
45
|
-
]
|
|
46
158
|
|
|
47
|
-
|
|
159
|
+
def _rule(severity, rule, types, categories, above_threshold=None):
|
|
160
|
+
"""One row of the rule table. above_threshold: True when the rule applies
|
|
161
|
+
only above ADDED_EXPORT_THRESHOLD added exports, False only at or below
|
|
162
|
+
it, None always."""
|
|
163
|
+
return {
|
|
164
|
+
"severity": severity,
|
|
165
|
+
"rule": rule,
|
|
166
|
+
"types": tuple(types),
|
|
167
|
+
"categories": tuple(categories),
|
|
168
|
+
"above_threshold": above_threshold,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# --- Severity Rules (severity-rules.md, in its order) ---
|
|
173
|
+
|
|
174
|
+
RULES = (
|
|
175
|
+
_rule("CRITICAL", "Removed or renamed public exports (functions, classes, types)",
|
|
176
|
+
("removed", "renamed"), PUBLIC_EXPORTS),
|
|
177
|
+
_rule("CRITICAL", "Changed function signatures (parameter count, parameter types, return type)",
|
|
178
|
+
("changed",), ("signature", "parameter_count", "parameter_type", "return_type")),
|
|
179
|
+
_rule("CRITICAL", "Removed or renamed modules/files referenced in skill",
|
|
180
|
+
("removed", "renamed"), ("module", "file")),
|
|
181
|
+
_rule("CRITICAL", "Changed class inheritance or interface contracts",
|
|
182
|
+
("changed",), ("inheritance", "interface", "interface_contract")),
|
|
183
|
+
_rule("HIGH", "New public API exports not documented in skill (>3 new exports)",
|
|
184
|
+
("added",), ("export",), above_threshold=True),
|
|
185
|
+
_rule("HIGH", "Removed internal helpers that are referenced in documented patterns",
|
|
186
|
+
("removed",), ("internal_helper",)),
|
|
187
|
+
_rule("HIGH", "Changed default parameter values that affect documented behavior",
|
|
188
|
+
("changed",), ("default_value",)),
|
|
189
|
+
_rule("HIGH", "New required parameters added to documented functions",
|
|
190
|
+
("changed",), ("required_parameter",)),
|
|
191
|
+
_rule("HIGH", "Deprecated APIs still documented as current in skill",
|
|
192
|
+
("deprecated",), PUBLIC_EXPORTS),
|
|
193
|
+
_rule("HIGH", CONSTITUENT_CHANGED_RULE, ("changed",), ("constituent",)),
|
|
194
|
+
_rule("HIGH", DOC_SOURCE_RULE, ("changed",), ("doc_source",)),
|
|
195
|
+
_rule("MEDIUM", "Implementation changes behind a stable public API",
|
|
196
|
+
("changed",), ("implementation",)),
|
|
197
|
+
_rule("MEDIUM", "Implementation changes behind a stable public API",
|
|
198
|
+
("semantic",), ("behavior", "dependency", "architecture")),
|
|
199
|
+
_rule("MEDIUM", "New optional parameters with defaults on documented functions",
|
|
200
|
+
("changed",), ("optional_parameter",)),
|
|
201
|
+
_rule("MEDIUM", "New public exports not in skill (1-3 new exports)",
|
|
202
|
+
("added",), ("export",), above_threshold=False),
|
|
203
|
+
_rule("MEDIUM", "Moved functions between files (same API, different location)",
|
|
204
|
+
("moved",), PUBLIC_EXPORTS),
|
|
205
|
+
_rule("MEDIUM", "Changed internal implementation patterns documented in skill conventions",
|
|
206
|
+
("changed",), ("internal_pattern",)),
|
|
207
|
+
_rule("MEDIUM", "Changed internal implementation patterns documented in skill conventions",
|
|
208
|
+
("semantic",), ("pattern", "convention", "deprecated_pattern")),
|
|
209
|
+
_rule("MEDIUM", SCRIPT_ASSET_RULE, ("added", "changed"), ("file",)),
|
|
210
|
+
_rule("MEDIUM", CONSTITUENT_MISSING_RULE, ("removed",), ("constituent",)),
|
|
211
|
+
_rule("LOW", "Style or convention changes (formatting, naming patterns)",
|
|
212
|
+
STRUCTURAL_TYPES, ("style", "convention")),
|
|
213
|
+
_rule("LOW", "Comment or documentation changes in source",
|
|
214
|
+
STRUCTURAL_TYPES, ("comment", "documentation")),
|
|
215
|
+
_rule("LOW", "Whitespace or structural reorganization",
|
|
216
|
+
STRUCTURAL_TYPES, ("whitespace",)),
|
|
217
|
+
_rule("LOW", LINE_ONLY_RULE, ("changed",), ("location",)),
|
|
218
|
+
_rule("LOW", "New private/internal functions not affecting public API",
|
|
219
|
+
STRUCTURAL_TYPES, ("private", "internal")),
|
|
220
|
+
_rule("LOW", "Test file changes", STRUCTURAL_TYPES, ("test",)),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _index_rules(rules):
|
|
225
|
+
"""(type, category) -> the rules that accept the pair."""
|
|
226
|
+
index = {}
|
|
227
|
+
for rule in rules:
|
|
228
|
+
for f_type in rule["types"]:
|
|
229
|
+
for category in rule["categories"]:
|
|
230
|
+
index.setdefault((f_type, category), []).append(rule)
|
|
231
|
+
return index
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
PAIRS = _index_rules(RULES)
|
|
235
|
+
|
|
236
|
+
# What --from-diff offers for a finding whose category needs judgment.
|
|
237
|
+
REMOVED_CHOICES = ("export", "internal_helper")
|
|
238
|
+
CHANGED_CHOICES = (
|
|
239
|
+
"signature", "parameter_count", "parameter_type", "return_type",
|
|
240
|
+
"inheritance", "interface", "interface_contract",
|
|
241
|
+
"default_value", "required_parameter",
|
|
242
|
+
"optional_parameter", "implementation",
|
|
243
|
+
"style", "whitespace",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _norm(value):
|
|
248
|
+
return value.strip().lower() if isinstance(value, str) else None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _pair(finding):
|
|
252
|
+
return _norm(finding.get("type")), _norm(finding.get("category"))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _count(finding):
|
|
256
|
+
return finding.get("count", 1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def match_rule(finding, added_export_count=0):
|
|
260
|
+
"""The rule that grades a finding, or None when no rule accepts its pair."""
|
|
261
|
+
above = added_export_count > ADDED_EXPORT_THRESHOLD
|
|
262
|
+
for rule in PAIRS.get(_pair(finding), ()):
|
|
263
|
+
if rule["above_threshold"] is None or rule["above_threshold"] == above:
|
|
264
|
+
return rule
|
|
265
|
+
return None
|
|
48
266
|
|
|
49
267
|
|
|
50
268
|
def classify_finding(finding, added_export_count=0):
|
|
51
|
-
"""Classify a single finding's severity. Returns severity
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return "LOW"
|
|
83
|
-
|
|
84
|
-
# Semantic findings default to MEDIUM
|
|
85
|
-
if f_type == "semantic":
|
|
86
|
-
return "MEDIUM"
|
|
87
|
-
|
|
88
|
-
# Default: MEDIUM for unrecognized patterns
|
|
89
|
-
return "MEDIUM"
|
|
269
|
+
"""Classify a single finding's severity. Returns the severity, or None
|
|
270
|
+
for a type/category pair no rule accepts."""
|
|
271
|
+
rule = match_rule(finding, added_export_count)
|
|
272
|
+
return rule["severity"] if rule else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _problem(finding):
|
|
276
|
+
"""Why a finding does not fit the rule table, or None."""
|
|
277
|
+
if not isinstance(finding, dict):
|
|
278
|
+
return "a finding must be a JSON object"
|
|
279
|
+
f_type, category = _pair(finding)
|
|
280
|
+
if f_type is None or category is None:
|
|
281
|
+
return "type and category must both be strings"
|
|
282
|
+
if (f_type, category) not in PAIRS:
|
|
283
|
+
return "unknown type/category pair"
|
|
284
|
+
choices = finding.get("category_choices")
|
|
285
|
+
if choices is not None and (not isinstance(choices, list) or category not in {_norm(c) for c in choices}):
|
|
286
|
+
return "category is not one of category_choices"
|
|
287
|
+
if finding.get("ambiguous_name") and f_type not in ("removed", "added"):
|
|
288
|
+
return "an ambiguous_name finding stays removed or added: a move judged from it drops the flag"
|
|
289
|
+
count = _count(finding)
|
|
290
|
+
if isinstance(count, bool) or not isinstance(count, int) or count < 1:
|
|
291
|
+
return "count must be a positive integer"
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _error(message, problems=None):
|
|
296
|
+
out = {"status": "error", "error": message}
|
|
297
|
+
if problems is not None:
|
|
298
|
+
out["problems"] = problems
|
|
299
|
+
return out
|
|
90
300
|
|
|
91
301
|
|
|
92
302
|
def compute_drift_score(classified):
|
|
@@ -105,59 +315,453 @@ def compute_drift_score(classified):
|
|
|
105
315
|
def classify_all(findings):
|
|
106
316
|
"""Classify all findings and compute drift score."""
|
|
107
317
|
if not isinstance(findings, list):
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
|
|
318
|
+
return _error("Input must be a JSON array of findings")
|
|
319
|
+
|
|
320
|
+
problems = []
|
|
321
|
+
for index, finding in enumerate(findings):
|
|
322
|
+
problem = _problem(finding)
|
|
323
|
+
if problem:
|
|
324
|
+
given = finding if isinstance(finding, dict) else {}
|
|
325
|
+
problems.append({
|
|
326
|
+
"index": index,
|
|
327
|
+
"type": given.get("type"),
|
|
328
|
+
"category": given.get("category"),
|
|
329
|
+
"problem": problem,
|
|
330
|
+
})
|
|
331
|
+
if problems:
|
|
332
|
+
named = "; ".join(
|
|
333
|
+
f"finding {p['index']} ({p['type']}/{p['category']}): {p['problem']}" for p in problems[:10]
|
|
334
|
+
)
|
|
335
|
+
more = f"; and {len(problems) - 10} more" if len(problems) > 10 else ""
|
|
336
|
+
return _error(
|
|
337
|
+
f"{len(problems)} finding(s) fit no severity rule: {named}{more}. "
|
|
338
|
+
"Run with --rules for the accepted type/category pairs.",
|
|
339
|
+
problems,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# The added-exports threshold counts exports: a rollup row adds its count.
|
|
343
|
+
added_export_count = sum(_count(f) for f in findings if _pair(f) == ("added", "export"))
|
|
115
344
|
|
|
116
345
|
classified = []
|
|
117
346
|
for finding in findings:
|
|
118
|
-
|
|
119
|
-
classified.append({**finding, "severity": severity})
|
|
347
|
+
rule = match_rule(finding, added_export_count)
|
|
348
|
+
classified.append({**finding, "severity": rule["severity"], "rule": rule["rule"]})
|
|
120
349
|
|
|
121
350
|
# Sort by severity: CRITICAL > HIGH > MEDIUM > LOW
|
|
122
|
-
severity_order = {
|
|
123
|
-
classified.sort(key=lambda f: severity_order
|
|
351
|
+
severity_order = {s: i for i, s in enumerate(SEVERITIES)}
|
|
352
|
+
classified.sort(key=lambda f: severity_order[f["severity"]])
|
|
124
353
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# Summary
|
|
128
|
-
by_severity = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
|
|
354
|
+
by_severity = {s: 0 for s in SEVERITIES}
|
|
129
355
|
for f in classified:
|
|
130
|
-
by_severity[f["severity"]]
|
|
356
|
+
by_severity[f["severity"]] += _count(f)
|
|
131
357
|
|
|
132
358
|
return {
|
|
133
359
|
"status": "ok",
|
|
134
|
-
"drift_score":
|
|
360
|
+
"drift_score": compute_drift_score(classified),
|
|
135
361
|
"total_findings": len(classified),
|
|
362
|
+
"total_items": sum(by_severity.values()),
|
|
363
|
+
"added_export_count": added_export_count,
|
|
136
364
|
"by_severity": by_severity,
|
|
137
365
|
"findings": classified,
|
|
138
366
|
}
|
|
139
367
|
|
|
140
368
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
369
|
+
# --------------------------------------------------------------------------
|
|
370
|
+
# --from-diff: a structural diff projected into findings
|
|
371
|
+
# --------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _text(value):
|
|
375
|
+
if isinstance(value, list):
|
|
376
|
+
return "(" + ", ".join(str(v) for v in value) + ")"
|
|
377
|
+
return str(value)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _shape(rec):
|
|
381
|
+
"""An export's signature for a detail: its signature, else its params and
|
|
382
|
+
return type, else None."""
|
|
383
|
+
if rec.get("signature"):
|
|
384
|
+
return str(rec["signature"])
|
|
385
|
+
if rec.get("params") is None and not rec.get("return_type"):
|
|
386
|
+
return None
|
|
387
|
+
shape = _text(rec.get("params") or [])
|
|
388
|
+
if rec.get("return_type"):
|
|
389
|
+
shape += f" -> {rec['return_type']}"
|
|
390
|
+
return shape
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _where(file, line):
|
|
394
|
+
return f"{file}:{line}" if line is not None else str(file)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _finding(f_type, category, rec, detail, choices=None, ambiguous=False):
|
|
398
|
+
finding = {"type": f_type, "category": category}
|
|
399
|
+
if choices:
|
|
400
|
+
finding["category_choices"] = list(choices)
|
|
401
|
+
finding.update(
|
|
402
|
+
name=rec.get("name"),
|
|
403
|
+
detail=detail,
|
|
404
|
+
file=rec.get("file"),
|
|
405
|
+
line=rec.get("line"),
|
|
406
|
+
confidence=rec.get("confidence"),
|
|
407
|
+
)
|
|
408
|
+
if "source_library" in rec:
|
|
409
|
+
finding["source_library"] = rec["source_library"]
|
|
410
|
+
if ambiguous:
|
|
411
|
+
finding["ambiguous_name"] = True
|
|
412
|
+
return finding
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def project_diff(diff):
|
|
416
|
+
"""The findings array for a skf-structural-diff.py result (see --from-diff).
|
|
417
|
+
|
|
418
|
+
Raises ValueError when diff is not a structural diff.
|
|
419
|
+
"""
|
|
420
|
+
buckets = ("added", "removed", "changed", "moved")
|
|
421
|
+
if not isinstance(diff, dict) or not all(isinstance(diff.get(b), list) for b in buckets):
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"not a structural diff: expected an object with added, removed, changed "
|
|
424
|
+
"and moved lists (the output of skf-structural-diff.py)"
|
|
425
|
+
)
|
|
426
|
+
ambiguous = {
|
|
427
|
+
(item.get("source_library"), item.get("name"))
|
|
428
|
+
for item in diff.get("ambiguous_names") or []
|
|
429
|
+
if isinstance(item, dict)
|
|
430
|
+
}
|
|
431
|
+
records = {b: [r for r in diff[b] if isinstance(r, dict)] for b in buckets}
|
|
432
|
+
|
|
433
|
+
findings = []
|
|
434
|
+
for rec in records["added"]:
|
|
435
|
+
kind = rec.get("type") or "export"
|
|
436
|
+
shape = _shape(rec)
|
|
437
|
+
detail = f"new {kind}: {shape}" if shape else f"new {kind}"
|
|
438
|
+
findings.append(_finding("added", "export", rec, detail,
|
|
439
|
+
ambiguous=(rec.get("source_library"), rec.get("name")) in ambiguous))
|
|
440
|
+
for rec in records["removed"]:
|
|
441
|
+
kind = rec.get("type") or "export"
|
|
442
|
+
shape = _shape(rec)
|
|
443
|
+
detail = f"removed {kind}: {shape}" if shape else f"removed {kind}"
|
|
444
|
+
findings.append(_finding("removed", "export", rec, detail, choices=REMOVED_CHOICES,
|
|
445
|
+
ambiguous=(rec.get("source_library"), rec.get("name")) in ambiguous))
|
|
446
|
+
|
|
447
|
+
# One finding per changed export, in the diff's order.
|
|
448
|
+
changes = {}
|
|
449
|
+
for item in records["changed"]:
|
|
450
|
+
key = (item.get("source_library"), item.get("name"), item.get("file"))
|
|
451
|
+
changes.setdefault(key, []).append(item)
|
|
452
|
+
for items in changes.values():
|
|
453
|
+
rec = items[0]
|
|
454
|
+
fields = [item.get("field") for item in items]
|
|
455
|
+
if fields == ["line"]:
|
|
456
|
+
detail = f"line {rec.get('baseline_value')} -> {rec.get('current_value')}"
|
|
457
|
+
findings.append(_finding("changed", "location", rec, detail))
|
|
458
|
+
continue
|
|
459
|
+
detail = "; ".join(
|
|
460
|
+
f"{item.get('field')}: {_text(item.get('baseline_value'))} -> {_text(item.get('current_value'))}"
|
|
461
|
+
for item in items
|
|
462
|
+
)
|
|
463
|
+
findings.append(_finding("changed", "signature", rec, detail, choices=CHANGED_CHOICES))
|
|
464
|
+
|
|
465
|
+
for rec in records["moved"]:
|
|
466
|
+
detail = (
|
|
467
|
+
f"moved from {_where(rec.get('previous_file'), rec.get('previous_line'))} "
|
|
468
|
+
f"to {_where(rec.get('current_file'), rec.get('line'))}"
|
|
469
|
+
)
|
|
470
|
+
findings.append(_finding("moved", "export", {**rec, "file": rec.get("current_file")}, detail))
|
|
471
|
+
return findings
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _lists(data, keys, what):
|
|
475
|
+
"""Raise ValueError unless data is an object holding a list at each key."""
|
|
476
|
+
if not isinstance(data, dict) or not all(isinstance(data.get(k), list) for k in keys):
|
|
477
|
+
raise ValueError(f"not {what}: expected an object with {', '.join(keys)} lists")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def project_file_drift(data):
|
|
481
|
+
"""The findings for a skf-compare-file-hashes.py compare result (see --file-drift).
|
|
482
|
+
|
|
483
|
+
Raises ValueError when data is not one.
|
|
484
|
+
"""
|
|
485
|
+
_lists(data, ("added", "removed", "changed"),
|
|
486
|
+
"a file drift result (the output of skf-compare-file-hashes.py compare)")
|
|
487
|
+
findings = []
|
|
488
|
+
for f_type, detail in (("added", "new file"), ("removed", "file removed")):
|
|
489
|
+
for path in data[f_type]:
|
|
490
|
+
if isinstance(path, str):
|
|
491
|
+
findings.append(_finding(f_type, "file", {"name": path, "file": path}, detail))
|
|
492
|
+
for item in data["changed"]:
|
|
493
|
+
if isinstance(item, dict):
|
|
494
|
+
path = item.get("path")
|
|
495
|
+
detail = f"content {item.get('stored_hash')} -> {item.get('current_hash')}"
|
|
496
|
+
findings.append(_finding("changed", "file", {"name": path, "file": path}, detail))
|
|
497
|
+
return findings
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def project_constituents(data):
|
|
501
|
+
"""The findings for a skf-hash-content.py compare-constituent-hashes result
|
|
502
|
+
(see --constituents).
|
|
503
|
+
|
|
504
|
+
Raises ValueError when data is not one.
|
|
505
|
+
"""
|
|
506
|
+
_lists(data, ("drifted", "missing"),
|
|
507
|
+
"a constituent freshness result (the output of skf-hash-content.py compare-constituent-hashes)")
|
|
508
|
+
findings = []
|
|
509
|
+
for rec in data["drifted"]:
|
|
510
|
+
if isinstance(rec, dict):
|
|
511
|
+
detail = f"metadata.json {rec.get('stored_hash')} -> {rec.get('current_hash')}"
|
|
512
|
+
findings.append(_finding("changed", "constituent",
|
|
513
|
+
{"name": rec.get("skill_name"), "file": rec.get("skill_path")}, detail))
|
|
514
|
+
for rec in data["missing"]:
|
|
515
|
+
if isinstance(rec, dict):
|
|
516
|
+
findings.append(_finding("removed", "constituent",
|
|
517
|
+
{"name": rec.get("skill_name"), "file": rec.get("skill_path")},
|
|
518
|
+
rec.get("reason")))
|
|
519
|
+
return findings
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def project_semantic(data):
|
|
523
|
+
"""The semantic diff's findings, kept as written (see --semantic).
|
|
524
|
+
|
|
525
|
+
Raises ValueError unless data is an array of semantic findings.
|
|
526
|
+
"""
|
|
527
|
+
if not isinstance(data, list) or not all(
|
|
528
|
+
isinstance(f, dict) and _norm(f.get("type")) == "semantic" for f in data):
|
|
529
|
+
raise ValueError("not a semantic findings file: expected an array of objects whose type is semantic")
|
|
530
|
+
return list(data)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def project_doc_drift(data):
|
|
534
|
+
"""The findings for a skf-detect-docs.py compare-hashes result (see
|
|
535
|
+
--doc-drift).
|
|
536
|
+
|
|
537
|
+
Raises ValueError when data is not one.
|
|
538
|
+
"""
|
|
539
|
+
_lists(data, ("changed", "unchanged", "fetch_failed"),
|
|
540
|
+
"a doc drift result (the output of skf-detect-docs.py compare-hashes)")
|
|
541
|
+
findings = []
|
|
542
|
+
for rec in data["changed"]:
|
|
543
|
+
if isinstance(rec, dict):
|
|
544
|
+
url = rec.get("url")
|
|
545
|
+
detail = f"content {rec.get('old_hash')} -> {rec.get('new_hash')}"
|
|
546
|
+
findings.append(_finding("changed", "doc_source", {"name": url, "file": url}, detail))
|
|
547
|
+
return findings
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# Each projection source: its name in the summary line, its argparse dest,
|
|
551
|
+
# its projection, and whether a file that does not exist is skipped.
|
|
552
|
+
SOURCES = (
|
|
553
|
+
("diff", "from_diff", project_diff, False),
|
|
554
|
+
("file_drift", "file_drift", project_file_drift, True),
|
|
555
|
+
("constituents", "constituents", project_constituents, True),
|
|
556
|
+
("semantic", "semantic", project_semantic, True),
|
|
557
|
+
("doc_drift", "doc_drift", project_doc_drift, True),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def project_sources(paths):
|
|
562
|
+
"""(findings, sources) for the projection flags.
|
|
563
|
+
|
|
564
|
+
paths maps each source name to its file, or None when its flag is not
|
|
565
|
+
given. sources gives the findings each given flag added, or None for a
|
|
566
|
+
skipped file. Raises ValueError, naming the file, when a file cannot be
|
|
567
|
+
read or is not what its flag reads, or when no source file exists.
|
|
568
|
+
"""
|
|
569
|
+
findings, sources = [], {}
|
|
570
|
+
for name, _, project, optional in SOURCES:
|
|
571
|
+
path = paths.get(name)
|
|
572
|
+
if path is None:
|
|
573
|
+
continue
|
|
574
|
+
path = Path(path)
|
|
575
|
+
if optional and not path.exists():
|
|
576
|
+
sources[name] = None
|
|
577
|
+
continue
|
|
578
|
+
try:
|
|
579
|
+
part = project(json.loads(path.read_text(encoding="utf-8")))
|
|
580
|
+
except (OSError, UnicodeDecodeError, ValueError) as e:
|
|
581
|
+
# json.JSONDecodeError is a ValueError.
|
|
582
|
+
raise ValueError(f"{path}: {e}") from e
|
|
583
|
+
findings.extend(part)
|
|
584
|
+
sources[name] = len(part)
|
|
585
|
+
if sources and all(count is None for count in sources.values()):
|
|
586
|
+
raise ValueError("none of the source files exists: " + ", ".join(str(paths[n]) for n in sources))
|
|
587
|
+
return findings, sources
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# --------------------------------------------------------------------------
|
|
591
|
+
# --rules
|
|
592
|
+
# --------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _added_exports(rule):
|
|
596
|
+
if rule["above_threshold"] is None:
|
|
597
|
+
return None
|
|
598
|
+
if rule["above_threshold"]:
|
|
599
|
+
return f"more than {ADDED_EXPORT_THRESHOLD}"
|
|
600
|
+
return f"{ADDED_EXPORT_THRESHOLD} or fewer"
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def rules_table():
|
|
604
|
+
"""The rule table as --rules prints it in JSON."""
|
|
605
|
+
return {
|
|
606
|
+
"status": "ok",
|
|
607
|
+
"added_export_threshold": ADDED_EXPORT_THRESHOLD,
|
|
608
|
+
"rules": [
|
|
609
|
+
{
|
|
610
|
+
"severity": rule["severity"],
|
|
611
|
+
"rule": rule["rule"],
|
|
612
|
+
"types": list(rule["types"]),
|
|
613
|
+
"categories": list(rule["categories"]),
|
|
614
|
+
"added_exports": _added_exports(rule),
|
|
615
|
+
}
|
|
616
|
+
for rule in RULES
|
|
617
|
+
],
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def rules_markdown():
|
|
622
|
+
"""The rule table as a Markdown table."""
|
|
623
|
+
lines = ["| Severity | Types | Categories | Rule |", "| --- | --- | --- | --- |"]
|
|
624
|
+
for rule in RULES:
|
|
625
|
+
types = ", ".join(f"`{t}`" for t in rule["types"])
|
|
626
|
+
categories = ", ".join(f"`{c}`" for c in rule["categories"])
|
|
627
|
+
lines.append(f"| {rule['severity']} | {types} | {categories} | {rule['rule']} |")
|
|
628
|
+
return "\n".join(lines)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# --------------------------------------------------------------------------
|
|
632
|
+
# CLI
|
|
633
|
+
# --------------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _load_findings(arg):
|
|
637
|
+
"""Parse FINDINGS: `-` for stdin, a JSON array given inline, or a file."""
|
|
638
|
+
if arg == "-":
|
|
639
|
+
return json.loads(sys.stdin.buffer.read().decode("utf-8"))
|
|
640
|
+
if arg.lstrip().startswith("["):
|
|
641
|
+
return json.loads(arg)
|
|
642
|
+
return json.loads(Path(arg).read_text(encoding="utf-8"))
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _build_parser():
|
|
646
|
+
# --help prints the module docstring: the findings, --from-diff and
|
|
647
|
+
# --rules contracts a calling step cites.
|
|
648
|
+
parser = argparse.ArgumentParser(
|
|
649
|
+
prog="skf-severity-classify.py",
|
|
650
|
+
description=__doc__,
|
|
651
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
652
|
+
)
|
|
653
|
+
parser.add_argument(
|
|
654
|
+
"findings",
|
|
655
|
+
nargs="?",
|
|
656
|
+
metavar="FINDINGS",
|
|
657
|
+
help="findings JSON file, - for stdin, or a JSON array",
|
|
658
|
+
)
|
|
659
|
+
parser.add_argument(
|
|
660
|
+
"--from-diff",
|
|
661
|
+
metavar="DIFF",
|
|
662
|
+
help="project a skf-structural-diff.py result file into a findings array",
|
|
663
|
+
)
|
|
664
|
+
parser.add_argument(
|
|
665
|
+
"--file-drift",
|
|
666
|
+
metavar="FILE",
|
|
667
|
+
help="also project a skf-compare-file-hashes.py compare result (skipped when FILE does not exist)",
|
|
668
|
+
)
|
|
669
|
+
parser.add_argument(
|
|
670
|
+
"--constituents",
|
|
671
|
+
metavar="FILE",
|
|
672
|
+
help="also project a compare-constituent-hashes result (skipped when FILE does not exist)",
|
|
673
|
+
)
|
|
674
|
+
parser.add_argument(
|
|
675
|
+
"--semantic",
|
|
676
|
+
metavar="FILE",
|
|
677
|
+
help="also add the semantic diff's findings array (skipped when FILE does not exist)",
|
|
678
|
+
)
|
|
679
|
+
parser.add_argument(
|
|
680
|
+
"--doc-drift",
|
|
681
|
+
metavar="FILE",
|
|
682
|
+
help="also project a skf-detect-docs.py compare-hashes result (skipped when FILE does not exist)",
|
|
683
|
+
)
|
|
684
|
+
parser.add_argument(
|
|
685
|
+
"--rules",
|
|
686
|
+
action="store_true",
|
|
687
|
+
help="print the rule table: severities and the accepted type/category pairs",
|
|
688
|
+
)
|
|
689
|
+
parser.add_argument(
|
|
690
|
+
"--format",
|
|
691
|
+
choices=["json", "markdown"],
|
|
692
|
+
help="with --rules: print JSON (the default) or a Markdown table",
|
|
693
|
+
)
|
|
694
|
+
parser.add_argument(
|
|
695
|
+
"-o",
|
|
696
|
+
"--output",
|
|
697
|
+
metavar="FILE",
|
|
698
|
+
help="write the output to FILE and print a summary line on stdout",
|
|
699
|
+
)
|
|
700
|
+
return parser
|
|
146
701
|
|
|
147
|
-
arg = sys.argv[1]
|
|
148
702
|
|
|
703
|
+
def _emit(text, output, summary):
|
|
704
|
+
"""Print text, or write it to output and print the summary line. Returns
|
|
705
|
+
an error dict when output cannot be written."""
|
|
706
|
+
if not output:
|
|
707
|
+
print(text)
|
|
708
|
+
return None
|
|
149
709
|
try:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
710
|
+
Path(output).write_text(text + "\n", encoding="utf-8")
|
|
711
|
+
except OSError as exc:
|
|
712
|
+
return _error(f"Cannot write output: {exc}")
|
|
713
|
+
print(json.dumps({"status": "ok", "output": str(output), **summary}))
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def main(argv=None):
|
|
718
|
+
parser = _build_parser()
|
|
719
|
+
args = parser.parse_args(argv)
|
|
720
|
+
paths = {name: getattr(args, dest) for name, dest, _, _ in SOURCES}
|
|
721
|
+
projecting = any(path is not None for path in paths.values())
|
|
722
|
+
modes = [args.findings is not None, projecting, args.rules]
|
|
723
|
+
if sum(modes) != 1:
|
|
724
|
+
parser.error("give exactly one of FINDINGS, the projection flags (--from-diff, --file-drift, "
|
|
725
|
+
"--constituents, --semantic, --doc-drift) or --rules")
|
|
726
|
+
if args.format and not args.rules:
|
|
727
|
+
parser.error("--format applies to --rules only")
|
|
728
|
+
|
|
729
|
+
if args.rules:
|
|
730
|
+
if args.format == "markdown":
|
|
731
|
+
text = rules_markdown()
|
|
154
732
|
else:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
733
|
+
text = json.dumps(rules_table(), indent=2)
|
|
734
|
+
failed = _emit(text, args.output, {})
|
|
735
|
+
elif projecting:
|
|
736
|
+
try:
|
|
737
|
+
findings, sources = project_sources(paths)
|
|
738
|
+
except ValueError as e:
|
|
739
|
+
print(json.dumps(_error(str(e)), indent=2))
|
|
740
|
+
return 1
|
|
741
|
+
failed = _emit(json.dumps(findings, indent=2), args.output, {
|
|
742
|
+
"findings": len(findings),
|
|
743
|
+
"needs_judgment": sum(1 for f in findings if "category_choices" in f),
|
|
744
|
+
"sources": sources,
|
|
745
|
+
})
|
|
746
|
+
else:
|
|
747
|
+
try:
|
|
748
|
+
data = _load_findings(args.findings)
|
|
749
|
+
except (OSError, UnicodeDecodeError, ValueError) as e:
|
|
750
|
+
print(json.dumps(_error(str(e)), indent=2))
|
|
751
|
+
return 1
|
|
752
|
+
result = classify_all(data)
|
|
753
|
+
if result["status"] != "ok":
|
|
754
|
+
print(json.dumps(result, indent=2))
|
|
755
|
+
return 1
|
|
756
|
+
failed = _emit(json.dumps(result, indent=2), args.output, {
|
|
757
|
+
key: result[key] for key in ("drift_score", "total_findings", "total_items", "by_severity")
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
if failed:
|
|
761
|
+
print(json.dumps(failed, indent=2))
|
|
762
|
+
return 1
|
|
763
|
+
return 0
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
if __name__ == "__main__":
|
|
767
|
+
sys.exit(main())
|