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,664 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Extraction Snapshot: audit-skill's current export inventory, built by script.
|
|
6
|
+
|
|
7
|
+
audit-skill's step 2 (re-index.md) re-extracts the files the provenance map
|
|
8
|
+
covers and writes `extraction-snapshot.json`, which step 3 diffs against the
|
|
9
|
+
map. The recipe runner (skf-extract-public-api.py --mode full) does the
|
|
10
|
+
extraction; this helper gives it the files to read and turns its JSON, with
|
|
11
|
+
the exports the step read by eye, into the snapshot, so no step opens the
|
|
12
|
+
runner's JSON, copies hundreds of exports by hand or decides by eye whether
|
|
13
|
+
every file was read.
|
|
14
|
+
|
|
15
|
+
CLI:
|
|
16
|
+
|
|
17
|
+
uv run skf-extraction-snapshot.py scan-list <provenance-map> -o <files.json>
|
|
18
|
+
uv run skf-extraction-snapshot.py build --source-root <dir> --tier <tier> \\
|
|
19
|
+
--date <timestamp> [--source-path <recorded root>] \\
|
|
20
|
+
[--provenance-map <map>] [--extraction <runner.json>] \\
|
|
21
|
+
[--details <by-eye.json>]... [--scope-type <type>] \\
|
|
22
|
+
-o <extraction-snapshot.json>
|
|
23
|
+
uv run skf-extraction-snapshot.py relocate <extraction-snapshot.json> \\
|
|
24
|
+
--diff <structural-diff.json> --extraction <runner.json>
|
|
25
|
+
|
|
26
|
+
scan-list
|
|
27
|
+
|
|
28
|
+
Writes the bounded scan list, the files the provenance map covers (the
|
|
29
|
+
union of `entries[].source_file` and `file_entries[].source_file`, sorted,
|
|
30
|
+
with forward slashes: skf-load-provenance.py's `bounded_scan_files`), as a
|
|
31
|
+
JSON list of paths, the form the runner's --files-from reads. Prints
|
|
32
|
+
{"status": "ok", "output": <file>, "files": N}.
|
|
33
|
+
|
|
34
|
+
build
|
|
35
|
+
|
|
36
|
+
The files in scope are the bounded scan list with --provenance-map (every
|
|
37
|
+
audit passes it), else every file the runner or the details name. Each
|
|
38
|
+
gets one status, in this order of precedence:
|
|
39
|
+
|
|
40
|
+
missing not a file under --source-root
|
|
41
|
+
hash-tracked only `file_entries[]` names it (a script, an asset or a
|
|
42
|
+
promoted doc): step 3 compares its content hash, so
|
|
43
|
+
nothing reads it for exports
|
|
44
|
+
read-by-eye the details name it in `files` (the step read it)
|
|
45
|
+
parse-failed the runner listed it in `file_issues` (a syntax error,
|
|
46
|
+
not UTF-8, unreadable, or a language no recipe reads):
|
|
47
|
+
read it by eye
|
|
48
|
+
unread no runner result, a runner that could not run
|
|
49
|
+
(`no-ast-grep`), or an incomplete run that names it as
|
|
50
|
+
unread (`errors[].unread`) or found no export in it
|
|
51
|
+
extracted the runner read it
|
|
52
|
+
|
|
53
|
+
`complete` is true when no file is `parse-failed` or `unread`: step 2
|
|
54
|
+
reads those by eye, adds them to the details and builds again. A file
|
|
55
|
+
carries `fallback: true` when the runner ran and left it `parse-failed`
|
|
56
|
+
or `unread`: read by eye after an ast-grep failure.
|
|
57
|
+
|
|
58
|
+
Exports: the runner's exports of the files in scope (each T1, with the
|
|
59
|
+
declaration on one line as `signature`), then each export of the details
|
|
60
|
+
(`name` or `export_name`, `file` or `source_file`, `line` or
|
|
61
|
+
`source_line`, `type` or `export_type`, `signature`; T1-low and
|
|
62
|
+
`source-read` unless it says otherwise). A details export with the name
|
|
63
|
+
and file of a runner export fills only the fields that one lacks; any
|
|
64
|
+
other is added. An export of a file outside the files in scope, or of a
|
|
65
|
+
`hash-tracked` one (the map records no export there, so it would read
|
|
66
|
+
as added), is left out, with a warning. With --provenance-map, each export of a file the
|
|
67
|
+
map gives a library (`source_library_by_file`) carries it as
|
|
68
|
+
`source_library`, which step 3's `--group-by source_library` diff reads.
|
|
69
|
+
|
|
70
|
+
Public surface (--scope-type public-api, in any case). Each export of a
|
|
71
|
+
language family whose entry points are a barrel is marked `public`, true
|
|
72
|
+
or false, by skf-extraction-inventory.py's public-surface rule
|
|
73
|
+
(`public_surface` over the runner's JSON, with the scope type this flag
|
|
74
|
+
gives, and `public(name, file, language)`, the language of the runner's
|
|
75
|
+
export of that name and file, else the file's extension): the rule
|
|
76
|
+
create-skill keeps a public-api skill's provenance map to, and
|
|
77
|
+
update-skill's change manifest too. Every export stays in the snapshot,
|
|
78
|
+
so a map entry off the surface is never reported removed; step 3's diff
|
|
79
|
+
lists an export marked false that pairs with no map entry under
|
|
80
|
+
`not_public[]`, not `added[]`. With no surface the rule can read (no
|
|
81
|
+
runner JSON, as at Quick tier, a runner that could not run, an
|
|
82
|
+
incomplete run, one with errors, or an entry point the runner could not
|
|
83
|
+
trace) nothing is marked, and `public_surface.problem` says why. Any
|
|
84
|
+
other scope type, and no flag, mark nothing and give `public_surface`
|
|
85
|
+
null; only --scope-type public-api loads skf-extraction-inventory.py,
|
|
86
|
+
from beside this script.
|
|
87
|
+
|
|
88
|
+
Details file: {"files": [paths read by eye], "exports": [export objects]}
|
|
89
|
+
(either key may be absent). Give --details once per file: one per worker.
|
|
90
|
+
|
|
91
|
+
Output (-o), the snapshot step 3 reads:
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
"extraction_date": "<--date>",
|
|
95
|
+
"confidence_tier": "<--tier>",
|
|
96
|
+
"source_root": "<--source-path, else --source-root>",
|
|
97
|
+
"bounded_scan": true | false,
|
|
98
|
+
"bounded_scan_source": "provenance-map" | "source-tree-fallback",
|
|
99
|
+
"runner_status": "ok" | "incomplete" | "no-ast-grep" | null,
|
|
100
|
+
"runner_errors": [the runner's errors[]],
|
|
101
|
+
"files_scanned": N,
|
|
102
|
+
"complete": bool,
|
|
103
|
+
"files": [{"file", "status", "issue": <the runner's issue> | null,
|
|
104
|
+
"fallback": bool, "exports": N}, ...],
|
|
105
|
+
"files_by_status": {"extracted": N, "read-by-eye": N,
|
|
106
|
+
"hash-tracked": N, "missing": N,
|
|
107
|
+
"parse-failed": N, "unread": N},
|
|
108
|
+
"ast_fallback_files": ["<file>", ...],
|
|
109
|
+
"extraction_gaps": [{"name", "file", "line", "entry",
|
|
110
|
+
"export_type"?}, ...],
|
|
111
|
+
"truncated": bool, # a recipe hit the runner's head cap
|
|
112
|
+
"cap_hits": ["<recipe id>", ...],
|
|
113
|
+
"outside_scope": [{"path": "<file>", "names": [...],
|
|
114
|
+
"entries": [...]}, ...],
|
|
115
|
+
"counts": {"exports": N, "by_type": {"<type>": N, ...},
|
|
116
|
+
"t1": N, "t1_low": N},
|
|
117
|
+
"exports": [{"name", "type", "signature", "file", "line",
|
|
118
|
+
"confidence", "extraction_method", "ast_node_type",
|
|
119
|
+
"source_library"?, "public"?}, ...],
|
|
120
|
+
"public_surface": {"problem": "<why no surface was read>" | null,
|
|
121
|
+
"marked_not_public": N} | null,
|
|
122
|
+
"relocations": [{"name", "file", "line"}, ...], # relocate's
|
|
123
|
+
"warnings": ["..."]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
`ast_fallback_files` names the files read after an ast-grep failure (the
|
|
127
|
+
`fallback` ones) when the runner ran; when it did not, it is
|
|
128
|
+
["all files (ast-grep unavailable)"], and [] at the Quick tier.
|
|
129
|
+
`extraction_gaps` lists the names a package entry point exports that no
|
|
130
|
+
recipe found (the runner's `entry_point_diff.extraction_gaps`), less the
|
|
131
|
+
ones an export of the snapshot now holds, then, with --provenance-map,
|
|
132
|
+
the baseline gaps: each entry of the map the snapshot lacks whose file
|
|
133
|
+
still declares it (the runner leaves out an underscore name such as
|
|
134
|
+
`__version__`, and a module), which would otherwise read as removed. A
|
|
135
|
+
baseline gap gives the line that declares the name, `entry` null and,
|
|
136
|
+
when the entry has one, its `export_type`. It is checked only for an
|
|
137
|
+
entry whose file is `extracted` or `read-by-eye`, whose name is not
|
|
138
|
+
dotted, and that the snapshot holds neither under its name nor under
|
|
139
|
+
its `reexported_as` target at that file. The selection and the
|
|
140
|
+
declaration are skf-verify-provenance-completeness.py's (`baseline_gaps`,
|
|
141
|
+
by `declared_line`), loaded from beside this script, the rule
|
|
142
|
+
update-skill's Category B applies through skf-build-change-manifest.py
|
|
143
|
+
baseline-gaps: a line at column 0 of a Python or TS/JS file that the
|
|
144
|
+
definition-line rules match with imports left out (an alias that
|
|
145
|
+
renames counts; a plain or star import and a re-export never do),
|
|
146
|
+
and for a `module` or `package` entry the module or package so named
|
|
147
|
+
(at its line 1). An entry no rule can check (its file neither Python
|
|
148
|
+
nor TS/JS, a file the lookup does not read, such as a test file or one
|
|
149
|
+
with a symlink on its path, or a dotted `module` or `package` name) is
|
|
150
|
+
named in one warning, as `name (file)`. Each name and file is listed
|
|
151
|
+
once, and each file is read once. Read them by eye.
|
|
152
|
+
`outside_scope` groups the runner's `entry_point_diff.outside_scope` by
|
|
153
|
+
the file that defines each name: public API a package entry point exports
|
|
154
|
+
from a file the skill does not cover, which the drift report lists under
|
|
155
|
+
Out-of-Scope New Public API for update-skill's scope reconciliation.
|
|
156
|
+
|
|
157
|
+
Prints {"status": "ok", "output", "runner_status", "complete",
|
|
158
|
+
"to_read": [{"file", "status", "issue"}, ...] (the `parse-failed` and
|
|
159
|
+
`unread` files), "extraction_gaps", "ast_fallback_files",
|
|
160
|
+
"files_by_status", "counts", "public_surface"} on stdout: what step 2
|
|
161
|
+
acts on, so it never opens the runner's JSON.
|
|
162
|
+
|
|
163
|
+
relocate
|
|
164
|
+
|
|
165
|
+
Step 3's relocation check: --extraction is the runner's JSON over the
|
|
166
|
+
files that hold a name the diff reports removed, outside the bounded scan
|
|
167
|
+
list. Each export the runner found there under a name of the diff's
|
|
168
|
+
`removed[]`, in a file outside the snapshot's files in scope, joins the
|
|
169
|
+
snapshot's exports (T1, with the removed entry's `source_library` when it
|
|
170
|
+
has one) and `relocations`; the counts are recounted and the snapshot is
|
|
171
|
+
written back in place. Prints {"status": "ok", "output", "added": N,
|
|
172
|
+
"names": [...]}; added 0 changes nothing.
|
|
173
|
+
|
|
174
|
+
Exit codes: 0 the file was written (or relocate found nothing to add); 2
|
|
175
|
+
an input that cannot be read or parsed, a helper beside this script that
|
|
176
|
+
cannot be loaded, or an output that cannot be written ({"status":
|
|
177
|
+
"error", "error"} on stdout), or a usage error (argparse, on stderr).
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
from __future__ import annotations
|
|
181
|
+
|
|
182
|
+
import argparse
|
|
183
|
+
import importlib.util
|
|
184
|
+
import json
|
|
185
|
+
import sys
|
|
186
|
+
from collections import Counter
|
|
187
|
+
from pathlib import Path
|
|
188
|
+
|
|
189
|
+
STATUSES = ("extracted", "read-by-eye", "hash-tracked", "missing", "parse-failed", "unread")
|
|
190
|
+
TO_READ = ("parse-failed", "unread")
|
|
191
|
+
RUNNER_READ = frozenset({"ok", "incomplete"})
|
|
192
|
+
# The files whose provenance entries a baseline gap checks: the ones read.
|
|
193
|
+
BASELINE_GAP_STATUSES = ("extracted", "read-by-eye")
|
|
194
|
+
AST_GREP_UNAVAILABLE = "all files (ast-grep unavailable)"
|
|
195
|
+
# The scope type whose exports are marked by the public-surface rule.
|
|
196
|
+
PUBLIC_API = "public-api"
|
|
197
|
+
EXIT_ERROR = 2
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class SnapshotError(Exception):
|
|
201
|
+
"""An input or output the helper cannot use (exit 2)."""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
_SIBLINGS: dict[str, object] = {}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _sibling(filename: str, *needs: str):
|
|
208
|
+
"""A script installed beside this one, loaded once. Any failure to load
|
|
209
|
+
it, or a script without each name `needs` lists, is a SnapshotError
|
|
210
|
+
(exit 2)."""
|
|
211
|
+
if filename not in _SIBLINGS:
|
|
212
|
+
path = Path(__file__).resolve().parent / filename
|
|
213
|
+
try:
|
|
214
|
+
spec = importlib.util.spec_from_file_location(filename[:-3].replace("-", "_"), path)
|
|
215
|
+
if spec is None or spec.loader is None:
|
|
216
|
+
raise ImportError(f"no loader for {path}")
|
|
217
|
+
module = importlib.util.module_from_spec(spec)
|
|
218
|
+
spec.loader.exec_module(module)
|
|
219
|
+
except Exception as exc: # whatever the script raises, the build reports it
|
|
220
|
+
raise SnapshotError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
|
|
221
|
+
missing = [need for need in needs if getattr(module, need, None) is None]
|
|
222
|
+
if missing:
|
|
223
|
+
raise SnapshotError(f"cannot load {filename} beside {Path(__file__).name}: "
|
|
224
|
+
f"it has no {', '.join(missing)}")
|
|
225
|
+
_SIBLINGS[filename] = module
|
|
226
|
+
return _SIBLINGS[filename]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _provenance():
|
|
230
|
+
"""skf-load-provenance.py, for the scan list and the libraries (the
|
|
231
|
+
verifier's `baseline_gaps` reads the re-export map itself)."""
|
|
232
|
+
return _sibling("skf-load-provenance.py", "bounded_scan_files", "source_library_by_file")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _verifier():
|
|
236
|
+
"""skf-verify-provenance-completeness.py, for its baseline gaps
|
|
237
|
+
(`baseline_gaps`, by its declaration rule `declared_line`)."""
|
|
238
|
+
return _sibling("skf-verify-provenance-completeness.py", "baseline_gaps", "declared_line")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _public_api(scope_type: str | None) -> bool:
|
|
242
|
+
return isinstance(scope_type, str) and scope_type.strip().lower() == PUBLIC_API
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _public_surface(runner: dict, scope_type: str):
|
|
246
|
+
"""(surface, problem) by skf-extraction-inventory.py's `public_surface` over the runner's JSON with the scope
|
|
247
|
+
type --scope-type gives (an empty one when no runner ran, so the rule names its own problem)."""
|
|
248
|
+
scope = runner.get("scope") if isinstance(runner.get("scope"), dict) else {}
|
|
249
|
+
extraction = {**runner, "scope": {**scope, "type": scope_type}}
|
|
250
|
+
return _sibling("skf-extraction-inventory.py", "public_surface").public_surface(extraction)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _mark_public(exports: list[dict], runner: dict, scope_type: str | None) -> dict | None:
|
|
254
|
+
"""Mark each export `public` by the public-surface rule for a public-api scope type; the snapshot's
|
|
255
|
+
`public_surface` (None for any other scope type)."""
|
|
256
|
+
if not _public_api(scope_type):
|
|
257
|
+
return None
|
|
258
|
+
surface, problem = _public_surface(runner, scope_type)
|
|
259
|
+
marked_not_public = 0
|
|
260
|
+
if surface is not None:
|
|
261
|
+
languages = {(e.get("export_name"), _posix(str(e.get("source_file") or ""))): e.get("language")
|
|
262
|
+
for e in runner.get("exports") or [] if isinstance(e, dict)}
|
|
263
|
+
for record in exports:
|
|
264
|
+
mark = surface.public(record["name"], record["file"], languages.get((record["name"], record["file"])))
|
|
265
|
+
if mark is not None:
|
|
266
|
+
record["public"] = mark
|
|
267
|
+
if mark is False:
|
|
268
|
+
marked_not_public += 1
|
|
269
|
+
return {"problem": problem, "marked_not_public": marked_not_public}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _posix(path: str) -> str:
|
|
273
|
+
"""A path as the scan list writes it: forward slashes, no leading `./`."""
|
|
274
|
+
rel = path.strip().replace("\\", "/")
|
|
275
|
+
while rel.startswith("./"):
|
|
276
|
+
rel = rel[2:]
|
|
277
|
+
return rel
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _read_json(path: Path, what: str) -> object:
|
|
281
|
+
try:
|
|
282
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
283
|
+
except OSError as exc:
|
|
284
|
+
raise SnapshotError(f"cannot read {what} {path}: {exc.strerror or exc}") from exc
|
|
285
|
+
except (UnicodeDecodeError, ValueError) as exc:
|
|
286
|
+
raise SnapshotError(f"{what} {path} is not JSON: {exc}") from exc
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _read_object(path: Path, what: str) -> dict:
|
|
290
|
+
data = _read_json(path, what)
|
|
291
|
+
if not isinstance(data, dict):
|
|
292
|
+
raise SnapshotError(f"{what} {path} must be a JSON object")
|
|
293
|
+
return data
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _write(path: Path, data: object) -> None:
|
|
297
|
+
try:
|
|
298
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
299
|
+
except OSError as exc:
|
|
300
|
+
raise SnapshotError(f"cannot write {path}: {exc.strerror or exc}") from exc
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _first(entry: dict, *keys: str) -> object:
|
|
304
|
+
for key in keys:
|
|
305
|
+
if entry.get(key) is not None:
|
|
306
|
+
return entry[key]
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# --------------------------------------------------------------------------
|
|
311
|
+
# Building the snapshot
|
|
312
|
+
# --------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _runner_export(export: dict) -> dict:
|
|
316
|
+
return {
|
|
317
|
+
"name": export.get("export_name"),
|
|
318
|
+
"type": export.get("export_type"),
|
|
319
|
+
"signature": _first(export, "signature", "signature_line"),
|
|
320
|
+
"file": _posix(str(export.get("source_file") or "")),
|
|
321
|
+
"line": export.get("source_line"),
|
|
322
|
+
"confidence": export.get("confidence") or "T1",
|
|
323
|
+
"extraction_method": export.get("extraction_method") or "ast-grep",
|
|
324
|
+
"ast_node_type": export.get("ast_node_type"),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _details_export(export: dict) -> dict | None:
|
|
329
|
+
name, file = _first(export, "name", "export_name"), _first(export, "file", "source_file")
|
|
330
|
+
if not isinstance(name, str) or not name.strip() or not isinstance(file, str) or not file.strip():
|
|
331
|
+
return None
|
|
332
|
+
return {
|
|
333
|
+
"name": name.strip(),
|
|
334
|
+
"type": _first(export, "type", "export_type"),
|
|
335
|
+
"signature": export.get("signature"),
|
|
336
|
+
"file": _posix(file),
|
|
337
|
+
"line": _first(export, "line", "source_line"),
|
|
338
|
+
"confidence": export.get("confidence") or "T1-low",
|
|
339
|
+
"extraction_method": export.get("extraction_method") or "source-read",
|
|
340
|
+
"ast_node_type": export.get("ast_node_type"),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _details(path: Path) -> tuple[list[str], list[dict]]:
|
|
345
|
+
data = _read_object(path, "details file")
|
|
346
|
+
files, exports = data.get("files", []), data.get("exports", [])
|
|
347
|
+
if not isinstance(files, list) or not all(isinstance(f, str) for f in files):
|
|
348
|
+
raise SnapshotError(f"details file {path}: `files` must be a list of paths")
|
|
349
|
+
if not isinstance(exports, list) or not all(isinstance(e, dict) for e in exports):
|
|
350
|
+
raise SnapshotError(f"details file {path}: `exports` must be a list of objects")
|
|
351
|
+
return [_posix(f) for f in files], exports
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _entry_point_diff(runner: dict) -> dict:
|
|
355
|
+
diff = runner.get("entry_point_diff")
|
|
356
|
+
return diff if isinstance(diff, dict) else {}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _outside_scope(runner: dict) -> list[dict]:
|
|
360
|
+
grouped: dict[str, dict] = {}
|
|
361
|
+
for item in _entry_point_diff(runner).get("outside_scope") or []:
|
|
362
|
+
if not isinstance(item, dict) or not isinstance(item.get("file"), str) or not item.get("name"):
|
|
363
|
+
continue
|
|
364
|
+
group = grouped.setdefault(_posix(item["file"]), {"names": [], "entries": []})
|
|
365
|
+
if item["name"] not in group["names"]:
|
|
366
|
+
group["names"].append(item["name"])
|
|
367
|
+
entry = item.get("entry")
|
|
368
|
+
if isinstance(entry, str) and entry not in group["entries"]:
|
|
369
|
+
group["entries"].append(entry)
|
|
370
|
+
return [{"path": path, "names": sorted(g["names"]), "entries": sorted(g["entries"])}
|
|
371
|
+
for path, g in sorted(grouped.items())]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _extraction_gaps(runner: dict, exports: list[dict]) -> list[dict]:
|
|
375
|
+
"""The runner's extraction gaps no export of the snapshot holds yet, one
|
|
376
|
+
per name and file."""
|
|
377
|
+
held = {(e["name"], e["file"]) for e in exports}
|
|
378
|
+
names = {e["name"] for e in exports}
|
|
379
|
+
gaps: dict[tuple, dict] = {}
|
|
380
|
+
for item in _entry_point_diff(runner).get("extraction_gaps") or []:
|
|
381
|
+
if not isinstance(item, dict) or not isinstance(item.get("name"), str) or not item["name"]:
|
|
382
|
+
continue
|
|
383
|
+
file = _posix(item["file"]) if isinstance(item.get("file"), str) else None
|
|
384
|
+
if (item["name"], file) in held or (file is None and item["name"] in names):
|
|
385
|
+
continue
|
|
386
|
+
gaps.setdefault((item["name"], file),
|
|
387
|
+
{"name": item["name"], "file": file, "line": item.get("line"), "entry": item.get("entry")})
|
|
388
|
+
return list(gaps.values())
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _baseline_gaps(provenance: dict, source_root: Path, statuses: dict[str, str], exports: list[dict],
|
|
392
|
+
listed: set[tuple]) -> tuple[list[dict], list[str]]:
|
|
393
|
+
"""The provenance entries the snapshot lacks whose cited file still
|
|
394
|
+
declares them, and the entries no rule can check, by the verifier's
|
|
395
|
+
`baseline_gaps` (the selection and the declaration rule both live
|
|
396
|
+
there, beside `declared_line`, for update-skill's `baseline-gaps` too):
|
|
397
|
+
the entries of the files `extracted` or `read-by-eye`, against the
|
|
398
|
+
snapshot's exports, less the gaps `listed` already holds."""
|
|
399
|
+
files = {file for file, status in statuses.items() if status in BASELINE_GAP_STATUSES}
|
|
400
|
+
held = {(e["name"], e["file"]) for e in exports}
|
|
401
|
+
return _verifier().baseline_gaps(provenance, source_root, held, files, listed)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _ordered(exports) -> list[dict]:
|
|
405
|
+
return sorted(exports, key=lambda e: (e["file"], e["line"] if isinstance(e["line"], int) else 0, e["name"]))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _counts(exports: list[dict]) -> dict:
|
|
409
|
+
by_type = Counter(str(e["type"]) for e in exports if e.get("type") is not None)
|
|
410
|
+
t1 = sum(1 for e in exports if e.get("confidence") == "T1")
|
|
411
|
+
return {"exports": len(exports), "by_type": dict(sorted(by_type.items())), "t1": t1,
|
|
412
|
+
"t1_low": len(exports) - t1}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def build(source_root: Path, tier: str, date: str, provenance: dict | None, runner: dict | None,
|
|
416
|
+
details_files: list[str], details_exports: list[dict], root_text: str | None = None,
|
|
417
|
+
scope_type: str | None = None) -> dict:
|
|
418
|
+
"""The snapshot (see the module docstring)."""
|
|
419
|
+
warnings: list[str] = []
|
|
420
|
+
runner = runner or {}
|
|
421
|
+
runner_status = runner.get("status") if runner else None
|
|
422
|
+
ran = runner_status in RUNNER_READ
|
|
423
|
+
issues = {_posix(i["file"]): i.get("issue") for i in runner.get("file_issues") or []
|
|
424
|
+
if isinstance(i, dict) and isinstance(i.get("file"), str)}
|
|
425
|
+
unread = {_posix(f) for e in runner.get("errors") or [] if isinstance(e, dict)
|
|
426
|
+
for f in e.get("unread") or [] if isinstance(f, str)}
|
|
427
|
+
runner_exports = [_runner_export(e) for e in runner.get("exports") or []
|
|
428
|
+
if isinstance(e, dict) and e.get("export_name")] if ran else []
|
|
429
|
+
extra = [x for x in (_details_export(e) for e in details_exports) if x is not None]
|
|
430
|
+
if len(extra) < len(details_exports):
|
|
431
|
+
warnings.append(f"{len(details_exports) - len(extra)} details export(s) name no export or no file: "
|
|
432
|
+
"left out")
|
|
433
|
+
|
|
434
|
+
if provenance is not None:
|
|
435
|
+
scope = list(_provenance().bounded_scan_files(provenance))
|
|
436
|
+
# the files a code entry names; any other is a file_entries[] row
|
|
437
|
+
code_files = set(_provenance().bounded_scan_files({"entries": provenance.get("entries")}))
|
|
438
|
+
libraries = _provenance().source_library_by_file(provenance)
|
|
439
|
+
else:
|
|
440
|
+
named = [e["file"] for e in runner_exports + extra] + list(issues) + list(unread) + details_files
|
|
441
|
+
scope = sorted(set(f for f in named if f))
|
|
442
|
+
code_files = set(scope)
|
|
443
|
+
libraries = {}
|
|
444
|
+
in_scope = set(scope)
|
|
445
|
+
|
|
446
|
+
exports: dict[tuple[str, str], dict] = {}
|
|
447
|
+
hashed = Counter()
|
|
448
|
+
for record in runner_exports + extra:
|
|
449
|
+
if record["file"] not in in_scope:
|
|
450
|
+
warnings.append(f"{record['name']} in {record['file']}: the file is outside the files in scope, "
|
|
451
|
+
"left out")
|
|
452
|
+
continue
|
|
453
|
+
if record["file"] not in code_files:
|
|
454
|
+
hashed[record["file"]] += 1
|
|
455
|
+
continue
|
|
456
|
+
key = (record["name"], record["file"])
|
|
457
|
+
held = exports.get(key)
|
|
458
|
+
if held is None:
|
|
459
|
+
exports[key] = record
|
|
460
|
+
else:
|
|
461
|
+
for field, value in record.items():
|
|
462
|
+
if held.get(field) is None and value is not None:
|
|
463
|
+
held[field] = value
|
|
464
|
+
warnings += [f"{count} export(s) in {file}: only file_entries[] names the file, which step 3 compares "
|
|
465
|
+
"by hash, left out" for file, count in sorted(hashed.items())]
|
|
466
|
+
per_file = Counter(file for _, file in exports)
|
|
467
|
+
|
|
468
|
+
files = []
|
|
469
|
+
read_by_eye = set(details_files)
|
|
470
|
+
for rel in scope:
|
|
471
|
+
issue = issues.get(rel)
|
|
472
|
+
if not (source_root / rel).is_file():
|
|
473
|
+
runner_said = "missing"
|
|
474
|
+
elif rel not in code_files:
|
|
475
|
+
runner_said = "hash-tracked"
|
|
476
|
+
elif ran and issue is not None and issue != "missing":
|
|
477
|
+
runner_said = "parse-failed"
|
|
478
|
+
elif not ran or (runner_status == "incomplete" and (rel in unread or not per_file[rel])):
|
|
479
|
+
runner_said = "unread"
|
|
480
|
+
else:
|
|
481
|
+
runner_said = "extracted"
|
|
482
|
+
by_eye = rel in read_by_eye and runner_said not in ("missing", "hash-tracked")
|
|
483
|
+
status = "read-by-eye" if by_eye else runner_said
|
|
484
|
+
files.append({"file": rel, "status": status,
|
|
485
|
+
"issue": issue if runner_said in ("parse-failed", "hash-tracked") else None,
|
|
486
|
+
"fallback": ran and runner_said in TO_READ, "exports": per_file[rel]})
|
|
487
|
+
|
|
488
|
+
ordered = _ordered(exports.values())
|
|
489
|
+
surface_state = _mark_public(ordered, runner, scope_type)
|
|
490
|
+
for record in ordered:
|
|
491
|
+
library = libraries.get(record["file"])
|
|
492
|
+
if library is not None:
|
|
493
|
+
record["source_library"] = library
|
|
494
|
+
by_status = Counter(f["status"] for f in files)
|
|
495
|
+
recipes = [r for r in runner.get("recipes") or [] if isinstance(r, dict)]
|
|
496
|
+
cap_hits = [r.get("id") for r in recipes if r.get("truncated")]
|
|
497
|
+
if ran:
|
|
498
|
+
fallback_files = [f["file"] for f in files if f["fallback"]]
|
|
499
|
+
else:
|
|
500
|
+
fallback_files = [] if tier.strip().lower() == "quick" else [AST_GREP_UNAVAILABLE]
|
|
501
|
+
gaps = _extraction_gaps(runner, ordered) if ran else []
|
|
502
|
+
if provenance is not None:
|
|
503
|
+
baseline, unchecked = _baseline_gaps(provenance, source_root, {f["file"]: f["status"] for f in files},
|
|
504
|
+
ordered, {(g["name"], g["file"]) for g in gaps})
|
|
505
|
+
gaps += baseline
|
|
506
|
+
if unchecked:
|
|
507
|
+
warnings.append(f"{len(unchecked)} map entr{'y' if len(unchecked) == 1 else 'ies'} the snapshot "
|
|
508
|
+
f"lacks that no rule can check (in a file neither Python nor TS/JS, a file the "
|
|
509
|
+
f"declaration lookup does not read, or a dotted module name), so step 3 may report "
|
|
510
|
+
f"{'it' if len(unchecked) == 1 else 'them'} removed: "
|
|
511
|
+
+ ", ".join(f"{u['name']} ({u['file']})" for u in unchecked))
|
|
512
|
+
return {
|
|
513
|
+
"extraction_date": date,
|
|
514
|
+
"confidence_tier": tier,
|
|
515
|
+
"source_root": root_text if root_text is not None else str(source_root),
|
|
516
|
+
"bounded_scan": provenance is not None,
|
|
517
|
+
"bounded_scan_source": "provenance-map" if provenance is not None else "source-tree-fallback",
|
|
518
|
+
"runner_status": runner_status,
|
|
519
|
+
"runner_errors": [e for e in runner.get("errors") or [] if isinstance(e, dict)],
|
|
520
|
+
"files_scanned": len(scope),
|
|
521
|
+
"complete": not any(by_status[status] for status in TO_READ),
|
|
522
|
+
"files": files,
|
|
523
|
+
"files_by_status": {status: by_status[status] for status in STATUSES},
|
|
524
|
+
"ast_fallback_files": fallback_files,
|
|
525
|
+
"extraction_gaps": gaps,
|
|
526
|
+
"truncated": bool(runner.get("truncated")) or bool(cap_hits),
|
|
527
|
+
"cap_hits": cap_hits,
|
|
528
|
+
"outside_scope": _outside_scope(runner) if ran else [],
|
|
529
|
+
"counts": _counts(ordered),
|
|
530
|
+
"exports": ordered,
|
|
531
|
+
"public_surface": surface_state,
|
|
532
|
+
"relocations": [],
|
|
533
|
+
"warnings": warnings,
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def relocate(snapshot: dict, diff: dict, runner: dict) -> list[dict]:
|
|
538
|
+
"""Add to `snapshot` each export `runner` found under a removed name of
|
|
539
|
+
`diff` in a file outside the snapshot's scope; the exports added."""
|
|
540
|
+
removed: dict[str, dict] = {}
|
|
541
|
+
for item in diff.get("removed") or []:
|
|
542
|
+
if isinstance(item, dict) and isinstance(item.get("name"), str):
|
|
543
|
+
removed.setdefault(item["name"], item)
|
|
544
|
+
exports = snapshot.get("exports")
|
|
545
|
+
if not isinstance(exports, list) or not all(isinstance(e, dict) for e in exports):
|
|
546
|
+
raise SnapshotError("the snapshot's `exports` must be a list of objects")
|
|
547
|
+
scope = {f.get("file") for f in snapshot.get("files") or [] if isinstance(f, dict)}
|
|
548
|
+
held = {(e.get("name"), e.get("file")) for e in exports}
|
|
549
|
+
added = []
|
|
550
|
+
if runner.get("status") in RUNNER_READ:
|
|
551
|
+
for export in runner.get("exports") or []:
|
|
552
|
+
if not isinstance(export, dict) or export.get("export_name") not in removed:
|
|
553
|
+
continue
|
|
554
|
+
record = _runner_export(export)
|
|
555
|
+
if record["file"] in scope or (record["name"], record["file"]) in held:
|
|
556
|
+
continue
|
|
557
|
+
library = removed[record["name"]].get("source_library")
|
|
558
|
+
if isinstance(library, str):
|
|
559
|
+
record["source_library"] = library
|
|
560
|
+
held.add((record["name"], record["file"]))
|
|
561
|
+
added.append(record)
|
|
562
|
+
if added:
|
|
563
|
+
snapshot["exports"] = _ordered(exports + added)
|
|
564
|
+
snapshot["counts"] = _counts(snapshot["exports"])
|
|
565
|
+
snapshot["relocations"] = list(snapshot.get("relocations") or []) + [
|
|
566
|
+
{"name": e["name"], "file": e["file"], "line": e["line"]} for e in added]
|
|
567
|
+
return added
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# --------------------------------------------------------------------------
|
|
571
|
+
# CLI
|
|
572
|
+
# --------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _cmd_scan_list(args: argparse.Namespace) -> dict:
|
|
576
|
+
provenance = _read_object(Path(args.provenance_map), "provenance map")
|
|
577
|
+
files = list(_provenance().bounded_scan_files(provenance))
|
|
578
|
+
_write(Path(args.output), files)
|
|
579
|
+
return {"status": "ok", "output": args.output, "files": len(files)}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _cmd_build(args: argparse.Namespace) -> dict:
|
|
583
|
+
root = Path(args.source_root)
|
|
584
|
+
if not root.is_dir():
|
|
585
|
+
raise SnapshotError(f"source root not found: {args.source_root}")
|
|
586
|
+
provenance = _read_object(Path(args.provenance_map), "provenance map") if args.provenance_map else None
|
|
587
|
+
runner = _read_object(Path(args.extraction), "runner result") if args.extraction else None
|
|
588
|
+
details_files: list[str] = []
|
|
589
|
+
details_exports: list[dict] = []
|
|
590
|
+
for path in args.details or []:
|
|
591
|
+
files, exports = _details(Path(path))
|
|
592
|
+
details_files += files
|
|
593
|
+
details_exports += exports
|
|
594
|
+
snapshot = build(root, args.tier, args.date, provenance, runner, details_files, details_exports,
|
|
595
|
+
root_text=args.source_path or args.source_root, scope_type=args.scope_type)
|
|
596
|
+
_write(Path(args.output), snapshot)
|
|
597
|
+
return {"status": "ok", "output": args.output, "runner_status": snapshot["runner_status"],
|
|
598
|
+
"complete": snapshot["complete"],
|
|
599
|
+
"to_read": [{"file": f["file"], "status": f["status"], "issue": f["issue"]}
|
|
600
|
+
for f in snapshot["files"] if f["status"] in TO_READ],
|
|
601
|
+
"extraction_gaps": snapshot["extraction_gaps"], "ast_fallback_files": snapshot["ast_fallback_files"],
|
|
602
|
+
"files_by_status": snapshot["files_by_status"], "counts": snapshot["counts"],
|
|
603
|
+
"public_surface": snapshot["public_surface"]}
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _cmd_relocate(args: argparse.Namespace) -> dict:
|
|
607
|
+
path = Path(args.snapshot)
|
|
608
|
+
snapshot = _read_object(path, "snapshot")
|
|
609
|
+
added = relocate(snapshot, _read_object(Path(args.diff), "structural diff"),
|
|
610
|
+
_read_object(Path(args.extraction), "runner result"))
|
|
611
|
+
if added:
|
|
612
|
+
_write(path, snapshot)
|
|
613
|
+
return {"status": "ok", "output": args.snapshot, "added": len(added),
|
|
614
|
+
"names": sorted({e["name"] for e in added})}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
618
|
+
parser = argparse.ArgumentParser(
|
|
619
|
+
prog="skf-extraction-snapshot",
|
|
620
|
+
description=__doc__,
|
|
621
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
622
|
+
)
|
|
623
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
624
|
+
scan = sub.add_parser("scan-list", help="write the files the provenance map covers as a JSON list")
|
|
625
|
+
scan.add_argument("provenance_map", help="path to provenance-map.json")
|
|
626
|
+
scan.add_argument("-o", "--output", required=True, help="the JSON list to write")
|
|
627
|
+
scan.set_defaults(func=_cmd_scan_list)
|
|
628
|
+
snap = sub.add_parser("build", help="build extraction-snapshot.json")
|
|
629
|
+
snap.add_argument("--source-root", required=True, help="the source tree the runner read")
|
|
630
|
+
snap.add_argument("--source-path",
|
|
631
|
+
help="the source root the provenance map records, written as the snapshot's source_root "
|
|
632
|
+
"(default: --source-root)")
|
|
633
|
+
snap.add_argument("--tier", required=True, help="the forge tier the re-index ran at")
|
|
634
|
+
snap.add_argument("--date", required=True, help="the run's timestamp, recorded as extraction_date")
|
|
635
|
+
snap.add_argument("--provenance-map", help="the provenance map: its files are the files in scope")
|
|
636
|
+
snap.add_argument("--extraction", help="the JSON skf-extract-public-api.py --mode full wrote")
|
|
637
|
+
snap.add_argument("--details", action="append",
|
|
638
|
+
help="the exports read by eye: {\"files\": [...], \"exports\": [...]}; repeat per file")
|
|
639
|
+
snap.add_argument("--scope-type",
|
|
640
|
+
help="the skill's scope type: public-api marks each export `public` by the public-surface "
|
|
641
|
+
"rule; any other marks nothing")
|
|
642
|
+
snap.add_argument("-o", "--output", required=True, help="the snapshot to write")
|
|
643
|
+
snap.set_defaults(func=_cmd_build)
|
|
644
|
+
move = sub.add_parser("relocate", help="add the exports the runner found under removed names elsewhere")
|
|
645
|
+
move.add_argument("snapshot", help="extraction-snapshot.json, rewritten in place")
|
|
646
|
+
move.add_argument("--diff", required=True, help="the structural diff whose removed[] names to look for")
|
|
647
|
+
move.add_argument("--extraction", required=True, help="the runner's JSON over the candidate files")
|
|
648
|
+
move.set_defaults(func=_cmd_relocate)
|
|
649
|
+
return parser
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def main(argv: list[str] | None = None) -> int:
|
|
653
|
+
args = _build_parser().parse_args(argv)
|
|
654
|
+
try:
|
|
655
|
+
result = args.func(args)
|
|
656
|
+
except SnapshotError as exc:
|
|
657
|
+
print(json.dumps({"status": "error", "error": str(exc)}))
|
|
658
|
+
return EXIT_ERROR
|
|
659
|
+
print(json.dumps(result))
|
|
660
|
+
return 0
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
if __name__ == "__main__":
|
|
664
|
+
sys.exit(main())
|