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/TODO.md b/TODO.md new file mode 100644 index 0000000..6408cd2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +# repo-impact-map branch + +1. ~~Don't try to combine both grep based searching and impact map searching into a single command. Rather enable the invoker to run either-or or both.~~ — Addressed: weed/propagate/distill default to grep + read; map mode is opt-in via an explicit user phrase ("use the impact map", "in map mode", "via impact"). See the `## Map mode` appendix in each affected SKILL.md and the `### Opting in` section of `skills/allium/references/impact-map.md`. +2. When building the spec->code mapping might as well build just a code-mapping (this will help with code generation without polluting context with specs if required) +3. Implement a decent test-suite on master branch before doing any more changes +4. Implement a harness for development.... \ No newline at end of file diff --git a/hooks/hooks.json b/hooks/hooks.json index 754294d..ef13de4 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,4 +1,13 @@ { + "permissions": { + "allow": [ + "Read(//home/matt/.claude/**)", + "Edit(~/.claude/skills/code-map/**)" + ], + "additionalDirectories": [ + "/home/matt/.claude/skills/code-map" + ] + }, "hooks": { "PostToolUse": [ { diff --git a/skills/allium/references/impact-map.md b/skills/allium/references/impact-map.md new file mode 100644 index 0000000..0e63051 --- /dev/null +++ b/skills/allium/references/impact-map.md @@ -0,0 +1,284 @@ +# 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` — 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. +- `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. +- 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: + +- 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. + +### Opting in + +Consumer skills do not auto-invoke this map. Their default flows (`weed`, `propagate`, `distill`) use grep + read correlation, just as they did before the impact map existed. The user (or the user's prompt to the consumer skill) selects map mode explicitly — typically by saying "use the impact map", "in map mode" or "via impact" in the request. + +The presence of `.allium/impact/.json` is **not** by itself an opt-in signal for `weed`, `propagate` or `distill` — they ignore it unless the user asks. The exception is `tend`, whose pre-rename orphan-link warning fires defensively whenever a map exists; that's a one-shot warning, not a gate. + +The map's value is on demand; its absence is not a bug for any consumer. + +### 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 — all conditional on the user having opted into map mode for the consumer skill in question: + +- Map mode `weed` run — refresh first if the map exists, build if it does not. +- Map mode `propagate` run — refresh first if the map exists, build if it does not. +- Map mode `distill` on a spec with an existing skeleton — build. +- After a large refactor — build (user-initiated, ahead of the next map-mode consumer run). +- When map mode `weed` reports a surprising volume of divergences, suggesting the map is stale — refresh. + +### Graceful degradation + +This section applies once the user has opted into map mode and the map cannot be built or refreshed. 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 their default (grep + read) flow rather than refusing the work. Consumers should: + +- Note the degradation reason to the user once, not on every step. +- Proceed with the default flow as they would have without map mode requested. +- 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 in **map mode** (the user asked for it) 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 in map mode the map got `weed` straight to the right file in one hop instead of grep'ing. The default `weed` flow does not consult this map; it greps for `Candidacy` and `ScheduleInterview` and reads the matches. diff --git a/skills/distill/SKILL.md b/skills/distill/SKILL.md index 5937ece..2393483 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 (opt-in) + +If the user has explicitly directed you to use the impact map ("use the impact map", "in map mode", "via impact") **and** 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 the user has not opted in, or you are distilling from a completely empty spec (no skeleton), skip this step. Distillation works with grep + read alone — that's the default. + +If the user opted in but 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. 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. In map mode, the impact map's surface links and `call_edges` entry points are your starting list; otherwise, grep for framework decorators / route definitions and read the matches. 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..5ad2987 --- /dev/null +++ b/skills/impact/SKILL.md @@ -0,0 +1,183 @@ +--- +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 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 + +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 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. + +**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 + +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.** 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. + + 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. 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.). 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. + +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 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. + +### 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) | +| `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) | +| `build.gradle.kts`, `settings.gradle.kts`, Kotlin-plugin'd `pom.xml`, `AndroidManifest.xml`, or `**/*.{kt,kts}` files | [kotlin.md](./adapters/kotlin.md) | +| `deps.edn`, `project.clj`, `build.boot`, `shadow-cljs.edn`, `bb.edn`, or `**/*.{clj,cljs,cljc,edn,bb}` files | [clojure.md](./adapters/clojure.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. +- **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). +- **Kotlin** — kotlin-lsp (JetBrains' official LSP server; requires JDK 17+). Covers Ktor routing DSL, Spring Boot / Micronaut / Quarkus / JAX-RS / gRPC / graphql-kotlin API surfaces; Android Activity / Fragment / Jetpack Compose / WorkManager UI and background surfaces; coroutines (`Flow` collectors, `Channel` receivers, `suspend` handlers), Kafka, RabbitMQ, Retrofit interface integration surfaces. Name variants include clean-architecture / Android MVVM suffixes (`UseCase`, `ViewModel`, `UiState`, companion-object factories). Multi-module Gradle-aware, Kotlin Multiplatform source-set-aware, mixed Kotlin/Java supported by loading the Java adapter alongside. +- **Clojure / ClojureScript** — clojure-lsp (GraalVM-native binary; optional JDK 11+ for the JAR distribution). Covers Ring / Compojure / Reitit / Pedestal / Liberator / Yada / Lacinia GraphQL / protojure gRPC API surfaces; Reagent / Re-frame / Fulcro UI surfaces; Kafka (jackdaw, kinsky) / RabbitMQ (langohr) / core.async / Manifold / Quartzite / chime integration surfaces; Integrant / Component / Mount lifecycle wiring; Babashka task surfaces. Kebab-case-first name variants with bang / question-mark suffix conventions, `->Record` and `map->Record` auto-factories, multimethod dispatch values, protocol extension via `extend-protocol`, and namespace-aliased calls. Honours `:paths` / `:source-paths` declared in `deps.edn`, `project.clj` and `shadow-cljs.edn`; reader-conditional-aware for `.cljc`; clj-kondo-hook-aware for custom macros. + +### 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/clojure.md b/skills/impact/adapters/clojure.md new file mode 100644 index 0000000..cadf358 --- /dev/null +++ b/skills/impact/adapters/clojure.md @@ -0,0 +1,257 @@ +# Clojure adapter + +The Clojure / ClojureScript language adapter for the `impact` skill. Follows the five-section contract in [README.md](./README.md). Covers Clojure (JVM), ClojureScript, Babashka and `.cljc` shared sources — clojure-lsp treats all four as one language with per-dialect reader conditionals. + +## 1. Fingerprint + +Activate this adapter if any of the following is present in the target project: + +- `deps.edn` at the project root (Clojure CLI / `tools.deps`). +- `project.clj` at the project root (Leiningen). +- `build.boot` at the project root (Boot — legacy, still seen). +- `shadow-cljs.edn` at the project root (ClojureScript via shadow-cljs). +- `bb.edn` at the project root (Babashka script project). +- Any `**/*.{clj,cljs,cljc,edn,bb}` file inside the project root. + +If both `deps.edn` and `project.clj` exist, prefer `deps.edn` as the authoritative manifest; many projects keep `project.clj` purely for editor tooling. If `shadow-cljs.edn` is present alongside `deps.edn`, the project is a mixed Clojure/ClojureScript build — the adapter still activates once and walks both source sets. + +## 2. LSP plugin + +**Plugin:** `clojure-lsp` (from JUXT's `juxt-plugins` marketplace — the official Anthropic marketplace does not currently ship a Clojure LSP plugin). Uses the upstream `clojure-lsp` server ([github.com/clojure-lsp/clojure-lsp](https://github.com/clojure-lsp/clojure-lsp)), which wraps [clj-kondo](https://github.com/clj-kondo/clj-kondo) for static analysis. + +**Requirements:** The GraalVM-compiled native binary has no runtime dependencies. The JAR distribution requires JDK 11 or later on PATH. Either is acceptable; the native binary is faster to start and is recommended for Claude Code use. + +**Install:** + +```bash +# 1. Install clojure-lsp so the `clojure-lsp` binary resolves on PATH. +# The Claude Code LSP tool spawns this binary from PATH. +# macOS: brew install clojure-lsp/brew/clojure-lsp-native +# Linux: bash < <(curl -s https://raw.githubusercontent.com/clojure-lsp/clojure-lsp/master/install) +# Arch: pacman -S clojure-lsp (or yay -S clojure-lsp-bin) +# Nix: nix-env -iA nixpkgs.clojure-lsp +# Other: download the native zip from +# https://github.com/clojure-lsp/clojure-lsp/releases +# and place `clojure-lsp` on PATH. + +# Verify: +which clojure-lsp # must print a path +clojure-lsp --version + +# 2. In Claude Code, add the JUXT marketplace and install the plugin: +/plugin marketplace add juxt/juxt-plugins +/plugin install clojure-lsp@juxt-plugins +``` + +After install, run `/reload-plugins` (or restart the session) and the built-in `LSP` tool will route `.clj`, `.cljs`, `.cljc`, `.edn` and `.bb` files to clojure-lsp. + +**Caveat — project indexing cost.** clojure-lsp indexes the full classpath on first contact, resolving `deps.edn` / `project.clj` dependencies into a `.lsp/.cache/` directory. On a fresh clone, the first invocation can take 10–30 seconds while it fetches dependencies and runs clj-kondo across the project. Subsequent calls are fast because the cache is reused. If the sentinel call below times out the first time, retry after 15–30 seconds before concluding the LSP is broken. If `clojure -P` (or `lein deps`) has never been run for this project, do that first — the LSP cannot index unresolved dependencies. + +**Caveat — reader conditionals.** `.cljc` files contain `#?(:clj ... :cljs ...)` blocks; clojure-lsp returns symbols for the dialect indicated by the file's context (deduced from `deps.edn` / `shadow-cljs.edn`). If a spec construct is implemented behind a reader-conditional branch you cannot see (e.g. only the `:cljs` branch but the project is being probed as `:clj`), you may get a false "unmapped" result. Record the construct in `unmapped.spec` with a note, not a guess. + +**Caveat — macro-heavy code.** Clojure relies heavily on macros (`defn`, `defroutes`, `defrecord`, `deftest`, `defmethod`, `defmulti`, `defschema`, custom DSLs). clj-kondo (and therefore clojure-lsp) reports macro-generated vars when it understands the macro; for unknown macros it either silently misses the vars or reports them with empty metadata. Most common macros have built-in clj-kondo support; project-specific DSLs may need `.clj-kondo/config.edn` hooks to be visible. Missing symbols from a custom macro are a tooling limitation, not a divergence. + +**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, Java, TypeScript and Kotlin adapters). clojure-lsp is genuinely workspace-aware under normal editor use and `workspaceSymbol` queries do return project-wide results when the `.lsp/.cache/` is populated — but the Claude Code harness has only been verified in single-file mode to date. The impact-skill pipeline uses Glob + per-file `documentSymbol`; treat `workspaceSymbol` as a bonus signal if it works rather than a primary discovery mechanism. + +**Sentinel:** open any `.clj` or `.cljs` file in the project and call `documentSymbol` on it. If clojure-lsp returns a non-empty symbol tree, the server is live. If the call errors with `Executable not found in $PATH`, `clojure-lsp` isn't installed — tell the user to run the install steps above. If it returns an empty or suspiciously shallow tree on a file that clearly contains `defn` / `defrecord` forms, the `.lsp/.cache/` is probably stale or missing — run `clojure -P` (or `lein deps`) in the project root, then retry. + +## 3. Name-variant generator + +Given a spec identifier (PascalCase in Allium), emit variants following Clojure convention. Clojure is **kebab-case-first** and case-distinctions carry meaning (PascalCase is reserved for types, records, protocols, and exceptions; everything else is kebab-case). The adapter reflects that. + +**For entities and variants:** + +- `` — kebab-case var, function, namespace segment, or keyword (Allium `Candidacy` → `candidacy`). +- `` — PascalCase `defrecord`, `deftype`, `defprotocol`, or exception class. +- `->` and `map->` — the two factory functions auto-generated by `defrecord` / `deftype`. Always emit these when `` is a likely record/type. +- `create-`, `make-`, `new-`, `build-` — factory functions. +- `?` — predicate function (Clojure convention for boolean-returning fns). +- `!` — side-effecting function (Clojure convention for fns with observable side effects). +- `-service`, `-repository`, `-store`, `-manager` — layered-architecture namespaces or component keys. +- `I` — protocol name variant (older style, still seen; the `I` prefix is not idiomatic in modern Clojure but present in Java-influenced codebases). +- `:` — keyword identifier. Treat keyword references as lower-confidence candidates: keywords often appear as keys in data (Integrant component keys, multimethod dispatch values, route identifiers) and are a strong signal even though they are not LSP-discoverable symbols in the `documentSymbol` tree. Use Grep to locate keyword literals and cross-reference them with nearby `defmethod` / `defmulti` / Integrant / Reitit forms. + +**For rules and triggers:** + +- `` — kebab-case function (Allium `ScheduleInterview` → `schedule-interview`). +- `!` — side-effecting variant (rules that mutate state typically use the bang suffix). +- `handle-`, `on-`, `process-`, `execute-` — handler functions. +- `` — PascalCase record / exception for command or event objects (rarer in Clojure than Java; seen in CQRS-ish or event-sourced codebases). +- `:` — keyword used as multimethod dispatch value or event-name: grep `(defmethod : [...])` to find handlers. + +**For surfaces:** + +- `-routes`, `-handler`, `-api`, `-app` — namespace or var naming for route definitions and Ring handlers. +- `-component`, `-system` — Integrant / Component / Mount top-level system keys. +- `-resource` — liberator-style resource definitions. + +**Case conversion:** split a PascalCase spec identifier on CamelCase boundaries, lowercase each component, join with hyphens (`ScheduleXmlImport` → `schedule-xml-import`). Acronyms: treat `XML`, `HTTP`, `ID`, `URL`, `SQL` as single lowercased units (`schedule-xml-import`, not `schedule-x-m-l-import`); emit both conventions when the spec identifier contains an acronym, since style varies across codebases. Namespaces are dot-separated kebab segments (`my.app.candidacy`); when matching a spec entity against a namespace, match the final segment only. + +## 4. Project-root rule + +**Root discovery:** walk upward from the spec file's directory. + +- If `deps.edn` is found, that directory is the root (tools.deps / Clojure CLI project). If `deps.edn` declares `:aliases` with `:local/root` dependencies, the referenced sub-projects have their own roots but participate in the same classpath — the top-level `deps.edn` directory remains the map root. +- Otherwise the first directory containing `project.clj` is the root (Leiningen). For multi-module Leiningen (`:sub` coordinates via lein-sub / lein-monolith), the topmost `project.clj` wins. +- Otherwise the first directory containing `build.boot` is the root. +- Otherwise the first directory containing `shadow-cljs.edn` is the root (ClojureScript-only project). +- Otherwise the first directory containing `bb.edn` is the root (Babashka-only project). +- Fallback: the first directory containing a `.git` folder. + +**Source globs:** + +- `src/**/*.{clj,cljs,cljc}` — Clojure / ClojureScript / shared sources (standard layout for tools.deps, Leiningen, and shadow-cljs). +- `src/main/clojure/**/*.clj` — Maven-style layout (seen when Clojure code is part of a larger JVM project; `clojure-maven-plugin`). +- `src/main/clojurescript/**/*.cljs` — same, ClojureScript counterpart. +- `dev/**/*.{clj,cljs,cljc}` — dev-only source directory (conventional in tools.deps projects with a `:dev` alias). Include when the spec describes dev-time behaviour; otherwise treat as out-of-scope. +- `resources/**/*.edn` — configuration data. Do not treat as source code for symbol discovery, but scan for keyword literals when mapping surfaces that use data-driven routing (Reitit route data, Integrant configs). +- `script/**/*.bb`, `scripts/**/*.bb`, `bb/**/*.clj` — Babashka scripts when `bb.edn` is present. + +Honour any `:paths` / `:source-paths` / `:extra-paths` declared in `deps.edn`, `project.clj`, or `shadow-cljs.edn` — they override the defaults above. A project that puts its sources under `core/` instead of `src/` is perfectly valid. + +**Exclusions (always):** + +- `test/**`, `src/test/**`, `**/*_test.{clj,cljs,cljc}`, `**/test_*.{clj,cljs,cljc}` — test sources (both by directory convention and `_test` filename suffix, which is the canonical `cljs.test` / `clojure.test` runner expectation). +- `target/**` — build output (Leiningen and tools.deps build tools both write here). +- `.cpcache/**` — tools.deps classpath cache. +- `.shadow-cljs/**`, `.clj-kondo/.cache/**`, `.lsp/.cache/**` — tooling caches. +- `classes/**`, `out/**` — AOT-compiled output / shadow-cljs build output. +- `node_modules/**` — present when a ClojureScript project has JS dependencies. +- `resources/public/js/**`, `public/js/**` — ClojureScript compilation artefacts. +- `.cljs_rhino_repl/**`, `.nrepl-port`, `.cider-repl-history` — REPL state. +- `pom.xml`, `pom.xml.asc` — Maven descriptors generated by `clj -T:build` (not hand-authored). +- 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 or `node_modules` (for ClojureScript). Be aware of: + +- **Vars vs functions.** Clojure vars are first-class: a `defn` defines a var bound to a fn, and call sites reference the var (late-bound). clojure-lsp reports the var declaration as the symbol; call-hierarchy resolves through the var. Treat `(my.ns/foo ...)` and `my.ns/foo` the same — both are edges to `foo`. +- **Multimethods.** `(defmulti name dispatch-fn)` creates one var; each `(defmethod name dispatch-val [...] body)` adds a method to that multimethod but does **not** create a new var. clojure-lsp reports the `defmethod` as an implementation of the multimethod var. Record each `defmethod` as an edge back to the `defmulti`, and annotate the link with the dispatch value in `via`. +- **Protocols.** `defprotocol` declares signatures; `extend-protocol`, `extend-type`, or `defrecord ... ` provide implementations. clojure-lsp can surface these relationships via `goToImplementation`; use that to bridge spec constructs that correspond to protocol method names. +- **Macros that define vars.** `defn`, `defn-`, `def`, `defrecord`, `deftype`, `defprotocol`, `defmethod`, `defschema`, `defroutes`, `defresource`, `deftest`, and project-specific DSL macros (`reg-event-db` in re-frame, `defcomponent` in some Component-ish libs) each produce analysable symbols when clj-kondo understands the macro. Unknown macros silently drop their defined vars; when a spec construct is unmapped and you suspect macro hiding, check `.clj-kondo/config.edn` / `.clj-kondo/hooks/`. +- **Anonymous inline fns (`#(...)`, `(fn [...] ...)`)** have no symbol; they are edges without endpoints. Record the enclosing named var as the edge target. +- **Namespace aliases.** A namespace required as `[my.app.candidacy :as c]` is called as `(c/create ...)`. clojure-lsp resolves the alias; record the edge to the real namespace, not the alias. + +## 5. Surface entry-point patterns + +Clojure's web ecosystem is unusually uniform under the hood: almost every framework compiles down to a **Ring handler** — a function `(fn [request] response)` or `(fn [request respond raise] nil)` — composed through middleware. The surface entry point is the routing-table entry that names that handler, not the handler function itself in isolation. + +### API surfaces + +**Ring / Ring-Jetty-Adapter / HTTP-Kit (the base layer):** + +- A Ring handler is `(defn handler [request] ...)` — a unary fn from request map to response map. +- The server entry point is typically `(run-jetty handler {:port ...})` or `(run-server handler {:port ...})` in an `-main` or Integrant/Component start function. +- Middleware composition (`wrap-json-body`, `wrap-params`, etc.) appears as `(-> handler wrap-json-body wrap-params)` — not itself a surface, but its presence tells you which handler is the outermost. + +**Compojure** (classic DSL over Ring): + +- `(defroutes app-routes (GET "/path" [] handler) (POST "/path" [params] ...))`. +- Method macros: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `ANY`, `context`. +- Each route clause is a surface entry; the inline body or the named handler var is the implementation. + +**Reitit** (data-driven, most common in modern Clojure): + +- Route data: `[["/path" {:get {:handler handler-var} :post {:handler other-handler}}]]`. +- Coercion and middleware in route data: `{:get {:parameters {...} :responses {...} :handler ...}}` — the `:parameters` / `:responses` nodes map directly onto surface `demands` / `provides` / `guarantees` contracts. +- Grep for the top-level route vector in a `-routes` namespace, then resolve each `:handler` keyword to its defining var via `documentSymbol` on the target file. + +**Pedestal:** + +- Interceptor chains: `[{:route-name :schedule-interview :handler handler-fn :enter (fn [ctx] ...)}]`. +- The `:route-name` keyword is a direct machine-readable identifier — a very strong signal when matching to a spec rule. + +**Liberator:** + +- `(defresource resource-name :allowed-methods [:get :post] :handle-ok (fn [ctx] ...))`. +- Each `defresource` is a surface. The `:handle-` and `:decision-points` map to behaviour. + +**Yada:** + +- `(yada/resource {:methods {:get {:response ...}}})` — data-driven, similar shape to Reitit. + +**Duct / Kit / Luminus frameworks:** these are project templates that compose Reitit / Compojure + Integrant / Component. The surface patterns are inherited from the underlying router; the framework contributes the lifecycle wiring. + +**GraphQL (Lacinia):** + +- Schema resolver map: `{:resolvers {:Query/fieldName resolver-fn :Mutation/fieldName ...}}`. +- Each `:TypeName/fieldName` keyword points at a resolver fn — map the spec operation to the resolver. + +**gRPC (protojure):** + +- `(defrecord ServiceImpl [] my.proto.Service (method-name [_ req] ...))` — the methods on a record extending a protobuf-generated protocol are the surface entries. + +### UI surfaces + +ClojureScript UI frameworks; uncommon for Clojure backends. + +**Reagent / Re-frame:** + +- Reagent components: fns returning Hiccup (`[:div ...]`). PascalCase is the *class* convention for React interop; Clojure components are typically named `-component` or just `` as a regular fn. +- Re-frame events: `(re-frame/reg-event-db :event-name (fn [db event] ...))` — the keyword `:event-name` is the surface identifier. The registered fn is the handler. +- Re-frame subscriptions: `(re-frame/reg-sub :sub-name (fn [db _] ...))` — same pattern; the keyword identifies the surface, the fn implements it. +- Re-frame effects: `(re-frame/reg-fx :effect-name ...)`. + +**Fulcro:** + +- `(defsc ComponentName [this props] {:query [...] :ident ...} body)` — `defsc` is the component-defining macro; the component name is the surface. +- Mutations: `(defmutation name [params] (action [env] ...))` — each `defmutation` is a surface operation. + +**Hoplon / Om / Rum:** less common; follow the same pattern — a named var holds the UI component, its props are the `exposes` contract. + +### Integration surfaces + +**Message brokers:** + +- Kafka (`jackdaw`): `(kafka/consumer {...})` followed by a `(loop [] (let [records (.poll consumer ...)] ...))` — the enclosing loop function is the handler; grep for `jackdaw.client/consumer` to find the entry points. +- Kafka (`kinsky`): similar; `(kinsky/consumer ...)` and a consume loop. +- RabbitMQ / AMQP (`langohr`): `(langohr.consumers/subscribe ch queue handler-fn)` — the third argument is the handler. +- Redis pub/sub (`carmine`): `(carmine/with-new-pubsub-listener ...)`, with a map of `channel → handler-fn`. + +**Scheduled / background:** + +- Quartzite: `(j/build ... (j/of-type MyJob))` where `MyJob` is a `(defrecord MyJob [] j/JobExecuteContext (execute [_ ctx] ...))`. +- `chime-core` / `at-at`: `(chime/chime-at times handler-fn)` — the handler fn is the surface. +- core.async scheduled loops: `(go-loop [] (-client`, `-gateway`, `-adapter`. +- `clj-http` / `hato` / `http-kit` wrapper fns: the exported fns of an `-client` namespace are the surface entries. + +### What not to match as surfaces + +- Anything matched by the test exclusion globs in §4 — `*_test.{clj,cljs,cljc}`, `test/**`. +- `deftest`, `defspec`, `deftest-async` — test defs that happen to look like surface handlers. +- `user.clj` / `dev.clj` / `user.cljc` — REPL scratchpad namespaces. +- `build.clj`, `build/**` — tools.build scripts; build behaviour, not application behaviour. +- `.clj-kondo/hooks/**` — tooling, not application code. +- Macros that merely generate boilerplate (`defschema`, `defrecord` without body) — the *use* of the generated code is the surface; the definition is plumbing unless the spec specifically talks about the shape. +- Anonymous inline `(fn ...)` bodies passed as middleware — they are composition, not surfaces. Walk outward to the named var. +- Reagent `(r/atom ...)` or re-frame `reg-sub` subscriptions that only slice state — these are derived data, not surfaces. Map them to the event or component that reads them. +- `main` / `-main` entry points that only wire `run-jetty handler` — the surface is the handler; `-main` is the bootstrap. +- Integrant lifecycle methods whose bodies are only `(jetty/run-jetty ...)` or similar — as noted above, these are wiring for a surface, not the surface itself. 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. diff --git a/skills/impact/adapters/kotlin.md b/skills/impact/adapters/kotlin.md new file mode 100644 index 0000000..59736be --- /dev/null +++ b/skills/impact/adapters/kotlin.md @@ -0,0 +1,204 @@ +# Kotlin adapter + +The Kotlin 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: + +- `build.gradle.kts` at the project root with the Kotlin plugin applied (`kotlin("jvm")`, `kotlin("multiplatform")`, `kotlin("android")`). +- `settings.gradle.kts` at the project root (Kotlin-DSL Gradle root, common for Kotlin-native projects). +- `pom.xml` at the project root declaring `kotlin-maven-plugin` (Kotlin-on-Maven, rarer). +- `AndroidManifest.xml` and `build.gradle.kts` / `build.gradle` (Android Kotlin project). +- Any `**/*.{kt,kts}` file inside the project root. + +For mixed Kotlin/Java projects, load both adapters — many Spring / Android / server-side Kotlin codebases have `.java` sources alongside `.kt`. The Java adapter's Gradle / Maven project-root rules apply unchanged. + +## 2. LSP plugin + +**Plugin:** `kotlin-lsp` (from Anthropic's `claude-plugins-official` marketplace). Uses JetBrains' official Kotlin LSP server ([github.com/Kotlin/kotlin-lsp](https://github.com/Kotlin/kotlin-lsp)), not the community fwcd/kotlin-language-server. + +**Requirements:** JDK 17 or later on PATH (`java -version`). The LSP server runs on the JVM. If a Java adapter is already active, the JDK requirement is shared. + +**Install:** + +```bash +# 1. Install JDK 17+ if you don't already have one (see the Java adapter +# for detailed JDK install options; any working JDK 17+ satisfies both). +# Verify: java -version + +# 2. Install the Kotlin LSP binary so that `kotlin-lsp` resolves on the +# global PATH. The Claude Code LSP tool spawns this binary from PATH. +# macOS: brew install JetBrains/utils/kotlin-lsp +# Other: download from https://github.com/Kotlin/kotlin-lsp/releases +# and place the launcher on your PATH. + +# Verify: +which kotlin-lsp # 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 kotlin-lsp +``` + +After install, run `/reload-plugins` (or restart the session) and the built-in `LSP` tool will route `.kt` and `.kts` files to the Kotlin LSP server. + +**Caveat — first-invocation cost.** Like JDT.LS, the Kotlin LSP indexes the Gradle / Maven classpath on cold start. For a project with Kotlin compiler plugins (KSP, kapt, serialisation) and many Gradle modules, expect a multi-second first-response delay. Retry once after 10–15 seconds before concluding the LSP is broken. + +**Caveat — Gradle-KTS interpretation.** The LSP runs Gradle to resolve dependencies. If the project has compile errors in its `build.gradle.kts` or hasn't fetched dependencies, the LSP will report degraded symbol information. `./gradlew help` (or `build`) should succeed before running the impact pipeline on a fresh clone. + +**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 and Java adapters). 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 `.kt` file in the project and call `documentSymbol` on it. If the LSP returns a non-empty symbol tree, the server is live. If the call errors with `Executable not found in $PATH`, `kotlin-lsp` isn't installed. If it errors on JDK version, JDK 17+ isn't the one on PATH. If it returns a nearly empty tree on a file that clearly has classes, the project probably hasn't had dependencies resolved yet — run `./gradlew help` first. + +## 3. Name-variant generator + +Given a spec identifier (PascalCase in Allium), emit variants following Kotlin convention. + +**For entities and variants:** + +- `` — PascalCase class, data class, sealed class, interface, object, enum, or type alias. +- `Impl`, `Default`, `Abstract` — implementation / default / abstract bases. +- `Service`, `Repository`, `UseCase`, `Interactor`, `Manager` — layered-architecture suffixes (clean architecture / Android MVVM / DDD). +- `ViewModel`, `UiState`, `Screen`, `Composable` — Android / Jetpack Compose component suffixes. +- `Dto`, `Entity`, `Model`, `Request`, `Response` — data-carrier suffixes. +- `.Companion` or `.Companion.create(...)` — companion-object factory pattern (Kotlin's idiomatic replacement for static factory methods). +- `Builder`, `Factory`, `Provider` — creational-pattern suffixes (when used explicitly rather than via companion objects). + +**For rules and triggers:** + +- `` — camelCase function (Allium `ScheduleInterview` → `scheduleInterview`). +- `` — PascalCase command / handler / event class (CQRS-style or sealed-class event hierarchies). +- `Command`, `Handler`, `UseCase`, `Event` — clean-architecture / CQRS naming. +- `handle`, `on`, `process`, `execute` — handler method variants. +- `suspend fun (...)` — coroutine-based handlers; the `suspend` modifier is visible on the LSP symbol signature. + +**For surfaces:** + +- `Controller`, `Resource`, `Endpoint` — Spring / JAX-RS / Micronaut / Quarkus REST surfaces. +- `Routes`, `Routing` — Ktor routing modules (Kotlin convention is a top-level function taking `Route.() -> Unit`). +- `Fragment`, `Activity`, `Screen` — Android UI surface names. + +**Case conversion:** splitting on CamelCase and converting the first char of each component to lower (camelCase for functions, PascalCase preserved for classes) is sufficient. Kotlin idiomatic extension functions are fine: a spec `Candidacy.schedule` may be realised as `fun Candidacy.schedule() = ...` — emit `schedule` as a plain name-variant candidate and rely on the call-hierarchy expansion to connect it back to the receiver type. + +## 4. Project-root rule + +**Root discovery:** walk upward from the spec file's directory. + +- If a `settings.gradle.kts` or `settings.gradle` is found, that directory is the root (Gradle multi-module). This covers the majority of modern Kotlin projects. +- Otherwise the first directory containing `build.gradle.kts` or `build.gradle` is the root. +- Otherwise the first directory containing `pom.xml` is the root (Kotlin-on-Maven). +- For Android projects, the project root is the directory containing `settings.gradle*` — not an individual app module (`app/`, `library/`). +- Fallback: the first directory containing a `.git` folder. + +**Source globs:** + +- `src/main/kotlin/**/*.kt` — Gradle standard layout. +- `src/main/java/**/*.kt` — some Kotlin projects colocate `.kt` files under `src/main/java` (valid; Gradle accepts it). +- `*/src/main/kotlin/**/*.kt` at the root, recursing into each module (multi-module). +- Kotlin Multiplatform: `src/commonMain/kotlin/**/*.kt`, `src/jvmMain/kotlin/**/*.kt`, `src/androidMain/kotlin/**/*.kt`, `src/nativeMain/kotlin/**/*.kt` — all are first-class source sets. The impact pipeline should walk each source set the project actually declares. +- Android: `app/src/main/kotlin/**/*.kt`, `app/src/main/java/**/*.kt` (Android tolerates both), plus per-flavor source sets if the project uses product flavors. + +**Exclusions (always):** + +- `src/test/**`, `src/androidTest/**`, `src/integrationTest/**`, `src/functionalTest/**` — test sources. +- `target/**`, `build/**`, `out/**`, `.gradle/**` — build outputs and Gradle cache. +- `build/generated/**`, `build/tmp/**`, `build/intermediates/**` — annotation-processor / KSP / kapt / Android codegen output. +- `**/generated/**`, `**/*.kt.generated` — other codegen paths. +- `.idea/**`, `.settings/**`, `*.iml`, `local.properties`, `gradle.properties` — IDE / local-only metadata. +- `**/*.class`, `**/*.jar`, `**/*.aar` — compiled artefacts. +- `buildSrc/**` — Gradle build logic, not application behaviour (unless the spec describes build behaviour specifically). +- 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 or the Kotlin standard library (`kotlin.*`, `kotlinx.*`). Be aware of: + +- **Extension functions** may appear as methods on the receiver type in LSP results — prefer the file where the extension is declared. +- **Top-level functions** in Kotlin have no enclosing class; the LSP reports them as file-level symbols. Record the file + line, not a synthetic class name. +- **Companion objects** expose their members under `.Companion`; the LSP usually shows them as static-like. Prefer the declaring class's FQN with `.Companion.` in the path. +- **Inline functions** may be source-inlined at call sites; call-hierarchy results can be noisy. Treat inline-function call sites as valid edges. + +## 5. Surface entry-point patterns + +### API surfaces + +**Ktor** (Kotlin-native, most common for Kotlin server-side): + +- Routing DSL: `routing { get("/path") { ... } }`, `post`, `put`, `delete`, `patch`, `head`, `options`. +- Route blocks: `route("/path") { get { ... } }`, nested routing via `Route.yourModule()` top-level functions. +- Plugin installation: `install(ContentNegotiation) { ... }`, `install(Authentication) { ... }` — affects surface contracts. +- Application entry: `fun Application.module() { ... }` or `fun main() { embeddedServer(Netty, port = 8080) { module() }.start() }`. + +**Spring Boot** (with Spring Kotlin support): same annotations as the [Java adapter](./java.md). `@RestController`, `@GetMapping`, `@RequestMapping`, etc. Spring Kotlin DSL (`router { GET("/path") { ... } }`) is also used; match `router { ... }` top-level bean definitions as surface containers. + +**Micronaut / Quarkus:** same annotations as the Java adapter. + +**JAX-RS (on Kotlin):** same annotations as the Java adapter. + +**gRPC:** classes extending the generated `*ImplBase` from a `.proto`, written in Kotlin. Usually combined with `kotlinx.coroutines` for `suspend` RPCs. + +**GraphQL (ExpediaGroup's graphql-kotlin):** `@GraphQLDescription` / `@GraphQLName` annotations on resolver classes; the functions are the surface operations. + +### UI surfaces + +**Android (XML views / Fragments / Activities):** + +- Classes extending `AppCompatActivity`, `ComponentActivity`, `Fragment`, `DialogFragment`. +- `@AndroidEntryPoint` (Hilt) — marks injection-capable entry points. +- The Activity / Fragment class is the surface; its public methods are the user-observable behaviour. + +**Jetpack Compose:** + +- Functions annotated `@Composable` are the UI unit. PascalCase by convention. +- Screen-level composables typically live in `screens/`, `ui/`, `features//`. +- The `@Composable` function is the surface entry point; its parameters are the `exposes` / `demands` contract. + +**Compose Multiplatform / Kotlin Multiplatform Mobile:** same `@Composable` convention, source set varies (`src/commonMain/kotlin/**`). + +### Integration surfaces + +**Message brokers and queues:** + +- Kafka (Spring): `@KafkaListener`. +- Kafka (plain): `Consumer.poll()` loops — grep the call sites; the enclosing function is the handler. +- RabbitMQ / AMQP (Spring): `@RabbitListener`. +- Ktor server WebSocket: `webSocket("/path") { ... }` inside a `routing { }` block. + +**Coroutines-based handlers:** + +- `Flow` collectors: `.collect { ... }` calls where the lambda is the handler. +- `Channel` receivers: `for (item in channel) { ... }` loops. +- `GlobalScope.launch`, `CoroutineScope.launch` — scope-bound background work; the launched block is the handler. +- Ktor client retrofit-style interfaces with `@GET`, `@POST` (Retrofit + Kotlin coroutines). + +**Scheduled / background:** + +- Spring: `@Scheduled(cron = ...)`. +- Quartz: classes implementing `Job`. +- Android WorkManager: classes extending `Worker`, `CoroutineWorker`, `ListenableWorker`. `doWork` / `startWork` is the surface entry. + +**Event listeners (in-process):** + +- Spring: `@EventListener`, `ApplicationListener` — same as the Java adapter. +- Kotlin-idiomatic: `EventBus`-style libraries (GreenRobot, LiveData / StateFlow observers). + +**Serverless handlers:** + +- AWS Lambda: classes implementing `RequestHandler` or `RequestStreamHandler`. Kotlin `suspend` variants exist via third-party wrappers. + +**SDK / outbound client surfaces:** + +- Classes named `Client`, `Gateway`, `Adapter`. +- Retrofit interfaces: `interface Api { @GET(...) suspend fun ... }` — each method is a contract entry. +- Ktor `HttpClient` wrapper classes — the public methods are the surface. + +### What not to match as surfaces + +- Anything under `src/test/**`, `src/androidTest/**`, or suffixed `Test` / `Tests` / `IT` / `Spec` — test classes and specs. +- Generated Kotlin files: Hilt `*_GeneratedInjector`, Dagger `*_Factory`, Room `*_Impl`, DataBinding, kapt `*.generated`, KSP-generated stubs. +- `buildSrc` modules — Gradle build logic, not application behaviour. +- `BuildConfig` and Android resource-binding classes (`R`, `Manifest`). +- Abstract controllers / handlers — follow `goToImplementation` to the concrete subclass and map the surface there. +- Companion-object-only classes that serve as namespaces for constants — not surfaces, just grouping. +- Jetpack Compose `@Preview` functions — these are IDE previews, not real UI surfaces. +- Ktor routing extensions defined in library code — map surfaces to the app's own `Application.module()` and its extension-function routes. diff --git a/skills/impact/adapters/python.md b/skills/impact/adapters/python.md new file mode 100644 index 0000000..d9a1d73 --- /dev/null +++ b/skills/impact/adapters/python.md @@ -0,0 +1,148 @@ +# 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` (from Anthropic's `claude-plugins-official` marketplace). + +**Install:** + +```bash +# 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 + +# (pip install pyright and npm install -g pyright also work, provided +# their install location ends up 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 +/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. + +**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: + +- `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 + +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/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 `