bmad-module-skill-forge 2.0.0 → 2.0.2
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/.nvmrc +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/docs/workflows.md +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- package/src/shared/health-check.md +40 -3
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +34 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +59 -28
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +51 -31
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +42 -10
- package/src/skf-drop-skill/references/select.md +88 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +27 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +13 -10
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +107 -78
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +109 -75
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +38 -20
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +131 -1
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +12 -24
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
24
|
package/docs/_data/pinned.yaml
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# Enforced two ways: (1) release.yaml bumps this line in the same commit
|
|
27
27
|
# as package.json + marketplace.json on every release; (2) validate-docs-drift.js
|
|
28
28
|
# cross-checks this value against package.json.version and fails on mismatch.
|
|
29
|
-
skf_version: "2.0.
|
|
29
|
+
skf_version: "2.0.2"
|
|
30
30
|
|
|
31
31
|
# Path to the oh-my-skills repo, resolved relative to this repo's root.
|
|
32
32
|
# Override at runtime with the OMS environment variable if oh-my-skills
|
package/docs/workflows.md
CHANGED
|
@@ -474,7 +474,7 @@ If the answer to all of these is "no", the health check exits in one line (`Clea
|
|
|
474
474
|
|
|
475
475
|
- **Severity gate.** Only `bug` findings submit live as GitHub issues by default. `friction` and `gap` findings — the most subjective categories — go to a **local queue** at `{forge_data_folder}/improvement-queue/` unless you explicitly opt in to submit them live during the review gate. This keeps the high-signal reports (real defects) flowing to maintainers while the softer observations sit safely on your disk for you to batch or revisit.
|
|
476
476
|
- **Fingerprint dedup.** Every finding gets a deterministic 7-hex fingerprint computed from `sha1(severity|workflow|step_file|section)` — no LLM similarity judgment, just a tuple hash. Before Ferris opens a new issue, it searches the repo for an existing open issue with the same `fp-*` label. If one exists, you're offered a choice: add a 👍 reaction (silent upvote), react + post a one-sentence environment delta, open a new issue anyway (if you're certain it's distinct), or skip. Re-reporting the same fingerprint is safe — it just adds to the signal-count on the canonical issue.
|
|
477
|
-
- **Global seen-cache.** Once you've submitted or reacted for a given fingerprint, it's recorded at `~/.skf/health-check-seen.json` so the same user
|
|
477
|
+
- **Global seen-cache.** Once you've submitted or reacted for a given fingerprint, it's recorded at `~/.skf/health-check-seen.json` so the same user doesn't re-report the same defect across sessions or across different projects on the same machine, for as long as the issue it points at stays open. Once that issue is closed as completed, the suppression lifts on purpose: a fresh sighting of a fixed defect is a regression, and it gets reported and linked back to the original.
|
|
478
478
|
- **Server-side safety net.** If two users race past the client-side search and both open issues with the same fingerprint, a GitHub Action on this repo catches it: the later issue is auto-closed as a duplicate, linked to the canonical (lowest-numbered) issue, and a 👍 is added there to preserve the signal-count. Manual filers using the [issue template](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/new/choose) feed the same pipeline.
|
|
479
479
|
|
|
480
480
|
**Net effect:** 1,000 users hitting the same bug produce **one canonical issue** with a reaction-count of roughly 1,000 — not 1,000 duplicate issues or a 1,000-comment thread. The maintainer sees population impact at a glance, and your report is never lost.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-module-skill-forge",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"description": "BMAD module — Turn code and docs into instructions AI agents can actually follow. Progressive capability tiers (Quick/Forge/Forge+/Deep).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"test:cli": "node test/test-cli-integration.js",
|
|
54
54
|
"test:install": "node test/test-installation-components.js",
|
|
55
55
|
"test:knowledge": "node test/test-knowledge-base.js",
|
|
56
|
-
"test:python": "uv run --with pytest --with pyyaml --with jsonschema pytest test/test-compute-score-contract.py test/test-skf-preflight.py test/test-skf-skill-inventory.py test/test-skf-validate-output.py test/test-skf-validate-frontmatter.py test/test-skf-manifest-ops.py test/test-skf-rebuild-managed-sections.py test/test-skf-atomic-write.py test/test-skf-severity-classify.py test/test-skf-structural-diff.py test/test-skf-detect-tools.py test/test-skf-forge-tier-rw.py test/test-skf-emit-result-envelope.py test/test-skf-qmd-classify-collections.py test/test-skf-merge-ccc-exclusions.py test/test-skf-resolve-package.py test/test-skf-extract-public-api.py test/test-skf-render-quick-metadata.py test/test-skf-validate-brief-inputs.py test/test-skf-emit-brief-result-envelope.py test/test-skf-write-skill-brief.py test/test-skf-detect-workspaces.py test/test-skf-recommend-scope-type.py test/test-skf-detect-language.py test/test-skf-language-corpora.py test/test-skf-merge-doc-urls.py test/test-skf-derive-assembly-shape.py test/test-skf-description-guard.py test/test-skf-detect-scripts-assets.py test/test-skf-hash-content.py test/test-skf-validate-brief-schema.py test/test-skf-check-workspace-drift.py test/test-skf-update-active-symlink.py test/test-skf-build-change-manifest.py test/test-skf-provenance-gap-dispatch.py test/test-skf-resolve-authoritative-files.py test/test-skf-scan-manifests.py test/test-skf-pair-intersect.py test/test-skf-enumerate-stack-skills.py test/test-skf-compare-file-hashes.py test/test-skf-load-provenance.py test/test-skf-scan-skill-md-structure.py test/test-skf-disqualify-candidates.py test/test-skf-chain-reachability.py test/test-skf-shape-detect.py test/test-skf-detect-docs.py test/test-skf-step-doc-sources.py test/test-skf-step-doc-drift.py test/test-skf-step-hard-gate.py test/test-skf-per-pipeline-thresholds.py test/test-skf-evidence-report-fallback.py test/test-skf-validate-pins.py test/test-skf-auto-shard.py test/test-skf-preapply.py test/test-skf-step-doc-rot.py test/test-skf-campaign-state.py test/test-skf-campaign-pins.py test/test-skf-campaign-deps.py test/test-skf-campaign-report.py test/test-skf-campaign-stepfiles.py test/test-skf-campaign-validate-state.py test/test-skf-campaign-provenance.py test/test-skf-campaign-parse-manifest.py test/test-skf-campaign-render-kickoff.py -v",
|
|
56
|
+
"test:python": "uv run --with pytest --with pyyaml --with jsonschema pytest test/test-compute-score-contract.py test/test-skf-preflight.py test/test-skf-skill-inventory.py test/test-skf-validate-output.py test/test-skf-validate-frontmatter.py test/test-skf-manifest-ops.py test/test-skf-rebuild-managed-sections.py test/test-skf-atomic-write.py test/test-skf-severity-classify.py test/test-skf-structural-diff.py test/test-skf-detect-tools.py test/test-skf-forge-tier-rw.py test/test-skf-emit-result-envelope.py test/test-skf-qmd-classify-collections.py test/test-skf-merge-ccc-exclusions.py test/test-skf-resolve-package.py test/test-skf-extract-public-api.py test/test-skf-render-quick-metadata.py test/test-skf-validate-brief-inputs.py test/test-skf-emit-brief-result-envelope.py test/test-skf-write-skill-brief.py test/test-skf-detect-workspaces.py test/test-skf-recommend-scope-type.py test/test-skf-detect-language.py test/test-skf-language-corpora.py test/test-skf-merge-doc-urls.py test/test-skf-derive-assembly-shape.py test/test-skf-description-guard.py test/test-skf-detect-scripts-assets.py test/test-skf-hash-content.py test/test-skf-validate-brief-schema.py test/test-skf-check-workspace-drift.py test/test-skf-update-active-symlink.py test/test-skf-build-change-manifest.py test/test-skf-provenance-gap-dispatch.py test/test-skf-resolve-authoritative-files.py test/test-skf-scan-manifests.py test/test-skf-pair-intersect.py test/test-skf-enumerate-stack-skills.py test/test-skf-compare-file-hashes.py test/test-skf-load-provenance.py test/test-skf-scan-skill-md-structure.py test/test-skf-disqualify-candidates.py test/test-skf-chain-reachability.py test/test-skf-shape-detect.py test/test-skf-detect-docs.py test/test-skf-step-doc-sources.py test/test-skf-step-doc-drift.py test/test-skf-step-hard-gate.py test/test-skf-per-pipeline-thresholds.py test/test-skf-evidence-report-fallback.py test/test-skf-validate-pins.py test/test-skf-auto-shard.py test/test-skf-preapply.py test/test-skf-step-doc-rot.py test/test-skf-campaign-state.py test/test-skf-campaign-pins.py test/test-skf-campaign-deps.py test/test-skf-campaign-report.py test/test-skf-campaign-stepfiles.py test/test-skf-campaign-validate-state.py test/test-skf-campaign-provenance.py test/test-skf-campaign-parse-manifest.py test/test-skf-campaign-render-kickoff.py test/test-skf-campaign-render-batch.py test/test-skf-shard-body.py test/test-skf-render-metadata-stats.py test/test-skf-comention-pairs.py test/test-skf-count-tokens.py test/test-skf-reconcile-coverage.py test/test-skf-verify-provenance-completeness.py test/test-skf-find-cycles.py test/test-skf-validate-feasibility-report.py test/test-skf-rewrite-skill-name.py test/test-skf-verify-no-trace.py test/test-skf-validate-inventory.py test/test-skf-check-metadata-coherence.py test/test-skf-scan-doc-rot.py test/test-skf-aggregate-coherence.py test/test-skf-dir-sizes.py test/test-skf-parse-pipeline.py test/test-skf-validate-rename-name.py test/test-skf-verify-declared-numerator.py test/test-skf-combine-external-scores.py test/test-skf-new-file-diff.py test/test-skf-coverage-tally.py test/test-skf-report-delta.py test/test-skf-verdict-rollup.py -v",
|
|
57
57
|
"test:schemas": "node test/test-agent-schema.js",
|
|
58
58
|
"test:workflow": "node test/test-workflow-state.js",
|
|
59
59
|
"validate:refs": "node tools/validate-file-refs.js --strict",
|
|
@@ -143,7 +143,7 @@ EX → agentskills.io bundle + context snippets (flat platform paths)
|
|
|
143
143
|
|
|
144
144
|
VS → feasibility-report-{project_name}.md (verdict + integration verdicts)
|
|
145
145
|
↓ (report consumed by RA)
|
|
146
|
-
RA → refined-architecture-{
|
|
146
|
+
RA → refined-architecture-{arch_project_name}.md (gaps filled, issues flagged, improvements suggested)
|
|
147
147
|
↓ (refined doc consumed by SS compose-mode)
|
|
148
148
|
SS (compose) → SKILL.md (stack skill synthesized from individual skills + architecture)
|
|
149
149
|
```
|
package/src/module-help.csv
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module,skill,display-name,menu-code,description,action,args,phase,
|
|
1
|
+
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
|
|
2
2
|
skf,skf-forger,Ferris — Skill Forge Agent,FF,"Skill compilation specialist — the forge master. Invoke to access all SKF workflows via guided menu.",,,anytime,,,false,,
|
|
3
3
|
skf,skf-setup,Setup Forge,SF,"Initialize forge environment — detect tools and set capability tier (Quick/Forge/Forge+/Deep)",,,anytime,,"skf-analyze-source,skf-brief-skill,skf-quick-skill,skf-verify-stack,skf-campaign",false,,forge-tier.yaml
|
|
4
4
|
skf,skf-campaign,Campaign,CA,"Orchestrate multi-library skill production across sessions — dependency tracking, file-based state, and resume",,resume [--from=<skill>],anytime,skf-setup,,false,forge_data_folder,_campaign-state.yaml
|
|
@@ -10,7 +10,7 @@ skf,skf-create-stack-skill,Stack Skill,SS,"Consolidated project stack skill with
|
|
|
10
10
|
skf,skf-update-skill,Update Skill,US,"Smart regeneration preserving [MANUAL] sections after source changes",,,anytime,skf-export-skill,,false,skills_output_folder,SKILL.md
|
|
11
11
|
skf,skf-audit-skill,Audit Skill,AS,"Drift detection between skill and current source code",,,anytime,skf-export-skill,,false,,drift report
|
|
12
12
|
skf,skf-verify-stack,Verify Stack,VS,"Pre-code stack feasibility verification against architecture and PRD documents",,,anytime,skf-setup,skf-refine-architecture,false,forge_data_folder,feasibility-report-{project_name}.md
|
|
13
|
-
skf,skf-refine-architecture,Refine Architecture,RA,"Improve architecture doc using verified skill data and VS feasibility findings",,,anytime,skf-verify-stack,,false,output_folder,refined-architecture-{
|
|
13
|
+
skf,skf-refine-architecture,Refine Architecture,RA,"Improve architecture doc using verified skill data and VS feasibility findings",,,anytime,skf-verify-stack,,false,output_folder,refined-architecture-{arch_project_name}.md
|
|
14
14
|
skf,skf-test-skill,Test Skill,TS,"Cognitive completeness verification — quality gate before export",,,anytime,"skf-create-skill,skf-create-stack-skill,skf-quick-skill",skf-export-skill,false,,completeness score
|
|
15
15
|
skf,skf-export-skill,Export Skill,EX,"Package for distribution and inject context into CLAUDE.md/AGENTS.md/.cursorrules",,,anytime,skf-test-skill,"skf-update-skill,skf-audit-skill,skf-rename-skill,skf-drop-skill",false,skills_output_folder,agentskills.io package
|
|
16
16
|
skf,skf-rename-skill,Rename Skill,RS,"Rename a skill across all its versions — transactional copy-verify-delete with platform context rebuild",,,anytime,skf-export-skill,,false,skills_output_folder,renamed skill group
|
|
@@ -151,7 +151,36 @@ The `section-slug` is a kebab-case normalized section heading (e.g. `missing-sta
|
|
|
151
151
|
|
|
152
152
|
**2. Check the local seen-cache** at `{seenCachePath}`:
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
Read `{seenCachePath}`. A missing file, an empty file, or one that does not parse as JSON is an empty cache — never an error, never a reason to halt.
|
|
155
|
+
|
|
156
|
+
**Presence in the cache is not the verdict.** A fingerprint whose defect was already fixed must NOT suppress: a fresh sighting of a fixed defect is a *regression*, and it is the single most valuable report this loop can produce. Look up the fingerprint and work these branches in order, stopping at the first that matches:
|
|
157
|
+
|
|
158
|
+
1. **No record for this fingerprint** → `unseen`. Go to sub-step 3.
|
|
159
|
+
2. **Record with `"action": "resolved"`** → `regression`. Recorded by hand when the defect was fixed. Go to sub-step 3 and apply the regression treatment below.
|
|
160
|
+
3. **Record with `"action"` of `created`, `reacted`, or `commented`, and a non-empty `issue_url`** → you must decide whether that issue is still live before you may suppress. The cache is machine-local and records only what *this* user did; the issue's own state is shared, so it is the better authority. Run exactly this, and give it a hard time bound so a hung network can never strand the run:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
timeout 10 gh issue view {issue_url} --json state,stateReason --jq '.state + "/" + (.stateReason // "")'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `CLOSED/COMPLETED`, or `MERGED` (the record points at a resolving PR) → `regression`. It was fixed and closed out.
|
|
167
|
+
- `CLOSED/NOT_PLANNED` → `handled`. **Not** a fix: this is how the dedup Action closes a duplicate, and how a maintainer closes a wontfix. Treating it as a regression would file a REGRESSION issue against a defect nobody ever fixed, every session, forever.
|
|
168
|
+
- `OPEN` → `handled`. Already filed and still being tracked.
|
|
169
|
+
- Any failure at all (offline, timeout, no auth, deleted issue, unparseable output) → `handled`. This refinement never blocks a run and never halts on error.
|
|
170
|
+
4. **Record with one of those actions but an empty `issue_url`** → `handled`. There is no issue to consult, so the local record stands.
|
|
171
|
+
5. **Record with no `action`, or any other value** → `unrecognized`. Hand-edited, or written by a newer version of this file, so it cannot be confirmed handled. Go to sub-step 3 as an ordinary submission.
|
|
172
|
+
|
|
173
|
+
**`handled` is the only verdict that suppresses.** Skip submission silently and log: `"fp-{hash}: already handled on {date}, issue {url} — skipping"`. Every other verdict proceeds to sub-step 3.
|
|
174
|
+
|
|
175
|
+
**Why anything uncertain reports instead of suppressing.** A duplicate report is self-healing: the dedup Action closes it and moves the upvote to the canonical issue. A silenced regression is caught by nothing at all. When the verdict is in doubt, report.
|
|
176
|
+
|
|
177
|
+
**Regression treatment** (verdicts `regression` only). Carry the prior record forward so the recurrence can never be filed as a first sighting:
|
|
178
|
+
|
|
179
|
+
- Append ` — REGRESSION` to the issue title, after `{short description}`.
|
|
180
|
+
- Add a `## Regression` section immediately above `## Finding`, containing one line: `Regression of {issue_url}, resolved {date}.` If the prior record has no `issue_url`, write `Regression of a previously resolved local finding, resolved {date}.` The `{fp}` label still ties the two together server-side.
|
|
181
|
+
- Leave `## Finding` and the other one-sentence sections exactly as specified below. The backlink lives in its own section precisely so those budgets are untouched.
|
|
182
|
+
|
|
183
|
+
**What this does and does not catch.** The fingerprint is `sha1` over the section slug, so a fix that renames or relocates the section it touches changes the fingerprint. Such a regression arrives as `unseen` and is reported as a first sighting, which is safe but unlinked. These branches catch the regressions that recur under the same heading in the same step file, which is the case where the old behaviour was silent.
|
|
155
184
|
|
|
156
185
|
**3. Check GitHub CLI availability** with `gh auth status`. If `gh` is unavailable, fall through to step 5c (local fallback).
|
|
157
186
|
|
|
@@ -205,9 +234,11 @@ gh issue create \
|
|
|
205
234
|
--body "{formatted body using issue template structure}"
|
|
206
235
|
```
|
|
207
236
|
|
|
237
|
+
When sub-step 2 returned `regression`, append ` — REGRESSION` to that `--title` value, after `{short description}`, and include the `## Regression` backlink section in the body. The command above is the literal one you run, so the marker has to be in it: an instruction that the title should say REGRESSION loses to a copied command that does not.
|
|
238
|
+
|
|
208
239
|
The fingerprint `{fp}` appears in both title (human-readable) and label (server-side filterable). Maintainers can query all reports for a defect via the `fp-*` label without relying on title text.
|
|
209
240
|
|
|
210
|
-
After the issue is created, write the fingerprint → issue-url mapping to the seen-cache at `{seenCachePath}` so this user
|
|
241
|
+
After the issue is created, write the fingerprint → issue-url mapping to the seen-cache at `{seenCachePath}` so this user does not re-report it while that issue stays open.
|
|
211
242
|
|
|
212
243
|
**Writing rules — non-negotiable:**
|
|
213
244
|
|
|
@@ -300,7 +331,13 @@ After each successful `gh issue create`, append to `{seenCachePath}`:
|
|
|
300
331
|
}
|
|
301
332
|
```
|
|
302
333
|
|
|
303
|
-
Ensure the parent directory exists. This file is global across the user's machine — not per-project — so the same defect is
|
|
334
|
+
Ensure the parent directory exists. This file is global across the user's machine — not per-project — so the same defect is not re-reported across different repos the user works in while its issue stays open.
|
|
335
|
+
|
|
336
|
+
**Merge, never replace.** Read the existing file, set this one key, write the whole object back. A write that emits only the new key silently discards every other fingerprint the user has ever recorded.
|
|
337
|
+
|
|
338
|
+
**Read before you write, and never downgrade a `resolved` record.** Before setting the key, check whether one already exists with `"action": "resolved"`. If it does, and you are here because that resolved defect just recurred, you are recording the regression's new issue: overwrite it with `created` and the new URL, which is correct because a live issue now tracks the recurrence. In every other case leave a `resolved` record alone. The record must never move from `resolved` back to a suppressing action without a new open issue to justify it, or the next run silently re-arms the suppression the fix removed.
|
|
339
|
+
|
|
340
|
+
**The `action` vocabulary is `created`, `reacted`, `commented`, `resolved`.** The first three are written by this workflow. `resolved` is written by hand when the defect is fixed, and it is the only one that does not suppress. See the health-check section of `CONTRIBUTING.md` for when to record it.
|
|
304
341
|
|
|
305
342
|
### 5c. Local-Queue Path (gh unavailable OR friction/gap default)
|
|
306
343
|
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF Co-mention Pairs — compute compose-mode co-mention pairs from an
|
|
7
|
+
architecture document with the same precision guards the prompt used to run
|
|
8
|
+
by hand.
|
|
9
|
+
|
|
10
|
+
`detect-integrations.md` §2 (compose branch) asks the LLM to scan an
|
|
11
|
+
architecture markdown document for skill-name co-mentions using three
|
|
12
|
+
deterministic guards:
|
|
13
|
+
|
|
14
|
+
1. Word-boundary matching (`\\b{skill_name}\\b`, case-insensitive) — no
|
|
15
|
+
substring false positives (`react` must not match inside `reactive`).
|
|
16
|
+
2. Section filtering — paragraphs governed by an H1/H2 header that
|
|
17
|
+
normalises to one of {introduction, overview, glossary, table of
|
|
18
|
+
contents, references, appendix, index} are excluded, and heading text
|
|
19
|
+
itself is never a co-mention source.
|
|
20
|
+
3. Two-paragraph minimum — a pair (A, B) qualifies only when at least two
|
|
21
|
+
distinct body paragraphs each contain word-boundary matches for both A
|
|
22
|
+
and B.
|
|
23
|
+
|
|
24
|
+
Given a fixed architecture doc and skill-name set the qualifying-pair set is
|
|
25
|
+
fully determined, so this belongs in a deterministic helper rather than
|
|
26
|
+
in-prompt regex/counting. It mirrors the co-import subprocess the code-mode
|
|
27
|
+
branch already delegates and the `skf-pair-intersect.py` pre-pass. The LLM
|
|
28
|
+
still does the semantic composition (integration-section authoring, tier
|
|
29
|
+
inheritance, VS-verdict parsing) — the script only emits the qualifying pairs
|
|
30
|
+
and their evidence paragraphs.
|
|
31
|
+
|
|
32
|
+
Subcommand:
|
|
33
|
+
comention --doc <md-file-or-'-'> --skills <json-file-or-'-'>
|
|
34
|
+
--doc path to the architecture markdown document, or '-' for stdin.
|
|
35
|
+
--skills path to a JSON array of loaded skill names
|
|
36
|
+
(e.g. `["react", "express"]`), or '-' for stdin.
|
|
37
|
+
(--doc and --skills cannot both read from stdin.)
|
|
38
|
+
|
|
39
|
+
Emit JSON:
|
|
40
|
+
{
|
|
41
|
+
"pairs": [
|
|
42
|
+
{
|
|
43
|
+
"a": "<skill>", "b": "<skill>",
|
|
44
|
+
"paragraph_count": N,
|
|
45
|
+
"evidence": [{"header": "<governing-header-or-null>",
|
|
46
|
+
"excerpt": "<collapsed paragraph text>"}, ...]
|
|
47
|
+
},
|
|
48
|
+
...
|
|
49
|
+
],
|
|
50
|
+
"excluded_section_count": M
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Only unordered pairs (a < b lexicographically) with paragraph_count >= 2
|
|
54
|
+
are emitted. `pairs[]` is sorted by paragraph_count DESC, then (a, b)
|
|
55
|
+
ASC — stable, mirroring skf-pair-intersect.py. `evidence[]` is in
|
|
56
|
+
document order. `excluded_section_count` is the number of DISTINCT
|
|
57
|
+
excluded H1/H2 sections (by normalised header) that governed at least
|
|
58
|
+
one body paragraph — a diagnostic for the caller, not a gate.
|
|
59
|
+
|
|
60
|
+
CLI examples:
|
|
61
|
+
uv run skf-comention-pairs.py comention --doc arch.md --skills skills.json
|
|
62
|
+
echo '["react","express"]' | uv run skf-comention-pairs.py comention \\
|
|
63
|
+
--doc arch.md --skills -
|
|
64
|
+
|
|
65
|
+
Exit codes:
|
|
66
|
+
0 — operation succeeded (including: no qualifying pairs → empty list)
|
|
67
|
+
1 — user error (bad JSON, missing/unreadable file, both inputs on stdin,
|
|
68
|
+
malformed skills array)
|
|
69
|
+
2 — unexpected internal error
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
from __future__ import annotations
|
|
73
|
+
|
|
74
|
+
import argparse
|
|
75
|
+
import json
|
|
76
|
+
import re
|
|
77
|
+
import sys
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Closed exclusion set — governing H1/H2 headers that normalise to one of
|
|
82
|
+
# these drop their paragraphs from co-mention analysis (they typically
|
|
83
|
+
# enumerate all libraries without describing an integration).
|
|
84
|
+
EXCLUDED_HEADERS = frozenset({
|
|
85
|
+
"introduction",
|
|
86
|
+
"overview",
|
|
87
|
+
"glossary",
|
|
88
|
+
"table of contents",
|
|
89
|
+
"references",
|
|
90
|
+
"appendix",
|
|
91
|
+
"index",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# ATX header line: 1-6 leading hashes followed by at least one space/tab.
|
|
95
|
+
# A line like `#foo` (no space) is NOT a header per CommonMark and is treated
|
|
96
|
+
# as body text. Capture group 1 = hashes (level), group 2 = header text.
|
|
97
|
+
_HEADER_RE = re.compile(r"^(#{1,6})[ \t]+(.*)$")
|
|
98
|
+
|
|
99
|
+
_EXCERPT_LIMIT = 240
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class UserError(Exception):
|
|
103
|
+
"""A caller-facing input error → exit code 1."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# --------------------------------------------------------------------------
|
|
107
|
+
# Markdown parsing
|
|
108
|
+
# --------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _strip_closing_hashes(text: str) -> str:
|
|
112
|
+
"""Remove a closed-ATX trailing run of hashes (e.g. `Overview ##`)."""
|
|
113
|
+
return re.sub(r"[ \t]*#+[ \t]*$", "", text)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def normalize_header(text: str) -> str:
|
|
117
|
+
"""Normalise a header for exclusion-set comparison: lowercase, collapse
|
|
118
|
+
internal whitespace, strip trailing punctuation."""
|
|
119
|
+
t = text.strip().lower()
|
|
120
|
+
t = re.sub(r"\s+", " ", t).strip()
|
|
121
|
+
t = t.rstrip(".,:;!?").strip()
|
|
122
|
+
return t
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_body_paragraphs(doc_text: str) -> list[tuple[str | None, str]]:
|
|
126
|
+
"""Split the document into body paragraphs, each tagged with the raw text
|
|
127
|
+
of its governing (nearest-preceding) H1/H2 header (or None).
|
|
128
|
+
|
|
129
|
+
Paragraphs are maximal runs of consecutive non-blank, non-header lines,
|
|
130
|
+
separated by blank lines. Header lines of ANY level are never emitted as
|
|
131
|
+
paragraphs (headings are not a co-mention source). Only H1/H2 headers
|
|
132
|
+
update the governing header; H3-H6 leave it unchanged.
|
|
133
|
+
"""
|
|
134
|
+
paragraphs: list[tuple[str | None, str]] = []
|
|
135
|
+
governing: str | None = None
|
|
136
|
+
buf: list[str] = []
|
|
137
|
+
|
|
138
|
+
def _flush() -> None:
|
|
139
|
+
if buf:
|
|
140
|
+
text = "\n".join(buf).strip()
|
|
141
|
+
if text:
|
|
142
|
+
paragraphs.append((governing, text))
|
|
143
|
+
buf.clear()
|
|
144
|
+
|
|
145
|
+
for raw_line in doc_text.splitlines():
|
|
146
|
+
m = _HEADER_RE.match(raw_line)
|
|
147
|
+
if m is not None:
|
|
148
|
+
# A header terminates the current paragraph; the header line
|
|
149
|
+
# itself is not body content.
|
|
150
|
+
_flush()
|
|
151
|
+
level = len(m.group(1))
|
|
152
|
+
header_text = _strip_closing_hashes(m.group(2)).strip()
|
|
153
|
+
if level <= 2:
|
|
154
|
+
governing = header_text if header_text else None
|
|
155
|
+
continue
|
|
156
|
+
if raw_line.strip() == "":
|
|
157
|
+
_flush()
|
|
158
|
+
continue
|
|
159
|
+
buf.append(raw_line)
|
|
160
|
+
_flush()
|
|
161
|
+
return paragraphs
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------------------
|
|
165
|
+
# Co-mention analysis
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _make_excerpt(text: str, limit: int = _EXCERPT_LIMIT) -> str:
|
|
170
|
+
"""Collapse whitespace and truncate for a stable, compact evidence quote."""
|
|
171
|
+
collapsed = re.sub(r"\s+", " ", text).strip()
|
|
172
|
+
if len(collapsed) > limit:
|
|
173
|
+
return collapsed[: limit - 3].rstrip() + "..."
|
|
174
|
+
return collapsed
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def analyze(doc_text: str, skill_names: list[str]) -> dict:
|
|
178
|
+
"""Compute qualifying co-mention pairs and their evidence.
|
|
179
|
+
|
|
180
|
+
Deterministic: same (doc_text, skill_names) → identical output.
|
|
181
|
+
"""
|
|
182
|
+
names = sorted(set(skill_names))
|
|
183
|
+
patterns = {
|
|
184
|
+
n: re.compile(r"\b" + re.escape(n) + r"\b", re.IGNORECASE)
|
|
185
|
+
for n in names
|
|
186
|
+
}
|
|
187
|
+
paragraphs = parse_body_paragraphs(doc_text)
|
|
188
|
+
|
|
189
|
+
excluded_sections: set[str] = set()
|
|
190
|
+
# (a, b) -> list of evidence dicts, one per qualifying paragraph
|
|
191
|
+
pair_evidence: dict[tuple[str, str], list[dict]] = {}
|
|
192
|
+
|
|
193
|
+
for governing, text in paragraphs:
|
|
194
|
+
if governing is not None:
|
|
195
|
+
norm = normalize_header(governing)
|
|
196
|
+
if norm in EXCLUDED_HEADERS:
|
|
197
|
+
excluded_sections.add(norm)
|
|
198
|
+
continue
|
|
199
|
+
# names is sorted, so `present` is sorted → pairs are (a < b)
|
|
200
|
+
present = [n for n in names if patterns[n].search(text)]
|
|
201
|
+
if len(present) < 2:
|
|
202
|
+
continue
|
|
203
|
+
excerpt = _make_excerpt(text)
|
|
204
|
+
evidence_entry = {"header": governing, "excerpt": excerpt}
|
|
205
|
+
for i in range(len(present)):
|
|
206
|
+
for j in range(i + 1, len(present)):
|
|
207
|
+
key = (present[i], present[j])
|
|
208
|
+
pair_evidence.setdefault(key, []).append(evidence_entry)
|
|
209
|
+
|
|
210
|
+
pairs: list[dict] = []
|
|
211
|
+
for (a, b), evidence in pair_evidence.items():
|
|
212
|
+
if len(evidence) >= 2:
|
|
213
|
+
pairs.append({
|
|
214
|
+
"a": a,
|
|
215
|
+
"b": b,
|
|
216
|
+
"paragraph_count": len(evidence),
|
|
217
|
+
"evidence": evidence,
|
|
218
|
+
})
|
|
219
|
+
pairs.sort(key=lambda p: (-p["paragraph_count"], p["a"], p["b"]))
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"pairs": pairs,
|
|
223
|
+
"excluded_section_count": len(excluded_sections),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# --------------------------------------------------------------------------
|
|
228
|
+
# CLI
|
|
229
|
+
# --------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _read_source(source: str, label: str) -> str:
|
|
233
|
+
"""Read text from a file path or stdin (if source == '-')."""
|
|
234
|
+
if source == "-":
|
|
235
|
+
try:
|
|
236
|
+
return sys.stdin.read()
|
|
237
|
+
except OSError as exc:
|
|
238
|
+
raise UserError(f"failed to read {label} from stdin: {exc}") from exc
|
|
239
|
+
path = Path(source)
|
|
240
|
+
if not path.is_file():
|
|
241
|
+
raise UserError(f"{label} file not found: {path}")
|
|
242
|
+
try:
|
|
243
|
+
return path.read_text(encoding="utf-8")
|
|
244
|
+
except OSError as exc:
|
|
245
|
+
raise UserError(f"failed to read {label} file {path}: {exc}") from exc
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_skills(raw_text: str) -> list[str]:
|
|
249
|
+
"""Parse and validate the --skills JSON array of non-empty strings."""
|
|
250
|
+
try:
|
|
251
|
+
data = json.loads(raw_text)
|
|
252
|
+
except json.JSONDecodeError as exc:
|
|
253
|
+
raise UserError(f"malformed JSON in --skills input: {exc}") from exc
|
|
254
|
+
if not isinstance(data, list):
|
|
255
|
+
raise UserError(
|
|
256
|
+
"--skills input must be a JSON array of strings; "
|
|
257
|
+
f"got {type(data).__name__}"
|
|
258
|
+
)
|
|
259
|
+
names: list[str] = []
|
|
260
|
+
for idx, item in enumerate(data):
|
|
261
|
+
if not isinstance(item, str) or not item:
|
|
262
|
+
raise UserError(
|
|
263
|
+
f"--skills[{idx}] must be a non-empty string; got {item!r}"
|
|
264
|
+
)
|
|
265
|
+
names.append(item)
|
|
266
|
+
return names
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _cmd_comention(args: argparse.Namespace) -> int:
|
|
270
|
+
if args.doc == "-" and args.skills == "-":
|
|
271
|
+
raise UserError(
|
|
272
|
+
"--doc and --skills cannot both read from stdin ('-')"
|
|
273
|
+
)
|
|
274
|
+
# Read the file-backed input first so a single stdin source stays intact.
|
|
275
|
+
if args.doc == "-":
|
|
276
|
+
skills_text = _read_source(args.skills, "--skills")
|
|
277
|
+
doc_text = _read_source(args.doc, "--doc")
|
|
278
|
+
else:
|
|
279
|
+
doc_text = _read_source(args.doc, "--doc")
|
|
280
|
+
skills_text = _read_source(args.skills, "--skills")
|
|
281
|
+
skill_names = parse_skills(skills_text)
|
|
282
|
+
|
|
283
|
+
result = analyze(doc_text, skill_names)
|
|
284
|
+
if args.verbose:
|
|
285
|
+
print(
|
|
286
|
+
f"analyzed {len(set(skill_names))} distinct skill names; "
|
|
287
|
+
f"{len(result['pairs'])} qualifying pairs; "
|
|
288
|
+
f"{result['excluded_section_count']} excluded sections",
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
291
|
+
json.dump(result, sys.stdout, indent=2)
|
|
292
|
+
sys.stdout.write("\n")
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
297
|
+
parser = argparse.ArgumentParser(
|
|
298
|
+
prog="skf-comention-pairs",
|
|
299
|
+
description=(
|
|
300
|
+
"Compute compose-mode co-mention pairs from an architecture "
|
|
301
|
+
"document (detect-integrations.md §2 compose branch)."
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
305
|
+
|
|
306
|
+
p_cm = sub.add_parser(
|
|
307
|
+
"comention",
|
|
308
|
+
help=(
|
|
309
|
+
"emit qualifying co-mention pairs (>=2 body paragraphs, "
|
|
310
|
+
"word-boundary, section-filtered) as JSON"
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
p_cm.add_argument(
|
|
314
|
+
"--doc",
|
|
315
|
+
required=True,
|
|
316
|
+
help="path to the architecture markdown document, or '-' for stdin",
|
|
317
|
+
)
|
|
318
|
+
p_cm.add_argument(
|
|
319
|
+
"--skills",
|
|
320
|
+
required=True,
|
|
321
|
+
help=(
|
|
322
|
+
"path to a JSON array of loaded skill names, or '-' for stdin. "
|
|
323
|
+
"Shape: [\"<skill>\", ...]"
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
p_cm.add_argument(
|
|
327
|
+
"--verbose",
|
|
328
|
+
action="store_true",
|
|
329
|
+
help="print a one-line summary to stderr",
|
|
330
|
+
)
|
|
331
|
+
p_cm.set_defaults(func=_cmd_comention)
|
|
332
|
+
|
|
333
|
+
return parser
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main(argv: list[str] | None = None) -> int:
|
|
337
|
+
parser = _build_parser()
|
|
338
|
+
args = parser.parse_args(argv)
|
|
339
|
+
try:
|
|
340
|
+
return args.func(args)
|
|
341
|
+
except UserError as exc:
|
|
342
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
343
|
+
return 1
|
|
344
|
+
except Exception as exc: # noqa: BLE001 — last-resort guard → exit 2
|
|
345
|
+
print(f"internal error: {exc}", file=sys.stderr)
|
|
346
|
+
return 2
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
raise SystemExit(main())
|