bmad-module-skill-forge 2.0.1 → 2.1.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/docs/404.md +4 -4
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/RELEASING.md +7 -4
- package/docs/agents.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/bmad-synergy.md +13 -13
- package/docs/campaign.md +3 -3
- package/docs/concepts.md +3 -3
- package/docs/examples.md +9 -9
- package/docs/forge-auto.md +6 -6
- package/docs/getting-started.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/index.md +7 -1
- package/docs/skill-model.md +2 -2
- package/docs/troubleshooting.md +7 -7
- package/docs/why-skf.md +8 -8
- package/docs/workflows.md +6 -6
- package/package.json +7 -3
- package/src/shared/health-check.md +40 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -0
- package/src/shared/scripts/skf-detect-language.py +20 -0
- package/src/shared/scripts/skf-detect-tools.py +34 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +20 -0
- package/src/shared/scripts/skf-language-corpora.py +20 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +20 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +44 -2
- package/src/shared/scripts/skf-recommend-scope-type.py +20 -0
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-shape-detect.py +20 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +2 -1
- package/src/skf-analyze-source/references/recommend.md +1 -1
- package/src/skf-audit-skill/references/re-index.md +1 -1
- package/src/skf-brief-skill/assets/description-voice-examples.md +5 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/analyze-target.md +2 -2
- package/src/skf-brief-skill/references/gather-intent.md +4 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +3 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -2
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/compile.md +12 -3
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +5 -3
- package/src/skf-create-skill/references/step-doc-rot.md +23 -6
- package/src/skf-create-skill/scripts/scan-doc-rot.py +191 -27
- package/src/skf-create-stack-skill/references/detect-integrations.md +1 -1
- package/src/skf-setup/references/ccc-index.md +2 -2
- package/src/skf-setup/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +11 -5
- package/src/skf-test-skill/references/score.md +7 -1
- package/src/skf-test-skill/scripts/aggregate-coherence.py +13 -3
- package/src/skf-test-skill/scripts/compute-score.py +17 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +36 -9
- package/tools/cli/lib/installer.js +31 -1
- package/tools/validate-docs-links.js +665 -0
package/docs/why-skf.md
CHANGED
|
@@ -44,7 +44,7 @@ For docs-only skills, the audit shape is the same — `provenance-map.json` stil
|
|
|
44
44
|
|
|
45
45
|
If it doesn't, **that's a bug.** [Open an issue](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/new/choose) and SKF republishes the skill with a new commit SHA and a new provenance map. No other AI-skills tool treats disagreement between claim and source as a defect. SKF does.
|
|
46
46
|
|
|
47
|
-
See the [Verifying a Skill](
|
|
47
|
+
See the [Verifying a Skill](/docs/verifying-a-skill.md) page for the full three-step audit on real skills, the test reports that log *exactly* where coverage falls short, and the scoring formula behind the 80% pass threshold.
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -53,27 +53,27 @@ See the [Verifying a Skill](../verifying-a-skill/) page for the full three-step
|
|
|
53
53
|
### The curious developer
|
|
54
54
|
|
|
55
55
|
Your agent just hallucinated a method that doesn't exist, again. You want this to stop, and you don't want to read a long architecture reference before running your first command.
|
|
56
|
-
→ Start with [Getting Started](
|
|
56
|
+
→ Start with [Getting Started](/docs/getting-started.md).
|
|
57
57
|
|
|
58
58
|
### The BMAD user
|
|
59
59
|
|
|
60
60
|
You already use BMAD Method, BMM phases, TEA, or BMB, and you want to know where SKF fits.
|
|
61
|
-
→ Read [BMAD Synergy](
|
|
61
|
+
→ Read [BMAD Synergy](/docs/bmad-synergy.md) for the phase-by-phase integration playbook.
|
|
62
62
|
|
|
63
63
|
### The skeptic
|
|
64
64
|
|
|
65
65
|
"AI docs for AI" sounds like the problem pretending to be the solution. You want receipts before you install anything.
|
|
66
|
-
→ Start with [Verifying a Skill](
|
|
66
|
+
→ Start with [Verifying a Skill](/docs/verifying-a-skill.md) — the three-step audit on real skills, including the 1% that fails.
|
|
67
67
|
|
|
68
68
|
### The OSS maintainer
|
|
69
69
|
|
|
70
70
|
You want to ship verified skills alongside your library releases — `npx skills publish`-ready, drift-detectable, version-pinned.
|
|
71
|
-
→ See [Examples → OSS Maintainer Publishing Official Skills](
|
|
71
|
+
→ See [Examples → OSS Maintainer Publishing Official Skills](/docs/examples.md#scenario-h-oss-maintainer-publishing-official-skills).
|
|
72
72
|
|
|
73
73
|
### The team lead evaluating adoption
|
|
74
74
|
|
|
75
75
|
You're considering running SKF across a brownfield platform. You need to know about rollback safety, `[MANUAL]` section preservation, and the health-check feedback loop before committing.
|
|
76
|
-
→ Start with [Architecture](
|
|
76
|
+
→ Start with [Architecture](/docs/architecture.md), then [Workflows → Workflow Health Check](/docs/workflows.md#terminal-step-health-check).
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
@@ -89,6 +89,6 @@ Everything else is downstream of one question: *are the instructions your AI rea
|
|
|
89
89
|
|
|
90
90
|
## Next
|
|
91
91
|
|
|
92
|
-
- **[Install SKF](
|
|
93
|
-
- **[Audit a skill in 60 seconds](
|
|
92
|
+
- **[Install SKF](/docs/getting-started.md#install)** — Node ≥ 22, Python ≥ 3.10, `uv`, one `npx` command
|
|
93
|
+
- **[Audit a skill in 60 seconds](/docs/verifying-a-skill.md)** — see the receipts before you install
|
|
94
94
|
- **[Browse real skills](https://github.com/armelhbobdad/oh-my-skills)** — four Deep-tier skills, all shipping their audit trails
|
package/docs/workflows.md
CHANGED
|
@@ -3,9 +3,9 @@ title: Workflows
|
|
|
3
3
|
description: All 15 SKF workflows with commands, steps, and connection diagram
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Trigger workflows by typing commands to [Ferris](
|
|
6
|
+
Trigger workflows by typing commands to [Ferris](/docs/agents.md). See [Concepts](/docs/concepts.md) for definitions.
|
|
7
7
|
|
|
8
|
-
> Already using BMAD? See [BMAD Synergy](
|
|
8
|
+
> Already using BMAD? See [BMAD Synergy](/docs/bmad-synergy.md) for when to invoke each SKF workflow during BMM phases and alongside TEA, BMB, and GDS.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -83,7 +83,7 @@ Trigger workflows by typing commands to [Ferris](../agents/). See [Concepts](../
|
|
|
83
83
|
|
|
84
84
|
**Purpose:** Brief-less fast skill with package-to-repo resolution.
|
|
85
85
|
|
|
86
|
-
**Note:** QS is **tier-unaware** — it always runs at community tier and does not use ast-grep, CCC, or QMD even when your forge is configured at Forge+/Deep. For tier-aware compilation, use `BS → CS`, `forge`, or `forge-auto`. See [Skill Model](
|
|
86
|
+
**Note:** QS is **tier-unaware** — it always runs at community tier and does not use ast-grep, CCC, or QMD even when your forge is configured at Forge+/Deep. For tier-aware compilation, use `BS → CS`, `forge`, or `forge-auto`. See [Skill Model](/docs/skill-model.md).
|
|
87
87
|
|
|
88
88
|
**When to Use:** When you need a skill quickly — no brief needed. Accepts package names or GitHub URLs. Append `@version` to target a specific version (e.g., `@Ferris QS cognee@1.0.0`).
|
|
89
89
|
|
|
@@ -164,7 +164,7 @@ Trigger workflows by typing commands to [Ferris](../agents/). See [Concepts](../
|
|
|
164
164
|
|
|
165
165
|
**Key Steps:** Load skill → Detect mode → Coverage check → Coherence check → External validation (skill-check, tessl) → Hard gate → Score → Gap report
|
|
166
166
|
|
|
167
|
-
**Scored Categories:** Export Coverage (36%), Signature Accuracy (22%), Type Coverage (14%), Coherence (18%), External Validation (10%). Default pass threshold: **80%** (per-pipeline defaults: forge-auto 90%, forge 80%). Pass routes to Export Skill; fail routes to Update Skill with a gap report. See [Completeness Scoring](
|
|
167
|
+
**Scored Categories:** Export Coverage (36%), Signature Accuracy (22%), Type Coverage (14%), Coherence (18%), External Validation (10%). Default pass threshold: **80%** (per-pipeline defaults: forge-auto 90%, forge 80%). Pass routes to Export Skill; fail routes to Update Skill with a gap report. See [Completeness Scoring](/docs/verifying-a-skill.md#how-the-score-is-computed) for the full formula and tier adjustments.
|
|
168
168
|
|
|
169
169
|
**Agent:** Ferris (Audit mode)
|
|
170
170
|
|
|
@@ -315,7 +315,7 @@ flowchart TD
|
|
|
315
315
|
EXPORT --> MAINT[Maintenance + Campaign Report]
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
-
> **One workflow per session** (unless using pipeline mode or campaign). Each arrow in the standard and compose-mode diagrams represents a new conversation session. Campaign manages its own multi-session orchestration internally — see [Campaign Orchestration](
|
|
318
|
+
> **One workflow per session** (unless using pipeline mode or campaign). Each arrow in the standard and compose-mode diagrams represents a new conversation session. Campaign manages its own multi-session orchestration internally — see [Campaign Orchestration](/docs/campaign.md). Clear your context between workflows for best results — or use pipeline mode to chain them automatically. See [Pipeline Mode](#pipeline-mode) below.
|
|
319
319
|
|
|
320
320
|
---
|
|
321
321
|
|
|
@@ -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.1.0",
|
|
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",
|
|
@@ -44,18 +44,21 @@
|
|
|
44
44
|
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
|
45
45
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
|
46
46
|
"prepare": "husky || exit 0",
|
|
47
|
-
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run test:schemas && npm run test:install && npm run test:cli && npm run test:workflow && npm run test:python && npm run test:knowledge && npm run validate:schemas && npm run validate:skills && npm run validate:refs && npm run docs:validate-drift",
|
|
47
|
+
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run test:schemas && npm run test:install && npm run test:cli && npm run test:workflow && npm run test:python && npm run test:rehype && npm run test:docs-links-tool && npm run test:knowledge && npm run validate:schemas && npm run validate:skills && npm run validate:refs && npm run validate:docs-links && npm run docs:validate-drift",
|
|
48
48
|
"skf:install": "node tools/cli/skf-cli.js install",
|
|
49
49
|
"skf:status": "node tools/cli/skf-cli.js status",
|
|
50
50
|
"skf:uninstall": "node tools/cli/skf-cli.js uninstall",
|
|
51
51
|
"skf:update": "node tools/cli/skf-cli.js update",
|
|
52
|
-
"test": "npm run test:schemas && npm run test:install && npm run test:cli && npm run test:workflow && npm run test:python && npm run test:knowledge && npm run validate:schemas && npm run validate:skills && npm run validate:refs && npm run lint && npm run lint:md && npm run format:check",
|
|
52
|
+
"test": "npm run test:schemas && npm run test:install && npm run test:cli && npm run test:workflow && npm run test:python && npm run test:rehype && npm run test:docs-links-tool && npm run test:knowledge && npm run validate:schemas && npm run validate:skills && npm run validate:refs && npm run validate:docs-links && npm run lint && npm run lint:md && npm run format:check",
|
|
53
53
|
"test:cli": "node test/test-cli-integration.js",
|
|
54
|
+
"test:docs-links-tool": "node test/test-validate-docs-links.js",
|
|
54
55
|
"test:install": "node test/test-installation-components.js",
|
|
55
56
|
"test:knowledge": "node test/test-knowledge-base.js",
|
|
56
57
|
"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",
|
|
58
|
+
"test:rehype": "node test/test-rehype-markdown-links.js",
|
|
57
59
|
"test:schemas": "node test/test-agent-schema.js",
|
|
58
60
|
"test:workflow": "node test/test-workflow-state.js",
|
|
61
|
+
"validate:docs-links": "node tools/validate-docs-links.js --strict",
|
|
59
62
|
"validate:refs": "node tools/validate-file-refs.js --strict",
|
|
60
63
|
"validate:schemas": "node test/validate-agent-schema.js",
|
|
61
64
|
"validate:skills": "node tools/validate-skills.js --strict"
|
|
@@ -111,6 +114,7 @@
|
|
|
111
114
|
"prettier-plugin-packagejson": "^2.5.19",
|
|
112
115
|
"semver": "^7.6.3",
|
|
113
116
|
"sharp": "^0.33.5",
|
|
117
|
+
"unist-util-visit": "^5.1.0",
|
|
114
118
|
"wrap-ansi": "^7.0.0",
|
|
115
119
|
"xml2js": "^0.6.2",
|
|
116
120
|
"yaml": "^2.7.0",
|
|
@@ -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
|
|
|
@@ -84,6 +84,12 @@
|
|
|
84
84
|
"enum": ["full-library", "specific-modules", "public-api", "component-library", "reference-app", "docs-only"]
|
|
85
85
|
},
|
|
86
86
|
"include": { "type": "array", "items": { "type": "string" } },
|
|
87
|
+
"tier_a_include": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"description": "Optional narrower tier-A include list. When present it becomes the coverage-denominator basis instead of the coarse scope.include, and it exempts the brief from the denominator deflation guard in skf-test-skill references/source-access-protocol.md — so an empty or blank-item list must not be accepted: it would silence that guard while narrowing nothing.",
|
|
90
|
+
"minItems": 1,
|
|
91
|
+
"items": { "type": "string", "minLength": 1 }
|
|
92
|
+
},
|
|
87
93
|
"exclude": { "type": "array", "items": { "type": "string" } },
|
|
88
94
|
"notes": { "type": "string" },
|
|
89
95
|
"rationale": {
|
|
@@ -378,7 +378,27 @@ def _parse_argv(argv: list[str]) -> dict:
|
|
|
378
378
|
return payload
|
|
379
379
|
|
|
380
380
|
|
|
381
|
+
def _force_utf8(*streams) -> None:
|
|
382
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
383
|
+
|
|
384
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
385
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
386
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
387
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
388
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
389
|
+
test doubles without reconfigure().
|
|
390
|
+
"""
|
|
391
|
+
for stream in streams:
|
|
392
|
+
if hasattr(stream, "reconfigure"):
|
|
393
|
+
errors = getattr(stream, "errors", None)
|
|
394
|
+
if errors is None:
|
|
395
|
+
stream.reconfigure(encoding="utf-8")
|
|
396
|
+
else:
|
|
397
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
398
|
+
|
|
399
|
+
|
|
381
400
|
def main(argv: list[str]) -> int:
|
|
401
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
382
402
|
payload = _parse_argv(argv)
|
|
383
403
|
result = detect(payload)
|
|
384
404
|
json.dump(result, sys.stdout, ensure_ascii=False)
|
|
@@ -142,6 +142,29 @@ def _ok(payload: dict) -> None:
|
|
|
142
142
|
print(json.dumps(payload))
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def _resolve_outside_cwd(command: str) -> str | None:
|
|
146
|
+
"""shutil.which with a CWD-shim guard. Returns the resolved path or None.
|
|
147
|
+
|
|
148
|
+
shutil.which on Windows searches the current directory ahead of PATH,
|
|
149
|
+
and CWD here is the repo under analysis — a bare-name lookup resolving
|
|
150
|
+
into CWD would execute a repo-planted shim (e.g. ast-grep.cmd). Such a
|
|
151
|
+
resolution is treated as not-found. Explicit paths supplied by callers
|
|
152
|
+
(containing a separator) are honored as-is. Keep identical to the
|
|
153
|
+
sibling guard in skf-qmd-classify-collections.py.
|
|
154
|
+
"""
|
|
155
|
+
resolved = shutil.which(command)
|
|
156
|
+
if resolved is None:
|
|
157
|
+
return None
|
|
158
|
+
if os.sep in command or (os.altsep and os.altsep in command):
|
|
159
|
+
return resolved
|
|
160
|
+
resolved_dir = os.path.dirname(resolved)
|
|
161
|
+
if resolved_dir:
|
|
162
|
+
cwd = os.path.normcase(os.path.abspath(os.getcwd()))
|
|
163
|
+
if os.path.normcase(os.path.abspath(resolved_dir)) == cwd:
|
|
164
|
+
return None
|
|
165
|
+
return resolved
|
|
166
|
+
|
|
167
|
+
|
|
145
168
|
def _run(cmd: list[str], timeout: int = PROBE_TIMEOUT_SEC) -> tuple[int, str, str]:
|
|
146
169
|
"""Run a subprocess. Return (returncode, stdout, stderr). Never raises.
|
|
147
170
|
|
|
@@ -149,6 +172,13 @@ def _run(cmd: list[str], timeout: int = PROBE_TIMEOUT_SEC) -> tuple[int, str, st
|
|
|
149
172
|
CalledProcessError) as a failed probe — returns rc=127 and an empty
|
|
150
173
|
stdout/stderr. Tool detection should never crash the workflow.
|
|
151
174
|
|
|
175
|
+
cmd[0] is resolved through shutil.which() before spawning (with the
|
|
176
|
+
CWD-shim guard in _resolve_outside_cwd): Windows CreateProcess only
|
|
177
|
+
ever appends .exe to a bare name, so npm-installed shims
|
|
178
|
+
(ast-grep.CMD, qmd.CMD) would raise FileNotFoundError and read as
|
|
179
|
+
"tool absent". An unresolvable command returns rc=127 without
|
|
180
|
+
spawning anything.
|
|
181
|
+
|
|
152
182
|
The child is wrapped in the OS `timeout(1)` utility (when available) so a
|
|
153
183
|
daemon-backed probe blocked in an uninterruptible syscall (e.g.
|
|
154
184
|
`qmd status` waiting on its daemon socket) is reaped at the OS level —
|
|
@@ -158,6 +188,10 @@ def _run(cmd: list[str], timeout: int = PROBE_TIMEOUT_SEC) -> tuple[int, str, st
|
|
|
158
188
|
`start_new_session=True` isolates the child's process group; the
|
|
159
189
|
Python-level timeout is a secondary net set slightly above the OS one.
|
|
160
190
|
"""
|
|
191
|
+
resolved = _resolve_outside_cwd(cmd[0])
|
|
192
|
+
if resolved is None:
|
|
193
|
+
return 127, "", ""
|
|
194
|
+
cmd = [resolved, *cmd[1:]]
|
|
161
195
|
if _TIMEOUT_BIN:
|
|
162
196
|
run_cmd = [_TIMEOUT_BIN, "--kill-after=2", str(timeout), *cmd]
|
|
163
197
|
py_timeout = timeout + 5
|
|
@@ -496,7 +496,27 @@ def cmd_emit_blocked() -> None:
|
|
|
496
496
|
print(emit_envelope_line(envelope))
|
|
497
497
|
|
|
498
498
|
|
|
499
|
+
def _force_utf8(*streams) -> None:
|
|
500
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
501
|
+
|
|
502
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
503
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
504
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
505
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
506
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
507
|
+
test doubles without reconfigure().
|
|
508
|
+
"""
|
|
509
|
+
for stream in streams:
|
|
510
|
+
if hasattr(stream, "reconfigure"):
|
|
511
|
+
errors = getattr(stream, "errors", None)
|
|
512
|
+
if errors is None:
|
|
513
|
+
stream.reconfigure(encoding="utf-8")
|
|
514
|
+
else:
|
|
515
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
516
|
+
|
|
517
|
+
|
|
499
518
|
def main() -> None:
|
|
519
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
500
520
|
parser = argparse.ArgumentParser(
|
|
501
521
|
description=__doc__,
|
|
502
522
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -80,7 +80,27 @@ def corpora_for(language: str) -> list[dict]:
|
|
|
80
80
|
return out
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def _force_utf8(*streams) -> None:
|
|
84
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
85
|
+
|
|
86
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
87
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
88
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
89
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
90
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
91
|
+
test doubles without reconfigure().
|
|
92
|
+
"""
|
|
93
|
+
for stream in streams:
|
|
94
|
+
if hasattr(stream, "reconfigure"):
|
|
95
|
+
errors = getattr(stream, "errors", None)
|
|
96
|
+
if errors is None:
|
|
97
|
+
stream.reconfigure(encoding="utf-8")
|
|
98
|
+
else:
|
|
99
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
100
|
+
|
|
101
|
+
|
|
83
102
|
def main(argv: list[str]) -> int:
|
|
103
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
84
104
|
parser = argparse.ArgumentParser(
|
|
85
105
|
description="Look up canonical companion prose corpora for a language.",
|
|
86
106
|
)
|
|
@@ -213,7 +213,27 @@ def merge_doc_urls(
|
|
|
213
213
|
return kept, suppressed
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _force_utf8(*streams) -> None:
|
|
217
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
218
|
+
|
|
219
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
220
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
221
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
222
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
223
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
224
|
+
test doubles without reconfigure().
|
|
225
|
+
"""
|
|
226
|
+
for stream in streams:
|
|
227
|
+
if hasattr(stream, "reconfigure"):
|
|
228
|
+
errors = getattr(stream, "errors", None)
|
|
229
|
+
if errors is None:
|
|
230
|
+
stream.reconfigure(encoding="utf-8")
|
|
231
|
+
else:
|
|
232
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
233
|
+
|
|
234
|
+
|
|
216
235
|
def main() -> int:
|
|
236
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
217
237
|
raw = sys.stdin.read()
|
|
218
238
|
if not raw or not raw.strip():
|
|
219
239
|
sys.stderr.write("skf-merge-doc-urls: empty stdin (expected JSON object)\n")
|
|
@@ -79,7 +79,9 @@ from __future__ import annotations
|
|
|
79
79
|
|
|
80
80
|
import argparse
|
|
81
81
|
import json
|
|
82
|
+
import os
|
|
82
83
|
import re
|
|
84
|
+
import shutil
|
|
83
85
|
import sys
|
|
84
86
|
from pathlib import Path
|
|
85
87
|
|
|
@@ -92,6 +94,10 @@ FOREIGN_SAMPLE_CAP = 5
|
|
|
92
94
|
# Header line emitted by newer qmd builds, e.g. "Collections (56):".
|
|
93
95
|
_QMD_HEADER_RE = re.compile(r"^Collections \(\d+\):\s*$")
|
|
94
96
|
|
|
97
|
+
# Empty-state message from `qmd collection list`, e.g.
|
|
98
|
+
# "No collections found. Run 'qmd collection add .' to create one."
|
|
99
|
+
_QMD_EMPTY_STATE_PREFIX = "No collections found"
|
|
100
|
+
|
|
95
101
|
|
|
96
102
|
def _die(code: int, message: str) -> None:
|
|
97
103
|
print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
|
|
@@ -199,7 +205,12 @@ def parse_collection_list_output(raw: str) -> list[str]:
|
|
|
199
205
|
the trailing ` (qmd://name/)` URI and is a no-op for the bare-name form.
|
|
200
206
|
Without this, suffixed entries fail the `is_forge_owned` suffix check and
|
|
201
207
|
every forge collection is mis-classified as foreign.
|
|
208
|
+
|
|
209
|
+
The empty-state message ("No collections found. …") is recognized before
|
|
210
|
+
per-line tokenizing — it would otherwise parse as a collection named "No".
|
|
202
211
|
"""
|
|
212
|
+
if raw.lstrip().startswith(_QMD_EMPTY_STATE_PREFIX):
|
|
213
|
+
return []
|
|
203
214
|
names: list[str] = []
|
|
204
215
|
for line in raw.splitlines():
|
|
205
216
|
if not line.strip():
|
|
@@ -212,6 +223,29 @@ def parse_collection_list_output(raw: str) -> list[str]:
|
|
|
212
223
|
return names
|
|
213
224
|
|
|
214
225
|
|
|
226
|
+
def _resolve_outside_cwd(command: str) -> str | None:
|
|
227
|
+
"""shutil.which with a CWD-shim guard. Returns the resolved path or None.
|
|
228
|
+
|
|
229
|
+
shutil.which on Windows searches the current directory ahead of PATH,
|
|
230
|
+
and CWD here is the repo under analysis — a bare-name lookup resolving
|
|
231
|
+
into CWD would execute a repo-planted shim (e.g. qmd.cmd). Such a
|
|
232
|
+
resolution is treated as not-found. Explicit paths supplied by callers
|
|
233
|
+
(containing a separator) are honored as-is. Keep identical to the
|
|
234
|
+
sibling guard in skf-detect-tools.py.
|
|
235
|
+
"""
|
|
236
|
+
resolved = shutil.which(command)
|
|
237
|
+
if resolved is None:
|
|
238
|
+
return None
|
|
239
|
+
if os.sep in command or (os.altsep and os.altsep in command):
|
|
240
|
+
return resolved
|
|
241
|
+
resolved_dir = os.path.dirname(resolved)
|
|
242
|
+
if resolved_dir:
|
|
243
|
+
cwd = os.path.normcase(os.path.abspath(os.getcwd()))
|
|
244
|
+
if os.path.normcase(os.path.abspath(resolved_dir)) == cwd:
|
|
245
|
+
return None
|
|
246
|
+
return resolved
|
|
247
|
+
|
|
248
|
+
|
|
215
249
|
def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
216
250
|
"""Invoke `qmd collection list` and return (names, error).
|
|
217
251
|
|
|
@@ -221,11 +255,19 @@ def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
|
221
255
|
counts as a "live collection name".
|
|
222
256
|
"""
|
|
223
257
|
import subprocess
|
|
258
|
+
# Resolve before spawning: on Windows qmd ships from npm as a .CMD
|
|
259
|
+
# shim, which a bare-name subprocess.run cannot launch (WinError 2).
|
|
260
|
+
qmd = _resolve_outside_cwd("qmd")
|
|
261
|
+
if qmd is None:
|
|
262
|
+
return [], "qmd not found on PATH"
|
|
224
263
|
try:
|
|
264
|
+
# qmd emits UTF-8; a locale-default decode (cp1252 on Windows)
|
|
265
|
+
# mojibakes names or raises UnicodeDecodeError on unmapped bytes.
|
|
225
266
|
result = subprocess.run(
|
|
226
|
-
[
|
|
267
|
+
[qmd, "collection", "list"],
|
|
227
268
|
capture_output=True,
|
|
228
|
-
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
errors="replace",
|
|
229
271
|
timeout=30,
|
|
230
272
|
check=False,
|
|
231
273
|
)
|
|
@@ -356,7 +356,27 @@ def _parse_argv(argv: list[str]) -> dict:
|
|
|
356
356
|
return payload
|
|
357
357
|
|
|
358
358
|
|
|
359
|
+
def _force_utf8(*streams) -> None:
|
|
360
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
361
|
+
|
|
362
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
363
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
364
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
365
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
366
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
367
|
+
test doubles without reconfigure().
|
|
368
|
+
"""
|
|
369
|
+
for stream in streams:
|
|
370
|
+
if hasattr(stream, "reconfigure"):
|
|
371
|
+
errors = getattr(stream, "errors", None)
|
|
372
|
+
if errors is None:
|
|
373
|
+
stream.reconfigure(encoding="utf-8")
|
|
374
|
+
else:
|
|
375
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
376
|
+
|
|
377
|
+
|
|
359
378
|
def main(argv: list[str]) -> int:
|
|
379
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
360
380
|
payload = _parse_argv(argv)
|
|
361
381
|
result = recommend(payload)
|
|
362
382
|
json.dump(result, sys.stdout, ensure_ascii=False)
|
|
@@ -64,7 +64,7 @@ Judgment payload (stdin JSON, compute mode)::
|
|
|
64
64
|
{
|
|
65
65
|
"exports_public_api": 40, # int — public-entry-point export count (judgment)
|
|
66
66
|
"exports_internal": 19, # int — non-underscore internal export count (judgment)
|
|
67
|
-
"effective_denominator": 12, # int|null — optional,
|
|
67
|
+
"effective_denominator": 12, # int|null — optional, curated-subset scopes only
|
|
68
68
|
"pattern_surfaces_documented": 8, # int — reference-app only (exports_documented proxy)
|
|
69
69
|
"exports_documented": 0, # int — stack only (own-barrel count, usually 0)
|
|
70
70
|
"scripts": [ ... ], # list — scripts[] array (or "scripts_count": int)
|
|
@@ -1075,7 +1075,27 @@ def path_to_manifest_name(ecosystem: str) -> str:
|
|
|
1075
1075
|
# CLI wiring
|
|
1076
1076
|
# ---------------------------------------------------------------------------
|
|
1077
1077
|
|
|
1078
|
+
def _force_utf8(*streams) -> None:
|
|
1079
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
1080
|
+
|
|
1081
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
1082
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
1083
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
1084
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
1085
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
1086
|
+
test doubles without reconfigure().
|
|
1087
|
+
"""
|
|
1088
|
+
for stream in streams:
|
|
1089
|
+
if hasattr(stream, "reconfigure"):
|
|
1090
|
+
errors = getattr(stream, "errors", None)
|
|
1091
|
+
if errors is None:
|
|
1092
|
+
stream.reconfigure(encoding="utf-8")
|
|
1093
|
+
else:
|
|
1094
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
1095
|
+
|
|
1096
|
+
|
|
1078
1097
|
def main(argv: list[str]) -> int:
|
|
1098
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
1079
1099
|
parser = argparse.ArgumentParser(
|
|
1080
1100
|
description="Classify a repo into a known skill shape from its manifest files.",
|
|
1081
1101
|
)
|
|
@@ -13,7 +13,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
|
|
|
13
13
|
| source_repo | string | GitHub URL or local path | Repository or project root (optional when `source_type: "docs-only"`) |
|
|
14
14
|
| language | string | Recognized language | Primary programming language |
|
|
15
15
|
| scope | object | See Scope Object below | Boundary definition |
|
|
16
|
-
| description | string | 1-3 sentences | What the skill covers
|
|
16
|
+
| description | string | 1-3 sentences | What the skill covers. Must contain a literal `Use when` clause — validators test for that exact phrase. |
|
|
17
17
|
| forge_tier | string | `Quick` / `Forge` / `Forge+` / `Deep` | Inherited from forge-tier.yaml (Title Case) |
|
|
18
18
|
| created | string | ISO date `YYYY-MM-DD` | Generation date |
|
|
19
19
|
| created_by | string | user_name from config | Who generated the brief |
|
|
@@ -31,6 +31,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
|
|
|
31
31
|
| `constituent_refs` | object | map of path→ref strings | Optional. Per-constituent git ref overrides for composite (multi-repo or multi-ref) sources. Keys must match entries in the analyze-source `project_paths` array. When absent, a single `target_ref` (or auto-detected ref) applies to all paths. Mutually exclusive with `target_ref`. |
|
|
32
32
|
| `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
|
|
33
33
|
| `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
|
|
34
|
+
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional narrower tier-A include list for a curated-subset skill — stratified monorepo packages, single-crate `specific-modules` scopes, and multi-entry exports-map packages. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`. **It also exempts the brief from the denominator deflation guard**, so supply it only when it genuinely narrows the surface — never as an empty list, and never as a way to widen. See `skf-test-skill/references/source-access-protocol.md`. |
|
|
34
35
|
|
|
35
36
|
When `source_type: "docs-only"`:
|
|
36
37
|
- `source_repo` becomes optional (set to doc site URL for reference)
|
|
@@ -59,7 +59,7 @@ For each qualifying unit, prepare a recommendation card:
|
|
|
59
59
|
- name: {suggested kebab-case name}
|
|
60
60
|
- scope.type: {full-library / specific-modules / public-api / component-library / reference-app / docs-only}
|
|
61
61
|
- scope.include: {suggested glob patterns}
|
|
62
|
-
- description: {suggested 1-3 sentence description}
|
|
62
|
+
- description: {suggested 1-3 sentence description, containing a literal `Use when` clause}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### 3. Present Recommendations
|
|
@@ -143,7 +143,7 @@ Skip this section. Temporal context requires Deep tier.
|
|
|
143
143
|
|
|
144
144
|
For each export in the skill baseline that was NOT found at its recorded file path during re-extraction (potential "deleted" export):
|
|
145
145
|
|
|
146
|
-
1. Run `ccc_bridge.search("{export_name}", source_root, top_k=5)` — **Tool resolution:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `ccc search "{export_name}"
|
|
146
|
+
1. Run `ccc_bridge.search("{export_name}", source_root, top_k=5)` — **Tool resolution:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `cd {source_root} && ccc search --limit 5 "{export_name}"` (CLI) — to find candidate current locations. `ccc search` reads the index in the current working directory and has no project-selector flag (`--path` is a file-path glob filter *within* the index, and the result cap is `--limit`, not `--top`) — see `knowledge/ccc-bridge.md`.
|
|
147
147
|
2. If CCC returns files containing the export name:
|
|
148
148
|
- Run ast-grep verification on each candidate file
|
|
149
149
|
- If verified at a new location: reclassify from "deleted" to "moved" with the new file:line reference
|
|
@@ -6,9 +6,9 @@ Loaded by step 1 §7b only. The five examples below show the *range* of acceptab
|
|
|
6
6
|
|
|
7
7
|
> Render Markdown to HTML using the marked library. Use when the user pastes raw Markdown and wants formatted output, or asks how to convert MD files in a build pipeline.
|
|
8
8
|
|
|
9
|
-
> Stripe API client for Node.js — payment intents, subscriptions, customer portal, webhooks.
|
|
9
|
+
> Stripe API client for Node.js — payment intents, subscriptions, customer portal, webhooks. Use when the task involves Stripe-managed payments, subscription billing, or webhook event handling.
|
|
10
10
|
|
|
11
|
-
> Charts and visualizations powered by D3.js.
|
|
11
|
+
> Charts and visualizations powered by D3.js. Use when the user asks to plot data, build interactive graphs, or wants bare D3 control instead of a React-charts abstraction.
|
|
12
12
|
|
|
13
13
|
> Lint Python code with Ruff. Use when the user wants to add or configure Ruff in a Python project, debug rule selectors, or understand why a specific check fired.
|
|
14
14
|
|
|
@@ -16,4 +16,6 @@ Loaded by step 1 §7b only. The five examples below show the *range* of acceptab
|
|
|
16
16
|
|
|
17
17
|
## Notes on Voice
|
|
18
18
|
|
|
19
|
-
Each example leads differently (verb / noun / "Charts and..." / verb / noun-phrase)
|
|
19
|
+
Each example leads differently (verb / noun / "Charts and..." / verb / noun-phrase) rather than copy-pasting a single template. Compose in that spirit using the gathered material — the target repo, the user's intent, the version if set, and any scope hints — but **do not template-stamp**.
|
|
20
|
+
|
|
21
|
+
The **lead** is where the voice varies; the **trigger clause** is fixed. Every description must contain a literal `Use when` clause somewhere, because validators test for that exact phrase — `skill-check`'s `description.use_when_phrase` rule scores a description below 100 without it. Phrasings like "Triggers on…" or "Reach for this when…" read well but do not satisfy it on their own, so keep them for additional text rather than as the only trigger.
|