From 0263c19144333977f63b9304bdcbc7868770d73a Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 13:30:44 +0100 Subject: [PATCH 01/13] Add impact skill for LSP-backed spec/code mapping Introduces allium:impact, a narrow skill that builds and queries a bidirectional JSON map linking spec constructs to implementation symbols via the built-in LSP tool. Python ships as the first language adapter (pyright-lsp); additional languages are added by dropping an adapter file under skills/impact/adapters/ -- no change to the core pipeline or schema. Wires weed, distill, propagate and tend to consult the map before falling back to grep-based correlation. All callers degrade gracefully when no adapter matches or the LSP is unavailable, so the map is an accuracy/consistency win rather than a prerequisite. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude-plugin/plugin.json | 1 + README.md | 3 + skills/allium/references/impact-map.md | 274 +++++++++++++++++++++++++ skills/distill/SKILL.md | 10 +- skills/impact/SKILL.md | 161 +++++++++++++++ skills/impact/adapters/README.md | 91 ++++++++ skills/impact/adapters/python.md | 112 ++++++++++ skills/propagate/SKILL.md | 55 ++--- skills/tend/SKILL.md | 2 + skills/weed/SKILL.md | 11 +- 10 files changed, 693 insertions(+), 27 deletions(-) create mode 100644 skills/allium/references/impact-map.md create mode 100644 skills/impact/SKILL.md create mode 100644 skills/impact/adapters/README.md create mode 100644 skills/impact/adapters/python.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 967b71a..0108db6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -14,6 +14,7 @@ "./skills/allium", "./skills/distill", "./skills/elicit", + "./skills/impact", "./skills/propagate", "./skills/tend", "./skills/weed" diff --git a/README.md b/README.md index 6f68dd7..46aa116 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Allium provides five skills, an entry point and two autonomous agents. | `/propagate ` (or `/allium:propagate`) | Generate tests from a spec. | | `/tend ` (or `/allium:tend`) | Targeted changes to existing specs. | | `/weed ` (or `/allium:weed`) | Find and fix divergences between spec and code. | +| `/impact ` (or `/allium:impact`) | Build the spec↔code impact map that `distill`, `weed` and `propagate` read to avoid re-discovering the mapping on every invocation. Python is supported today via `pyright-lsp`; additional languages are added by dropping an adapter into `skills/impact/adapters/`. | How skills appear depends on your editor. Some show the fully qualified form (`/allium:weed`), others show the short form (`/weed`), and some support both. If one form isn't recognised, try the other. Skills also auto-trigger when you open or edit `.allium` files. @@ -72,6 +73,8 @@ Tend and weed are also available as autonomous **agents** that run in their own For larger codebases, distillation and other ambitious tasks may need several passes to capture everything. Consider an iterative approach like the [Ralph Wiggum loop](https://ghuntley.com/ralph/), repeating until there's nothing further to do. +The [`impact` skill](skills/impact/SKILL.md) makes each pass yield better results, not just faster ones. Grep finds names that match; the LSP-backed map resolves actual references, catching implementations text search misses — methods reached through polymorphism, framework dependency injection or dynamic dispatch. Its `unmapped` section makes "no implementation found" a finding rather than an oversight. And because `weed`, `distill` and `propagate` read one persisted mapping instead of each re-discovering it per run, their conclusions stay consistent across a long loop rather than drifting against one another. + ## Why not just point the LLM at the code? Within a session, meaning drifts: by prompt ten or twenty, the model is pattern-matching on its own outputs rather than the original intent. Across sessions, knowledge evaporates entirely. Modern LLMs navigate codebases effectively, but the limitation appears when you need to distinguish what the code *does* from what it *should do*. Code captures implementation, including bugs and expedient decisions. The model treats all of it as intended behaviour. diff --git a/skills/allium/references/impact-map.md b/skills/allium/references/impact-map.md new file mode 100644 index 0000000..1d941e3 --- /dev/null +++ b/skills/allium/references/impact-map.md @@ -0,0 +1,274 @@ +# Impact map reference + +The impact map is a JSON artifact produced by the [`impact` skill](../skills/impact/SKILL.md) that links Allium spec constructs to implementation code symbols. This document defines the schema and the integration contract that other Allium skills read. + +## File location + +One JSON file per spec, under the target project's `.allium/impact/` directory: + +```text +/ + spec.allium + .allium/ + impact/ + .gitignore # contains "*", committed so the directory is tracked but contents are not + spec.json +``` + +The directory is a gitignored cache. The skill writes `.gitignore` on first build. Do not commit impact map files. + +## Top-level schema + +```json +{ + "spec": "spec.allium", + "language": "python", + "commit": "", + "built_at": "", + "adapter_version": "python-v1", + "nodes": { ... }, + "links": [ ... ], + "call_edges": [ ... ], + "unmapped": { "spec": [ ... ], "code": [ ... ] } +} +``` + +| Field | Meaning | +| ----- | ------- | +| `spec` | Filename of the `.allium` file this map covers. | +| `language` | Target language of the implementation (e.g. `python`, `typescript`). Multiple adapters active means this is an array. | +| `commit` | Git SHA of the target project at build time. Used by `refresh` mode to detect stale entries. | +| `built_at` | ISO-8601 timestamp of the build. Informational. | +| `adapter_version` | Which language adapter and adapter version produced the map. Bump when an adapter's rules change materially. | +| `nodes` | Keyed by node ID. Every `spec:` or `code:` reference in `links` or `call_edges` resolves to a node. | +| `links` | Cross-side edges (spec vs. code). This is the primary thing other skills read. | +| `call_edges` | Same-side edges on the code graph (caller to callee). Used by `propagate` for state-machine action maps and by the "blast radius" query. | +| `unmapped` | Spec nodes with no confirmed code match, and code symbols with no confirming spec link. Load-bearing for `weed`. | + +## Nodes + +Every node has a stable string ID: `spec:` or `code:`. IDs are the only way other skills reference nodes; do not rely on array positions. + +### Spec node + +```json +"spec:Candidacy": { + "kind": "entity", + "file": "interview.allium", + "line": 42 +} +``` + +`kind` ∈ `entity`, `variant`, `value_type`, `enum`, `rule`, `trigger`, `surface`, `contract`, `invariant`, `config`, `default`, `actor`. + +### Code node + +```json +"code:interview.services.candidacy.create_candidacy": { + "kind": "function", + "file": "src/interview/services/candidacy.py", + "line": 87, + "fqn": "interview.services.candidacy.create_candidacy" +} +``` + +`kind` ∈ `function`, `method`, `class`, `module`, `decorator`, `constant`, `type_alias`. The set is language-agnostic; the adapter maps LSP symbol kinds onto it. + +`fqn` is the fully-qualified name as LSP reports it, with the language's native separator (`.` in Python, `/` in Go with the package path, etc.). Use it for cross-file identification. + +## Links + +```json +{ + "from": "spec:Candidacy", + "to": "code:interview.services.candidacy.create_candidacy", + "via": "name-match+hover", + "confidence": "high", + "rejected_candidates": [] +} +``` + +| Field | Meaning | +| ----- | ------- | +| `from` | A `spec:` node ID. | +| `to` | A `code:` node ID. | +| `via` | How the link was proven. Enumerated below. | +| `confidence` | `high`, `medium`, `low`. Per the adapter's confidence heuristic. | +| `rejected_candidates` | Other `code:` IDs that were plausible but lost the disambiguation. Empty when the match was unambiguous. | + +### `via` values + +- `name-match+hover` — workspaceSymbol hit plus hover/docstring confirmation. Strongest automatic signal. +- `name-match+single` — workspaceSymbol returned exactly one candidate; no secondary signal needed. +- `name-match+ambiguous` — multiple candidates survived; this link is one of several recorded at low confidence. +- `surface-decorator` — matched via framework entry-point pattern from the adapter (e.g. Flask route decorator). +- `docstring-ref` — code docstring explicitly references the spec construct by name. +- `manual` — hand-curated; the skill never writes this. Reserved for user annotation. + +Other skills must treat links they don't recognise as opaque: skip them rather than erroring. + +## Call edges + +```json +{ + "caller": "code:...create_candidacy", + "callee": "code:...persist", + "cross_module": false +} +``` + +Only edges within the project root are recorded. `cross_module` is true when caller and callee live in different top-level packages — `propagate` uses this to identify integration-test candidates. + +## Unmapped + +```json +"unmapped": { + "spec": [ + { "id": "spec:Rule.ReassignOnDecline", "reason": "no-workspace-symbol-match" } + ], + "code": [ + { "id": "code:interview.legacy.old_flow.handle", "reason": "no-link" } + ] +} +``` + +`reason` is a short tag, not free-form prose. Values: + +- `no-workspace-symbol-match` — the adapter generated variants, no LSP result. +- `low-confidence-only` — candidates existed but all fell below the adapter's confidence floor. +- `no-link` — code symbol found during traversal with no confirming link back to any spec node. +- `out-of-scope` — deliberately excluded by the adapter's exclusion rules (test files, migrations, etc.). + +## Integration contract + +This section is the compatibility promise between the `impact` skill and its consumers. Other skills read the JSON directly; the impact skill never renegotiates the schema without bumping `adapter_version`. + +### Reading the map + +Consumers MUST: + +- Resolve every node reference through the `nodes` table. Do not parse IDs into fields. +- Treat unknown `via` values as opaque — record the link but do not rely on its provenance. +- Treat unknown `kind` values the same way. New kinds may appear as Allium evolves. +- Respect the `unmapped` section. A spec node in `unmapped.spec` has no implementation candidate; treat that as a finding, not a bug. + +Consumers MUST NOT: + +- Write to the map. Only the `impact` skill produces it. +- Invent links. If the map says a spec node is unmapped, the consumer does not silently "find" a match. +- Cross-reference maps from different specs. Each map is scoped to one `.allium` file. + +### When to rebuild + +The `impact` skill decides when a rebuild is needed. Consumers request a rebuild by invoking `impact` in `refresh` mode (cheap) or `build` mode (full). Typical triggers: + +- Before running `weed` — refresh. +- Before running `propagate` — refresh. +- After a large refactor — build. +- When `weed` reports a surprising volume of divergences, suggesting the map is stale — refresh. + +### Graceful degradation + +The map is an optimisation, not a prerequisite. When the `impact` skill returns `degraded: true` (no language adapter matches the project, the required LSP plugin is missing, or the LSP is installed but not indexing), consumers MUST fall back to pre-map behaviour (`grep` + `read` correlation) rather than refusing the work. Consumers should: + +- Note the degradation reason to the user once, not on every step. +- Proceed with manual correlation as they would have before the map existed. +- Not write a stub or partial JSON to `.allium/impact/` — only the `impact` skill produces map files, and it writes nothing when degraded. + +### Versioning + +`adapter_version` bumps when: + +- A language adapter's name-variant or confidence rules change. +- A new `via` value is added. +- A new `kind` is added. + +Consumers may log a warning if they see an `adapter_version` they don't recognise, but must still read the map. Forward compatibility is the goal. + +## Worked example + +Given this spec fragment: + +```allium +-- interview.allium + +entity Candidacy { + status: pending | active | closed +} + +rule ScheduleInterview { + when: SchedulerTriggered(candidacy) + requires: candidacy.status = active + ensures: Interview.created(candidacy: candidacy, status: scheduled) +} + +surface CandidateAPI { + provides: ScheduleInterview +} +``` + +And this Python implementation: + +```python +# src/interview/models.py +class Candidacy: + status: str # "pending" | "active" | "closed" + +# src/interview/services/scheduler.py +def schedule_interview(candidacy: Candidacy) -> Interview: + """Witnesses rule ScheduleInterview.""" + assert candidacy.status == "active" + return Interview.create(candidacy=candidacy, status="scheduled") + +# src/interview/api/routes.py +@router.post("/candidacies/{id}/interviews") +def create_interview(id: str): + return schedule_interview(get_candidacy(id)) +``` + +The produced map: + +```json +{ + "spec": "interview.allium", + "language": "python", + "commit": "abc123", + "built_at": "2026-04-20T10:00:00Z", + "adapter_version": "python-v1", + "nodes": { + "spec:Candidacy": { "kind": "entity", "file": "interview.allium", "line": 3 }, + "spec:Rule.ScheduleInterview": { "kind": "rule", "file": "interview.allium", "line": 7 }, + "spec:Surface.CandidateAPI": { "kind": "surface", "file": "interview.allium", "line": 13 }, + "code:interview.models.Candidacy": { + "kind": "class", "file": "src/interview/models.py", "line": 2, + "fqn": "interview.models.Candidacy" + }, + "code:interview.services.scheduler.schedule_interview": { + "kind": "function", "file": "src/interview/services/scheduler.py", "line": 2, + "fqn": "interview.services.scheduler.schedule_interview" + }, + "code:interview.api.routes.create_interview": { + "kind": "function", "file": "src/interview/api/routes.py", "line": 2, + "fqn": "interview.api.routes.create_interview" + } + }, + "links": [ + { "from": "spec:Candidacy", "to": "code:interview.models.Candidacy", + "via": "name-match+single", "confidence": "high", "rejected_candidates": [] }, + { "from": "spec:Rule.ScheduleInterview", + "to": "code:interview.services.scheduler.schedule_interview", + "via": "docstring-ref", "confidence": "high", "rejected_candidates": [] }, + { "from": "spec:Surface.CandidateAPI", + "to": "code:interview.api.routes.create_interview", + "via": "surface-decorator", "confidence": "high", "rejected_candidates": [] } + ], + "call_edges": [ + { "caller": "code:interview.api.routes.create_interview", + "callee": "code:interview.services.scheduler.schedule_interview", + "cross_module": true } + ], + "unmapped": { "spec": [], "code": [] } +} +``` + +A `weed` run reads this map, sees every spec node linked and no unmapped code, and reports no structural divergences. It then reads the rule's `requires` (`status = active`) and compares to the code (`assert candidacy.status == "active"`) — that check is unchanged by the map, but the map got `weed` straight to the right file in one hop instead of greping. diff --git a/skills/distill/SKILL.md b/skills/distill/SKILL.md index 5937ece..5fabfd2 100644 --- a/skills/distill/SKILL.md +++ b/skills/distill/SKILL.md @@ -314,11 +314,19 @@ The presence of multiple implementations suggests the variation itself is a doma ## Distillation process +### Step 0: Build the impact map + +If a spec skeleton already exists (even just entity names), invoke the [`impact` skill](../impact/SKILL.md) in `build` mode first. The resulting `.allium/impact/.json` gives you a head start: existing links tell you which code symbols already correspond to spec constructs, and `unmapped.code` is your candidate pool for new spec nodes to extract. + +If you are distilling from a completely empty spec (no skeleton), skip this step and come back to it once you have enough entities named to seed the map. + +If the impact skill returns `degraded: true` (no adapter for this language, or the target LSP is unavailable), note the reason once and proceed without a map — distillation worked with grep + read alone before the map existed and still does. Come back to building a map in a later pass if the language adapter gap is worth filling. + ### Step 1: Map the territory Before extracting any specification, understand the codebase structure: -1. **Identify entry points.** API routes, CLI commands, message handlers, scheduled jobs. +1. **Identify entry points.** API routes, CLI commands, message handlers, scheduled jobs. If the impact map exists, its surface links and `call_edges` entry points are your starting list. 2. **Find the domain models.** Usually in `models/`, `entities/`, `domain/`. 3. **Locate business logic.** Services, use cases, handlers. 4. **Note external integrations.** What third parties does it talk to? diff --git a/skills/impact/SKILL.md b/skills/impact/SKILL.md new file mode 100644 index 0000000..7bce54a --- /dev/null +++ b/skills/impact/SKILL.md @@ -0,0 +1,161 @@ +--- +name: impact +description: "Build and query the repository impact map — a bidirectional graph linking Allium spec constructs to implementation code symbols. Use when the user wants to build or refresh the impact map, ask which code implements a spec entity or rule, ask which spec constructs cover a given symbol, or trace the blast radius of a proposed code change. Python is supported today; extensible to any language with an LSP via an adapter file." +--- + +# Impact + +You build and query the repository impact map. The map is a bidirectional graph linking Allium spec constructs (entities, rules, triggers, surfaces) to implementation code symbols (functions, classes, methods). Other Allium skills read the map to avoid re-discovering spec↔code correspondences on every invocation. + +You are narrow and mechanical. You do not write specs, you do not write implementation code, and you do not judge divergences. Your only side effect is writing JSON under `.allium/impact/` in the target project. + +## Startup + +1. Read [language reference](../../references/language-reference.md) for the Allium syntax. +2. Read [impact map reference](../../references/impact-map.md) for the schema and integration contract. +3. Locate the `.allium` files for the spec(s) being mapped. +4. Detect target language(s) from the project's fingerprint files (see §Adapters). Load the matching adapter(s) from `skills/impact/adapters/`. If no adapter matches, exit degraded (see §Degraded exit). +5. Verify the LSP tool is available for each chosen language. Call `workspaceSymbol` with a sentinel query (the adapter supplies one). If the LSP is not responding, exit degraded. +6. If the `allium` CLI is available, run `allium plan ` and `allium model ` to seed the spec-side nodes. Fall back to reading the `.allium` file directly. + +### Degraded exit + +The impact map is an optimisation, not a prerequisite. If you cannot produce a map — no adapter matches, the required LSP plugin is missing, LSP is installed but not indexing — do not refuse the invocation outright. Instead: + +1. Do not write or modify any `.allium/impact/.json`. +2. Print a short, actionable message naming the exact cause (which fingerprints were checked, which LSP plugin to install). +3. Return a summary with `degraded: true` and a `reason` tag (`no-adapter`, `lsp-unavailable`, `lsp-not-indexing`). + +The callers (weed, distill, propagate, tend) treat a degraded response as "no map available" and fall back to manual correlation. They must not hard-fail on your behalf. + +## Modes + +You operate in one of three modes, determined by the caller's request: + +**Build.** Run the pipeline end-to-end for one or more spec files. Overwrite `.allium/impact/.json`. Emit a summary: node counts, link counts, count of `unmapped.spec`, count of `unmapped.code`, count of low-confidence links. Do not try to resolve unmapped nodes by guessing. + +**Query.** Answer a targeted question against the existing map without rebuilding. Typical queries: +- "What code implements `Candidacy`?" → lookup `links` with `from: spec:Candidacy`. +- "What spec constructs reference `create_candidacy`?" → lookup `links` with `to: code:...create_candidacy`. +- "Which rules fan out from `ScheduleInterview`?" → walk `call_edges` from the mapped code node. +- "What breaks if I edit `src/foo/bar.py:42`?" → find code nodes at or containing that line, report their linked spec nodes and transitive callers. + +If the map file does not exist, tell the caller to run Build first. Do not auto-build. + +**Refresh.** Rebuild only the links whose target files have changed since the map's `commit` field (compare to `git log`), plus any links marked low-confidence. Do not touch high-confidence links pointing at unchanged files. Output the same summary as Build, plus the count of links refreshed vs skipped. + +If no mode is specified, default to **Build** and warn the caller that refresh would be cheaper if a map already exists. + +## How you work + +### The pipeline (Build and Refresh) + +Every step below uses only the built-in LSP tool and language-neutral Allium constructs. Per-language knowledge lives in the adapter. Ask the adapter at the marked steps; do not hardcode language specifics in this skill. + +1. **Seed spec nodes.** From `allium plan` / `allium model` output (or direct `.allium` parse), list every entity, rule, trigger, surface, contract and invariant. Each becomes a `spec:` node with `file` and `line` from the spec. + +2. **Generate name variants.** For each spec node, ask the **adapter** for the identifier variants a programmer would likely choose in the target language. Do not invent variants — rely on the adapter's rules. + +3. **Find candidate code symbols.** For each variant, call LSP `workspaceSymbol`. Collect candidates. Exclude test files per the adapter's project-root and test-directory rules. + +4. **Confirm candidates.** For each candidate, call LSP `hover` and `documentSymbol`. Use the adapter's confidence heuristic to decide: + - *Exact.* Single match, docstring/type/signature lines up → record a high-confidence link with `via: "name-match+hover"`. + - *Ambiguous.* Multiple plausible matches → record each with `via: "name-match+ambiguous"` and mark low-confidence. + - *None.* No candidate survives → add the spec node to `unmapped.spec`. Do not force a match. + +5. **Expand the code-side graph.** For each confirmed code node, call `prepareCallHierarchy`, then `incomingCalls` and `outgoingCalls`. Record each edge in `call_edges`. Stop expanding at the project boundary defined by the adapter (same `pyproject.toml`, same `go.mod`, etc.). Stop at depth 2 by default; the adapter may override. + +6. **Map surfaces.** For each spec `surface` block, ask the adapter for framework entry-point patterns (Python: `@app.route`, `@router.post`, `APIRouter` etc.; Go: `http.HandleFunc` etc.). Resolve them via `workspaceSymbol` and `findReferences`. Link surfaces to their entry points. + +7. **Collect unmapped code.** Walk the project's source files per the adapter's globs. For each top-level symbol not referenced by any confirmed link or `call_edges` node, record the symbol in `unmapped.code`. This is the honest bit — `weed` reads it to find behaviour the spec is silent about. + +8. **Emit JSON.** Write `.allium/impact/.json` atomically. On first build, also write `.allium/impact/.gitignore` with `*` so the cache never enters version control. Print the summary described in §Modes. + +### Disambiguation + +When `workspaceSymbol` returns multiple candidates for a spec name, never silently drop any of them: +- If the adapter's confidence heuristic picks one decisively, record the winner with high confidence and the losers under a `rejected_candidates` field on the link for debugging. +- If the heuristic is indecisive, record every surviving candidate as a separate link with low confidence. `weed` and `propagate` know how to read these. + +### Honesty about unmapped + +The `unmapped` section is load-bearing. `weed` uses `unmapped.spec` to find spec constructs that have no implementation and `unmapped.code` to find implementation behaviour the spec does not describe. If you suppress unmapped entries to make the summary look cleaner, you are actively hiding divergences. Do not do that. + +## Adapters + +Adapters are short Markdown files under `skills/impact/adapters/`. Each defines the five pieces of language-specific knowledge the pipeline needs. See [adapters/README.md](./adapters/README.md) for the authoring contract. + +### Language selection + +On startup, detect the target language by fingerprint: + +| Fingerprint | Adapter | +|---|---| +| `pyproject.toml`, `setup.py`, `setup.cfg`, or `**/*.py` files | [python.md](./adapters/python.md) | + +A spec may also declare its target language explicitly via a `language:` field on its `use` declarations or the caller may pass `--language `. Explicit selection overrides auto-detection. + +If a project mixes languages (e.g. a Python service with a TypeScript frontend), load every adapter whose fingerprint matches and run the pipeline once per language. Merge the results into a single map keyed by language. + +### Currently supported + +- **Python** — pyright-lsp. Covers Flask/FastAPI/Django surfaces, PascalCase classes, snake_case functions, `create_X`/`X_service`/`X_repository` patterns. `pyproject.toml` or `setup.py` defines the project root. + +### Adding a language + +Create `skills/impact/adapters/.md` following [adapters/README.md](./adapters/README.md). Add its fingerprint row to the table above. No change to this file's pipeline steps is required; if you find yourself editing them to support a new language, the adapter contract has a gap and should be extended instead. + +## Called by other skills + +Other Allium skills invoke you via the `Skill` tool as a precursor to their own work: + +- **weed** calls you in `refresh` mode before doing divergence classification. +- **distill** calls you in `build` mode before starting spec extraction from an existing codebase. +- **propagate** calls you in `refresh` mode before building its implementation bridge. +- **tend** optionally calls you in `query` mode to check for orphaned links before writing a spec edit. + +When called by another skill, emit the JSON and the summary. The caller reads the JSON directly; it does not need a narrative explanation. + +## Boundaries + +- You do not modify `.allium` files. That is `tend` or `weed` in spec-update mode. +- You do not modify implementation code. That is `weed` in code-update mode. +- You do not extract new spec content from code. That is `distill`. +- You do not generate tests. That is `propagate`. +- You do not classify divergences as spec bugs or code bugs. That is `weed`. You surface candidates; `weed` judges them. +- You do not edit files outside `.allium/impact/`. + +## Verification + +After every Build or Refresh, sanity-check your output before returning: + +- Every spec node from the `allium plan` output is either in `links` (as a `from`) or in `unmapped.spec`. No silent drops. +- Every link's `to` points at an existing file and line (the LSP result you got, not an invented location). +- `unmapped.code` does not contain files outside the project root. +- The JSON parses. + +If any check fails, do not emit the file. Tell the caller what went wrong. + +## Output format + +**Build / Refresh summary:** + +``` +Impact map: .json + Spec nodes: ( linked, unmapped) + Code nodes: ( linked, unmapped) + Call edges: + Low-confidence links: + Refreshed / skipped: / (Refresh mode only) +``` + +**Degraded summary** (when no map could be produced): + +``` +Impact map: degraded + Reason: + Details: + Action: | add an adapter for | check pyright config> +``` + +**Query response:** plain text answering the specific question, followed by the relevant JSON fragment for the caller to quote. diff --git a/skills/impact/adapters/README.md b/skills/impact/adapters/README.md new file mode 100644 index 0000000..d8e2e80 --- /dev/null +++ b/skills/impact/adapters/README.md @@ -0,0 +1,91 @@ +# Impact-map language adapters + +A language adapter tells the `impact` skill how to look for implementation code in a particular language. The skill's pipeline is language-agnostic; everything that varies between Python, Go, TypeScript, Rust etc. lives here. + +Adapters are Markdown files interpreted by the LLM, not compiled code. Consistency comes from following the five sections below in the same order. Do not invent additional sections. + +## Adding a new adapter + +Create `skills/impact/adapters/.md` with exactly these five sections, in this order. Then add a fingerprint row to the table in [../SKILL.md](../SKILL.md) under "Language selection". + +### 1. Fingerprint + +How to tell this language is in use in the target project. List the files and globs the skill should check. If any match, this adapter activates. + +Example (Python): + +> - Presence of `pyproject.toml`, `setup.py` or `setup.cfg` at the project root. +> - Any `**/*.py` file inside the project root. + +### 2. LSP plugin + +Which Claude Code LSP plugin the skill must have installed. Name the plugin exactly as it appears in the marketplace. Supply a sentinel query that confirms the LSP is live before the pipeline runs. + +Example (Python): + +> **Plugin:** `pyright-lsp`. +> +> **Sentinel:** call `workspaceSymbol` with a query matching any public symbol the skill expects to exist in a Python project (e.g. a class name discovered from the spec). If the result is empty *and* no Python file in the project defines that symbol, pyright is likely not indexing — tell the user to install/enable `pyright-lsp`. + +### 3. Name-variant generator + +Given a spec identifier (PascalCase in Allium), list the identifiers a programmer is likely to have used in this language. Keep the rules simple and grounded in convention — do not brute-force every casing permutation. + +Example (Python): + +> For a spec entity `Candidacy`: +> - `Candidacy` — PascalCase class. +> - `candidacy` — snake_case module, variable, or function. +> - `create_candidacy`, `make_candidacy`, `new_candidacy` — factory functions. +> - `CandidacyService`, `CandidacyRepository` — service/repository classes. +> +> For a spec rule `ScheduleInterview`: +> - `schedule_interview` — function name. +> - `ScheduleInterview` — class (for command/event object style). +> - `handle_schedule_interview`, `on_schedule_interview` — handlers. + +### 4. Project-root rule + +What counts as "inside the project" when the pipeline bounds call-hierarchy expansion and `unmapped.code` collection. The adapter names the manifest file(s) or directory marker that defines the root, and the globs that count as project source files (excluding vendored code, tests, and generated files). + +Example (Python): + +> **Root:** the directory containing the nearest `pyproject.toml`, `setup.py` or `setup.cfg` walking up from the spec file. +> +> **Source globs:** `src/**/*.py`, `/**/*.py` as declared in `pyproject.toml`. +> +> **Exclusions:** `tests/**`, `**/test_*.py`, `**/conftest.py`, `.venv/**`, `build/**`, `dist/**`, any directory on `.gitignore`. + +### 5. Surface entry-point patterns + +For each spec `surface` kind the language commonly implements, give the framework patterns the skill should search for. Broken out by framework because a single language typically has multiple competing frameworks. + +Example (Python): + +> **API surfaces:** +> - *Flask:* `@app.route`, `@blueprint.route`, `app.add_url_rule`. +> - *FastAPI:* `@app.get/post/put/delete/patch`, `@router.*`, `APIRouter`, `Depends`. +> - *Django:* `path(...)` / `re_path(...)` in `urls.py`; class-based views inheriting from `View`, `APIView` (DRF). +> +> **UI surfaces:** not typical for Python backends. Skip. +> +> **Integration surfaces:** `@celery.task`, `@app.task` (Celery); `@consumer`, `@subscribe` (message bus libraries); SDK methods with `@classmethod` on service clients. + +## Confidence heuristic + +Every adapter inherits this default confidence ladder. Override only if a language has strong conventions that make one signal load-bearing. + +1. **High** — exact name match, docstring or symbol doc mentions the spec construct by name, single candidate in the workspace. +2. **Medium** — exact name match, single candidate, no docstring evidence. +3. **Low** — name variant match (not exact), or multiple candidates, or candidate lives in a test file. + +Low-confidence links are still recorded; they just get flagged in the summary so `weed` treats them with suspicion. + +## Things adapters must not do + +- Do not define the JSON schema. That is fixed in [references/impact-map.md](../../../references/impact-map.md). +- Do not reimplement pipeline steps. That is fixed in [../SKILL.md](../SKILL.md). +- Do not branch on framework features in the adapter file — the five sections above are the whole contract. +- Do not add application-specific rules (e.g. "in this repo, handlers live in `handlers/`"). That is a project-level concern, not a language adapter. + +If you find yourself wanting to add a sixth section, something is wrong with the pipeline's decomposition. Raise it as an issue rather than forking the adapter contract. diff --git a/skills/impact/adapters/python.md b/skills/impact/adapters/python.md new file mode 100644 index 0000000..6b47252 --- /dev/null +++ b/skills/impact/adapters/python.md @@ -0,0 +1,112 @@ +# Python adapter + +The Python language adapter for the `impact` skill. Follows the five-section contract in [README.md](./README.md). + +## 1. Fingerprint + +Activate this adapter if any of the following is present in the target project: + +- `pyproject.toml` at the project root. +- `setup.py` or `setup.cfg` at the project root. +- Any `**/*.py` file inside the project root (fallback for scripts-only projects with no packaging manifest). + +## 2. LSP plugin + +**Plugin:** `pyright-lsp`. + +**Sentinel:** pick a PascalCase entity name from the spec you are mapping. Call LSP `workspaceSymbol` with that name. If the result is empty and at least one `*.py` file in the project defines a symbol of that name (verified by a quick `Grep`), pyright is not indexing — tell the user to install or enable `pyright-lsp`. + +If the spec has no entities yet (greenfield distillation), use `__init__` as the sentinel query — every non-trivial Python project has at least one. + +## 3. Name-variant generator + +Given a spec identifier (PascalCase in Allium), emit variants following Python convention. + +**For entities and variants:** + +- `` — PascalCase class. +- `` — snake_case module, variable, function, or dataclass attribute. +- `create_`, `make_`, `new_` — factory functions. +- `Service`, `Repository`, `Manager` — service/repository/manager classes. +- `_service`, `_repository` — the same in snake_case when the code uses modules rather than classes. + +**For rules and triggers:** + +- `` — function name (Allium `ScheduleInterview` → `schedule_interview`). +- `` — class (command/event object style). +- `handle_`, `on_`, `process_` — handler functions. +- `_` — leading underscore for internal implementations of the same rule. + +**For surfaces:** + +- `Router`, `View`, `Endpoint` — framework-specific route containers. +- `_routes`, `_api` — module names in URL/router setup. + +**Case conversion:** splitting on CamelCase is sufficient; do not try to stem verbs or handle plurals. If a spec identifier contains an underscore or is already snake_case, emit it verbatim as well. + +## 4. Project-root rule + +**Root discovery:** walk upward from the spec file's directory. The first directory containing `pyproject.toml`, `setup.py` or `setup.cfg` is the project root. If none is found, use the first directory containing a `.git` folder. + +**Source globs:** + +- If `pyproject.toml` declares `[tool.setuptools.packages.find]` or `[tool.poetry]` packages, use those package directories. +- Otherwise default to `src/**/*.py` and `/**/*.py`. + +**Exclusions (always):** + +- `tests/**`, `test/**`, `**/test_*.py`, `**/*_test.py`, `**/conftest.py` — test code. +- `.venv/**`, `venv/**`, `env/**` — virtual environments. +- `build/**`, `dist/**`, `*.egg-info/**` — build artefacts. +- `__pycache__/**`, `*.pyc`. +- Any path matched by the project's `.gitignore`. +- `migrations/**` when using Django or Alembic — these are generated from models, not hand-authored behaviour. + +**Depth:** default call-hierarchy expansion depth is 2. Stop when a call crosses into a third-party package (imports from outside the project root). + +## 5. Surface entry-point patterns + +### API surfaces + +**Flask:** +- Decorators: `@app.route`, `@.route`. +- Functions: `app.add_url_rule(...)`. +- View-class pattern: `MethodView` subclasses registered via `add_url_rule(..., view_func=View.as_view(...))`. + +**FastAPI / Starlette:** +- Decorators: `@app.get`, `@app.post`, `@app.put`, `@app.delete`, `@app.patch`, `@app.head`, `@app.options`. +- Router decorators: `@router.` where `router = APIRouter(...)`. +- Dependency injection: `Depends(...)` arguments indicate the surface's dependencies and belong in the surface's `demands` contracts when present. + +**Django:** +- URL patterns: `path(...)` and `re_path(...)` entries in `urls.py` / `urlpatterns`. +- Class-based views: subclasses of `View`, `TemplateView`, `ListView`, `DetailView`, `CreateView`, `UpdateView`, `DeleteView`. +- DRF: subclasses of `APIView`, `ViewSet`, `ModelViewSet`, `GenericAPIView`; `@api_view(...)` decorator for function-based DRF views. + +**Quart / Sanic / aiohttp / Tornado:** same shape as Flask/FastAPI — look for `@app.route`, `@app.`, or framework-specific `RouteTableDef.get/post`. + +### UI surfaces + +Rare for Python backends. When present: +- Jinja / Django template rendering points (`render_template`, `TemplateResponse`) — treat the view function as the surface entry point, not the template itself. + +### Integration surfaces + +**Task queues:** +- Celery: `@celery.task`, `@app.task`, `@shared_task`. +- RQ: functions enqueued via `queue.enqueue(func, ...)` — grep the call sites to find the handlers. +- Dramatiq: `@dramatiq.actor`. + +**Message bus / event handlers:** +- Kafka/faust: `@app.agent(topic)`. +- pub/sub libraries: `@subscriber(...)`, `@consumer(...)`, `@on_event(...)`. +- AWS Lambda: functions matching `def handler(event, context)` or `def lambda_handler(event, context)`. + +**SDK / client surfaces (outbound):** +- Service client classes exposing public methods — look for classes named `Client`, `SDK`, `Gateway`. Public (non-underscore) methods are the surface entry points. + +### What not to match as surfaces + +- Internal helpers, private methods (leading underscore). +- Test fixtures, even if they look like route handlers. +- Abstract base classes whose subclasses are where the real routing lives — follow `goToImplementation` to the concrete class and map the surface to that. diff --git a/skills/propagate/SKILL.md b/skills/propagate/SKILL.md index 44ca8ee..cad86de 100644 --- a/skills/propagate/SKILL.md +++ b/skills/propagate/SKILL.md @@ -104,33 +104,38 @@ For entities with status enums. When a transition graph is declared, walk every State machine tests require an **action map**: a function per transition edge that takes the entity in the source state and produces it in the target state by calling the actual implementation code. Without this map, the test framework can describe valid paths through the graph but cannot execute them. To build the action map: -1. For each edge in the transition graph, find the witnessing rule in the spec -2. Find the code implementing that rule (the implementation bridge) -3. Write a test action that sets up the preconditions (`requires` clauses), invokes the code, and returns the entity in the target state -4. Register the action under the `(from_state, to_state)` key +1. For each edge in the transition graph, find the witnessing rule in the spec. +2. Find the code implementing that rule by reading the impact map's link from `spec:Rule.` (§The implementation bridge). Fall back to manual discovery only if the rule is in `unmapped.spec`. +3. Write a test action that sets up the preconditions (`requires` clauses), invokes the code, and returns the entity in the target state. +4. Register the action under the `(from_state, to_state)` key. Once the map is built, the PBT framework can walk random valid paths: start at any non-terminal state, pick a random outbound edge, apply its action, check all entity-level invariants, repeat. The path length and starting state are generated randomly. This is the fullest expression of the spec's transition graph as a test. ## The implementation bridge -You correlate spec constructs with implementation code, the same way the weed skill correlates for divergence checking. +You correlate spec constructs with implementation code. The [`impact` skill](../impact/SKILL.md) does the bulk of this work and persists the result to `.allium/impact/.json`. Invoke it in `refresh` mode before generating tests (or `build` mode if no map exists), then read the JSON. + +The map's `links` give you the spec → code correspondence directly. The `call_edges` give you the code-side call graph, which feeds the state-machine action map and the cross-module integration test planning below. Only fall back to manual codebase exploration when a spec construct lands in `unmapped.spec` — in that case the test you generate must be flagged as pending, because there is nothing to exercise yet. + +If the impact skill returns `degraded: true` (no language adapter matches, or the target LSP is unavailable), note the reason once and fall back to manual correlation for the remainder of this run: explore the codebase directly, find rule implementations by reading, and generate tests as you would have before the map existed. Tests still get written; they just cost more context to produce. ### For surface tests -Map surfaces to their implementation: -- API surfaces map to endpoints (REST routes, GraphQL resolvers, gRPC services) -- UI surfaces map to components or pages -- Integration surfaces map to message handlers or SDK methods +Read links where `from` is a `spec:Surface.*` node: +- API surfaces link to route-handler functions (`via: "surface-decorator"` is the signal the impact skill used a framework pattern). +- UI surfaces link to components or pages. +- Integration surfaces link to message handlers or SDK methods. -Discover the mapping by reading the codebase. Look for naming patterns, route definitions and handler registrations. +If a surface has no link, the map either could not identify the framework (adapter gap — report it) or the surface is not implemented yet (aspirational — generate a pending test skeleton). ### For internal tests -For each rule in the spec: -1. Find the code implementing the rule (service method, event handler, state machine transition) -2. Determine how to instantiate the entities involved (factories, builders, fixtures) -3. Determine how to invoke the rule (API call, method call, event dispatch) -4. Determine how to assert postconditions (database queries, return values, event assertions) +For each rule in the spec, look up the link from `spec:Rule.` to a `code:` node. The linked function or method is the rule's implementation. + +1. Implementation is the `to` node of the link; open it to understand the signature. +2. Instantiation: walk `call_edges` backward from the implementation to find what code constructs the entities it operates on (factories, builders, fixtures). +3. Invocation: walk `call_edges` backward further to find the public-facing entry point (the surface or a higher-level service method). +4. Postcondition assertions: check what the implementation returns or mutates, and map `ensures` clauses onto those outcomes. ### For temporal tests @@ -143,11 +148,12 @@ Before attempting temporal tests, check whether the component accepts an injecte When a rule emits a trigger that another spec's rule receives (e.g. the Arbiter emits `ClerkReceivesEvent`, the Clerk handles it), testing the chain requires multiple components wired together. Before generating cross-module tests: -1. Trace the trigger emission graph from the plan output: which rules emit triggers, and which rules in other specs receive them -2. Check whether the codebase has an existing integration test fixture that wires the participating components (a pipeline test, an end-to-end test helper, a test harness class) -3. If a fixture exists, reuse it. Cross-module tests should compose existing wiring, not rebuild it -4. If no fixture exists but the codebase structure is clear enough to understand the wiring (service constructors, dependency injection, event bus configuration), generate the fixture and the test -5. If the wiring is too complex or opaque to generate confidently, generate a test skeleton with TODOs marking where component wiring is needed +1. Trace the trigger emission graph from the plan output: which rules emit triggers, and which rules in other specs receive them. +2. Read the impact map's `call_edges` where `cross_module: true` — these are the code-level hand-offs that correspond to cross-module trigger chains. The two endpoints of a cross-module edge are strong candidates for the "emitter" and "receiver" sides of the test. +3. Check whether the codebase has an existing integration test fixture that wires the participating components (a pipeline test, an end-to-end test helper, a test harness class) +4. If a fixture exists, reuse it. Cross-module tests should compose existing wiring, not rebuild it. +5. If no fixture exists but the codebase structure is clear enough to understand the wiring (service constructors, dependency injection, event bus configuration), generate the fixture and the test +6. If the wiring is too complex or opaque to generate confidently, generate a test skeleton with TODOs marking where component wiring is needed Cross-module tests are integration tests by nature. They verify that the spec's trigger chains are faithfully implemented across component boundaries. Prioritise them after single-component tests are passing. @@ -171,10 +177,11 @@ Deferred specifications are fully specified in separate files. When the target c 1. **Read the spec** — understand entities, rules, surfaces, invariants, transition graphs, state-dependent fields, contracts, config, defaults. Read [assessing specs](../allium/references/assessing-specs.md) to gauge the spec's maturity. A coarse spec (entities and transition graphs but no rules) will produce limited test obligations — mostly structural tests. If the spec is too coarse for meaningful test generation, suggest using the `elicit` or `distill` skill to develop it further before propagating tests. A spec with rules and surfaces enables the full test taxonomy including data flow chain tests and reachability tests. 2. **Read test obligations** — from `allium plan` output or manual derivation 3. **Read domain model** — from `allium model` output or manual derivation -4. **Explore the codebase** — find existing tests, test framework, entity implementations, rule implementations -5. **Map constructs to code** — correlate spec entities/rules/surfaces with implementation classes/functions/endpoints -6. **Generate tests** — produce test files following the project's conventions -7. **Verify tests compile/run** — ensure generated tests are syntactically valid +4. **Refresh the impact map** — invoke the [`impact` skill](../impact/SKILL.md) in `refresh` mode and read `.allium/impact/.json`; this replaces most of the old "explore the codebase" step +5. **Explore remaining gaps** — find the test framework, read existing tests, and manually investigate anything in `unmapped.spec` that still needs tests +6. **Map constructs to code** — use the impact map's `links` directly; only correlate by hand for `unmapped.spec` entries +7. **Generate tests** — produce test files following the project's conventions +8. **Verify tests compile/run** — ensure generated tests are syntactically valid ### Discovery checklist diff --git a/skills/tend/SKILL.md b/skills/tend/SKILL.md index cd185f6..0d54ecd 100644 --- a/skills/tend/SKILL.md +++ b/skills/tend/SKILL.md @@ -41,6 +41,8 @@ If the caller describes a feature in implementation terms ("the API returns a 40 **Be minimal.** Add what's needed and nothing more. Don't speculatively add fields, rules or config that weren't asked for. Don't restructure working specs for aesthetic reasons. +**Check for orphaned links before renaming.** If a `.allium/impact/.json` exists in the project, check it before renaming or removing a spec construct. Invoke the [`impact` skill](../impact/SKILL.md) in `query` mode to ask which code symbols are currently linked to the construct you are changing. If links exist, warn the caller that the rename will orphan those links until the map is refreshed and suggest running `weed` after the edit to resolve the resulting divergences. Never silently edit a construct that the map says has implementation. If the impact skill returns `degraded: true` or no map exists, skip this check and proceed — the warning is a best-effort convenience, not a gate. + ## Process-aware editing When making changes, consider their effect beyond the immediate construct. diff --git a/skills/weed/SKILL.md b/skills/weed/SKILL.md index 4b11892..2120b82 100644 --- a/skills/weed/SKILL.md +++ b/skills/weed/SKILL.md @@ -12,7 +12,8 @@ You weed the Allium garden. You compare `.allium` specifications against impleme 1. Read [language reference](../allium/references/language-reference.md) for the Allium syntax and validation rules. 2. Read the relevant `.allium` files (search the project to find them if not specified). 3. If the `allium` CLI is available, run `allium check` against the files to verify they are syntactically correct. -4. Read the corresponding implementation code. +4. Invoke the [`impact` skill](../impact/SKILL.md) in `refresh` mode (or `build` mode if no map exists yet) so the spec↔code mapping is current. Read the resulting `.allium/impact/.json`. This replaces the old "grep for corresponding code" step: `links` tells you where each spec construct is implemented, and `unmapped.*` is your candidate list for divergences. If the impact skill returns `degraded: true` (no adapter, LSP unavailable), note the reason once to the user and fall back to grep-based correlation for the rest of this run — do not refuse the work. +5. Read the corresponding implementation code, guided by the map (or by grep, in degraded mode). ## Modes @@ -28,7 +29,13 @@ If no mode is specified, default to **check** and report all findings. ## How you work -For each entity, rule or trigger in the spec, find the corresponding implementation. For each significant code path, check whether the spec accounts for it. Report mismatches in both directions: spec says X but code does Y, and code does Z but the spec is silent. +Drive divergence detection from the impact map: + +1. **Spec → code.** For each entity, rule, trigger or surface in the spec, follow `links[*].to` to the implementing symbol. Read that symbol and check whether its behaviour matches the spec construct (preconditions, ensures clauses, invariants, transition graph). Low-confidence links (`confidence: "low"` or `via: "name-match+ambiguous"`) deserve extra scrutiny — the map surfaces candidates but does not guarantee them. +2. **Unmapped spec.** Every entry in `unmapped.spec` is a spec construct with no confirmed implementation. Decide whether this is a missing-code divergence (the code should implement it), an aspirational-design gap (intentional, not yet built) or a spec bug (the construct should not exist). +3. **Unmapped code.** Every entry in `unmapped.code` is a code symbol the spec is silent about. Decide whether the code is incidental (infrastructure, not domain-level), represents undocumented behaviour the spec should cover, or is dead/legacy code the spec deliberately omits. + +If the map is missing a link you know exists (e.g. you spot the implementation manually), refresh the map rather than working around it — a stale map hurts future invocations. Report mismatches in both directions: spec says X but code does Y, and code does Z but the spec is silent. ### Process-level checks From 0ab34e4886dd9258ea9cb387aff8eed481fd21f5 Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 16:36:53 +0100 Subject: [PATCH 02/13] Tighten weed's behavioural pass over the impact map Re-runs against a real Python codebase showed the impact map was pulling weed toward structural findings (fields, types, renames) at the expense of behavioural ones (does the code actually emit the warning this rule promises?). The first fix split "How you work" into structural and behavioural passes; the second recognised that @guidance and @guarantee live on surfaces, not rules, and extended the behavioural pass to iterate spec:Surface.* and spec:Invariant.* links alongside spec:Rule.* After both edits, weed recovered 6/6 of the behavioural findings the no-map baseline caught, plus produced surface-obligation tick-lists and contract-level per-invariant reads that neither run could produce before. Also adds one line to the impact-map reference's "Reading the map" contract: the map points consumers at code, it does not replace reading it. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/allium/references/impact-map.md | 1 + skills/weed/SKILL.md | 27 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/skills/allium/references/impact-map.md b/skills/allium/references/impact-map.md index 1d941e3..1ee3b7a 100644 --- a/skills/allium/references/impact-map.md +++ b/skills/allium/references/impact-map.md @@ -151,6 +151,7 @@ Consumers MUST: - Treat unknown `via` values as opaque — record the link but do not rely on its provenance. - Treat unknown `kind` values the same way. New kinds may appear as Allium evolves. - Respect the `unmapped` section. A spec node in `unmapped.spec` has no implementation candidate; treat that as a finding, not a bug. +- Read the linked code. The map points consumers at code; it does not replace reading it. A `link` tells you *where* the implementation lives, not *whether the implementation satisfies the spec construct's clauses* — that's the consumer's job. Consumers MUST NOT: diff --git a/skills/weed/SKILL.md b/skills/weed/SKILL.md index 2120b82..fc7e041 100644 --- a/skills/weed/SKILL.md +++ b/skills/weed/SKILL.md @@ -29,11 +29,30 @@ If no mode is specified, default to **check** and report all findings. ## How you work -Drive divergence detection from the impact map: +Drive divergence detection from the impact map. For every linked spec construct, you MUST do both the structural and the behavioural pass — stopping at structural is the most common weeding failure mode, and produces reports rich in field/type mismatches but blind to whether the code actually does what the rule promises. -1. **Spec → code.** For each entity, rule, trigger or surface in the spec, follow `links[*].to` to the implementing symbol. Read that symbol and check whether its behaviour matches the spec construct (preconditions, ensures clauses, invariants, transition graph). Low-confidence links (`confidence: "low"` or `via: "name-match+ambiguous"`) deserve extra scrutiny — the map surfaces candidates but does not guarantee them. -2. **Unmapped spec.** Every entry in `unmapped.spec` is a spec construct with no confirmed implementation. Decide whether this is a missing-code divergence (the code should implement it), an aspirational-design gap (intentional, not yet built) or a spec bug (the construct should not exist). -3. **Unmapped code.** Every entry in `unmapped.code` is a code symbol the spec is silent about. Decide whether the code is incidental (infrastructure, not domain-level), represents undocumented behaviour the spec should cover, or is dead/legacy code the spec deliberately omits. +1. **Structural pass.** For each link, confirm the `to` symbol still exists, is a plausible implementation (not a stub, re-export or test fixture), and matches the spec construct's shape: fields on entities, parameters on rules, return types on value functions. Record shape-level divergences (missing fields, extra fields, type mismatches, renamed parameters). + +2. **Behavioural pass.** For every linked spec construct that carries semantic clauses — rules, surfaces, invariants — read those clauses alongside the implementation end-to-end. The clauses vary by construct; iterate all three scopes, not just rules. + + **For each `spec:Rule.*` link:** + - Each `requires` clause — find the guard, assert or early-return in code that enforces it. Missing guards are divergences. + - Each `ensures` clause — find the state change in code (entity mutation, creation via constructor/factory, trigger or event emission, notification, side effect). Missing state changes are divergences. + - Each referenced `config` value — confirm the code reads from the config layer rather than hardcoding the value. + + **For each `spec:Surface.*` link:** + - Each `provides` operation — the operation exists in code (route, UI control, handler) and its availability matches the spec's `when` / `requires`. + - Each `exposes` entry — the data is accessible to the declared actor and not to others; context and within scoping are honoured. + - Each `@guarantee` and `@guidance` annotation — the narrative claim holds end-to-end in the linked code flow. "Messages are streamed to the user" must correspond to a streaming API call; "the user is informed on failure" must correspond to an error-path notification. **This is the weakest-signal sub-check and the easiest to skip — don't.** Surface-attached narrative is load-bearing and is exactly what a structural pass misses. + - Each `contracts: demands X` / `fulfils Y` — the code relies on X from its counterpart, and supplies Y; signatures match. + + **For each `spec:Invariant.*` link:** expression-bearing `invariant Name { expr }` should correspond to a runtime assertion or a structural guarantee in code. Prose-only `@invariant` cannot be mechanically checked — flag it as such and move on, do not silently treat absence as satisfaction. + + Report an obligation-by-obligation tally, not a single yes/no. Low-confidence links (`confidence: "low"` or `via: "name-match+ambiguous"`) deserve extra scrutiny in every scope — the map surfaces candidates but does not guarantee them. + +3. **Unmapped spec.** Every entry in `unmapped.spec` is a spec construct with no confirmed implementation. Decide whether this is a missing-code divergence (the code should implement it), an aspirational-design gap (intentional, not yet built) or a spec bug (the construct should not exist). + +4. **Unmapped code.** Every entry in `unmapped.code` is a code symbol the spec is silent about. Decide whether the code is incidental (infrastructure, not domain-level), represents undocumented behaviour the spec should cover, or is dead/legacy code the spec deliberately omits. If the map is missing a link you know exists (e.g. you spot the implementation manually), refresh the map rather than working around it — a stale map hurts future invocations. Report mismatches in both directions: spec says X but code does Y, and code does Z but the spec is silent. From 54095a7eed17cc58c70b6fc726fa5715286851b1 Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 16:40:34 +0100 Subject: [PATCH 03/13] Document pyright-lsp install for the Python adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adapter named `pyright-lsp` as its LSP plugin dependency but didn't say how to get it. Adds uv tool / uv pip install commands for the pyright binary and the /plugin marketplace steps for the Claude Code plugin, plus /reload-plugins as the no-restart path. Also tidies pre-existing MD032 lint warnings in §5 (blank lines around bullet lists under bold framework headings). Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/impact/adapters/python.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/skills/impact/adapters/python.md b/skills/impact/adapters/python.md index 6b47252..a256eec 100644 --- a/skills/impact/adapters/python.md +++ b/skills/impact/adapters/python.md @@ -12,9 +12,29 @@ Activate this adapter if any of the following is present in the target project: ## 2. LSP plugin -**Plugin:** `pyright-lsp`. +**Plugin:** `pyright-lsp` (from Anthropic's `claude-plugins-official` marketplace). -**Sentinel:** pick a PascalCase entity name from the spec you are mapping. Call LSP `workspaceSymbol` with that name. If the result is empty and at least one `*.py` file in the project defines a symbol of that name (verified by a quick `Grep`), pyright is not indexing — tell the user to install or enable `pyright-lsp`. +**Install:** + +```bash +# 1. Install the pyright language server binary. uv tool keeps it isolated +# from project virtualenvs, which is usually what you want: +uv tool install pyright + +# Or project-local, if you'd rather pin it per-project: +uv pip install pyright + +# (pip install pyright and npm install -g pyright also work — the plugin +# invokes whichever `pyright` is on PATH.) + +# 2. In Claude Code, add the marketplace and install the plugin: +/plugin marketplace add anthropics/claude-plugins-official +/plugin install pyright-lsp +``` + +After install, run `/reload-plugins` (or restart the session) and the built-in `LSP` tool will route Python files to pyright. + +**Sentinel:** pick a PascalCase entity name from the spec you are mapping. Call LSP `workspaceSymbol` with that name. If the result is empty and at least one `*.py` file in the project defines a symbol of that name (verified by a quick `Grep`), pyright is not indexing — tell the user to install or enable `pyright-lsp` per the steps above. If the spec has no entities yet (greenfield distillation), use `__init__` as the sentinel query — every non-trivial Python project has at least one. @@ -69,16 +89,19 @@ Given a spec identifier (PascalCase in Allium), emit variants following Python c ### API surfaces **Flask:** + - Decorators: `@app.route`, `@.route`. - Functions: `app.add_url_rule(...)`. - View-class pattern: `MethodView` subclasses registered via `add_url_rule(..., view_func=View.as_view(...))`. **FastAPI / Starlette:** + - Decorators: `@app.get`, `@app.post`, `@app.put`, `@app.delete`, `@app.patch`, `@app.head`, `@app.options`. - Router decorators: `@router.` where `router = APIRouter(...)`. - Dependency injection: `Depends(...)` arguments indicate the surface's dependencies and belong in the surface's `demands` contracts when present. **Django:** + - URL patterns: `path(...)` and `re_path(...)` entries in `urls.py` / `urlpatterns`. - Class-based views: subclasses of `View`, `TemplateView`, `ListView`, `DetailView`, `CreateView`, `UpdateView`, `DeleteView`. - DRF: subclasses of `APIView`, `ViewSet`, `ModelViewSet`, `GenericAPIView`; `@api_view(...)` decorator for function-based DRF views. @@ -88,21 +111,25 @@ Given a spec identifier (PascalCase in Allium), emit variants following Python c ### UI surfaces Rare for Python backends. When present: + - Jinja / Django template rendering points (`render_template`, `TemplateResponse`) — treat the view function as the surface entry point, not the template itself. ### Integration surfaces **Task queues:** + - Celery: `@celery.task`, `@app.task`, `@shared_task`. - RQ: functions enqueued via `queue.enqueue(func, ...)` — grep the call sites to find the handlers. - Dramatiq: `@dramatiq.actor`. **Message bus / event handlers:** + - Kafka/faust: `@app.agent(topic)`. - pub/sub libraries: `@subscriber(...)`, `@consumer(...)`, `@on_event(...)`. - AWS Lambda: functions matching `def handler(event, context)` or `def lambda_handler(event, context)`. **SDK / client surfaces (outbound):** + - Service client classes exposing public methods — look for classes named `Client`, `SDK`, `Gateway`. Public (non-underscore) methods are the surface entry points. ### What not to match as surfaces From 8571a30bcd0104d8b4a3d118e0edeea807294be0 Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 16:58:41 +0100 Subject: [PATCH 04/13] Reflect Claude Code's single-file LSP reality in impact pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Empirical testing against a real Python project with pyright installed on PATH showed Claude Code's LSP tool runs pyright in single-file mode: documentSymbol and hover work, workspaceSymbol returns empty, and cross-file findReferences / goToDefinition don't resolve project-internal imports. The impact skill was designed assuming workspace-wide symbol search via LSP workspaceSymbol, which is not available. Rewrites the Build pipeline's step 3 to use Glob + per-file documentSymbol for candidate discovery, with workspaceSymbol treated as an optional additional source rather than the primary one. Surface mapping now uses Grep + documentSymbol instead of workspaceSymbol + findReferences. Adds a fourth degraded-exit reason tag (lsp-single-file-mode), clarifies the other three with required details strings, and explicitly instructs callers to quote the reason and details verbatim rather than paraphrase — the prior paraphrase-as-diagnosis pattern turned "Executable not found in $PATH" into "workspaceSymbol doesn't accept query strings" in a weed report footer, which was misleading. The Python adapter's install note now makes explicit that uv pip install into a project venv does not put pyright-langserver on the global PATH that Claude Code's LSP tool inspects; uv tool install is required. Added a "which pyright-langserver" verification step. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/allium/references/impact-map.md | 4 ++-- skills/impact/SKILL.md | 32 ++++++++++++++++++-------- skills/impact/adapters/python.md | 25 +++++++++++++------- skills/weed/SKILL.md | 2 +- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/skills/allium/references/impact-map.md b/skills/allium/references/impact-map.md index 1ee3b7a..b193e54 100644 --- a/skills/allium/references/impact-map.md +++ b/skills/allium/references/impact-map.md @@ -98,8 +98,8 @@ Every node has a stable string ID: `spec:` or `code:`. IDs are the on ### `via` values -- `name-match+hover` — workspaceSymbol hit plus hover/docstring confirmation. Strongest automatic signal. -- `name-match+single` — workspaceSymbol returned exactly one candidate; no secondary signal needed. +- `name-match+hover` — symbol-index hit (via `documentSymbol` walk or `workspaceSymbol` if available) confirmed by LSP `hover` docstring/type. Strongest automatic signal. +- `name-match+single` — the symbol-index walk returned exactly one candidate; no secondary signal needed. - `name-match+ambiguous` — multiple candidates survived; this link is one of several recorded at low confidence. - `surface-decorator` — matched via framework entry-point pattern from the adapter (e.g. Flask route decorator). - `docstring-ref` — code docstring explicitly references the spec construct by name. diff --git a/skills/impact/SKILL.md b/skills/impact/SKILL.md index 7bce54a..7e89b3b 100644 --- a/skills/impact/SKILL.md +++ b/skills/impact/SKILL.md @@ -15,7 +15,7 @@ You are narrow and mechanical. You do not write specs, you do not write implemen 2. Read [impact map reference](../../references/impact-map.md) for the schema and integration contract. 3. Locate the `.allium` files for the spec(s) being mapped. 4. Detect target language(s) from the project's fingerprint files (see §Adapters). Load the matching adapter(s) from `skills/impact/adapters/`. If no adapter matches, exit degraded (see §Degraded exit). -5. Verify the LSP tool is available for each chosen language. Call `workspaceSymbol` with a sentinel query (the adapter supplies one). If the LSP is not responding, exit degraded. +5. Verify the LSP tool is available for each chosen language by running the adapter's sentinel — typically a `documentSymbol` call against any source file, since in the Claude Code harness LSP servers often run in single-file mode rather than workspace-indexed mode. If the call errors with `Executable not found in $PATH` or equivalent, exit degraded with `reason: lsp-unavailable`. If the call returns an empty result on a file that is known to contain symbols, exit degraded with `reason: lsp-not-indexing`. 6. If the `allium` CLI is available, run `allium plan ` and `allium model ` to seed the spec-side nodes. Fall back to reading the `.allium` file directly. ### Degraded exit @@ -23,10 +23,17 @@ You are narrow and mechanical. You do not write specs, you do not write implemen The impact map is an optimisation, not a prerequisite. If you cannot produce a map — no adapter matches, the required LSP plugin is missing, LSP is installed but not indexing — do not refuse the invocation outright. Instead: 1. Do not write or modify any `.allium/impact/.json`. -2. Print a short, actionable message naming the exact cause (which fingerprints were checked, which LSP plugin to install). -3. Return a summary with `degraded: true` and a `reason` tag (`no-adapter`, `lsp-unavailable`, `lsp-not-indexing`). +2. Print a short, actionable message quoting the exact cause. Callers will relay this message verbatim to the user; do not paraphrase. +3. Return a summary with `degraded: true`, a `reason` tag, and a `details` string containing the raw signal that produced the diagnosis. -The callers (weed, distill, propagate, tend) treat a degraded response as "no map available" and fall back to manual correlation. They must not hard-fail on your behalf. +**Reason tags** (use exactly one): + +- `no-adapter` — no language adapter matched the project's fingerprints. `details` should list which fingerprints were checked. +- `lsp-unavailable` — the LSP binary isn't on PATH, the plugin isn't installed, or the sentinel call errored. `details` should quote the LSP tool's error message (e.g. `Executable not found in $PATH: "pyright-langserver"`). +- `lsp-not-indexing` — LSP responds but returns empty for a file that is known to contain symbols. Typically means single-file mode (no workspace index) or mis-rooted workspace detection. `details` should say which sentinel was used and what it returned. +- `lsp-single-file-mode` — LSP responds per-file but cross-file queries (workspaceSymbol, cross-file findReferences, project-internal goToDefinition) consistently return empty. This is the **expected** behaviour for Claude Code's pyright wrapper today; see the Python adapter. `details` should note that symbol discovery will be driven by `documentSymbol` + Glob rather than workspace-wide calls. + +The callers (weed, distill, propagate, tend) treat a degraded response as "no map available" and fall back to manual correlation. They must not hard-fail on your behalf, and they must quote the `reason` tag and `details` string verbatim to the user — never paraphrase, because the paraphrase becomes its own bug (one recent weed run turned `Executable not found in $PATH` into `workspaceSymbol doesn't accept query strings`, which is false). ## Modes @@ -56,16 +63,22 @@ Every step below uses only the built-in LSP tool and language-neutral Allium con 2. **Generate name variants.** For each spec node, ask the **adapter** for the identifier variants a programmer would likely choose in the target language. Do not invent variants — rely on the adapter's rules. -3. **Find candidate code symbols.** For each variant, call LSP `workspaceSymbol`. Collect candidates. Exclude test files per the adapter's project-root and test-directory rules. +3. **Find candidate code symbols.** The Claude Code LSP tool typically runs language servers in single-file mode (confirmed for pyright as of this writing; see the Python adapter), so `workspaceSymbol` cannot be relied on for workspace-wide search. Instead: + a. Use Glob over the adapter's source-file globs (e.g. `**/*.py` minus exclusions) to enumerate candidate files. + b. For each candidate file, call LSP `documentSymbol` to get its symbol tree. + c. Match each variant from step 2 against the symbol trees. A match is a (file, symbol name, line) tuple. + d. Exclude test files per the adapter's project-root and test-directory rules. -4. **Confirm candidates.** For each candidate, call LSP `hover` and `documentSymbol`. Use the adapter's confidence heuristic to decide: + Optional: where the LSP server does support workspaceSymbol (rare), use it as an additional source; merge results with the Glob+documentSymbol pass. Never rely on it alone. + +4. **Confirm candidates.** For each candidate, call LSP `hover` at the symbol's position to get type/docstring. Use the adapter's confidence heuristic to decide: - *Exact.* Single match, docstring/type/signature lines up → record a high-confidence link with `via: "name-match+hover"`. - *Ambiguous.* Multiple plausible matches → record each with `via: "name-match+ambiguous"` and mark low-confidence. - *None.* No candidate survives → add the spec node to `unmapped.spec`. Do not force a match. -5. **Expand the code-side graph.** For each confirmed code node, call `prepareCallHierarchy`, then `incomingCalls` and `outgoingCalls`. Record each edge in `call_edges`. Stop expanding at the project boundary defined by the adapter (same `pyproject.toml`, same `go.mod`, etc.). Stop at depth 2 by default; the adapter may override. +5. **Expand the code-side graph.** For each confirmed code node, call `prepareCallHierarchy`, then `incomingCalls` and `outgoingCalls`. Record each edge in `call_edges`. Stop expanding at the project boundary defined by the adapter (same `pyproject.toml`, same `go.mod`, etc.). Stop at depth 2 by default; the adapter may override. Note: in single-file LSP mode these calls may only return in-file results; treat missing cross-file edges as a known limitation, not a divergence. -6. **Map surfaces.** For each spec `surface` block, ask the adapter for framework entry-point patterns (Python: `@app.route`, `@router.post`, `APIRouter` etc.; Go: `http.HandleFunc` etc.). Resolve them via `workspaceSymbol` and `findReferences`. Link surfaces to their entry points. +6. **Map surfaces.** For each spec `surface` block, ask the adapter for framework entry-point patterns (Python: `@app.route`, `@router.post`, `APIRouter` etc.; Go: `http.HandleFunc` etc.). Use Grep to locate the pattern's occurrences in project files, then resolve each hit to its containing function via `documentSymbol` on that file. Link surfaces to the resolved entry points. 7. **Collect unmapped code.** Walk the project's source files per the adapter's globs. For each top-level symbol not referenced by any confirmed link or `call_edges` node, record the symbol in `unmapped.code`. This is the honest bit — `weed` reads it to find behaviour the spec is silent about. @@ -73,7 +86,8 @@ Every step below uses only the built-in LSP tool and language-neutral Allium con ### Disambiguation -When `workspaceSymbol` returns multiple candidates for a spec name, never silently drop any of them: +When step 3 returns multiple candidates for a spec name, never silently drop any of them: + - If the adapter's confidence heuristic picks one decisively, record the winner with high confidence and the losers under a `rejected_candidates` field on the link for debugging. - If the heuristic is indecisive, record every surviving candidate as a separate link with low confidence. `weed` and `propagate` know how to read these. diff --git a/skills/impact/adapters/python.md b/skills/impact/adapters/python.md index a256eec..d9a1d73 100644 --- a/skills/impact/adapters/python.md +++ b/skills/impact/adapters/python.md @@ -17,15 +17,17 @@ Activate this adapter if any of the following is present in the target project: **Install:** ```bash -# 1. Install the pyright language server binary. uv tool keeps it isolated -# from project virtualenvs, which is usually what you want: +# 1. Install pyright so that `pyright-langserver` resolves on the global +# PATH. The Claude Code LSP tool spawns this binary from PATH — it +# does not inspect per-project venvs — so `uv pip install pyright` +# inside a project venv will NOT work for Claude Code: uv tool install pyright -# Or project-local, if you'd rather pin it per-project: -uv pip install pyright +# (pip install pyright and npm install -g pyright also work, provided +# their install location ends up on PATH.) -# (pip install pyright and npm install -g pyright also work — the plugin -# invokes whichever `pyright` is on PATH.) +# Verify: +which pyright-langserver # must print a path # 2. In Claude Code, add the marketplace and install the plugin: /plugin marketplace add anthropics/claude-plugins-official @@ -34,9 +36,16 @@ uv pip install pyright After install, run `/reload-plugins` (or restart the session) and the built-in `LSP` tool will route Python files to pyright. -**Sentinel:** pick a PascalCase entity name from the spec you are mapping. Call LSP `workspaceSymbol` with that name. If the result is empty and at least one `*.py` file in the project defines a symbol of that name (verified by a quick `Grep`), pyright is not indexing — tell the user to install or enable `pyright-lsp` per the steps above. +**Caveat — single-file indexing:** In the Claude Code harness, pyright runs per invocation against the file passed in `filePath`; it does not index the wider workspace. Empirically: -If the spec has no entities yet (greenfield distillation), use `__init__` as the sentinel query — every non-trivial Python project has at least one. +- `documentSymbol` and `hover` return the expected per-file results. +- `findReferences` only returns hits in the single file, not across the workspace. +- `workspaceSymbol` returns empty. +- Cross-file `goToDefinition` does not resolve project-internal imports (they appear as `reportMissingImports`). + +Treat the LSP tool as a **single-file symbol and type oracle**, not a workspace index. The impact-skill pipeline reflects this: symbol discovery uses Glob + per-file `documentSymbol`, not `workspaceSymbol`. Do not issue `workspaceSymbol` calls expecting workspace-wide results — they will mislead the caller. + +**Sentinel:** open any `.py` file in the project and call `documentSymbol` on it. If pyright returns a non-empty symbol tree, the LSP server is live. If the call errors with `Executable not found in $PATH`, pyright is not installed — tell the user to run the install steps above. ## 3. Name-variant generator diff --git a/skills/weed/SKILL.md b/skills/weed/SKILL.md index fc7e041..61f8781 100644 --- a/skills/weed/SKILL.md +++ b/skills/weed/SKILL.md @@ -12,7 +12,7 @@ You weed the Allium garden. You compare `.allium` specifications against impleme 1. Read [language reference](../allium/references/language-reference.md) for the Allium syntax and validation rules. 2. Read the relevant `.allium` files (search the project to find them if not specified). 3. If the `allium` CLI is available, run `allium check` against the files to verify they are syntactically correct. -4. Invoke the [`impact` skill](../impact/SKILL.md) in `refresh` mode (or `build` mode if no map exists yet) so the spec↔code mapping is current. Read the resulting `.allium/impact/.json`. This replaces the old "grep for corresponding code" step: `links` tells you where each spec construct is implemented, and `unmapped.*` is your candidate list for divergences. If the impact skill returns `degraded: true` (no adapter, LSP unavailable), note the reason once to the user and fall back to grep-based correlation for the rest of this run — do not refuse the work. +4. Invoke the [`impact` skill](../impact/SKILL.md) in `refresh` mode (or `build` mode if no map exists yet) so the spec↔code mapping is current. Read the resulting `.allium/impact/.json`. This replaces the old "grep for corresponding code" step: `links` tells you where each spec construct is implemented, and `unmapped.*` is your candidate list for divergences. If the impact skill returns `degraded: true`, quote the `reason` tag and `details` string **verbatim** to the user once — do not paraphrase, as paraphrase becomes its own bug — then fall back to grep-based correlation for the rest of this run. Do not refuse the work. 5. Read the corresponding implementation code, guided by the map (or by grep, in degraded mode). ## Modes From ed0fb01f37d3e289034bfc5a78b5e29a7981fbd4 Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 18:11:18 +0100 Subject: [PATCH 05/13] Force per-spec coverage and surface tick-lists in weed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related tightenings to weed's §How you work, both driven by empirical regressions observed in runs against a real Python project with a working impact map. Run 4 showed that a dense impact map can crowd out behavioural coverage: weed visited 2 of 9 specs and called the report done. Adds a testable coverage constraint: count the specs in the map, count the specs mentioned in the report, the two numbers must match. Adds a per-spec budget rule: behavioural pass first, structural/orphan findings collected in a batch at the end, so structural signal doesn't exhaust context before later specs get visited. Run 5 confirmed 9/9 coverage but showed that surface-level `@guidance` and `@guarantee` narrative findings were still getting lost (streaming guidance and FK target-table guarantee missed) — the behavioural pass's sub-bullets were present but not producing structured output, so surfaces were being compressed into one-line summaries. Adds a required output format: every visited `spec:Surface.*` MUST appear as a tick-list sub-section with one row per obligation, each tagged ✓/✗/⚠ with a file:line citation. All-✓ surfaces still belong in the report as positive alignment evidence. Includes a worked example showing exactly the target output shape. Run 6 (post-edit) achieved 9/9 coverage and 5/6 behavioural recovery — both previously missed narrative findings returned, plus new signal only the tick-list format could produce (exposed-but-not-rendered ✗ rows, undeclared state-graph transitions, all-✓ positive-evidence rows). The remaining miss (`batch_size` hardcoded) is a separate failure class — declared config values not referenced by any rule — not addressed by these edits. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/weed/SKILL.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/skills/weed/SKILL.md b/skills/weed/SKILL.md index 61f8781..f763f19 100644 --- a/skills/weed/SKILL.md +++ b/skills/weed/SKILL.md @@ -31,6 +31,10 @@ If no mode is specified, default to **check** and report all findings. Drive divergence detection from the impact map. For every linked spec construct, you MUST do both the structural and the behavioural pass — stopping at structural is the most common weeding failure mode, and produces reports rich in field/type mismatches but blind to whether the code actually does what the rule promises. +**Coverage constraint.** List every `.allium` spec file that has at least one link in the map, and visit every one before completing the report. Each visited spec must produce either at least one finding or an explicit "no divergences found" note naming the spec. A report that covers two specs out of nine and leaves the rest unvisited is incomplete, not concise — fix it before presenting. This is a testable constraint: count the specs in the map, count the specs mentioned in your report, the two numbers must match. + +**Per-spec budget.** Do the behavioural pass on a spec's rules, surfaces and invariants before collecting its structural and orphan findings. Structural signal accumulates quickly — orphan fields, type mismatches, unmapped code — and will crowd out behavioural coverage on later specs if you interleave the two. If you find yourself several hundred words deep on one spec's structural minutiae, you have almost certainly under-covered another spec's behavioural obligations — move on. Collect structural/orphan findings as a batch after every spec has been behaviourally walked. + 1. **Structural pass.** For each link, confirm the `to` symbol still exists, is a plausible implementation (not a stub, re-export or test fixture), and matches the spec construct's shape: fields on entities, parameters on rules, return types on value functions. Record shape-level divergences (missing fields, extra fields, type mismatches, renamed parameters). 2. **Behavioural pass.** For every linked spec construct that carries semantic clauses — rules, surfaces, invariants — read those clauses alongside the implementation end-to-end. The clauses vary by construct; iterate all three scopes, not just rules. @@ -46,6 +50,20 @@ Drive divergence detection from the impact map. For every linked spec construct, - Each `@guarantee` and `@guidance` annotation — the narrative claim holds end-to-end in the linked code flow. "Messages are streamed to the user" must correspond to a streaming API call; "the user is informed on failure" must correspond to an error-path notification. **This is the weakest-signal sub-check and the easiest to skip — don't.** Surface-attached narrative is load-bearing and is exactly what a structural pass misses. - Each `contracts: demands X` / `fulfils Y` — the code relies on X from its counterpart, and supplies Y; signatures match. + **Required output format for surfaces.** Every visited `spec:Surface.*` MUST appear in the report as a tick-list sub-section listing every obligation it declares — one row per `provides`, `exposes`, `@guarantee`, `@guidance`, and each contract obligation. Mark each ✓ (honoured, cite file:line), ✗ (divergence — describe in a row immediately below), or ⚠ (partial / aspirational). All-✓ surfaces still belong in the report as positive alignment evidence. A surface that appears in the report without a tick-list has **not** been behaviourally visited, regardless of any prose you wrote about it — go back and complete it. This is the mechanism that catches `@guidance` / `@guarantee` divergences reliably; collapsing a surface's obligations into a single summary sentence consistently loses narrative-level findings like "messages are streamed". + + Example shape: + + ``` + ### Surface.ConversationChat (chat-with-data.allium:498) + - provides SendMessage — chat_page.py:244 ✓ + - provides RetryQuery — sql_generator.py:89 ⚠ (retry budget wired to _MAX_FC_TURNS=4, not config.max_query_retries) + - exposes conversation.messages — chat_page.py:210 ✓ + - @guarantee MessagesOrderedByTime — db_models.py:173 (ORDER BY created_at) ✓ + - @guidance "SQL queries shown in a code block" — chat_page.py:258 (st.code) ✓ + - @guidance "Messages are streamed to the user as the LLM generates them" — sql_generator.py:212 uses non-streaming generate_content ✗ + ``` + **For each `spec:Invariant.*` link:** expression-bearing `invariant Name { expr }` should correspond to a runtime assertion or a structural guarantee in code. Prose-only `@invariant` cannot be mechanically checked — flag it as such and move on, do not silently treat absence as satisfaction. Report an obligation-by-obligation tally, not a single yes/no. Low-confidence links (`confidence: "low"` or `via: "name-match+ambiguous"`) deserve extra scrutiny in every scope — the map surfaces candidates but does not guarantee them. From 8c3178835796844121d7f5716142557fd9e7a116 Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 21:06:26 +0100 Subject: [PATCH 06/13] Suppress positive tick-rows in weed output; add config-coverage pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups after run 6: The surface tick-list format required every visited surface to emit every obligation (✓/✗/⚠). That made the report heavy — surfaces with no issues still produced long subsections of positive evidence. The internal walk stays exhaustive (still load-bearing for @guidance / @guarantee catches), but output is now filtered: only ✗ and ⚠ rows are emitted, and a surface with no issues produces no subsection. Run 6 caught 5/6 of the behavioural baseline — the missing finding was `config.batch_size` declared in the spec, hardcoded `BATCH_SIZE = 200` in code, with no rule referencing the config value so the rule-level "each referenced config value" sub-check never fired. Adds a per-spec-config pass: enumerate every declared config value and check both (a) at least one rule references it by qualified name, and (b) code reads from the config layer using that name. Catches values no rule happens to mention. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/weed/SKILL.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skills/weed/SKILL.md b/skills/weed/SKILL.md index f763f19..053faaf 100644 --- a/skills/weed/SKILL.md +++ b/skills/weed/SKILL.md @@ -50,23 +50,23 @@ Drive divergence detection from the impact map. For every linked spec construct, - Each `@guarantee` and `@guidance` annotation — the narrative claim holds end-to-end in the linked code flow. "Messages are streamed to the user" must correspond to a streaming API call; "the user is informed on failure" must correspond to an error-path notification. **This is the weakest-signal sub-check and the easiest to skip — don't.** Surface-attached narrative is load-bearing and is exactly what a structural pass misses. - Each `contracts: demands X` / `fulfils Y` — the code relies on X from its counterpart, and supplies Y; signatures match. - **Required output format for surfaces.** Every visited `spec:Surface.*` MUST appear in the report as a tick-list sub-section listing every obligation it declares — one row per `provides`, `exposes`, `@guarantee`, `@guidance`, and each contract obligation. Mark each ✓ (honoured, cite file:line), ✗ (divergence — describe in a row immediately below), or ⚠ (partial / aspirational). All-✓ surfaces still belong in the report as positive alignment evidence. A surface that appears in the report without a tick-list has **not** been behaviourally visited, regardless of any prose you wrote about it — go back and complete it. This is the mechanism that catches `@guidance` / `@guarantee` divergences reliably; collapsing a surface's obligations into a single summary sentence consistently loses narrative-level findings like "messages are streamed". + **Required obligation walk for surfaces.** For every visited `spec:Surface.*`, walk every obligation it declares — `provides`, `exposes`, `@guarantee`, `@guidance`, and each contract obligation — and check each internally against the linked code. This exhaustive walk is load-bearing: it is the mechanism that catches `@guidance` / `@guarantee` divergences reliably. Collapsing a surface's obligations into a single summary sentence consistently loses narrative-level findings like "messages are streamed". - Example shape: + **Output only divergences.** Emit a subsection for a surface only if it has at least one ✗ (divergence) or ⚠ (partial / aspirational) row after the walk. Do not emit ✓ rows — a surface with no issues produces no output. When you do emit a subsection, cite `file:line` for the code side of every row. + + Example shape (only the non-✓ rows from a fuller internal walk): ``` ### Surface.ConversationChat (chat-with-data.allium:498) - - provides SendMessage — chat_page.py:244 ✓ - - provides RetryQuery — sql_generator.py:89 ⚠ (retry budget wired to _MAX_FC_TURNS=4, not config.max_query_retries) - - exposes conversation.messages — chat_page.py:210 ✓ - - @guarantee MessagesOrderedByTime — db_models.py:173 (ORDER BY created_at) ✓ - - @guidance "SQL queries shown in a code block" — chat_page.py:258 (st.code) ✓ - - @guidance "Messages are streamed to the user as the LLM generates them" — sql_generator.py:212 uses non-streaming generate_content ✗ + - ⚠ provides RetryQuery — sql_generator.py:89 retry budget wired to _MAX_FC_TURNS=4, not config.max_query_retries + - ✗ @guidance "Messages are streamed to the user as the LLM generates them" — sql_generator.py:212 uses non-streaming generate_content ``` **For each `spec:Invariant.*` link:** expression-bearing `invariant Name { expr }` should correspond to a runtime assertion or a structural guarantee in code. Prose-only `@invariant` cannot be mechanically checked — flag it as such and move on, do not silently treat absence as satisfaction. - Report an obligation-by-obligation tally, not a single yes/no. Low-confidence links (`confidence: "low"` or `via: "name-match+ambiguous"`) deserve extra scrutiny in every scope — the map surfaces candidates but does not guarantee them. + **For each spec's `config` block:** enumerate every declared config value. For each, check (a) at least one rule in this spec references it by qualified name (e.g. `config.batch_size`), and (b) code reads the value from an app-level config layer using the same name (or a documented env-var alias). Missing either side is a divergence. A config value consumed by a rule but hardcoded in code defeats the config mechanism (e.g. spec `config.batch_size = 200`, code `BATCH_SIZE = 200` as a module constant not read from config). A config value declared in the spec but never referenced by any rule is dead spec. Rule-level "each referenced `config` value" above only covers case (b) for values a rule happens to mention; this pass covers every declared value independently. + + Walk exhaustively; emit only ✗ / ⚠ rows. 3. **Unmapped spec.** Every entry in `unmapped.spec` is a spec construct with no confirmed implementation. Decide whether this is a missing-code divergence (the code should implement it), an aspirational-design gap (intentional, not yet built) or a spec bug (the construct should not exist). From f614b725d3a3c5349f282bd90b1ee192c8d20cae Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 21:06:26 +0100 Subject: [PATCH 07/13] Add Java adapter for the impact skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds skills/impact/adapters/java.md covering jdtls-lsp with JDK 17+. Name-variant generator handles Java's layered-architecture conventions (Service, Repository, Controller, Impl, Dto, Factory, I interface prefix) plus CQRS-style handlers (Command, handle). Surface entry-points cover Spring Boot (@RestController, @GetMapping etc.), JAX-RS (@Path, @GET), Micronaut, Quarkus, Servlet API, gRPC, and the integration patterns Spring/Micronaut projects actually use (@KafkaListener, @RabbitListener, @JmsListener, @Scheduled, @EventListener, AWS Lambda RequestHandler). Project-root rule handles Maven multi-module (topmost pom.xml with packaging=pom wins), Gradle multi-module (settings.gradle* marker), and excludes the usual generated-sources paths that Lombok, MapStruct and codegen tools produce. Install path is more involved than pyright — brew install jdtls on macOS, AUR on Arch, manual download elsewhere — and a first-call cost warning is included since JDT.LS indexes the classpath on cold start. Registers the adapter in the impact SKILL.md fingerprint table and the "currently supported" list. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/impact/SKILL.md | 2 + skills/impact/adapters/java.md | 186 +++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 skills/impact/adapters/java.md diff --git a/skills/impact/SKILL.md b/skills/impact/SKILL.md index 7e89b3b..4fb9ad6 100644 --- a/skills/impact/SKILL.md +++ b/skills/impact/SKILL.md @@ -106,6 +106,7 @@ On startup, detect the target language by fingerprint: | Fingerprint | Adapter | |---|---| | `pyproject.toml`, `setup.py`, `setup.cfg`, or `**/*.py` files | [python.md](./adapters/python.md) | +| `pom.xml`, `build.gradle`, `build.gradle.kts`, `settings.gradle*`, `build.xml`, or `**/*.java` files | [java.md](./adapters/java.md) | A spec may also declare its target language explicitly via a `language:` field on its `use` declarations or the caller may pass `--language `. Explicit selection overrides auto-detection. @@ -114,6 +115,7 @@ If a project mixes languages (e.g. a Python service with a TypeScript frontend), ### Currently supported - **Python** — pyright-lsp. Covers Flask/FastAPI/Django surfaces, PascalCase classes, snake_case functions, `create_X`/`X_service`/`X_repository` patterns. `pyproject.toml` or `setup.py` defines the project root. +- **Java** — jdtls-lsp (requires JDK 17+). Covers Spring Boot / Spring MVC / JAX-RS / Micronaut / Quarkus / gRPC / Kafka / RabbitMQ / JMS / Scheduled / AWS Lambda surfaces; `Service` / `Repository` / `Controller` / `Impl` / `Dto` / `Factory` layered-architecture name variants. Maven `pom.xml` or Gradle `settings.gradle*` defines the project root, with multi-module support. ### Adding a language diff --git a/skills/impact/adapters/java.md b/skills/impact/adapters/java.md new file mode 100644 index 0000000..d4bcde7 --- /dev/null +++ b/skills/impact/adapters/java.md @@ -0,0 +1,186 @@ +# Java adapter + +The Java language adapter for the `impact` skill. Follows the five-section contract in [README.md](./README.md). + +## 1. Fingerprint + +Activate this adapter if any of the following is present in the target project: + +- `pom.xml` at the project root (Maven). +- `build.gradle`, `build.gradle.kts`, or `settings.gradle` / `settings.gradle.kts` at the project root (Gradle, including multi-module). +- `build.xml` at the project root (Ant — rare, legacy). +- Any `**/*.java` file inside the project root (fallback for manifest-less projects). + +For multi-module builds, the project root is the directory of the **top-level** `pom.xml` or `settings.gradle*` — not an individual module's manifest. + +## 2. LSP plugin + +**Plugin:** `jdtls-lsp` (from Anthropic's `claude-plugins-official` marketplace). Uses Eclipse JDT.LS. + +**Requirements:** Java 17 or later (JDK, not JRE) must be on PATH. `java -version` should print `openjdk version "17"` or higher. + +**Install:** + +```bash +# 1. Install JDK 17+ if you don't already have one. +# macOS: brew install openjdk@17 +# Ubuntu: apt install openjdk-17-jdk +# SDKMAN: sdk install java 21-tem +# Verify: java -version + +# 2. Install jdtls so that `jdtls` resolves on the global PATH. The +# Claude Code LSP tool spawns this binary from PATH. +# macOS: brew install jdtls +# Arch: yay -S jdtls (AUR) +# Other: download from https://download.eclipse.org/jdtls/snapshots/ +# extract to e.g. ~/.local/share/jdtls, then create a +# wrapper script named `jdtls` on your PATH. + +# Verify: +which jdtls # must print a path +which java # must print a path + +# 3. In Claude Code, add the marketplace and install the plugin: +/plugin marketplace add anthropics/claude-plugins-official +/plugin install jdtls-lsp +``` + +After install, run `/reload-plugins` (or restart the session) and the built-in `LSP` tool will route Java files to JDT.LS. + +**Caveat — first-invocation cost.** JDT.LS has a substantially higher cold-start cost than pyright: it indexes the project's classpath (Maven/Gradle dependencies, target/build outputs, generated sources) on first contact. Expect a multi-second delay on the first `documentSymbol` or `hover` call in a fresh session. Subsequent calls are fast. If the sentinel call below times out the first time, retry after 10–15 seconds before concluding the LSP is broken. + +**Caveat — single-file indexing is likely here too.** Treat the LSP tool as a single-file symbol and type oracle by default (same guidance as the Python adapter). JDT.LS is more workspace-aware than pyright under normal usage, but the Claude Code harness has only been verified in single-file mode. The impact-skill pipeline uses Glob + per-file `documentSymbol`; do not rely on `workspaceSymbol` without empirically confirming it works for your setup. + +**Sentinel:** open any `.java` file in the project and call `documentSymbol` on it. If JDT.LS returns a non-empty symbol tree, the server is live. If the call errors with `Executable not found in $PATH`, jdtls isn't installed — tell the user to run the install steps above. If it errors with a JDK-version message, Java 17+ isn't installed or isn't the one on PATH. + +## 3. Name-variant generator + +Given a spec identifier (PascalCase in Allium), emit variants following Java convention. + +**For entities and variants:** + +- `` — PascalCase class, interface, enum, or record. +- `Impl`, `Default`, `Abstract` — implementation / default / abstract-base classes. +- `I` — interface prefix (Hungarian-style, uncommon but present in some shops). +- `Service`, `Repository`, `Controller`, `Manager`, `Facade` — standard layered-architecture suffixes. +- `Dto`, `Entity`, `Model`, `Record` — data-carrier suffixes. +- `Factory`, `Builder`, `Provider` — creational-pattern suffixes. + +**For rules and triggers:** + +- `` — camelCase method (Allium `ScheduleInterview` → `scheduleInterview`). +- `` — PascalCase command/event/handler class. +- `Command`, `Handler`, `Event` — CQRS / event-sourcing pattern. +- `handle`, `on`, `process`, `execute` — handler method variants. + +**For surfaces:** + +- `Controller`, `Resource`, `Endpoint` — REST controller classes (Spring / JAX-RS / Micronaut). +- `GrpcService` — gRPC service implementations. + +**Case conversion:** splitting on CamelCase and converting the first char of each component to lower (camelCase for methods, PascalCase preserved for classes) is sufficient. Acronyms in identifiers: treat `XML`, `HTTP`, `ID`, `URL`, `SQL` etc. as single units both as `XML` and `Xml` — emit both variants when the spec uses an acronym (`ScheduleXmlImport` and `ScheduleXMLImport` as alternates). + +## 4. Project-root rule + +**Root discovery:** walk upward from the spec file's directory. + +- If a `settings.gradle` or `settings.gradle.kts` is found, that directory is the root (multi-module Gradle). +- Otherwise the first directory containing `pom.xml` is the root — but for Maven multi-module, continue walking up: the topmost `pom.xml` whose `` is `pom` wins. If none have `pom` packaging, the deepest-located `pom.xml` on the walk wins. +- Otherwise the first directory containing `build.gradle` / `build.gradle.kts` / `build.xml` is the root. +- Fallback: the first directory containing a `.git` folder. + +**Source globs:** + +- `src/main/java/**/*.java` (Maven and Gradle standard layout). +- For multi-module: `*/src/main/java/**/*.java` at the root, recursing into each module. +- Kotlin-mixed projects: also include `src/main/kotlin/**/*.kt` if the Kotlin adapter is not loaded; Kotlin call sites into Java types are resolvable by JDT.LS in many setups but the adapter's primary source remains `.java`. + +**Exclusions (always):** + +- `src/test/**`, `src/integrationTest/**` — test sources. +- `target/**` — Maven build output. +- `build/**`, `out/**` — Gradle / IntelliJ build output. +- `target/generated-sources/**`, `build/generated/**`, `**/generated/**` — annotation-processor and code-generator output (Lombok, MapStruct, Protobuf, QueryDSL). +- `.idea/**`, `.settings/**`, `.project`, `.classpath`, `*.iml` — IDE metadata. +- `**/*.class`, `**/*.jar`, `**/*.war`, `**/*.ear` — compiled artefacts. +- Any path matched by the project's `.gitignore`. + +**Depth:** default call-hierarchy expansion depth is 2. Stop when a call crosses into a dependency JAR (anything outside the project's source set). Be aware that Lombok-generated methods (`getX`, `setX`, `builder()`) may appear as synthetic symbols — prefer the declaring field over the synthetic accessor when recording links. + +## 5. Surface entry-point patterns + +### API surfaces + +**Spring Boot / Spring MVC:** + +- Class-level: `@RestController`, `@Controller`, `@RequestMapping("/path")`. +- Method-level: `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`, `@RequestMapping(method = ...)`. +- Error handlers: `@ExceptionHandler`, `@ControllerAdvice`, `@RestControllerAdvice`. +- Outbound clients: `@FeignClient` interfaces (Spring Cloud) — treat as surface-level contracts the code demands from an external service. + +**JAX-RS (Jakarta RESTful Web Services):** + +- Class-level: `@Path("/path")`. +- Method-level: `@GET`, `@POST`, `@PUT`, `@DELETE`, `@HEAD`, `@OPTIONS`, `@PATCH`. +- Content negotiation: `@Produces(...)`, `@Consumes(...)` — relevant to contract `demands`/`fulfils`. + +**Micronaut / Quarkus:** both accept Spring-style and JAX-RS annotations. Also: + +- Micronaut: `@Controller("/path")` at class level, `@Get`, `@Post`, `@Put`, `@Delete`, `@Patch` at method level. `@Client(...)` for outbound HTTP clients. +- Quarkus: predominantly JAX-RS-style; `@RegisterRestClient` for MicroProfile REST clients. + +**Servlet API (legacy):** + +- `@WebServlet("/path")` on classes extending `HttpServlet`. `doGet`, `doPost`, etc. are the per-method entry points. + +**gRPC:** + +- Classes extending a generated `*ImplBase` class (from a `.proto`). +- Micronaut: `@GrpcService`. Spring: grpc-spring-boot-starter auto-registers `@GrpcService` beans. + +### UI surfaces + +Rare for Java backends. When present: + +- **Thymeleaf / JSP:** `@Controller` methods returning view names — treat the controller method as the surface entry point, not the template. +- **JavaFX:** classes extending `Application`; controllers referenced from `.fxml` via `fx:controller=`; methods annotated `@FXML` are event handlers. +- **Swing / AWT (desktop, legacy):** classes extending `JFrame`, `JPanel`, `JDialog`; `ActionListener` implementations are event entry points. + +### Integration surfaces + +**Message brokers:** + +- Kafka: `@KafkaListener(topics = ...)`. +- RabbitMQ / AMQP: `@RabbitListener(queues = ...)`, `@RabbitHandler`. +- JMS: `@JmsListener(destination = ...)`. +- Spring Cloud Stream: `@Bean Function` / `Consumer` / `Supplier` binding names. + +**Scheduled / background:** + +- Spring: `@Scheduled(cron = ...)`, `@Scheduled(fixedRate = ...)`. +- Quartz: classes implementing `Job`, `execute(JobExecutionContext)`. +- Micronaut: `@Scheduled`. + +**Event listeners (in-process):** + +- Spring: `@EventListener`, `@TransactionalEventListener`, `ApplicationListener` implementations. + +**AWS Lambda handlers:** + +- Classes implementing `RequestHandler` or `RequestStreamHandler` — `handleRequest` is the surface entry point. +- The `MANIFEST.MF` / `pom.xml` `mainClass` or the deployment descriptor is the dispatch target. + +**SDK / outbound client surfaces:** + +- Classes named `Client`, `Gateway`, `Adapter`, `Proxy`. +- Feign interfaces annotated `@FeignClient(name = ...)`. +- MicroProfile REST clients annotated `@RegisterRestClient`. + +### What not to match as surfaces + +- Anything under `src/test/**` — test classes, even those annotated with route/handler annotations (e.g. `@WebMvcTest`-scoped controllers). +- Classes suffixed `Test`, `Tests`, `IT`, `E2E`, or annotated `@Test`, `@SpringBootTest`, `@WebMvcTest`, `@DataJpaTest`, etc. +- Abstract controllers / handlers — follow `goToImplementation` to the concrete subclass and map the surface there. +- Lombok-generated accessors surfaced by the LSP — prefer the declaring field or the method you authored. +- `@Configuration` / `@Bean` wiring classes — these are composition plumbing, not behavioural surfaces (unless the bean itself is a controller/listener, in which case that bean is the surface). +- Auto-generated stubs from `.proto`, OpenAPI codegen, or JAXB — map surfaces to the hand-authored service class that *implements* them, not the generated base. From d15393ee7facbdfc9b3a5c8ed8673472ae91748c Mon Sep 17 00:00:00 2001 From: Matt Ford Date: Mon, 20 Apr 2026 21:14:22 +0100 Subject: [PATCH 08/13] Add TypeScript adapter for the impact skill Adds skills/impact/adapters/typescript.md covering typescript-lsp (typescript-language-server + tsserver). Name-variant generator handles camelCase / PascalCase conventions plus React's use hook prefix, CQRS suffixes (Command / Handler / UseCase), and common DTO / schema / props suffixes. Surface entry-points cover Express / Fastify / Koa / NestJS / Hono for HTTP APIs, Next.js App Router and Pages Router (both), tRPC procedures, GraphQL resolvers (Apollo and NestJS), React / Vue / Svelte / React Native for UI, and the JS-ecosystem integration patterns (BullMQ, node-cron, kafkajs, NATS, WebSocket, AWS Lambda, Vercel/Netlify functions, Cloudflare Workers, Temporal workflows). Project-root rule handles monorepos: npm/pnpm/yarn workspaces, Nx, Turborepo. Exclusions cover the build outputs (dist, .next, .turbo, .vercel), test/story files, ambient .d.ts types, and the generated-code patterns TS codegen produces (graphql-codegen, Prisma, Protobuf). Notes the tsconfig scope caveat: tsserver degrades on files outside the nearest tsconfig.json's include/files set, which matters when picking sentinel files. Registers the adapter in the impact SKILL.md fingerprint table and currently-supported list. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/impact/SKILL.md | 2 + skills/impact/adapters/typescript.md | 205 +++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 skills/impact/adapters/typescript.md diff --git a/skills/impact/SKILL.md b/skills/impact/SKILL.md index 4fb9ad6..e31a0a2 100644 --- a/skills/impact/SKILL.md +++ b/skills/impact/SKILL.md @@ -107,6 +107,7 @@ On startup, detect the target language by fingerprint: |---|---| | `pyproject.toml`, `setup.py`, `setup.cfg`, or `**/*.py` files | [python.md](./adapters/python.md) | | `pom.xml`, `build.gradle`, `build.gradle.kts`, `settings.gradle*`, `build.xml`, or `**/*.java` files | [java.md](./adapters/java.md) | +| `tsconfig.json`, `jsconfig.json`, `package.json`, or `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` files | [typescript.md](./adapters/typescript.md) | A spec may also declare its target language explicitly via a `language:` field on its `use` declarations or the caller may pass `--language `. Explicit selection overrides auto-detection. @@ -116,6 +117,7 @@ If a project mixes languages (e.g. a Python service with a TypeScript frontend), - **Python** — pyright-lsp. Covers Flask/FastAPI/Django surfaces, PascalCase classes, snake_case functions, `create_X`/`X_service`/`X_repository` patterns. `pyproject.toml` or `setup.py` defines the project root. - **Java** — jdtls-lsp (requires JDK 17+). Covers Spring Boot / Spring MVC / JAX-RS / Micronaut / Quarkus / gRPC / Kafka / RabbitMQ / JMS / Scheduled / AWS Lambda surfaces; `Service` / `Repository` / `Controller` / `Impl` / `Dto` / `Factory` layered-architecture name variants. Maven `pom.xml` or Gradle `settings.gradle*` defines the project root, with multi-module support. +- **TypeScript / JavaScript** — typescript-lsp (requires Node + `typescript-language-server` + `typescript` on PATH). Covers Express / Fastify / Koa / NestJS / Hono / Next.js (Pages + App Router) / tRPC / GraphQL / React component and hook surfaces; BullMQ / node-cron / Kafka / NATS / WebSocket / AWS Lambda / Vercel / Cloudflare Workers integration surfaces. Monorepo-aware (npm/pnpm/yarn workspaces, Nx, Turborepo). Excludes generated code (`*.generated.ts`, `graphql-codegen`, Prisma, Protobuf). ### Adding a language diff --git a/skills/impact/adapters/typescript.md b/skills/impact/adapters/typescript.md new file mode 100644 index 0000000..5d9aacb --- /dev/null +++ b/skills/impact/adapters/typescript.md @@ -0,0 +1,205 @@ +# TypeScript adapter + +The TypeScript / JavaScript language adapter for the `impact` skill. Follows the five-section contract in [README.md](./README.md). Covers both TypeScript and JavaScript projects — the LSP treats `.js` / `.jsx` as untyped TS. + +## 1. Fingerprint + +Activate this adapter if any of the following is present in the target project: + +- `tsconfig.json` at the project root (TypeScript). +- `jsconfig.json` at the project root (type-checked JavaScript). +- `package.json` at the project root with a `typescript` dependency (direct or indirect). +- Any `**/*.{ts,tsx,mts,cts}` file inside the project root. +- `package.json` at the project root plus any `**/*.{js,jsx,mjs,cjs}` file (fallback for plain JS projects). + +For monorepos — workspaces declared in `package.json`, pnpm `pnpm-workspace.yaml`, Nx, Turborepo, Lerna — the project root is the top-level workspace root, and per-package `tsconfig.json` files are loaded as sub-project boundaries. + +## 2. LSP plugin + +**Plugin:** `typescript-lsp` (from Anthropic's `claude-plugins-official` marketplace). Uses `typescript-language-server` on top of the `typescript` package's `tsserver`. + +**Requirements:** Node.js and npm (or yarn / pnpm) on PATH. + +**Install:** + +```bash +# 1. Install the language server and the TypeScript package globally so +# `typescript-language-server` resolves on the global PATH. The +# Claude Code LSP tool spawns this binary from PATH — project-local +# installs under node_modules/.bin will NOT be picked up. +npm install -g typescript-language-server typescript + +# (yarn global add and pnpm add -g also work, provided their global +# bin directory is on PATH.) + +# Verify: +which typescript-language-server # must print a path +which node # must print a path + +# 2. In Claude Code, add the marketplace and install the plugin: +/plugin marketplace add anthropics/claude-plugins-official +/plugin install typescript-lsp +``` + +After install, run `/reload-plugins` (or restart the session) and the built-in `LSP` tool will route TypeScript and JavaScript files to the server. + +**Caveat — tsconfig awareness.** `typescript-language-server` spawns a `tsserver` process that reads the nearest `tsconfig.json` / `jsconfig.json` walking up from the opened file. If a file lives outside every `tsconfig.json`'s `include` / `files` list, tsserver will report it as loose-mode (no type information, limited symbol intelligence). When building the impact map, avoid pointing the LSP at files that are `exclude`d from the nearest `tsconfig.json` — they'll return degraded results. + +**Caveat — single-file indexing is likely here too.** Treat the LSP tool as a single-file symbol and type oracle by default (same guidance as the Python adapter). `tsserver` is more workspace-aware than pyright under normal IDE usage, but the Claude Code harness has only been verified in single-file mode. The impact-skill pipeline uses Glob + per-file `documentSymbol`; do not rely on `workspaceSymbol` without empirically confirming it works for your setup. + +**Sentinel:** open any `.ts` or `.tsx` file in the project and call `documentSymbol` on it. If tsserver returns a non-empty symbol tree, the server is live. If the call errors with `Executable not found in $PATH`, the language server isn't installed — tell the user to run the install steps above. If it returns a minimal tree for a file that clearly has classes or exports, the file is likely outside the project's `tsconfig.json` scope. + +## 3. Name-variant generator + +Given a spec identifier (PascalCase in Allium), emit variants following TypeScript / JavaScript convention. + +**For entities and variants:** + +- `` — PascalCase class, interface, type alias, or enum (TS). +- `I` — interface prefix (uncommon in modern TS but present in codebases migrated from C# / Java). +- `` — camelCase variable, function, or named export (Allium `Candidacy` → `candidacy`). +- `create`, `make`, `new` — factory functions in camelCase. +- `Service`, `Repository`, `Store`, `Manager`, `Provider` — layered-architecture classes. +- `use` — React hook convention (Allium `Candidacy` → `useCandidacy`). +- `Props`, `State`, `Schema`, `Dto` — component / schema / DTO suffixes. + +**For rules and triggers:** + +- `` — camelCase function (Allium `ScheduleInterview` → `scheduleInterview`). +- `` — PascalCase class (command / handler / controller style). +- `handle`, `on`, `process`, `execute` — handler method / function variants. +- `Handler`, `Command`, `UseCase` — CQRS / clean-architecture naming. + +**For surfaces:** + +- `Router`, `Controller`, `Resolver` — framework-specific route / handler containers. +- `Page`, `Layout`, `View`, `Screen` — UI surface containers (Next.js / React / React Native). +- `Api`, `Routes`, `Resolvers` — module-level naming. + +**Case conversion:** splitting on CamelCase is sufficient. Modern TS discourages the `I` interface prefix but emit it as a lower-priority alternate when a spec identifier is an interface-like name. For React components specifically, always emit both PascalCase (the component itself) and `use` (the conventional hook). + +## 4. Project-root rule + +**Root discovery:** walk upward from the spec file's directory. + +- If a `pnpm-workspace.yaml`, a `package.json` with a `workspaces` field, or an `nx.json` / `turbo.json` is found, that directory is the monorepo root. Per-package `tsconfig.json` files remain sub-project boundaries. +- Otherwise the first directory containing `tsconfig.json` or `jsconfig.json` is the project root. +- Otherwise the first directory containing `package.json` is the root. +- Fallback: the first directory containing a `.git` folder. + +**Source globs:** + +- If `tsconfig.json` declares `include` / `files`, honour them. +- Otherwise default to `src/**/*.{ts,tsx,mts,cts}` and `/**/*.{ts,tsx}`. +- For Next.js: `pages/**/*.{ts,tsx}` and `app/**/*.{ts,tsx}` in addition to `src/**`. +- For monorepos: `packages/*/src/**/*.{ts,tsx}`, `apps/*/src/**/*.{ts,tsx}`, or whatever the `workspaces` glob declares. + +**Exclusions (always):** + +- `node_modules/**` — dependency code. +- `dist/**`, `build/**`, `out/**`, `.next/**`, `.nuxt/**`, `.turbo/**`, `.vercel/**` — build outputs. +- `coverage/**` — test-coverage reports. +- `**/*.test.{ts,tsx,js,jsx}`, `**/*.spec.{ts,tsx,js,jsx}`, `**/__tests__/**`, `**/__mocks__/**` — test code and mocks. +- `**/*.d.ts` — ambient type declarations (unless the project's own `.d.ts` files are load-bearing behaviour). +- `**/*.stories.{ts,tsx,js,jsx}`, `**/*.mdx` — Storybook / MDX. +- `**/*.generated.{ts,tsx}`, `**/generated/**` — GraphQL / OpenAPI / Prisma / Protobuf codegen output. +- Any path matched by the project's `.gitignore`. + +**Depth:** default call-hierarchy expansion depth is 2. Stop when a call crosses into `node_modules`. Note: barrel re-exports (`export * from "./foo"`) can inflate reference counts; prefer the declared source of a symbol over its re-export sites when recording links. + +## 5. Surface entry-point patterns + +### API surfaces + +**Express:** + +- `app.get|post|put|delete|patch(...)`, `router.get|post|...`. +- Middleware signatures: `(req, res, next) => ...`. + +**Fastify:** + +- `fastify.get|post|...`, `fastify.route({ method, url, handler })`. +- Plugin registration: `fastify.register(plugin)`. + +**Koa:** + +- `router.get|post|...` via `@koa/router`; middleware signature `(ctx, next) => ...`. + +**NestJS:** + +- Class-level decorators: `@Controller('/path')`, `@Resolver()`, `@WebSocketGateway()`. +- Method-level decorators: `@Get`, `@Post`, `@Put`, `@Delete`, `@Patch`, `@Options`, `@Head`; GraphQL `@Query`, `@Mutation`, `@Subscription`; WebSocket `@SubscribeMessage`. +- Dependency injection via constructor — relevant to surface `demands` contracts. + +**Hono:** + +- `app.get|post|...`, `app.route(path, sub)`. + +**Next.js:** + +- App Router: `app/**/route.{ts,tsx}` files exporting named HTTP methods (`export async function GET(request) { … }`). +- Pages Router: `pages/api/**/*.{ts,tsx}` files exporting a default handler. +- Server Actions: functions annotated with `"use server"` inside server components. + +**tRPC:** + +- Router definitions: `t.router({ procedure: t.procedure.query(...) | .mutation(...) })`. +- Procedure calls are the surface entry points; the input / output types are the contract obligations. + +**GraphQL:** + +- Apollo Server / Mercurius resolvers — resolver functions on the `Query`, `Mutation`, `Subscription` root types. +- NestJS `@Resolver()` + `@Query` / `@Mutation`. + +### UI surfaces + +**React / Next.js:** + +- Components: PascalCase functions or classes in `components/**`, `app/**/page.tsx`, `pages/**/*.tsx`. The component is the surface entry point. +- Hooks: `use` in `hooks/**` — treat as the surface's state contract when the spec refers to them by name. +- Server components (Next.js app router): `app/**/page.tsx`, `app/**/layout.tsx` — server-rendered surfaces. + +**Vue / Svelte / Solid:** + +- `.vue` / `.svelte` / `.jsx` single-file components — the `