bmad-module-skill-forge 2.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +143 -18
- package/docs/verifying-a-skill.md +87 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +3 -3
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +31 -59
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +233 -64
- package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +772 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +5247 -96
- package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +700 -0
- package/src/shared/scripts/skf-hash-content.py +180 -20
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +161 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +176 -28
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +787 -121
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +120 -67
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
- package/src/shared/scripts/skf-write-skill-brief.py +406 -25
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +149 -210
- package/src/skf-audit-skill/references/re-index.md +103 -110
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +34 -25
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +80 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
- package/src/skf-brief-skill/SKILL.md +37 -26
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +18 -25
- package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +144 -127
- package/src/skf-brief-skill/references/confirm-brief.md +11 -9
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +7 -10
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
- package/src/skf-create-skill/assets/skill-sections.md +21 -11
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +119 -70
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
- package/src/skf-create-skill/references/extract.md +204 -162
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
- package/src/skf-create-skill/references/extraction-patterns.md +588 -201
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +37 -57
- package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +83 -146
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +122 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +209 -264
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +164 -91
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +164 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +42 -155
- package/src/skf-test-skill/references/source-access-protocol.md +18 -94
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +380 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
- package/src/skf-test-skill/scripts/score-signatures.py +479 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +253 -223
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +254 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +182 -49
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +102 -35
- package/src/skf-update-skill/references/write.md +357 -155
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF render drift tables: the drift report's per-item tables, from the JSON
|
|
7
|
+
audit-skill saved.
|
|
8
|
+
|
|
9
|
+
Step 3 (references/structural-diff.md section 5) and step 5
|
|
10
|
+
(references/severity-classify.md section 3) each append a section of
|
|
11
|
+
tables to the drift report, and step 6 (references/report.md section 2)
|
|
12
|
+
its Out-of-Scope New Public API table. Every cell of those tables is a
|
|
13
|
+
field of the JSON an earlier step saved, so this script prints them,
|
|
14
|
+
rather than the model copying hundreds of rows by hand, where a row can be
|
|
15
|
+
dropped or mis-matched. The model keeps only what the JSON does not hold:
|
|
16
|
+
the section heading and step 3's Comparison and Method lines.
|
|
17
|
+
|
|
18
|
+
structural DIFF [--file-drift FILE]
|
|
19
|
+
DIFF is the skf-structural-diff.py result step 3 section 1 saved
|
|
20
|
+
(structural-diff.json). Prints, in this order:
|
|
21
|
+
### Added Exports (N) Export | Type | Signature | Location | Confidence
|
|
22
|
+
### Removed Exports (N) Export | Type | Original Signature |
|
|
23
|
+
Original Location | Confidence
|
|
24
|
+
### Moved Exports (N) Export | From | To | Confidence
|
|
25
|
+
### Changed Exports (N) Export | Change Type | Before | After |
|
|
26
|
+
Location | Confidence (one row per changed
|
|
27
|
+
field; a line change reads `location`)
|
|
28
|
+
### Ambiguous Names (N) Export | Removed At | Added At (only when
|
|
29
|
+
ambiguous_names[] is non-empty)
|
|
30
|
+
### Summary Added, Removed, Moved, Changed and
|
|
31
|
+
**Total Drift Items**, their sum
|
|
32
|
+
**Signatures not compared:** (only when summary.signature_unverified
|
|
33
|
+
is above 0)
|
|
34
|
+
**Off the public surface:** (only when summary.not_public is above 0:
|
|
35
|
+
the exports of a public-api skill the
|
|
36
|
+
diff left out of Added Exports, which are
|
|
37
|
+
not drift; the first ten of not_public[]
|
|
38
|
+
as `name` (`file`), then `and N more`)
|
|
39
|
+
### By Library a stack diff (--group-by source_library):
|
|
40
|
+
each groups[] entry's counts
|
|
41
|
+
### Script/Asset Drift (added N, removed N, changed N)
|
|
42
|
+
File | Change | Detail, from FILE, the
|
|
43
|
+
skf-compare-file-hashes.py compare result
|
|
44
|
+
step 3 section 4b saved (only when FILE is
|
|
45
|
+
given and exists; when FILE cannot be read
|
|
46
|
+
or is not such a result, the heading reads
|
|
47
|
+
`### Script/Asset Drift: skipped (<reason>)`
|
|
48
|
+
and the other tables still print). When
|
|
49
|
+
FILE's `added_not_checked` names a reason
|
|
50
|
+
(no brief, or one the comparison could not
|
|
51
|
+
read), a **New files not checked:** line
|
|
52
|
+
follows the table
|
|
53
|
+
### Provenance label differences (not drift) (N)
|
|
54
|
+
Export | Baseline label | Current label
|
|
55
|
+
(only when label_changes[] is non-empty)
|
|
56
|
+
Every heading count comes from the diff's summary (FILE's stats for
|
|
57
|
+
Script/Asset Drift), never from the rows. An export of a stack reads
|
|
58
|
+
`<library>: <name>`.
|
|
59
|
+
|
|
60
|
+
severity CLASSIFICATION
|
|
61
|
+
CLASSIFICATION is the skf-severity-classify.py result step 5 section 2
|
|
62
|
+
saved (severity.json). Prints the **Overall Drift Score** line, one table
|
|
63
|
+
per severity, CRITICAL first:
|
|
64
|
+
### <SEVERITY> (by_severity[<SEVERITY>])
|
|
65
|
+
# | Finding | Type | Detail | Location |
|
|
66
|
+
Confidence
|
|
67
|
+
and the Classification Summary, whose **Total** is total_items. Type is
|
|
68
|
+
`semantic` for a semantic finding, `doc` for a changed document of a
|
|
69
|
+
docs-only skill and `structural` for every other one; a finding with no
|
|
70
|
+
confidence or file (a hash comparison) shows `n/a`.
|
|
71
|
+
|
|
72
|
+
outside-scope SNAPSHOT
|
|
73
|
+
SNAPSHOT is the extraction-snapshot.json step 2 wrote
|
|
74
|
+
(skf-extraction-snapshot.py build). Prints
|
|
75
|
+
### Out-of-Scope New Public API
|
|
76
|
+
Path | Evidence, one row per outside_scope
|
|
77
|
+
entry: `path` | exports `name`, ... through
|
|
78
|
+
`entry`, ... (no `through` part when the
|
|
79
|
+
entry names no entry point)
|
|
80
|
+
and nothing when outside_scope is empty or absent (Quick tier). The table
|
|
81
|
+
is the shape skf-provenance-gap-dispatch.py parses back for
|
|
82
|
+
update-skill's scope reconciliation, and never rolls up: the snapshot
|
|
83
|
+
already groups the names by file.
|
|
84
|
+
|
|
85
|
+
Rollup (structural and severity, mechanical):
|
|
86
|
+
10 or more rows of one table that share a file roll up into one row, so a
|
|
87
|
+
deleted or a new file reads as one row. Removed rows go one level on: 10
|
|
88
|
+
or more of the removed rows left that share a directory (never the top
|
|
89
|
+
level) roll up too, so a removed package tree reads as one row. Added
|
|
90
|
+
rows stop at the file, since a directory is no shared cause: exports
|
|
91
|
+
added across the files of one directory keep a row each.
|
|
92
|
+
Rows roll up only with rows of their own kind:
|
|
93
|
+
- Added and Removed Exports: per library. The Export cell reads
|
|
94
|
+
`N exports (rep: a, b, c, ...)`, the Location the file or directory.
|
|
95
|
+
- Script/Asset Drift: per change (added, removed, changed); a removed
|
|
96
|
+
file by directory.
|
|
97
|
+
- Provenance label differences: 10 or more rows with one baseline and
|
|
98
|
+
one current label (compared case-insensitively) roll up whatever
|
|
99
|
+
their files.
|
|
100
|
+
- Severity tables: added and removed findings and Script/Asset files,
|
|
101
|
+
per type, category and library, a removed one by directory too.
|
|
102
|
+
The Finding cell reads
|
|
103
|
+
`<type> in <file or directory> (xN; rep: a, b, c, ...)`, N counting
|
|
104
|
+
each finding's `count`, and the Detail cell the findings' shared
|
|
105
|
+
detail, else their rule.
|
|
106
|
+
Moved, Changed and Ambiguous rows, and changed-signature, moved and
|
|
107
|
+
semantic findings, never roll up. A rollup changes how a table reads,
|
|
108
|
+
never a count: every heading and summary comes from the JSON.
|
|
109
|
+
|
|
110
|
+
Output: the markdown on stdout, UTF-8.
|
|
111
|
+
|
|
112
|
+
Exit codes:
|
|
113
|
+
0 the tables were printed (for outside-scope, also when there was
|
|
114
|
+
nothing to print)
|
|
115
|
+
1 DIFF, CLASSIFICATION or SNAPSHOT is missing, is not JSON or is not
|
|
116
|
+
what the command reads ({"status": "error", "error"} on stdout, the
|
|
117
|
+
error starting with the input's path)
|
|
118
|
+
2 usage error (argparse, usage on stderr)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
from __future__ import annotations
|
|
122
|
+
|
|
123
|
+
import argparse
|
|
124
|
+
import json
|
|
125
|
+
import posixpath
|
|
126
|
+
import sys
|
|
127
|
+
from pathlib import Path
|
|
128
|
+
|
|
129
|
+
# Rows of one table that share a file or a directory roll up from this many on.
|
|
130
|
+
ROLLUP_MIN = 10
|
|
131
|
+
# Representative names a rollup row shows.
|
|
132
|
+
REP_COUNT = 3
|
|
133
|
+
# The exports the Off the public surface note names, as update-skill's not_public warning does.
|
|
134
|
+
NOT_PUBLIC_NAMED = 10
|
|
135
|
+
SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW")
|
|
136
|
+
NONE = "(none)"
|
|
137
|
+
NA = "n/a"
|
|
138
|
+
# Characters a plain table cell escapes, so text renders as written.
|
|
139
|
+
_MARKDOWN = "\\`*_[]<>|"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class RenderError(Exception):
|
|
143
|
+
"""An input the tables cannot be rendered from: exit 1."""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --------------------------------------------------------------------------
|
|
147
|
+
# Cells
|
|
148
|
+
# --------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _flat(value) -> str:
|
|
152
|
+
"""One line of text for a value; a list reads (a, b)."""
|
|
153
|
+
if isinstance(value, list):
|
|
154
|
+
value = "(" + ", ".join(str(v) for v in value) + ")"
|
|
155
|
+
return " ".join(str(value).split())
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def text(value) -> str:
|
|
159
|
+
"""A plain cell: markdown characters escaped, `n/a` for no value.
|
|
160
|
+
|
|
161
|
+
>>> text("def __init__(self) | None")
|
|
162
|
+
'def \\\\_\\\\_init\\\\_\\\\_(self) \\\\| None'
|
|
163
|
+
>>> text(None)
|
|
164
|
+
'n/a'
|
|
165
|
+
"""
|
|
166
|
+
if value is None or value == "":
|
|
167
|
+
return NA
|
|
168
|
+
return "".join("\\" + ch if ch in _MARKDOWN else ch for ch in _flat(value))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def code(value) -> str:
|
|
172
|
+
"""A code span holding the value as written, `(none)` for no value.
|
|
173
|
+
|
|
174
|
+
A pipe is escaped, which a GFM table needs even inside a code span, and
|
|
175
|
+
the fence is longer than any run of backticks in the value.
|
|
176
|
+
|
|
177
|
+
>>> code("a | b")
|
|
178
|
+
'`a \\\\| b`'
|
|
179
|
+
>>> code("x`y")
|
|
180
|
+
'`` x`y ``'
|
|
181
|
+
>>> code(None)
|
|
182
|
+
'(none)'
|
|
183
|
+
"""
|
|
184
|
+
if value is None or value == "":
|
|
185
|
+
return NONE
|
|
186
|
+
flat = _flat(value).replace("|", "\\|")
|
|
187
|
+
longest = run = 0
|
|
188
|
+
for ch in flat:
|
|
189
|
+
run = run + 1 if ch == "`" else 0
|
|
190
|
+
longest = max(longest, run)
|
|
191
|
+
fence = "`" * (longest + 1)
|
|
192
|
+
pad = " " if longest else ""
|
|
193
|
+
return f"{fence}{pad}{flat}{pad}{fence}"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def where(file, line=None) -> str:
|
|
197
|
+
"""`file:line`, `file` without a line, `n/a` without a file."""
|
|
198
|
+
if file is None or file == "":
|
|
199
|
+
return NA
|
|
200
|
+
return code(f"{file}:{line}" if line is not None else file)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def export_cell(item: dict) -> str:
|
|
204
|
+
"""An export's name, after its library for a stack."""
|
|
205
|
+
name = code(item.get("name"))
|
|
206
|
+
if item.get("source_library") is not None:
|
|
207
|
+
return f"{text(item['source_library'])}: {name}"
|
|
208
|
+
return name
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _table(header: tuple[str, ...], rows: list[list[str]]) -> str:
|
|
212
|
+
"""A markdown table, or `None.` when it has no row."""
|
|
213
|
+
if not rows:
|
|
214
|
+
return "None."
|
|
215
|
+
lines = ["| " + " | ".join(header) + " |", "|" + "|".join("-" * (len(h) + 2) for h in header) + "|"]
|
|
216
|
+
lines += ["| " + " | ".join(row) + " |" for row in rows]
|
|
217
|
+
return "\n".join(lines)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _distinct(values) -> list:
|
|
221
|
+
"""The values in first-seen order, each once."""
|
|
222
|
+
out = []
|
|
223
|
+
for value in values:
|
|
224
|
+
if value not in out:
|
|
225
|
+
out.append(value)
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _labels(values) -> str:
|
|
230
|
+
"""Confidence labels of rolled-up rows, each once."""
|
|
231
|
+
return ", ".join(text(v) for v in _distinct(values))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _rep(names: list) -> str:
|
|
235
|
+
"""The first REP_COUNT names as code spans, then `…` when there are more."""
|
|
236
|
+
shown = ", ".join(code(n) for n in names[:REP_COUNT])
|
|
237
|
+
return shown + (", …" if len(names) > REP_COUNT else "")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------
|
|
241
|
+
# Rollup
|
|
242
|
+
# --------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _directory(path) -> str | None:
|
|
246
|
+
"""A path's directory, or None at the top level."""
|
|
247
|
+
if not isinstance(path, str) or not path:
|
|
248
|
+
return None
|
|
249
|
+
parent = posixpath.dirname(path.replace("\\", "/").rstrip("/"))
|
|
250
|
+
return parent or None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def plan_rows(items: list, key, path, eligible=lambda item: True, by_directory=lambda item: True) -> list[tuple]:
|
|
254
|
+
"""The rows of one table in order: ("item", item) for a row of its own,
|
|
255
|
+
("rollup", place, members) for a rollup row at its first member's place.
|
|
256
|
+
|
|
257
|
+
Eligible items of one key that share a path roll up when ROLLUP_MIN or
|
|
258
|
+
more do; then the eligible items left that by_directory admits and that
|
|
259
|
+
share a directory, the directory read as `<dir>/`.
|
|
260
|
+
|
|
261
|
+
>>> rows = plan_rows([{"f": "a.py"}] * 10 + [{"f": "b.py"}], key=lambda i: 0, path=lambda i: i["f"])
|
|
262
|
+
>>> [(r[0], r[1] if r[0] == "rollup" else r[1]["f"]) for r in rows]
|
|
263
|
+
[('rollup', 'a.py'), ('item', 'b.py')]
|
|
264
|
+
"""
|
|
265
|
+
member_of: dict[int, int] = {}
|
|
266
|
+
groups: list[tuple[str, list[int]]] = []
|
|
267
|
+
for level in ("file", "directory"):
|
|
268
|
+
buckets: dict[tuple, list[int]] = {}
|
|
269
|
+
for index, item in enumerate(items):
|
|
270
|
+
if index in member_of or not eligible(item) or (level == "directory" and not by_directory(item)):
|
|
271
|
+
continue
|
|
272
|
+
place = path(item) if level == "file" else _directory(path(item))
|
|
273
|
+
if not isinstance(place, str) or not place:
|
|
274
|
+
continue
|
|
275
|
+
buckets.setdefault((key(item), place), []).append(index)
|
|
276
|
+
for (_, place), members in buckets.items():
|
|
277
|
+
if len(members) >= ROLLUP_MIN:
|
|
278
|
+
for index in members:
|
|
279
|
+
member_of[index] = len(groups)
|
|
280
|
+
groups.append((place if level == "file" else place + "/", members))
|
|
281
|
+
rows: list[tuple] = []
|
|
282
|
+
shown: set[int] = set()
|
|
283
|
+
for index, item in enumerate(items):
|
|
284
|
+
group = member_of.get(index)
|
|
285
|
+
if group is None:
|
|
286
|
+
rows.append(("item", item))
|
|
287
|
+
elif group not in shown:
|
|
288
|
+
shown.add(group)
|
|
289
|
+
place, members = groups[group]
|
|
290
|
+
rows.append(("rollup", place, [items[m] for m in members]))
|
|
291
|
+
return rows
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# --------------------------------------------------------------------------
|
|
295
|
+
# Inputs
|
|
296
|
+
# --------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def load_json(path: Path, what: str):
|
|
300
|
+
try:
|
|
301
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
302
|
+
except FileNotFoundError:
|
|
303
|
+
raise RenderError(f"{path}: no such file ({what})") from None
|
|
304
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
305
|
+
raise RenderError(f"{path}: cannot read {what}: {exc}") from None
|
|
306
|
+
except json.JSONDecodeError as exc:
|
|
307
|
+
raise RenderError(f"{path}: {what} is not JSON: {exc}") from None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _records(data: dict, key: str) -> list[dict]:
|
|
311
|
+
return [r for r in data.get(key) or [] if isinstance(r, dict)]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _count(summary: dict, key: str) -> int:
|
|
315
|
+
value = summary.get(key, 0)
|
|
316
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else 0
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# --------------------------------------------------------------------------
|
|
320
|
+
# structural
|
|
321
|
+
# --------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _shape(rec: dict):
|
|
325
|
+
"""An export's signature: its signature, else its params and return type."""
|
|
326
|
+
if rec.get("signature"):
|
|
327
|
+
return rec["signature"]
|
|
328
|
+
if rec.get("params") is None and not rec.get("return_type"):
|
|
329
|
+
return None
|
|
330
|
+
shape = _flat(rec.get("params") or [])
|
|
331
|
+
if rec.get("return_type"):
|
|
332
|
+
shape += f" -> {rec['return_type']}"
|
|
333
|
+
return shape
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _export_rows(records: list[dict], removed: bool) -> list[list[str]]:
|
|
337
|
+
"""Added or Removed Exports rows; only removed ones roll up by directory."""
|
|
338
|
+
rows = []
|
|
339
|
+
plan = plan_rows(records, key=lambda r: r.get("source_library"), path=lambda r: r.get("file"),
|
|
340
|
+
by_directory=lambda r: removed)
|
|
341
|
+
for row in plan:
|
|
342
|
+
if row[0] == "item":
|
|
343
|
+
rec = row[1]
|
|
344
|
+
rows.append([export_cell(rec), text(rec.get("type")), code(_shape(rec)),
|
|
345
|
+
where(rec.get("file"), rec.get("line")), text(rec.get("confidence"))])
|
|
346
|
+
continue
|
|
347
|
+
_, place, members = row
|
|
348
|
+
library = members[0].get("source_library")
|
|
349
|
+
cell = f"{len(members)} exports (rep: {_rep([m.get('name') for m in members])})"
|
|
350
|
+
if library is not None:
|
|
351
|
+
cell = f"{text(library)}: {cell}"
|
|
352
|
+
rows.append([cell, ", ".join(text(t) for t in _distinct(m.get("type") for m in members)), NA,
|
|
353
|
+
code(place), _labels(m.get("confidence") for m in members)])
|
|
354
|
+
return rows
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _label(side) -> str:
|
|
358
|
+
side = side if isinstance(side, dict) else {}
|
|
359
|
+
return " / ".join(text(side.get(k)) if side.get(k) is not None else NONE
|
|
360
|
+
for k in ("confidence", "extraction_method"))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _label_rows(records: list[dict]) -> list[list[str]]:
|
|
364
|
+
def pair(rec):
|
|
365
|
+
return tuple(_label(rec.get(side)).casefold() for side in ("baseline", "current"))
|
|
366
|
+
|
|
367
|
+
buckets: dict[tuple, list[int]] = {}
|
|
368
|
+
for index, rec in enumerate(records):
|
|
369
|
+
buckets.setdefault(pair(rec), []).append(index)
|
|
370
|
+
rolled = {members[0]: members for members in buckets.values() if len(members) >= ROLLUP_MIN}
|
|
371
|
+
skip = {i for members in rolled.values() for i in members[1:]}
|
|
372
|
+
rows = []
|
|
373
|
+
for index, rec in enumerate(records):
|
|
374
|
+
if index in skip:
|
|
375
|
+
continue
|
|
376
|
+
cell = export_cell(rec)
|
|
377
|
+
if index in rolled:
|
|
378
|
+
members = [records[i] for i in rolled[index]]
|
|
379
|
+
cell = f"{len(members)} exports (rep: {_rep([m.get('name') for m in members])})"
|
|
380
|
+
rows.append([cell, _label(rec.get("baseline")), _label(rec.get("current"))])
|
|
381
|
+
return rows
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def check_file_drift(data) -> None:
|
|
385
|
+
"""Raise RenderError unless data is a skf-compare-file-hashes.py compare result."""
|
|
386
|
+
if not isinstance(data, dict) or not all(isinstance(data.get(k), list) for k in ("added", "removed", "changed")):
|
|
387
|
+
raise RenderError("not a file drift result: expected an object with added, removed and changed lists "
|
|
388
|
+
"(the output of skf-compare-file-hashes.py compare)")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _file_drift_section(data) -> str:
|
|
392
|
+
check_file_drift(data)
|
|
393
|
+
stats = data.get("stats") if isinstance(data.get("stats"), dict) else {}
|
|
394
|
+
items = [{"path": p, "change": "added", "detail": "new file"} for p in data["added"] if isinstance(p, str)]
|
|
395
|
+
items += [{"path": p, "change": "removed", "detail": "file removed"} for p in data["removed"]
|
|
396
|
+
if isinstance(p, str)]
|
|
397
|
+
items += [{"path": c.get("path"), "change": "changed",
|
|
398
|
+
"detail": f"content {c.get('stored_hash')} -> {c.get('current_hash')}"}
|
|
399
|
+
for c in data["changed"] if isinstance(c, dict)]
|
|
400
|
+
rows = []
|
|
401
|
+
for row in plan_rows(items, key=lambda i: i["change"], path=lambda i: i["path"],
|
|
402
|
+
by_directory=lambda i: i["change"] == "removed"):
|
|
403
|
+
if row[0] == "item":
|
|
404
|
+
item = row[1]
|
|
405
|
+
rows.append([code(item["path"]), item["change"], text(item["detail"])])
|
|
406
|
+
continue
|
|
407
|
+
_, place, members = row
|
|
408
|
+
names = [posixpath.basename(str(m["path"]).replace("\\", "/")) for m in members]
|
|
409
|
+
details = _distinct(m["detail"] for m in members)
|
|
410
|
+
rows.append([f"{code(place)} ({len(members)} files, rep: {_rep(names)})", members[0]["change"],
|
|
411
|
+
text(details[0]) if len(details) == 1 else NA])
|
|
412
|
+
heading = (f"### Script/Asset Drift (added {_count(stats, 'added')}, removed {_count(stats, 'removed')}, "
|
|
413
|
+
f"changed {_count(stats, 'changed')})")
|
|
414
|
+
section = heading + "\n\n" + _table(("File", "Change", "Detail"), rows)
|
|
415
|
+
reason = data.get("added_not_checked")
|
|
416
|
+
if isinstance(reason, str) and reason.strip():
|
|
417
|
+
section += (f"\n\n**New files not checked:** {text(reason.strip())}. The tracked files were compared, but "
|
|
418
|
+
"no new script, asset or document was looked for, so `added` lists none.")
|
|
419
|
+
return section
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def render_structural(diff, file_drift=None, file_drift_skipped=None) -> str:
|
|
423
|
+
"""The Structural Drift tables of a skf-structural-diff.py result (and,
|
|
424
|
+
given one, a skf-compare-file-hashes.py compare result, or the reason
|
|
425
|
+
one could not be read, which its skipped heading shows)."""
|
|
426
|
+
buckets = ("added", "removed", "changed", "moved")
|
|
427
|
+
if not isinstance(diff, dict) or not all(isinstance(diff.get(b), list) for b in buckets) \
|
|
428
|
+
or not isinstance(diff.get("summary"), dict):
|
|
429
|
+
raise RenderError("not a structural diff: expected an object with a summary and added, removed, changed "
|
|
430
|
+
"and moved lists (the output of skf-structural-diff.py)")
|
|
431
|
+
summary = diff["summary"]
|
|
432
|
+
parts = []
|
|
433
|
+
|
|
434
|
+
parts.append(f"### Added Exports ({_count(summary, 'added')})\n\n" + _table(
|
|
435
|
+
("Export", "Type", "Signature", "Location", "Confidence"),
|
|
436
|
+
_export_rows(_records(diff, "added"), removed=False)))
|
|
437
|
+
parts.append(f"### Removed Exports ({_count(summary, 'removed')})\n\n" + _table(
|
|
438
|
+
("Export", "Type", "Original Signature", "Original Location", "Confidence"),
|
|
439
|
+
_export_rows(_records(diff, "removed"), removed=True)))
|
|
440
|
+
|
|
441
|
+
moved = [[export_cell(m), where(m.get("previous_file"), m.get("previous_line")),
|
|
442
|
+
where(m.get("current_file"), m.get("line")), text(m.get("confidence"))]
|
|
443
|
+
for m in _records(diff, "moved")]
|
|
444
|
+
parts.append(f"### Moved Exports ({_count(summary, 'moved')})\n\n"
|
|
445
|
+
+ _table(("Export", "From", "To", "Confidence"), moved))
|
|
446
|
+
|
|
447
|
+
changed = []
|
|
448
|
+
for c in _records(diff, "changed"):
|
|
449
|
+
field = c.get("field")
|
|
450
|
+
changed.append([export_cell(c), text("location" if field == "line" else field),
|
|
451
|
+
code(c.get("baseline_value")), code(c.get("current_value")),
|
|
452
|
+
where(c.get("file"), c.get("line")), text(c.get("confidence"))])
|
|
453
|
+
parts.append(f"### Changed Exports ({_count(summary, 'changed')})\n\n"
|
|
454
|
+
+ _table(("Export", "Change Type", "Before", "After", "Location", "Confidence"), changed))
|
|
455
|
+
|
|
456
|
+
ambiguous = _records(diff, "ambiguous_names")
|
|
457
|
+
if ambiguous:
|
|
458
|
+
rows = []
|
|
459
|
+
for a in ambiguous:
|
|
460
|
+
sides = [", ".join(where(s.get("file"), s.get("line")) for s in a.get(side) or [] if isinstance(s, dict))
|
|
461
|
+
or NA for side in ("removed", "added")]
|
|
462
|
+
rows.append([export_cell(a), *sides])
|
|
463
|
+
parts.append(f"### Ambiguous Names ({_count(summary, 'ambiguous_names')})\n\n"
|
|
464
|
+
+ _table(("Export", "Removed At", "Added At"), rows))
|
|
465
|
+
|
|
466
|
+
counts = [(label, _count(summary, key)) for label, key in
|
|
467
|
+
(("Added", "added"), ("Removed", "removed"), ("Moved", "moved"), ("Changed", "changed"))]
|
|
468
|
+
rows = [[label, str(n)] for label, n in counts] + [["**Total Drift Items**", str(sum(n for _, n in counts))]]
|
|
469
|
+
block = "### Summary\n\n" + _table(("Category", "Count"), rows)
|
|
470
|
+
unverified = _count(summary, "signature_unverified")
|
|
471
|
+
if unverified:
|
|
472
|
+
block += (f"\n\n**Signatures not compared:** {unverified} matched exports hold their signature in "
|
|
473
|
+
"different fields on the two sides, so a change there cannot be seen.")
|
|
474
|
+
not_public = _count(summary, "not_public")
|
|
475
|
+
if not_public:
|
|
476
|
+
one = not_public == 1
|
|
477
|
+
named = [f"{export_cell(r)} ({where(r.get('file'))})"
|
|
478
|
+
for r in _records(diff, "not_public")[:NOT_PUBLIC_NAMED]]
|
|
479
|
+
more = f" and {not_public - len(named)} more" if named and not_public > len(named) else ""
|
|
480
|
+
listed = f": {', '.join(named)}{more}" if named else ""
|
|
481
|
+
block += (f"\n\n**Off the public surface:** {not_public} {'export' if one else 'exports'} the provenance "
|
|
482
|
+
f"map does not hold {'is' if one else 'are'} off this public-api skill's public surface "
|
|
483
|
+
f"(`not_public[]` in the saved diff), so {'it is' if one else 'they are'} not reported as added "
|
|
484
|
+
f"and {'is' if one else 'are'} excluded from Total Drift Items{listed}.")
|
|
485
|
+
parts.append(block)
|
|
486
|
+
|
|
487
|
+
groups = _records(diff, "groups")
|
|
488
|
+
if groups:
|
|
489
|
+
rows = [[text(g.get("source_library")) if g.get("source_library") is not None else NONE,
|
|
490
|
+
*(str(_count(g.get("summary") or {}, k)) for k in ("added", "removed", "moved", "changed"))]
|
|
491
|
+
for g in groups]
|
|
492
|
+
parts.append("### By Library\n\n" + _table(("Library", "Added", "Removed", "Moved", "Changed"), rows))
|
|
493
|
+
|
|
494
|
+
if file_drift_skipped is not None:
|
|
495
|
+
parts.append(f"### Script/Asset Drift: skipped ({file_drift_skipped})")
|
|
496
|
+
elif file_drift is not None:
|
|
497
|
+
parts.append(_file_drift_section(file_drift))
|
|
498
|
+
|
|
499
|
+
labels = _records(diff, "label_changes")
|
|
500
|
+
if labels:
|
|
501
|
+
parts.append(f"### Provenance label differences (not drift) ({_count(summary, 'label_changes')})\n\n"
|
|
502
|
+
"These rows are informational: a label names the tool that extracted the export, so they "
|
|
503
|
+
"are excluded from Total Drift Items and are not findings.\n\n"
|
|
504
|
+
+ _table(("Export", "Baseline label", "Current label"), _label_rows(labels)))
|
|
505
|
+
return "\n\n".join(parts) + "\n"
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# --------------------------------------------------------------------------
|
|
509
|
+
# severity
|
|
510
|
+
# --------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _norm(value):
|
|
514
|
+
return value.strip().lower() if isinstance(value, str) else None
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _weight(finding: dict) -> int:
|
|
518
|
+
count = finding.get("count", 1)
|
|
519
|
+
return count if isinstance(count, int) and not isinstance(count, bool) and count > 0 else 1
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _kind(finding: dict) -> str:
|
|
523
|
+
if _norm(finding.get("type")) == "semantic":
|
|
524
|
+
return "semantic"
|
|
525
|
+
if _norm(finding.get("category")) == "doc_source":
|
|
526
|
+
return "doc"
|
|
527
|
+
return "structural"
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _rolls_up(finding: dict) -> bool:
|
|
531
|
+
"""Added and removed findings and Script/Asset files: the rows step 3 rolls up."""
|
|
532
|
+
return _norm(finding.get("type")) in ("added", "removed") or _norm(finding.get("category")) == "file"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _rep_name(finding: dict):
|
|
536
|
+
name = finding.get("name")
|
|
537
|
+
if isinstance(name, str) and name == finding.get("file"):
|
|
538
|
+
return posixpath.basename(name.replace("\\", "/")) or name
|
|
539
|
+
return name
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _severity_rows(findings: list[dict]) -> list[list[str]]:
|
|
543
|
+
plan = plan_rows(findings,
|
|
544
|
+
key=lambda f: (_norm(f.get("type")), _norm(f.get("category")), f.get("source_library")),
|
|
545
|
+
path=lambda f: f.get("file"), eligible=_rolls_up,
|
|
546
|
+
by_directory=lambda f: _norm(f.get("type")) == "removed")
|
|
547
|
+
rows = []
|
|
548
|
+
for number, row in enumerate(plan, start=1):
|
|
549
|
+
if row[0] == "item":
|
|
550
|
+
f = row[1]
|
|
551
|
+
finding = export_cell(f)
|
|
552
|
+
if _weight(f) > 1:
|
|
553
|
+
finding += f" (×{_weight(f)})"
|
|
554
|
+
rows.append([str(number), finding, _kind(f), text(f.get("detail")), where(f.get("file"), f.get("line")),
|
|
555
|
+
text(f.get("confidence"))])
|
|
556
|
+
continue
|
|
557
|
+
_, place, members = row
|
|
558
|
+
first = members[0]
|
|
559
|
+
cell = (f"{text(first.get('type'))} in {code(place)} "
|
|
560
|
+
f"(×{sum(_weight(m) for m in members)}; rep: {_rep([_rep_name(m) for m in members])})")
|
|
561
|
+
if first.get("source_library") is not None:
|
|
562
|
+
cell = f"{text(first['source_library'])}: {cell}"
|
|
563
|
+
details = _distinct(m.get("detail") for m in members)
|
|
564
|
+
detail = details[0] if len(details) == 1 else first.get("rule")
|
|
565
|
+
rows.append([str(number), cell, _kind(first), text(detail), code(place),
|
|
566
|
+
_labels(m.get("confidence") for m in members)])
|
|
567
|
+
return rows
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def render_severity(result) -> str:
|
|
571
|
+
"""The Severity Classification section of a skf-severity-classify.py result."""
|
|
572
|
+
if not isinstance(result, dict) or not isinstance(result.get("findings"), list) \
|
|
573
|
+
or not isinstance(result.get("by_severity"), dict) or "drift_score" not in result:
|
|
574
|
+
raise RenderError("not a severity classification: expected an object with drift_score, by_severity and "
|
|
575
|
+
"findings (the output of skf-severity-classify.py)")
|
|
576
|
+
by_severity = result["by_severity"]
|
|
577
|
+
findings = [f for f in result["findings"] if isinstance(f, dict)]
|
|
578
|
+
parts = [f"**Overall Drift Score: {text(result['drift_score'])}**"]
|
|
579
|
+
header = ("#", "Finding", "Type", "Detail", "Location", "Confidence")
|
|
580
|
+
for level in SEVERITIES:
|
|
581
|
+
rows = _severity_rows([f for f in findings if f.get("severity") == level])
|
|
582
|
+
parts.append(f"### {level} ({_count(by_severity, level)})\n\n" + _table(header, rows))
|
|
583
|
+
rows = [[level, str(_count(by_severity, level))] for level in SEVERITIES]
|
|
584
|
+
rows.append(["**Total**", str(_count(result, "total_items"))])
|
|
585
|
+
parts.append("### Classification Summary\n\n" + _table(("Severity", "Count"), rows))
|
|
586
|
+
return "\n\n".join(parts) + "\n"
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# --------------------------------------------------------------------------
|
|
590
|
+
# outside-scope
|
|
591
|
+
# --------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _strings(values) -> list[str]:
|
|
595
|
+
return [v for v in values if isinstance(v, str) and v] if isinstance(values, list) else []
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def render_outside_scope(snapshot) -> str:
|
|
599
|
+
"""The Out-of-Scope New Public API table of an extraction snapshot, or
|
|
600
|
+
nothing when its outside_scope is empty or absent.
|
|
601
|
+
|
|
602
|
+
>>> render_outside_scope({"exports": [], "outside_scope": [
|
|
603
|
+
... {"path": "src/b.ts", "names": ["Beta"], "entries": ["index.ts"]}]}).splitlines()[-1]
|
|
604
|
+
'| `src/b.ts` | exports `Beta` through `index.ts` |'
|
|
605
|
+
"""
|
|
606
|
+
if not isinstance(snapshot, dict) or not isinstance(snapshot.get("exports"), list) \
|
|
607
|
+
or not isinstance(snapshot.get("outside_scope", []), list):
|
|
608
|
+
raise RenderError("not an extraction snapshot: expected an object with an exports list and, when present, "
|
|
609
|
+
"an outside_scope list (the output of skf-extraction-snapshot.py build)")
|
|
610
|
+
rows = []
|
|
611
|
+
for item in _records(snapshot, "outside_scope"):
|
|
612
|
+
names = _strings(item.get("names"))
|
|
613
|
+
if not isinstance(item.get("path"), str) or not item["path"] or not names:
|
|
614
|
+
continue
|
|
615
|
+
evidence = "exports " + ", ".join(code(n) for n in names)
|
|
616
|
+
entries = _strings(item.get("entries"))
|
|
617
|
+
if entries:
|
|
618
|
+
evidence += " through " + ", ".join(code(e) for e in entries)
|
|
619
|
+
rows.append([code(item["path"]), evidence])
|
|
620
|
+
if not rows:
|
|
621
|
+
return ""
|
|
622
|
+
return "### Out-of-Scope New Public API\n\n" + _table(("Path", "Evidence"), rows) + "\n"
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
# --------------------------------------------------------------------------
|
|
626
|
+
# CLI
|
|
627
|
+
# --------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _named(path: Path, func, *args):
|
|
631
|
+
"""func(*args), a RenderError it raises starting with the input's path."""
|
|
632
|
+
try:
|
|
633
|
+
return func(*args)
|
|
634
|
+
except RenderError as exc:
|
|
635
|
+
raise RenderError(f"{path}: {exc}") from None
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _structural(args) -> str:
|
|
639
|
+
path = Path(args.diff)
|
|
640
|
+
diff = load_json(path, "the structural diff")
|
|
641
|
+
# The Script/Asset Drift check is supplementary: a saved file it cannot
|
|
642
|
+
# read skips its table, never the export tables.
|
|
643
|
+
file_drift = skipped = None
|
|
644
|
+
if args.file_drift is not None and Path(args.file_drift).is_file():
|
|
645
|
+
drift_path = Path(args.file_drift)
|
|
646
|
+
try:
|
|
647
|
+
file_drift = load_json(drift_path, "the file drift result")
|
|
648
|
+
_named(drift_path, check_file_drift, file_drift)
|
|
649
|
+
except RenderError as exc:
|
|
650
|
+
file_drift, skipped = None, str(exc)
|
|
651
|
+
return _named(path, render_structural, diff, file_drift, skipped)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _severity(args) -> str:
|
|
655
|
+
path = Path(args.classification)
|
|
656
|
+
return _named(path, render_severity, load_json(path, "the severity classification"))
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _outside_scope(args) -> str:
|
|
660
|
+
path = Path(args.snapshot)
|
|
661
|
+
return _named(path, render_outside_scope, load_json(path, "the extraction snapshot"))
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
665
|
+
parser = argparse.ArgumentParser(
|
|
666
|
+
prog="render-drift-tables.py",
|
|
667
|
+
description=__doc__,
|
|
668
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
669
|
+
)
|
|
670
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
671
|
+
structural = sub.add_parser("structural", help="Print step 3's Structural Drift tables from structural-diff.json")
|
|
672
|
+
structural.add_argument("diff", help="structural-diff.json, the skf-structural-diff.py result step 3 saved")
|
|
673
|
+
structural.add_argument("--file-drift", default=None,
|
|
674
|
+
help="file-drift.json, step 3 section 4b's comparison: its Script/Asset Drift table "
|
|
675
|
+
"(none when the file does not exist, a skipped heading when it cannot be read)")
|
|
676
|
+
structural.set_defaults(func=_structural)
|
|
677
|
+
severity = sub.add_parser("severity", help="Print step 5's Severity Classification section from severity.json")
|
|
678
|
+
severity.add_argument("classification", help="severity.json, the skf-severity-classify.py result step 5 saved")
|
|
679
|
+
severity.set_defaults(func=_severity)
|
|
680
|
+
outside = sub.add_parser("outside-scope",
|
|
681
|
+
help="Print step 6's Out-of-Scope New Public API table from extraction-snapshot.json")
|
|
682
|
+
outside.add_argument("snapshot", help="extraction-snapshot.json, the snapshot step 2 wrote (nothing is printed "
|
|
683
|
+
"when its outside_scope is empty)")
|
|
684
|
+
outside.set_defaults(func=_outside_scope)
|
|
685
|
+
return parser
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _force_utf8(*streams) -> None:
|
|
689
|
+
"""Reconfigure the output streams to UTF-8 (a Windows console uses cp1252)."""
|
|
690
|
+
for stream in streams:
|
|
691
|
+
if hasattr(stream, "reconfigure"):
|
|
692
|
+
errors = getattr(stream, "errors", None)
|
|
693
|
+
if errors is None:
|
|
694
|
+
stream.reconfigure(encoding="utf-8")
|
|
695
|
+
else:
|
|
696
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def main(argv: list[str] | None = None) -> int:
|
|
700
|
+
args = _build_parser().parse_args(argv)
|
|
701
|
+
try:
|
|
702
|
+
output = args.func(args)
|
|
703
|
+
except RenderError as exc:
|
|
704
|
+
print(json.dumps({"status": "error", "error": str(exc)}))
|
|
705
|
+
return 1
|
|
706
|
+
sys.stdout.write(output)
|
|
707
|
+
return 0
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
if __name__ == "__main__":
|
|
711
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
712
|
+
sys.exit(main())
|