bmad-module-skill-forge 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""Locate where a skill describes each export, each example call to it, and whether the call awaits it.
|
|
7
|
+
|
|
8
|
+
coherence-check.md §2.5 (naive mode) raises a High finding when a skill
|
|
9
|
+
describes an export as async and its examples never await it, or describes
|
|
10
|
+
it as sync and an example awaits it. What a description asserts about an
|
|
11
|
+
export is the model's call, never a keyword's: "`readFileSync` is the sync
|
|
12
|
+
counterpart of the async `readFile`" holds the word `async` and says that
|
|
13
|
+
`readFileSync` is sync. This script does only the part with one correct
|
|
14
|
+
answer per input and judges no meaning. For each export name it lists:
|
|
15
|
+
|
|
16
|
+
- descriptions[]: where SKILL.md and references/**/*.md describe the
|
|
17
|
+
export, outside the frontmatter, fenced code and HTML comments: a heading
|
|
18
|
+
that names it (bare or in code), with the prose of its section, and each
|
|
19
|
+
table row, list item or paragraph that names it in an inline code span.
|
|
20
|
+
Within a section whose heading names the export, nothing else is listed
|
|
21
|
+
for that export: the section text holds it. Each line of fenced code
|
|
22
|
+
that declares the export (`def`, `function`, `fn`, a typed declaration,
|
|
23
|
+
a signature line) is listed too, as kind "signature". An entry carries
|
|
24
|
+
the headings above it (`section`), a table row its table's header row
|
|
25
|
+
(`header`), and its text, cut at a fixed length (`truncated`).
|
|
26
|
+
- calls[]: each call to the export in fenced code: the name, as a whole
|
|
27
|
+
identifier, followed by `(` (a generic `<...>` or a `::` may stand
|
|
28
|
+
between), with any receiver before it (`client.fetchData(`), and not a
|
|
29
|
+
declaration. Comments and string literals are blanked first in the
|
|
30
|
+
languages this script knows by the fence's info string; a block tagged
|
|
31
|
+
as data or output (`json`, `yaml`, `text`, `console` ...) holds none.
|
|
32
|
+
|
|
33
|
+
A call is `awaited` when `await` stands before it (through its receiver:
|
|
34
|
+
`await client.fetchData(...)`), `.await` follows it (Rust), an asynchronous
|
|
35
|
+
loop or context manager consumes it (`async for row in stream(...)`, `async
|
|
36
|
+
with session(...)`, `for await (const row of stream(...))`), or it sits in
|
|
37
|
+
the arguments of a call that is itself awaited or is an event-loop runner
|
|
38
|
+
(`await Promise.all([fetchData(a)])`, `asyncio.run(fetch_data())`), without
|
|
39
|
+
crossing into a function body. `chainedMember` names the member read or
|
|
40
|
+
called on its result right after it (`then`, `catch`, `join`, `get`), else
|
|
41
|
+
null: whether that waits for the result is the caller's call.
|
|
42
|
+
|
|
43
|
+
Name match: the name as a fixed string, case-sensitive, never inside a
|
|
44
|
+
longer identifier (the Export match rule of skf-scan-skill-md-structure.py
|
|
45
|
+
usage-scope). A qualified name (`client.fetch`, `Store::load`) matches only
|
|
46
|
+
as written, except that a declaration also matches its last part
|
|
47
|
+
(`def fetch(`): a declaration never carries the qualifier.
|
|
48
|
+
|
|
49
|
+
Line model: UTF-8 with a leading byte order mark dropped, split on `\\n`
|
|
50
|
+
(a `\\r\\n` pair loses its `\\r`), so each `line` matches `grep -n`. The
|
|
51
|
+
frontmatter is a leading `---` ... `---` block. A fence opens on three or
|
|
52
|
+
more backticks or tildes (any indent) and closes on the same character
|
|
53
|
+
repeated at least as often with nothing after it; an unclosed fence runs to
|
|
54
|
+
the end of the file. A heading is an ATX heading (at most three spaces of
|
|
55
|
+
indent); its section runs to the next heading of the same or a higher level.
|
|
56
|
+
|
|
57
|
+
Input (one JSON object):
|
|
58
|
+
{
|
|
59
|
+
"names": ["fetchData", "Client.fetch", ...], # the §1 inventory's function and method names
|
|
60
|
+
"skillPackagePath": "/path/to/skill" # holds SKILL.md and references/
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
or the files: --inventory <file> --skill-dir <dir>. --inventory reads the
|
|
64
|
+
validated inventory validate-inventory.py wrote (its --output) and takes the
|
|
65
|
+
name of each entry whose kind is `function` or `method`, so no name is
|
|
66
|
+
copied by hand.
|
|
67
|
+
|
|
68
|
+
Output (stdout, one object):
|
|
69
|
+
{
|
|
70
|
+
"skillPackagePath": "...",
|
|
71
|
+
"files": ["SKILL.md", "references/api.md"], # read, in this order
|
|
72
|
+
"exports": [ # each name with at least one call, in input order
|
|
73
|
+
{
|
|
74
|
+
"name": "fetchData",
|
|
75
|
+
"descriptionCount": 3, # all found; descriptions[] holds the first 12
|
|
76
|
+
"descriptions": [{"file", "line", "endLine", "kind", "section", "header", "text", "truncated"}],
|
|
77
|
+
"calls": [{"file", "line", "fence", "language", "awaited", "chainedMember", "text"}],
|
|
78
|
+
"callCount": 2,
|
|
79
|
+
"awaitedCount": 1
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"notCalled": ["parseConfig"], # names no example calls
|
|
83
|
+
"summary": {"names": N, "called": N, "notCalled": N}
|
|
84
|
+
}
|
|
85
|
+
or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
|
|
86
|
+
|
|
87
|
+
`kind` is "heading", "table-row", "list-item", "paragraph" or "signature".
|
|
88
|
+
Lines are 1-based; `fence` is the line of the call's opening fence.
|
|
89
|
+
|
|
90
|
+
CLI usage (mirrors verify-declared-numerator.py):
|
|
91
|
+
uv run locate-export-segments.py --inventory <file> --skill-dir <dir>
|
|
92
|
+
uv run locate-export-segments.py '<JSON>' # positional
|
|
93
|
+
uv run locate-export-segments.py --json-input '<JSON>' # explicit flag
|
|
94
|
+
cat input.json | uv run locate-export-segments.py --stdin # piped input
|
|
95
|
+
|
|
96
|
+
Exit codes:
|
|
97
|
+
0 segments emitted
|
|
98
|
+
1 no input / input could not be parsed as JSON, or an --inventory file that cannot be read
|
|
99
|
+
2 input parsed but schema invalid (error object emitted as JSON)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
from __future__ import annotations
|
|
103
|
+
|
|
104
|
+
import argparse
|
|
105
|
+
import json
|
|
106
|
+
import re
|
|
107
|
+
import sys
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
MAX_DESCRIPTIONS = 12
|
|
111
|
+
TEXT_CAP = 600
|
|
112
|
+
SECTION_TEXT_CAP = 1200
|
|
113
|
+
LINE_TEXT_CAP = 200
|
|
114
|
+
SECTION_PATH_CAP = 200
|
|
115
|
+
|
|
116
|
+
# The identifier characters of skf-scan-skill-md-structure.py's Export match
|
|
117
|
+
# (its _IDENT_CHARS): any Unicode letter, digit or `_`, and `$`.
|
|
118
|
+
IDENT_CHARS = r"\w$"
|
|
119
|
+
IDENT_RE = re.compile(f"[{IDENT_CHARS}]")
|
|
120
|
+
WORD_BEFORE_RE = re.compile(f"([{IDENT_CHARS}]+)$")
|
|
121
|
+
|
|
122
|
+
FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})(.*)$")
|
|
123
|
+
HEADING_RE = re.compile(r"^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$")
|
|
124
|
+
CLOSING_HASHES_RE = re.compile(r"(?:^|[ \t]+)#+$")
|
|
125
|
+
LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d{1,9}[.)])(?:[ \t]+|$)")
|
|
126
|
+
TABLE_ROW_RE = re.compile(r"^\s*\|")
|
|
127
|
+
TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$")
|
|
128
|
+
GENERIC_OR_PATH = r"(?:[ \t]*::)?(?:[ \t]*<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?"
|
|
129
|
+
AFTER_CALL_RE = re.compile(f"\\s*\\??\\.\\s*((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)")
|
|
130
|
+
AWAIT_BEFORE_RE = re.compile(f"(?<![{IDENT_CHARS}])await$")
|
|
131
|
+
AWAIT_AFTER_RE = re.compile(f"\\s*\\.\\s*await(?![{IDENT_CHARS}])")
|
|
132
|
+
# An expression an asynchronous loop or context manager consumes: `async for
|
|
133
|
+
# x in NAME(...)`, `async with NAME(...)`, `for await (const x of NAME(...))`,
|
|
134
|
+
# `await foreach (var x in NAME(...))`. Matched against its line up to it.
|
|
135
|
+
ASYNC_CONSUMER_RE = re.compile(
|
|
136
|
+
f"(?<![{IDENT_CHARS}])(?:async[ \\t]+for[ \\t].*[ \\t]in|async[ \\t]+with(?:[ \\t].*,)?"
|
|
137
|
+
f"|for[ \\t]+await[ \\t]*\\(.*[ \\t]of|await[ \\t]+foreach[ \\t]*\\(.*[ \\t]in)[ \\t]*$")
|
|
138
|
+
|
|
139
|
+
# Info strings of blocks that hold data or output, never calls.
|
|
140
|
+
DATA_LANGUAGES = frozenset({
|
|
141
|
+
"json", "jsonc", "json5", "yaml", "yml", "toml", "ini", "xml", "csv", "tsv", "text", "txt",
|
|
142
|
+
"plaintext", "plain", "mermaid", "markdown", "md", "log", "output", "diff", "console",
|
|
143
|
+
})
|
|
144
|
+
# Comment and string syntax by info string. Code under an unknown or missing
|
|
145
|
+
# info string is read as written.
|
|
146
|
+
HASH_COMMENTS = frozenset({
|
|
147
|
+
"python", "py", "python3", "ruby", "rb", "sh", "bash", "shell", "zsh", "fish", "r", "perl", "pl",
|
|
148
|
+
"elixir", "ex", "exs", "coffee", "coffeescript", "julia", "jl", "powershell", "ps1", "pwsh",
|
|
149
|
+
})
|
|
150
|
+
JS_FAMILY = frozenset({"js", "javascript", "jsx", "mjs", "cjs", "ts", "typescript", "tsx", "mts", "cts",
|
|
151
|
+
"vue", "svelte"})
|
|
152
|
+
C_FAMILY = frozenset({"java", "cs", "csharp", "c#", "c", "h", "cpp", "c++", "cc", "hpp", "dart", "php"})
|
|
153
|
+
SLASH_COMMENTS = JS_FAMILY | C_FAMILY | frozenset({
|
|
154
|
+
"rust", "rs", "go", "golang", "kotlin", "kt", "kts", "swift", "scala", "groovy", "zig",
|
|
155
|
+
})
|
|
156
|
+
# `'` opens a string here; elsewhere it opens a character literal or a Rust
|
|
157
|
+
# lifetime, both read as written.
|
|
158
|
+
SINGLE_QUOTE_STRINGS = HASH_COMMENTS | JS_FAMILY | frozenset({"php", "dart", "groovy"})
|
|
159
|
+
TEMPLATE_STRINGS = JS_FAMILY | frozenset({"go", "golang"})
|
|
160
|
+
TRIPLE_QUOTES = {
|
|
161
|
+
"python": ('"""', "'''"), "py": ('"""', "'''"), "python3": ('"""', "'''"),
|
|
162
|
+
"kotlin": ('"""',), "kt": ('"""',), "kts": ('"""',), "swift": ('"""',), "scala": ('"""',),
|
|
163
|
+
"java": ('"""',), "groovy": ('"""', "'''"), "dart": ('"""', "'''"), "julia": ('"""',), "jl": ('"""',),
|
|
164
|
+
}
|
|
165
|
+
# Languages where `Type name(...)` declares `name`: a word other than an
|
|
166
|
+
# expression word right before the name is a return type there. Untagged
|
|
167
|
+
# code reads the same way, since it is most often a signature.
|
|
168
|
+
TYPED_DECLARATIONS = C_FAMILY | frozenset({"groovy", ""})
|
|
169
|
+
# A `{` after a call at the start of a line opens a method body here; in
|
|
170
|
+
# Kotlin, Swift or Ruby it can be a trailing closure of a call.
|
|
171
|
+
BRACE_DECLARATIONS = JS_FAMILY | C_FAMILY
|
|
172
|
+
|
|
173
|
+
# A word right before the name that declares it.
|
|
174
|
+
DECLARATION_WORDS = frozenset({
|
|
175
|
+
"def", "function", "fn", "func", "fun", "sub", "macro", "class", "interface", "struct", "enum",
|
|
176
|
+
"trait", "type", "record", "object", "protocol", "actor", "typealias", "impl",
|
|
177
|
+
"async", "static", "public", "private", "protected", "internal", "override", "abstract", "virtual",
|
|
178
|
+
"readonly", "get", "set", "export", "default", "pub", "unsafe", "extern", "inline", "final", "open",
|
|
179
|
+
"suspend", "operator", "infix", "tailrec", "external", "native", "synchronized", "sealed", "data",
|
|
180
|
+
})
|
|
181
|
+
# A word that can stand right before a call, and before a `(` that only groups.
|
|
182
|
+
EXPRESSION_WORDS = frozenset({
|
|
183
|
+
"await", "return", "yield", "throw", "raise", "new", "else", "in", "of", "case", "typeof", "void",
|
|
184
|
+
"delete", "not", "and", "or", "is", "if", "elif", "while", "until", "unless", "assert", "print",
|
|
185
|
+
"from", "do", "then", "go", "defer", "try", "when", "with", "lambda", "puts", "echo", "instanceof",
|
|
186
|
+
"as", "match", "loop", "del", "move", "ref", "box", "using", "lock",
|
|
187
|
+
})
|
|
188
|
+
# Callees that run the coroutine they are given until it completes: any
|
|
189
|
+
# `run_until_complete` or `block_on`, and `run` of these event loops.
|
|
190
|
+
RUNNERS = frozenset({"run_until_complete", "block_on"})
|
|
191
|
+
RUN_OWNERS = frozenset({"asyncio", "trio", "anyio"})
|
|
192
|
+
BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
193
|
+
CLOSERS = {v: k for k, v in BRACKETS.items()}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def make_error(message):
|
|
197
|
+
return {"error": message, "code": "INVALID_INPUT"}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _cap(text, cap):
|
|
201
|
+
return (text[:cap], True) if len(text) > cap else (text, False)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# --------------------------------------------------------------------------
|
|
205
|
+
# Reading the skill
|
|
206
|
+
# --------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _split_lines(text):
|
|
210
|
+
"""Split on newlines only, so line numbers match `grep -n`."""
|
|
211
|
+
lines = text.replace("\r\n", "\n").split("\n")
|
|
212
|
+
if lines and lines[-1] == "":
|
|
213
|
+
lines.pop()
|
|
214
|
+
return lines
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def skill_files(root):
|
|
218
|
+
"""(relative POSIX path, lines) of SKILL.md, then of each references/**/*.md in path order."""
|
|
219
|
+
root = Path(root)
|
|
220
|
+
paths = [root / "SKILL.md"] if (root / "SKILL.md").is_file() else []
|
|
221
|
+
refs = root / "references"
|
|
222
|
+
if refs.is_dir():
|
|
223
|
+
paths += sorted((p for p in refs.rglob("*.md") if p.is_file()),
|
|
224
|
+
key=lambda p: p.relative_to(root).as_posix())
|
|
225
|
+
return [(p.relative_to(root).as_posix(), _split_lines(p.read_bytes().decode("utf-8-sig", errors="replace")))
|
|
226
|
+
for p in paths]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _frontmatter_end(lines):
|
|
230
|
+
"""Index of the first body line, past a leading `---` ... `---` block."""
|
|
231
|
+
if lines and lines[0].strip() == "---":
|
|
232
|
+
for i in range(1, len(lines)):
|
|
233
|
+
if lines[i].strip() == "---":
|
|
234
|
+
return i + 1
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _fenced_blocks(lines, start):
|
|
239
|
+
"""(opening fence, closing fence, info string) of each fenced block, and a per-line code flag.
|
|
240
|
+
|
|
241
|
+
An unclosed fence runs to the end of the file (its closing index is
|
|
242
|
+
len(lines)). A backtick line whose info string holds a backtick is
|
|
243
|
+
inline code, not a fence.
|
|
244
|
+
"""
|
|
245
|
+
blocks, code = [], [False] * len(lines)
|
|
246
|
+
fence = None
|
|
247
|
+
for i in range(start, len(lines)):
|
|
248
|
+
m = FENCE_RE.match(lines[i])
|
|
249
|
+
if fence is None:
|
|
250
|
+
if m and not (m.group(1)[0] == "`" and "`" in m.group(2)):
|
|
251
|
+
fence = (m.group(1)[0], len(m.group(1)), i, m.group(2).strip())
|
|
252
|
+
code[i] = True
|
|
253
|
+
continue
|
|
254
|
+
code[i] = True
|
|
255
|
+
if m and m.group(1)[0] == fence[0] and len(m.group(1)) >= fence[1] and not m.group(2).strip():
|
|
256
|
+
blocks.append((fence[2], i, fence[3]))
|
|
257
|
+
fence = None
|
|
258
|
+
if fence is not None:
|
|
259
|
+
blocks.append((fence[2], len(lines), fence[3]))
|
|
260
|
+
return blocks, code
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _language(info):
|
|
264
|
+
"""The info string's first word, lower case: `ts title="x"` and `{.ts}` give `ts`."""
|
|
265
|
+
words = info.split()
|
|
266
|
+
return words[0].strip("{}").lstrip(".").lower() if words else ""
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _inline_code(line):
|
|
270
|
+
"""The text of each inline code span: a backtick run closed by an equal run."""
|
|
271
|
+
spans, i, n = [], 0, len(line)
|
|
272
|
+
while i < n:
|
|
273
|
+
if line[i] != "`":
|
|
274
|
+
i += 1
|
|
275
|
+
continue
|
|
276
|
+
j = i
|
|
277
|
+
while j < n and line[j] == "`":
|
|
278
|
+
j += 1
|
|
279
|
+
run, k, close = j - i, j, -1
|
|
280
|
+
while k < n:
|
|
281
|
+
if line[k] != "`":
|
|
282
|
+
k += 1
|
|
283
|
+
continue
|
|
284
|
+
m = k
|
|
285
|
+
while m < n and line[m] == "`":
|
|
286
|
+
m += 1
|
|
287
|
+
if m - k == run:
|
|
288
|
+
close = k
|
|
289
|
+
break
|
|
290
|
+
k = m
|
|
291
|
+
if close == -1:
|
|
292
|
+
i = j
|
|
293
|
+
continue
|
|
294
|
+
spans.append(line[j:close])
|
|
295
|
+
i = close + run
|
|
296
|
+
return spans
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _bounded(name):
|
|
300
|
+
"""Regex for `name` as a fixed string never inside a longer identifier."""
|
|
301
|
+
body = re.escape(name)
|
|
302
|
+
if IDENT_RE.match(name[0]):
|
|
303
|
+
body = f"(?<![{IDENT_CHARS}])" + body
|
|
304
|
+
if IDENT_RE.match(name[-1]):
|
|
305
|
+
body += f"(?![{IDENT_CHARS}])"
|
|
306
|
+
return re.compile(body)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _call_pattern(name):
|
|
310
|
+
return re.compile(_bounded(name).pattern + GENERIC_OR_PATH + r"[ \t]*\(")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _last_part(name):
|
|
314
|
+
"""`fetch` for `client.fetch` or `Store::fetch`; None for a plain name."""
|
|
315
|
+
part = re.split(r"\.|::|#|->", name)[-1]
|
|
316
|
+
return part if part and part != name else None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# --------------------------------------------------------------------------
|
|
320
|
+
# Descriptions in prose
|
|
321
|
+
# --------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _prose(lines, start, code):
|
|
325
|
+
"""Headings and prose blocks outside the frontmatter, fenced code and HTML comments.
|
|
326
|
+
|
|
327
|
+
A heading is (index, level, text, headings above it). A block is a
|
|
328
|
+
table row, a list item (with its continuation lines) or a paragraph:
|
|
329
|
+
{kind, start, end (inclusive), path (headings above it), header (a
|
|
330
|
+
table row's header row), code (the text of its inline code spans)}.
|
|
331
|
+
"""
|
|
332
|
+
headings, blocks = [], []
|
|
333
|
+
stack = []
|
|
334
|
+
current = None
|
|
335
|
+
header = None
|
|
336
|
+
in_comment = False
|
|
337
|
+
|
|
338
|
+
def close():
|
|
339
|
+
nonlocal current
|
|
340
|
+
if current is not None:
|
|
341
|
+
blocks.append(current)
|
|
342
|
+
current = None
|
|
343
|
+
|
|
344
|
+
def block(kind, i, path, table_header=None):
|
|
345
|
+
return {"kind": kind, "start": i, "end": i, "path": path, "header": table_header,
|
|
346
|
+
"code": "\n".join(_inline_code(lines[i]))}
|
|
347
|
+
|
|
348
|
+
for i in range(start, len(lines)):
|
|
349
|
+
line, stripped = lines[i], lines[i].strip()
|
|
350
|
+
if code[i] or not stripped:
|
|
351
|
+
close()
|
|
352
|
+
header = None
|
|
353
|
+
continue
|
|
354
|
+
if in_comment:
|
|
355
|
+
in_comment = "-->" not in line
|
|
356
|
+
continue
|
|
357
|
+
if stripped.startswith("<!--"):
|
|
358
|
+
close()
|
|
359
|
+
in_comment = "-->" not in stripped[4:]
|
|
360
|
+
continue
|
|
361
|
+
m = HEADING_RE.match(line)
|
|
362
|
+
if m:
|
|
363
|
+
close()
|
|
364
|
+
header = None
|
|
365
|
+
level = len(m.group(1))
|
|
366
|
+
text = CLOSING_HASHES_RE.sub("", m.group(2) or "").strip()
|
|
367
|
+
while stack and stack[-1][0] >= level:
|
|
368
|
+
stack.pop()
|
|
369
|
+
headings.append((i, level, text, [t for _, t in stack]))
|
|
370
|
+
stack.append((level, text))
|
|
371
|
+
continue
|
|
372
|
+
path = [t for _, t in stack]
|
|
373
|
+
if TABLE_ROW_RE.match(line):
|
|
374
|
+
close()
|
|
375
|
+
if TABLE_SEPARATOR_RE.match(line):
|
|
376
|
+
continue
|
|
377
|
+
blocks.append(block("table-row", i, path, header))
|
|
378
|
+
header = header or stripped
|
|
379
|
+
continue
|
|
380
|
+
header = None
|
|
381
|
+
if LIST_ITEM_RE.match(line):
|
|
382
|
+
close()
|
|
383
|
+
current = block("list-item", i, path)
|
|
384
|
+
elif current is None:
|
|
385
|
+
current = block("paragraph", i, path)
|
|
386
|
+
else:
|
|
387
|
+
current["end"] = i
|
|
388
|
+
current["code"] += "\n" + "\n".join(_inline_code(line))
|
|
389
|
+
close()
|
|
390
|
+
return headings, blocks
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _heading_path(headings, index):
|
|
394
|
+
"""The headings above line `index`, outermost first."""
|
|
395
|
+
path = []
|
|
396
|
+
for idx, _, text, above in headings:
|
|
397
|
+
if idx >= index:
|
|
398
|
+
break
|
|
399
|
+
path = above + [text]
|
|
400
|
+
return path
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _section_end(headings, pos, total):
|
|
404
|
+
"""Exclusive end of the section of headings[pos]."""
|
|
405
|
+
level = headings[pos][1]
|
|
406
|
+
for idx, lvl, _, _ in headings[pos + 1:]:
|
|
407
|
+
if lvl <= level:
|
|
408
|
+
return idx
|
|
409
|
+
return total
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _entry(rel, line, end_line, kind, path, header, text, cap):
|
|
413
|
+
text, truncated = _cap(text, cap)
|
|
414
|
+
return {"file": rel, "line": line, "endLine": end_line, "kind": kind,
|
|
415
|
+
"section": _cap(" > ".join(path), SECTION_PATH_CAP)[0], "header": header,
|
|
416
|
+
"text": text, "truncated": truncated}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def describe(rel, lines, code, headings, blocks, pattern):
|
|
420
|
+
"""The heading, table-row, list-item and paragraph entries of one file for one export."""
|
|
421
|
+
found, covered = [], []
|
|
422
|
+
for pos, (idx, _, text, above) in enumerate(headings):
|
|
423
|
+
if any(a <= idx < b for a, b in covered) or not pattern.search(text):
|
|
424
|
+
continue
|
|
425
|
+
end = _section_end(headings, pos, len(lines))
|
|
426
|
+
covered.append((idx, end))
|
|
427
|
+
body = [lines[i].strip() for i in range(idx + 1, end)
|
|
428
|
+
if not code[i] and lines[i].strip() and not HEADING_RE.match(lines[i])
|
|
429
|
+
and not lines[i].strip().startswith("<!--") and not TABLE_SEPARATOR_RE.match(lines[i])]
|
|
430
|
+
found.append(_entry(rel, idx + 1, end, "heading", above, None, " ".join([text] + body), SECTION_TEXT_CAP))
|
|
431
|
+
for block in blocks:
|
|
432
|
+
if any(a <= block["start"] < b for a, b in covered) or not pattern.search(block["code"]):
|
|
433
|
+
continue
|
|
434
|
+
text = " ".join(lines[i].strip() for i in range(block["start"], block["end"] + 1))
|
|
435
|
+
found.append(_entry(rel, block["start"] + 1, block["end"] + 1, block["kind"], block["path"],
|
|
436
|
+
block["header"], text, TEXT_CAP))
|
|
437
|
+
return found
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# --------------------------------------------------------------------------
|
|
441
|
+
# Calls and declarations in fenced code
|
|
442
|
+
# --------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def mask_code(code, language):
|
|
446
|
+
"""The code with comments and string literals blanked, every offset and newline kept.
|
|
447
|
+
|
|
448
|
+
Only a language this script knows is masked: code under an unknown or
|
|
449
|
+
missing info string is returned as written.
|
|
450
|
+
"""
|
|
451
|
+
hash_comments = language in HASH_COMMENTS
|
|
452
|
+
slash_comments = language in SLASH_COMMENTS
|
|
453
|
+
if not (hash_comments or slash_comments):
|
|
454
|
+
return code
|
|
455
|
+
quotes = {'"'}
|
|
456
|
+
if language in SINGLE_QUOTE_STRINGS:
|
|
457
|
+
quotes.add("'")
|
|
458
|
+
if language in TEMPLATE_STRINGS:
|
|
459
|
+
quotes.add("`")
|
|
460
|
+
triples = TRIPLE_QUOTES.get(language, ())
|
|
461
|
+
out, n, i = list(code), len(code), 0
|
|
462
|
+
while i < n:
|
|
463
|
+
c = code[i]
|
|
464
|
+
triple = next((t for t in triples if code.startswith(t, i)), None)
|
|
465
|
+
if (hash_comments and c == "#") or (slash_comments and code.startswith("//", i)):
|
|
466
|
+
j = code.find("\n", i)
|
|
467
|
+
j = n if j == -1 else j
|
|
468
|
+
elif slash_comments and code.startswith("/*", i):
|
|
469
|
+
j = code.find("*/", i + 2)
|
|
470
|
+
j = n if j == -1 else j + 2
|
|
471
|
+
elif triple:
|
|
472
|
+
j = code.find(triple, i + 3)
|
|
473
|
+
j = n if j == -1 else j + 3
|
|
474
|
+
elif c in quotes:
|
|
475
|
+
j = i + 1
|
|
476
|
+
while j < n and code[j] != c and (c == "`" or code[j] != "\n"):
|
|
477
|
+
j += 2 if code[j] == "\\" else 1
|
|
478
|
+
j = min(j + 1, n)
|
|
479
|
+
else:
|
|
480
|
+
i += 1
|
|
481
|
+
continue
|
|
482
|
+
for k in range(i, j):
|
|
483
|
+
if out[k] != "\n":
|
|
484
|
+
out[k] = " "
|
|
485
|
+
i = j
|
|
486
|
+
return "".join(out)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _matching_close(text, open_idx):
|
|
490
|
+
stack = []
|
|
491
|
+
for i in range(open_idx, len(text)):
|
|
492
|
+
c = text[i]
|
|
493
|
+
if c in BRACKETS:
|
|
494
|
+
stack.append(c)
|
|
495
|
+
elif c in CLOSERS:
|
|
496
|
+
if not stack or stack.pop() != CLOSERS[c]:
|
|
497
|
+
return None
|
|
498
|
+
if not stack:
|
|
499
|
+
return i
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _matching_open(text, close_idx):
|
|
504
|
+
stack = []
|
|
505
|
+
for i in range(close_idx, -1, -1):
|
|
506
|
+
c = text[i]
|
|
507
|
+
if c in CLOSERS:
|
|
508
|
+
stack.append(c)
|
|
509
|
+
elif c in BRACKETS:
|
|
510
|
+
if not stack or stack.pop() != BRACKETS[c]:
|
|
511
|
+
return None
|
|
512
|
+
if not stack:
|
|
513
|
+
return i
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _skip_space_back(text, i):
|
|
518
|
+
while i > 0 and text[i - 1] in " \t\n":
|
|
519
|
+
i -= 1
|
|
520
|
+
return i
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _chain_start(text, pos):
|
|
524
|
+
"""Start of the expression whose last member starts at `pos`: `client.api.` in `client.api.name`."""
|
|
525
|
+
i = pos
|
|
526
|
+
while True:
|
|
527
|
+
k = _skip_space_back(text, i)
|
|
528
|
+
accessor = next((a for a in ("?.", "!.", "::", "->", ".") if text.endswith(a, 0, k)), None)
|
|
529
|
+
if accessor is None:
|
|
530
|
+
return i
|
|
531
|
+
k = _skip_space_back(text, k - len(accessor))
|
|
532
|
+
start = k
|
|
533
|
+
while start > 0 and text[start - 1] in ")]":
|
|
534
|
+
opened = _matching_open(text, start - 1)
|
|
535
|
+
if opened is None:
|
|
536
|
+
break
|
|
537
|
+
start = opened
|
|
538
|
+
word = WORD_BEFORE_RE.search(text, 0, start)
|
|
539
|
+
if word:
|
|
540
|
+
start = word.start()
|
|
541
|
+
if start == k:
|
|
542
|
+
return i
|
|
543
|
+
i = start
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _awaited_at(text, start):
|
|
547
|
+
"""True when `await` stands right before the expression that starts at `start`, or an
|
|
548
|
+
asynchronous loop or context manager consumes it."""
|
|
549
|
+
if AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)):
|
|
550
|
+
return True
|
|
551
|
+
return bool(ASYNC_CONSUMER_RE.search(text[text.rfind("\n", 0, start) + 1:start]))
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _is_runner(callee):
|
|
555
|
+
"""True for `asyncio.run`, `loop.run_until_complete`, `rt.block_on` and the like."""
|
|
556
|
+
parts = [p for p in re.split(r"\.|::", callee) if p]
|
|
557
|
+
if not parts:
|
|
558
|
+
return False
|
|
559
|
+
if parts[-1] == "run":
|
|
560
|
+
return len(parts) >= 2 and parts[-2] in RUN_OWNERS
|
|
561
|
+
return parts[-1] in RUNNERS
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _opens_body(text, brace_idx):
|
|
565
|
+
"""True when the `{` at brace_idx opens a function or statement body, not an object literal."""
|
|
566
|
+
k = _skip_space_back(text, brace_idx)
|
|
567
|
+
return k > 0 and (text[k - 1] in ")>" or bool(IDENT_RE.match(text[k - 1])))
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _enclosing_awaits(text, start):
|
|
571
|
+
"""True when a call around the expression at `start` is awaited or runs it, within one function body."""
|
|
572
|
+
i, depth = start, 0
|
|
573
|
+
while i > 0:
|
|
574
|
+
i -= 1
|
|
575
|
+
c = text[i]
|
|
576
|
+
if c in CLOSERS:
|
|
577
|
+
depth += 1
|
|
578
|
+
elif c in BRACKETS:
|
|
579
|
+
if depth:
|
|
580
|
+
depth -= 1
|
|
581
|
+
elif c == "{" and _opens_body(text, i):
|
|
582
|
+
return False
|
|
583
|
+
elif c == "(":
|
|
584
|
+
k = _skip_space_back(text, i)
|
|
585
|
+
word = WORD_BEFORE_RE.search(text, 0, k)
|
|
586
|
+
if word and word.group(1) not in EXPRESSION_WORDS:
|
|
587
|
+
expr = _chain_start(text, word.start())
|
|
588
|
+
if _is_runner(re.sub(r"\s+", "", text[expr:k])):
|
|
589
|
+
return True
|
|
590
|
+
else:
|
|
591
|
+
expr = i # a grouping `(`: `await (fetch())`
|
|
592
|
+
close = _matching_close(text, i)
|
|
593
|
+
if _awaited_at(text, expr) or (close is not None and AWAIT_AFTER_RE.match(text, close + 1)):
|
|
594
|
+
return True
|
|
595
|
+
i = expr
|
|
596
|
+
return False
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _closes_generic(tail):
|
|
600
|
+
"""True when `tail` ends in a generic type such as `Task<Data>`."""
|
|
601
|
+
depth = 0
|
|
602
|
+
for i in range(len(tail) - 1, -1, -1):
|
|
603
|
+
c = tail[i]
|
|
604
|
+
if c == ">":
|
|
605
|
+
depth += 1
|
|
606
|
+
elif c == "<":
|
|
607
|
+
depth -= 1
|
|
608
|
+
if depth == 0:
|
|
609
|
+
return i > 0 and bool(IDENT_RE.match(tail[i - 1]))
|
|
610
|
+
elif c in "()=;{}":
|
|
611
|
+
return False
|
|
612
|
+
return False
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _is_declaration(text, line_start, chain, name_start, close, language):
|
|
616
|
+
"""True when the name at name_start is declared there, not called."""
|
|
617
|
+
tail = text[line_start:chain].rstrip(" \t")
|
|
618
|
+
if chain == name_start and tail:
|
|
619
|
+
word = WORD_BEFORE_RE.search(tail)
|
|
620
|
+
w = word.group(1) if word else ""
|
|
621
|
+
if w in DECLARATION_WORDS:
|
|
622
|
+
return True
|
|
623
|
+
if tail.endswith("*") and tail[:-1].rstrip(" \t").endswith("function"):
|
|
624
|
+
return True # `function* name(`
|
|
625
|
+
if language in TYPED_DECLARATIONS:
|
|
626
|
+
if w and (w not in EXPRESSION_WORDS or w == "void"):
|
|
627
|
+
return True # a return type: `Data name(`, `void name(`
|
|
628
|
+
if tail.endswith("[]") or (tail.endswith(">") and not tail.endswith(("=>", "->"))
|
|
629
|
+
and _closes_generic(tail)):
|
|
630
|
+
return True # `int[] name(`, `Task<Data> name(`
|
|
631
|
+
if tail.endswith(")"):
|
|
632
|
+
opened = _matching_open(text, line_start + len(tail) - 1)
|
|
633
|
+
if opened is not None and text[line_start:opened].rstrip().endswith("func"):
|
|
634
|
+
return True # Go: `func (c *Client) Name(`
|
|
635
|
+
if not tail and close is not None:
|
|
636
|
+
after = text[close + 1:].lstrip(" \t")
|
|
637
|
+
if after.startswith("->") or (after.startswith(":") and not after.startswith(("::", ":="))):
|
|
638
|
+
return True # a signature: `name(url: string): Data`, `name(url) -> Data`
|
|
639
|
+
if after.startswith("{") and language in BRACE_DECLARATIONS:
|
|
640
|
+
return True # a method body: `name(url) {`
|
|
641
|
+
return False
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def scan_block(rel, lines, opening, closing, info, patterns):
|
|
645
|
+
"""{name: {"calls": [...], "signatures": [...]}} for one fenced block.
|
|
646
|
+
|
|
647
|
+
`patterns` maps each name to (its call regex, the regexes of the other
|
|
648
|
+
forms a declaration of it may take).
|
|
649
|
+
"""
|
|
650
|
+
language = _language(info)
|
|
651
|
+
found = {name: {"calls": [], "signatures": []} for name in patterns}
|
|
652
|
+
if language in DATA_LANGUAGES:
|
|
653
|
+
return found
|
|
654
|
+
body = lines[opening + 1:closing]
|
|
655
|
+
text = mask_code("\n".join(body), language)
|
|
656
|
+
for name, (call_re, declaration_res) in patterns.items():
|
|
657
|
+
seen = set()
|
|
658
|
+
for regex in (call_re, *declaration_res):
|
|
659
|
+
for m in regex.finditer(text):
|
|
660
|
+
name_start, paren = m.start(), m.end() - 1
|
|
661
|
+
if name_start in seen:
|
|
662
|
+
continue
|
|
663
|
+
seen.add(name_start)
|
|
664
|
+
close = _matching_close(text, paren)
|
|
665
|
+
chain = _chain_start(text, name_start)
|
|
666
|
+
line_start = text.rfind("\n", 0, name_start) + 1
|
|
667
|
+
row = text.count("\n", 0, name_start)
|
|
668
|
+
if _is_declaration(text, line_start, chain, name_start, close, language):
|
|
669
|
+
found[name]["signatures"].append({"line": opening + row + 2, "text": body[row].strip()})
|
|
670
|
+
continue
|
|
671
|
+
if regex is not call_re:
|
|
672
|
+
continue # the last part of a qualified name counts only as a declaration
|
|
673
|
+
awaited, chained = _awaited_at(text, chain), None
|
|
674
|
+
if close is not None:
|
|
675
|
+
if AWAIT_AFTER_RE.match(text, close + 1):
|
|
676
|
+
awaited = True
|
|
677
|
+
elif member := AFTER_CALL_RE.match(text, close + 1):
|
|
678
|
+
chained = member.group(1)
|
|
679
|
+
found[name]["calls"].append({
|
|
680
|
+
"file": rel, "line": opening + row + 2, "fence": opening + 1, "language": language or None,
|
|
681
|
+
"awaited": awaited or _enclosing_awaits(text, chain), "chainedMember": chained,
|
|
682
|
+
"text": _cap(body[row].strip(), LINE_TEXT_CAP)[0],
|
|
683
|
+
})
|
|
684
|
+
return found
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# --------------------------------------------------------------------------
|
|
688
|
+
# Locate
|
|
689
|
+
# --------------------------------------------------------------------------
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _validate(inp):
|
|
693
|
+
if not isinstance(inp, dict):
|
|
694
|
+
return "Input must be a JSON object"
|
|
695
|
+
if not isinstance(inp.get("names"), list):
|
|
696
|
+
return "names must be a list of export names"
|
|
697
|
+
path = inp.get("skillPackagePath")
|
|
698
|
+
if not isinstance(path, str) or not path:
|
|
699
|
+
return "skillPackagePath is required: the folder that holds SKILL.md and references/"
|
|
700
|
+
if not Path(path).is_dir():
|
|
701
|
+
return f"skillPackagePath is not a folder: {path}"
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def locate(inp):
|
|
706
|
+
"""Pure location over SKILL.md and references/**/*.md under skillPackagePath."""
|
|
707
|
+
err = _validate(inp)
|
|
708
|
+
if err:
|
|
709
|
+
return make_error(err)
|
|
710
|
+
names = list(dict.fromkeys(n for n in inp["names"] if isinstance(n, str) and n.strip()))
|
|
711
|
+
bounded = {name: _bounded(name) for name in names}
|
|
712
|
+
patterns = {name: (_call_pattern(name), [_call_pattern(p)] if (p := _last_part(name)) else [])
|
|
713
|
+
for name in names}
|
|
714
|
+
files = skill_files(inp["skillPackagePath"])
|
|
715
|
+
descriptions = {name: [] for name in names}
|
|
716
|
+
calls = {name: [] for name in names}
|
|
717
|
+
for rel, lines in files:
|
|
718
|
+
start = _frontmatter_end(lines)
|
|
719
|
+
fenced, code = _fenced_blocks(lines, start)
|
|
720
|
+
headings, blocks = _prose(lines, start, code)
|
|
721
|
+
per_file = {name: describe(rel, lines, code, headings, blocks, bounded[name]) for name in names}
|
|
722
|
+
for opening, closing, info in fenced:
|
|
723
|
+
for name, got in scan_block(rel, lines, opening, closing, info, patterns).items():
|
|
724
|
+
calls[name] += got["calls"]
|
|
725
|
+
per_file[name] += [_entry(rel, s["line"], s["line"], "signature",
|
|
726
|
+
_heading_path(headings, s["line"] - 1), None, s["text"], LINE_TEXT_CAP)
|
|
727
|
+
for s in got["signatures"]]
|
|
728
|
+
for name in names:
|
|
729
|
+
descriptions[name] += sorted(per_file[name], key=lambda e: e["line"])
|
|
730
|
+
exports = [{
|
|
731
|
+
"name": name,
|
|
732
|
+
"descriptionCount": len(descriptions[name]),
|
|
733
|
+
"descriptions": descriptions[name][:MAX_DESCRIPTIONS],
|
|
734
|
+
"calls": calls[name],
|
|
735
|
+
"callCount": len(calls[name]),
|
|
736
|
+
"awaitedCount": sum(1 for c in calls[name] if c["awaited"]),
|
|
737
|
+
} for name in names if calls[name]]
|
|
738
|
+
not_called = [name for name in names if not calls[name]]
|
|
739
|
+
return {
|
|
740
|
+
"skillPackagePath": inp["skillPackagePath"],
|
|
741
|
+
"files": [rel for rel, _ in files],
|
|
742
|
+
"exports": exports,
|
|
743
|
+
"notCalled": not_called,
|
|
744
|
+
"summary": {"names": len(names), "called": len(exports), "notCalled": len(not_called)},
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
# --- CLI --------------------------------------------------------------------
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _build_parser():
|
|
752
|
+
parser = argparse.ArgumentParser(
|
|
753
|
+
prog="locate-export-segments",
|
|
754
|
+
description=(
|
|
755
|
+
"Locate where a skill describes each export and each example call to it, and "
|
|
756
|
+
"whether the call awaits it (coherence-check.md §2.5). It judges no meaning: the "
|
|
757
|
+
"caller decides what each description asserts."
|
|
758
|
+
),
|
|
759
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
760
|
+
epilog=(
|
|
761
|
+
"Example:\n"
|
|
762
|
+
" uv run locate-export-segments.py "
|
|
763
|
+
"'{\"names\":[\"fetchData\"],\"skillPackagePath\":\"/path/to/skill\"}'"
|
|
764
|
+
),
|
|
765
|
+
)
|
|
766
|
+
src = parser.add_mutually_exclusive_group()
|
|
767
|
+
src.add_argument("json_input", nargs="?",
|
|
768
|
+
help="JSON object as a positional argument (single-quote it on the shell).")
|
|
769
|
+
src.add_argument("--json-input", dest="json_input_flag",
|
|
770
|
+
help="JSON object passed via flag (overrides positional).")
|
|
771
|
+
src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
|
|
772
|
+
src.add_argument("--inventory", metavar="PATH",
|
|
773
|
+
help="the validated inventory file: its function and method names (needs --skill-dir)")
|
|
774
|
+
parser.add_argument("--skill-dir", metavar="DIR", help="with --inventory: the skill package")
|
|
775
|
+
return parser
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
# The inventory kinds §2.5 checks: the ones a caller awaits or calls.
|
|
779
|
+
CALLABLE_KINDS = ("function", "method")
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def inventory_input(path, skill_dir):
|
|
783
|
+
"""The JSON input for the function and method names of a validated inventory file."""
|
|
784
|
+
data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
785
|
+
if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
|
|
786
|
+
data = data["inventory"] # validate-inventory.py's whole result
|
|
787
|
+
exports = data.get("exports") if isinstance(data, dict) else None
|
|
788
|
+
if not isinstance(exports, list):
|
|
789
|
+
raise ValueError(f"--inventory {path} holds no `exports` list")
|
|
790
|
+
names = [e["name"] for e in exports
|
|
791
|
+
if isinstance(e, dict) and e.get("kind") in CALLABLE_KINDS and isinstance(e.get("name"), str)]
|
|
792
|
+
return {"names": names, "skillPackagePath": skill_dir}
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _resolve_input(args):
|
|
796
|
+
if args.stdin:
|
|
797
|
+
return sys.stdin.read()
|
|
798
|
+
if args.json_input_flag is not None:
|
|
799
|
+
return args.json_input_flag
|
|
800
|
+
if args.json_input is not None:
|
|
801
|
+
return args.json_input
|
|
802
|
+
return ""
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def main(argv=None):
|
|
806
|
+
parser = _build_parser()
|
|
807
|
+
args = parser.parse_args(argv)
|
|
808
|
+
if args.inventory is not None:
|
|
809
|
+
if not args.skill_dir:
|
|
810
|
+
parser.error("--inventory needs --skill-dir")
|
|
811
|
+
try:
|
|
812
|
+
data = inventory_input(args.inventory, args.skill_dir)
|
|
813
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
814
|
+
print(f"error: cannot read --inventory {args.inventory}: {exc}", file=sys.stderr)
|
|
815
|
+
return 1
|
|
816
|
+
result = locate(data)
|
|
817
|
+
print(json.dumps(result, indent=2))
|
|
818
|
+
return 2 if result.get("code") == "INVALID_INPUT" else 0
|
|
819
|
+
raw = _resolve_input(args)
|
|
820
|
+
if not raw.strip():
|
|
821
|
+
parser.print_usage(file=sys.stderr)
|
|
822
|
+
print("error: no input provided (positional arg, --json-input, or --stdin)", file=sys.stderr)
|
|
823
|
+
return 1
|
|
824
|
+
try:
|
|
825
|
+
data = json.loads(raw)
|
|
826
|
+
except json.JSONDecodeError as exc:
|
|
827
|
+
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
828
|
+
return 1
|
|
829
|
+
result = locate(data)
|
|
830
|
+
print(json.dumps(result, indent=2))
|
|
831
|
+
return 2 if result.get("code") == "INVALID_INPUT" else 0
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
if __name__ == "__main__":
|
|
835
|
+
sys.exit(main())
|