diff --git a/.claude/skills/radar-decision-log/SKILL.md b/.claude/skills/radar-decision-log/SKILL.md new file mode 100644 index 0000000..f6847fa --- /dev/null +++ b/.claude/skills/radar-decision-log/SKILL.md @@ -0,0 +1,110 @@ +--- +name: radar-decision-log +description: Use when the user wants a timeline, history, decision log, or movement report for tech radar entries — for example "show the history of React on the radar", "decision log for terraform", "which techs moved last year", "generate an ADR index", "what changed between snapshots", or "build me a movement table". Reads radar// snapshots, reconstructs the per-tech ring history across time, and emits a chronological decision log similar to an ADR index. Can scope to a single tech, a quadrant, or the whole radar. Trigger when the user asks about radar history or wants to see how decisions evolved. +--- + +# radar-decision-log + +Reconstruct the temporal decision log for INFO tech radar entries. + +## When this fires + +- "What's the history of Terraform on the radar?" +- "Show me everything that moved into hold since 2023." +- "Generate the ADR index for the radar." +- "Which techs were added in 2024?" +- "Diff the last two snapshots." + +## How to read snapshots + +`radar/` contains dated dirs (`YYYY-MM-DD/`). Each dir holds *only the entries that were created or changed on that date*. To reconstruct a tech's history: + +1. List every dated dir in chronological order. +2. For each tech (filename slug), find every snapshot that contains it. +3. Read frontmatter from each occurrence to capture `ring` over time. +4. The first occurrence = creation. Subsequent occurrences = ring changes or prose updates. The last occurrence = current state. + +A tech that appears once and never again is still active — its state is whatever the last snapshot said. + +## Modes + +### Single-tech timeline + +Input: one slug (e.g. `terraform`). Output: a table of every snapshot that touched it. + +```markdown +## Terraform — decision log + +| Date | Ring | Quadrant | Note | +|------------|-------|---------------------------|------| +| 2022-09-22 | trial | platforms-and-operations | Added | +| 2022-12-05 | trial | platforms-and-operations | Prose update | +| 2024-12-01 | adopt | platforms-and-operations | Promoted | + +**Current ring:** adopt (as of 2024-12-01) +``` + +If the user wants the *why*, also surface the supersession note or the diff of body text between consecutive entries. + +### Quadrant or whole-radar movement report + +Input: a date range, optionally a quadrant filter. Output: every ring change in that range. + +```markdown +## Movement report — 2024-01-01 to 2025-01-22 + +### platforms-and-operations +- **Terraform**: trial → adopt (2024-12-01) +- **Pulumi**: → assess (2024-12-01, new) + +### tools +- **React Testing Library**: → adopt (2025-01-22, new) +- **Cypress**: → hold (2024-12-01) +``` + +Group by quadrant; within a quadrant, order by snapshot date then alphabetical. + +### ADR index + +Input: none. Output: a flat list of every entry with its current ring, sorted by quadrant, formatted like an ADR index. Useful as a published-artifact rendering of the radar. + +```markdown +## INFO Tech Radar — Decision Index + +### Languages & Frameworks +- [React](/languages-and-frameworks/react) — adopt (since 2020-09-01, last touched 2023-03-01) +- ... + +### Methods & Patterns +- ... +``` + +## Procedure + +1. Confirm scope: single tech / quadrant / whole radar / date range. +2. Run `scripts/scan_radar.py` (bundled with this skill, mirrored from `radar-entry-audit`) to get a JSON index. It already builds `slug → [(date, ring, quadrant, ...), ...]` for you — no need to re-walk the tree in the conversation. + + ```bash + # Whole-radar dump: + python3 .claude/skills/radar-decision-log/scripts/scan_radar.py --root radar > /tmp/radar.json + + # Single tech: + python3 .claude/skills/radar-decision-log/scripts/scan_radar.py --slug terraform + ``` + +3. Use the JSON's `entries.` for timelines and `current` for "what's the current ring". +4. Render the requested view. +5. If the user asks for *why* a transition happened, read the supersession note (if any) from the entry on that date, or diff the body against the previous occurrence. Note: the repo's idiomatic move-style is prose, not a `> **Status:**` blockquote — so look at the opening paragraph of the new entry, not just metadata. + +## Output rules + +- Always include the date a transition happened — the radar's value is the timeline, not just the current state. +- For "current ring", trust the most recent snapshot containing the slug. +- If a slug appears in only one snapshot, label it "Added " and use that ring as current. +- Don't infer a "removed" status from absence — the radar doesn't model deletion. If a tech genuinely should be removed, that's a `hold` entry, not a missing file. + +## Anti-patterns + +- Don't assume snapshots are full re-statements. They aren't — they're differential. Reading only the latest snapshot misses every tech that hasn't changed recently. +- Don't sort by file path. Always sort by snapshot date for timelines. +- Don't generate the report by hand for large scans. Script it. diff --git a/.claude/skills/radar-decision-log/evals/evals.json b/.claude/skills/radar-decision-log/evals/evals.json new file mode 100644 index 0000000..947abd5 --- /dev/null +++ b/.claude/skills/radar-decision-log/evals/evals.json @@ -0,0 +1,26 @@ +{ + "skill_name": "radar-decision-log", + "evals": [ + { + "id": 0, + "name": "react-history", + "prompt": "Show me the full history of the React entry on the radar.", + "expected_output": "report.md with a chronological table of every snapshot containing react.md, columns: date, ring, quadrant, note. Current ring stated.", + "files": [] + }, + { + "id": 1, + "name": "movement-report", + "prompt": "Generate a movement report of every ring change between snapshots from 2024-01-01 onward.", + "expected_output": "report.md grouping ring changes by quadrant, listing tech, transition (old → new), date.", + "files": [] + }, + { + "id": 2, + "name": "adr-index", + "prompt": "Build me an ADR-style index of every current radar entry, grouped by quadrant.", + "expected_output": "report.md with each quadrant as a section, each entry listed with its current ring and the date it was last touched.", + "files": [] + } + ] +} diff --git a/.claude/skills/radar-decision-log/scripts/scan_radar.py b/.claude/skills/radar-decision-log/scripts/scan_radar.py new file mode 100755 index 0000000..6552a8a --- /dev/null +++ b/.claude/skills/radar-decision-log/scripts/scan_radar.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +scan_radar.py — walk radar//.md, parse frontmatter, emit JSON. + +Designed to be shared by radar-entry-audit and radar-decision-log. Reading the +whole tree once and emitting a structured index avoids each skill reinventing +the file walk + frontmatter parser. + +Output shape: + +{ + "snapshots": ["2020-09-01", ...], # chronological + "entries": { # one record per (date, slug) + "react": [ + {"date": "2020-09-01", "ring": "adopt", "quadrant": "...", "title": "...", + "featured": true, "path": "radar/2020-09-01/react.md", + "body_chars": 1234, "internal_links": ["/tools/vitest/", ...]}, + ... + ] + }, + "current": { # latest occurrence per slug + "react": {"date": "...", "ring": "...", "quadrant": "...", "path": "..."} + } +} + +Usage: + python scan_radar.py [--root radar/] [--quadrant tools] + # prints JSON to stdout +""" +from __future__ import annotations +import argparse, json, re, sys +from pathlib import Path + +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +LINK_RE = re.compile(r"\]\((/[a-z0-9-]+/[a-z0-9-]+/?)\)") +FM_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL) + +VALID_RINGS = {"adopt", "trial", "assess", "hold"} +VALID_QUADRANTS = { + "languages-and-frameworks", "methods-and-patterns", + "platforms-and-operations", "tools", +} + + +def parse_frontmatter(text: str) -> tuple[dict, str]: + m = FM_RE.match(text) + if not m: + return {}, text + fm_text, body = m.groups() + fm: dict = {} + for line in fm_text.splitlines(): + if ":" not in line: + continue + k, _, v = line.partition(":") + v = v.strip().strip('"').strip("'") + if v in ("true", "false"): + v = v == "true" + fm[k.strip()] = v + return fm, body + + +def scan(root: Path) -> dict: + snapshots = sorted( + p.name for p in root.iterdir() if p.is_dir() and DATE_RE.match(p.name) + ) + entries: dict[str, list[dict]] = {} + for date in snapshots: + for md in sorted((root / date).glob("*.md")): + slug = md.stem + text = md.read_text(encoding="utf-8") + fm, body = parse_frontmatter(text) + entries.setdefault(slug, []).append({ + "date": date, + "slug": slug, + "ring": fm.get("ring"), + "quadrant": fm.get("quadrant"), + "title": fm.get("title"), + "featured": fm.get("featured"), + "path": str(md), + "body_chars": len(body.strip()), + "internal_links": LINK_RE.findall(body), + }) + current = { + slug: occurrences[-1] + for slug, occurrences in entries.items() + } + return {"snapshots": snapshots, "entries": entries, "current": current} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--root", default="radar", type=Path) + ap.add_argument("--quadrant", help="filter current entries by quadrant") + ap.add_argument("--slug", help="dump only this slug's history") + args = ap.parse_args() + + if not args.root.is_dir(): + print(f"error: {args.root} not a directory", file=sys.stderr) + return 1 + + data = scan(args.root) + + if args.slug: + data = {"slug": args.slug, "history": data["entries"].get(args.slug, [])} + elif args.quadrant: + data["current"] = { + slug: rec for slug, rec in data["current"].items() + if rec.get("quadrant") == args.quadrant + } + + json.dump(data, sys.stdout, indent=2, default=str) + print() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/radar-entry-audit/SKILL.md b/.claude/skills/radar-entry-audit/SKILL.md new file mode 100644 index 0000000..48b27ad --- /dev/null +++ b/.claude/skills/radar-entry-audit/SKILL.md @@ -0,0 +1,100 @@ +--- +name: radar-entry-audit +description: Use when the user wants to review, audit, lint, or quality-check radar entries — for example "audit the radar", "review my radar entries", "check the radar for issues", "are these entries ADR-compliant", "find broken links in the radar", "which entries are missing rationale", or after a batch of new entries has been added. Scans markdown files under radar/ and reports on frontmatter completeness, ADR-shape rationale (context / decision / consequences), internal-link integrity, slug/title consistency, and orphan entries (present in old snapshots but never updated). Also flags entries that read like marketing rather than decisions. Trigger proactively after radar-entry-new or radar-entry-move runs. +--- + +# radar-entry-audit + +Lint INFO tech radar entries against ADR-style quality criteria. + +## When this fires + +- "Audit the radar." +- "Review the latest snapshot." +- "Are there broken links in radar/?" +- "Which entries are missing a rationale?" +- After bulk additions or before publishing. + +## What "good" looks like + +A good radar entry is a small ADR. It has: + +1. **Complete frontmatter** — `title` (quoted), `ring` (one of adopt/trial/assess/hold), `quadrant` (one of the four ids), `featured` (bool). +2. **A decision, not a description** — the body explains *why we chose this here*, not just what the tech does. +3. **Consequences captured** — a "Considerations at INFO" (or equivalent) section noting trade-offs, current focus, or constraints. +4. **Working internal links** — links of the form `/[quadrant]/[slug]/` resolve to a file under some `radar/*/.md`. +5. **Slug ↔ title coherence** — filename slug clearly matches the `title` field. + +## Audit dimensions + +Run these checks across all entries (or a user-specified subset like "the latest snapshot only"): + +### 1. Frontmatter +- Missing fields → flag. +- `ring` outside the allowed set → flag. +- `quadrant` outside the allowed set → flag. +- Title not quoted, or empty → flag. + +### 2. ADR shape +- Body shorter than ~80 words → likely a stub. Flag with severity *info* (some legitimate stubs exist, e.g. `superblocks.md`). +- No "why" framing — no occurrence of words like "because", "we decided", "in the context", "current focus", "considerations" — flag as *missing rationale*. +- Reads like marketing — heavy use of "powerful", "seamless", "best-in-class" without concrete INFO context — flag as *too promotional*. + +### 3. Internal links +- For each `[text](/quadrant/slug/)` or `[text](/quadrant/slug)` link in the body: + - `quadrant` segment must be one of the four valid quadrant ids. + - At least one file `radar/*/.md` must exist. +- External links (http/https) — optionally check, but don't block on flakiness. + +### 4. Cross-snapshot coherence +- For each slug, find every snapshot containing it. The latest occurrence is the "current" entry. +- If an entry's `featured: true` but the tech only appears in snapshots older than ~2 years → flag *possibly stale*. +- If an entry was moved to `hold` and a newer entry replaces it without a supersession note → flag *missing supersession link*. + +### 5. Slug / title +- Slug should be the kebab-case lowercase form of the title (allowing for minor abbreviations like "and" → "and", "&" handled). Mismatches like `title: "Postgres"` in `postgresql.md` → flag. + +## Procedure + +1. Decide scope. Default = all entries under `radar/`. If the user names a snapshot, restrict to that dir. +2. Run `scripts/scan_radar.py` (bundled with this skill) to get a JSON index of every entry — frontmatter, body length, internal links, current ring per slug. Don't re-parse files one-by-one in the conversation; pipe the JSON through `jq`/Python for the actual checks. The script is also shared with `radar-decision-log` so the two skills produce comparable indices. + + ```bash + python3 .claude/skills/radar-entry-audit/scripts/scan_radar.py --root radar > /tmp/radar.json + ``` + + The output's `current` map is what you want for "is this the latest entry for the slug"; the per-slug `entries` list is the history. +3. Group findings by severity: + - **Error** — frontmatter invalid, broken internal link. + - **Warning** — missing rationale, missing supersession note. + - **Info** — short stubs, possibly stale, slug/title mismatch. +4. Report as a table grouped by file. For each finding give: file path, dimension, what's wrong, suggested fix. Keep it scannable. +5. Offer to auto-fix the trivial ones (frontmatter quoting, slug case) but ask before editing. + +## Output shape + +```markdown +## Radar audit — , entries + +### Errors (must fix) +| File | Issue | Fix | +| ---- | ----- | --- | +| radar/.../foo.md | ring "adopted" not in {adopt,trial,assess,hold} | change to `adopt` | + +### Warnings +... + +### Info +... + +### Summary +- Errors: X +- Warnings: Y +- Info: Z +``` + +## Anti-patterns + +- Don't auto-rewrite prose. "Reads like marketing" is a flag, not a fix — surface it, let a human rephrase. +- Don't enforce a strict template on body sections. The "Why X / Considerations at INFO" pattern is common but not universal; allow Nygard-sentence entries (see `radar/2020-09-01/react.md`). +- Don't treat `featured: false` as a defect. Some entries are deliberately unfeatured (industry givens). diff --git a/.claude/skills/radar-entry-audit/evals/evals.json b/.claude/skills/radar-entry-audit/evals/evals.json new file mode 100644 index 0000000..87f06ea --- /dev/null +++ b/.claude/skills/radar-entry-audit/evals/evals.json @@ -0,0 +1,26 @@ +{ + "skill_name": "radar-entry-audit", + "evals": [ + { + "id": 0, + "name": "snapshot-audit", + "prompt": "Audit the latest snapshot at radar/2025-01-22 for ADR quality issues.", + "expected_output": "A report.md with findings grouped by severity (errors/warnings/info) for each file in radar/2025-01-22, covering frontmatter, rationale, internal links, slug/title.", + "files": [] + }, + { + "id": 1, + "name": "broken-links", + "prompt": "Run an audit across the whole radar/ tree and report broken internal links.", + "expected_output": "report.md listing each /quadrant/slug/ link in any radar entry that does not resolve to an existing file under radar/*/.md. Indicate file + line + bad link.", + "files": [] + }, + { + "id": 2, + "name": "slug-mismatch", + "prompt": "Find any radar entries where the slug filename doesn't match the title field.", + "expected_output": "report.md listing files where the kebab-case form of `title:` does not equal the filename slug.", + "files": [] + } + ] +} diff --git a/.claude/skills/radar-entry-audit/scripts/scan_radar.py b/.claude/skills/radar-entry-audit/scripts/scan_radar.py new file mode 100755 index 0000000..6552a8a --- /dev/null +++ b/.claude/skills/radar-entry-audit/scripts/scan_radar.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +scan_radar.py — walk radar//.md, parse frontmatter, emit JSON. + +Designed to be shared by radar-entry-audit and radar-decision-log. Reading the +whole tree once and emitting a structured index avoids each skill reinventing +the file walk + frontmatter parser. + +Output shape: + +{ + "snapshots": ["2020-09-01", ...], # chronological + "entries": { # one record per (date, slug) + "react": [ + {"date": "2020-09-01", "ring": "adopt", "quadrant": "...", "title": "...", + "featured": true, "path": "radar/2020-09-01/react.md", + "body_chars": 1234, "internal_links": ["/tools/vitest/", ...]}, + ... + ] + }, + "current": { # latest occurrence per slug + "react": {"date": "...", "ring": "...", "quadrant": "...", "path": "..."} + } +} + +Usage: + python scan_radar.py [--root radar/] [--quadrant tools] + # prints JSON to stdout +""" +from __future__ import annotations +import argparse, json, re, sys +from pathlib import Path + +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +LINK_RE = re.compile(r"\]\((/[a-z0-9-]+/[a-z0-9-]+/?)\)") +FM_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL) + +VALID_RINGS = {"adopt", "trial", "assess", "hold"} +VALID_QUADRANTS = { + "languages-and-frameworks", "methods-and-patterns", + "platforms-and-operations", "tools", +} + + +def parse_frontmatter(text: str) -> tuple[dict, str]: + m = FM_RE.match(text) + if not m: + return {}, text + fm_text, body = m.groups() + fm: dict = {} + for line in fm_text.splitlines(): + if ":" not in line: + continue + k, _, v = line.partition(":") + v = v.strip().strip('"').strip("'") + if v in ("true", "false"): + v = v == "true" + fm[k.strip()] = v + return fm, body + + +def scan(root: Path) -> dict: + snapshots = sorted( + p.name for p in root.iterdir() if p.is_dir() and DATE_RE.match(p.name) + ) + entries: dict[str, list[dict]] = {} + for date in snapshots: + for md in sorted((root / date).glob("*.md")): + slug = md.stem + text = md.read_text(encoding="utf-8") + fm, body = parse_frontmatter(text) + entries.setdefault(slug, []).append({ + "date": date, + "slug": slug, + "ring": fm.get("ring"), + "quadrant": fm.get("quadrant"), + "title": fm.get("title"), + "featured": fm.get("featured"), + "path": str(md), + "body_chars": len(body.strip()), + "internal_links": LINK_RE.findall(body), + }) + current = { + slug: occurrences[-1] + for slug, occurrences in entries.items() + } + return {"snapshots": snapshots, "entries": entries, "current": current} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--root", default="radar", type=Path) + ap.add_argument("--quadrant", help="filter current entries by quadrant") + ap.add_argument("--slug", help="dump only this slug's history") + args = ap.parse_args() + + if not args.root.is_dir(): + print(f"error: {args.root} not a directory", file=sys.stderr) + return 1 + + data = scan(args.root) + + if args.slug: + data = {"slug": args.slug, "history": data["entries"].get(args.slug, [])} + elif args.quadrant: + data["current"] = { + slug: rec for slug, rec in data["current"].items() + if rec.get("quadrant") == args.quadrant + } + + json.dump(data, sys.stdout, indent=2, default=str) + print() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/radar-entry-move/SKILL.md b/.claude/skills/radar-entry-move/SKILL.md new file mode 100644 index 0000000..09f0f62 --- /dev/null +++ b/.claude/skills/radar-entry-move/SKILL.md @@ -0,0 +1,73 @@ +--- +name: radar-entry-move +description: Use when a tech radar entry needs to change ring (adopt/trial/assess/hold), be promoted, demoted, deprecated, or superseded — for example "move Terraform from trial to adopt", "demote X to hold", "we're dropping Y", "Z is now adopted", "supersede the old entry", or "update the ring for ...". Creates an updated copy of the entry in a new or current dated snapshot under radar/, preserving the prior entry untouched (snapshots are immutable history) and adding a supersession note that links back to the previous decision. Trigger whenever a radar entry's status changes over time. +--- + +# radar-entry-move + +Record a ring change for an existing radar entry as an ADR-style supersession. + +## When this fires + +- "Move React Testing Library from assess to adopt." +- "We're dropping Cypress — put it in hold." +- "Promote Pulumi to trial." +- "Mark Snowflake as superseded by Databricks." + +## Why this is a *new file*, not an edit + +Snapshots under `radar//` are immutable — they record what the radar said *on that date*. A ring change is a new decision that supersedes the prior one. Never edit a file in an old snapshot dir; instead, write a new file in the current (or a fresh) snapshot dir. + +This mirrors ADR practice: superseded ADRs stay on disk; a new ADR points back at the one it replaces. + +## Procedure + +1. **Locate prior entry.** Search `radar/*/.md` for the most recent occurrence. Read it to capture the previous ring and the existing prose. +2. **Pick target snapshot.** + - If a snapshot dated today exists, use it. + - If the latest snapshot is recent (e.g. <2 weeks) and the user is just nudging one entry, ask whether to add to that snapshot or open a new dated dir. + - Otherwise create `radar//` for today. +3. **Copy + update.** Start from the previous entry's content. Update frontmatter `ring:` to the new value. Reconsider `featured:` — promotions to adopt for a now-mainstream tech often flip to `false` (see `radar/2023-03-01/react.md`). +4. **Record the transition.** The repo's idiomatic style is **prose-led**: rewrite the opening paragraph so it states the new status and (briefly) why. Real example, `radar/2024-12-01/cypress.md` opens with: + + > [Cypress](https://www.cypress.io/) has been a valuable tool for functional testing at INFO… However, as we prioritize advanced testing capabilities and cross-browser support, Cypress is now on hold while we focus on [Playwright](/tools/playwright) as our preferred solution. + + No blockquote, no machine-readable "Status:" header — just an honest paragraph. Match that. + + If the user wants an explicit ADR-style supersession note (useful for promotions where the prior decision is itself worth citing), add it as an optional blockquote near the top: + + ```markdown + > **Status (YYYY-MM-DD):** Moved from `` to ``. + > Reason: . + > Supersedes the entry from radar//. + ``` + + Default to prose. Use the blockquote only if the user explicitly asks for a machine-parseable trail, or if you're moving the same entry repeatedly and want the audit trail to read cleanly. + + Keep the rest of the body intact unless the user has new context worth adding. Don't silently rewrite prose that's still accurate. +5. **Hold/deprecate variant.** If moving to `hold`, the body should explain *why we're moving away* and (if known) what replaces it. Link the replacement via `/[quadrant]/[slug]/`. Renaming the existing "Why X?" section to "Previous Benefits" + adding a "Reasons for Holding" section is the established pattern (see `radar/2024-12-01/cypress.md`). +6. **Verify links.** Cross-references in the prose may now point to entries that themselves moved; spot-check the linked slugs still resolve to a file somewhere under `radar/*/`. + +## Reasoning for ring transitions + +| From → To | Typical reason | Body emphasis | +|-----------|---------------|---------------| +| assess → trial | Pilot worked, ready for limited rollout | What the pilot showed | +| trial → adopt | Proven in production | Where it's now standard | +| adopt → hold | Better alternative emerged, or tech is end-of-life | What replaces it | +| any → assess | Re-evaluation triggered by new info | What changed | +| trial → hold | Pilot failed | What didn't work | + +## Output checklist + +- [ ] New file written under `radar//.md` — original snapshot file untouched. +- [ ] Frontmatter ring updated; quadrant and slug unchanged. +- [ ] Opening paragraph (or optional blockquote) names the new status and why. Don't ship a silent ring-flip. +- [ ] Body still reads coherently — not just a frontmatter flip. +- [ ] If moving to hold: replacement (if any) is linked. + +## Anti-patterns + +- Don't edit the file in the old snapshot dir. That erases history. +- Don't change the slug or quadrant — that breaks URLs and the "same tech across time" link. If the quadrant is genuinely wrong, that's a separate cleanup, not a ring move. +- Don't fabricate a reason for the move. If the user hasn't said why, ask. diff --git a/.claude/skills/radar-entry-move/evals/evals.json b/.claude/skills/radar-entry-move/evals/evals.json new file mode 100644 index 0000000..07e4c0d --- /dev/null +++ b/.claude/skills/radar-entry-move/evals/evals.json @@ -0,0 +1,26 @@ +{ + "skill_name": "radar-entry-move", + "evals": [ + { + "id": 0, + "name": "terraform-promote", + "prompt": "Promote terraform from trial to adopt — it's now our standard for IaC across all platform teams.", + "expected_output": "A new file in a current/new dated snapshot under radar/ containing terraform.md with ring=adopt, supersession note linking radar/2024-12-01/. Original 2024-12-01 file untouched.", + "files": [] + }, + { + "id": 1, + "name": "cypress-hold", + "prompt": "Move Cypress to hold. We're standardizing on Playwright instead.", + "expected_output": "New cypress.md in current/new snapshot with ring=hold, supersession note, and an internal link to the Playwright entry. Prior cypress.md untouched.", + "files": [] + }, + { + "id": 2, + "name": "react-unfeature", + "prompt": "We've decided React is now so standard it shouldn't be featured anymore — keep it adopt but unfeature it.", + "expected_output": "New react.md in current snapshot with ring=adopt, featured=false, supersession note explaining 'industry given' rationale.", + "files": [] + } + ] +} diff --git a/.claude/skills/radar-entry-new/SKILL.md b/.claude/skills/radar-entry-new/SKILL.md new file mode 100644 index 0000000..9a0d337 --- /dev/null +++ b/.claude/skills/radar-entry-new/SKILL.md @@ -0,0 +1,119 @@ +--- +name: radar-entry-new +description: Use when the user wants to add a new technology to the INFO tech radar, propose a new entry, draft a radar item, capture a tech decision in ADR style, or asks "add X to the radar", "create radar entry for Y", "propose Z for assess/trial/adopt/hold". Scaffolds a markdown file in the latest dated snapshot under radar/ with correct frontmatter (title, ring, quadrant, featured) and an ADR-shaped body (context, decision, considerations at INFO, consequences). Cross-links related entries by quadrant slug. Trigger this skill whenever a tech radar entry needs to be created, even if the user phrases it as "we decided to use X" or "let's evaluate Y". +--- + +# radar-entry-new + +Scaffold a new INFO tech radar entry as an ADR-style decision record. + +## When this fires + +- "Add React Testing Library to the radar" +- "We're adopting Vitest, write the entry" +- "Propose Pulumi for assess" +- "Draft a hold entry for Snowflake" +- The user describes a tooling/language/platform/method choice that should be recorded. + +## Repo conventions + +Each radar entry is `radar//.md`. Snapshots are *differential* — an entry is only re-written into a new dated dir when its ring or prose changes. The latest dated dir under `radar/` is the active snapshot; if no snapshot exists for today, ask the user whether to add to the most recent snapshot or create a new one (use today's date `YYYY-MM-DD`). + +**Quadrants** (from `config.json`): +- `languages-and-frameworks` +- `methods-and-patterns` +- `platforms-and-operations` +- `tools` + +**Rings**: `adopt`, `trial`, `assess`, `hold`. + +**Slug**: lowercase, hyphenated, derived from title. Match how it would render in the URL `///`. + +## Frontmatter + +```yaml +--- +title: "Display Name" +ring: adopt | trial | assess | hold +quadrant: languages-and-frameworks | methods-and-patterns | platforms-and-operations | tools +featured: true | false +--- +``` + +`featured: true` for entries that should surface on the landing page. Default `true` for new entries unless the user says otherwise or the tech is already widely assumed (see `radar/2023-03-01/react.md` for an example of `featured: false` on a "given"). + +## Body — ADR shape + +Existing entries follow a Nygard-style decision sentence. Use this skeleton; adapt prose so it reads naturally rather than like a template. + +```markdown +[Optional one-line link/intro: [Tech](https://...) is a short description.] + +### Why [Tech]? + +- **Bullet 1:** the leading benefit, plain language. +- **Bullet 2:** secondary benefit. +- **Bullet 3:** integration / fit with existing stack — link to related radar entries via `/[quadrant-id]/[slug]/`. + +### Considerations at INFO + +- Adoption context — who is using it, on what projects. +- Trade-offs we explicitly accept. +- **Current Focus:** what we're doing with it now. + +[Closing sentence: one-line restatement of the decision and its purpose.] +``` + +For deeper "decision sentence" entries (see `radar/2020-09-01/react.md`), open with: + +> In the context of [need], facing [alternatives], we decided for [Tech] and neglected [rejected options], to achieve [goal], accepting [trade-off], because [reason]. + +Use this longer form when the decision rejects specific alternatives. Use the bulleted form when adopting an additive tool. + +## Internal linking + +Whenever you mention another tech that already exists in the radar, link it as `/[quadrant-id]/[slug]/`. Find existing entries by checking `radar/*/` for a matching filename. Trailing slash is *inconsistent in the repo* — both `/tools/vitest/` and `/tools/playwright` exist in production entries. Either is fine; pick one form and stay consistent inside a single file. + +To discover related entries quickly, run `scripts/scan_radar.py` (bundled in `radar-entry-audit` and `radar-decision-log`) — its `--quadrant ` output lists every current slug in that quadrant. Use it to pick the 1–3 most relevant cross-links. + +## Formatting conventions to match + +These are repo conventions, not style preferences — match them or the new entry visually clashes with neighbouring files: + +- **Quote the title.** `title: "Vitest"` not `title: Vitest`. +- **Blank line between bullet items** in the body. Existing entries (`cypress.md`, `playwright.md`, `snowflake.md`) use paragraph-style bullets: + + ```markdown + - **Speed:** first benefit prose. + + - **DX:** second benefit prose. + ``` + + Not tight bullets. The blank line is intentional — it's how the radar's markdown renderer breathes. +- **Open with a one-line external link sentence**: `` `[Tech](https://…) is a short description.` `` Then the `### Why X?` heading. See any entry under `radar/2024-12-01/` for the pattern. +- **Close with a one-sentence restatement** of the decision (most entries do this — it reads as the "so what" line). +- **Don't add a `tags:` field.** The radar derives tags from quadrant + filename, not frontmatter. + +## Procedure + +1. Confirm with the user (if not already given): title, ring, quadrant, featured. +2. Determine target snapshot dir. Default = latest `radar//`. If user wants a fresh snapshot or it's been months, ask before creating a new dated dir. +3. Compute slug. Verify no clash: if `radar/*/.md` exists, this is a *move*, not a new entry — hand off to `radar-entry-move` instead. +4. Draft body. Reuse the user's words for rationale where possible — don't invent justifications. +5. Cross-link 1–3 related radar entries. Look in the latest snapshot and earlier ones for matching slugs. +6. Write the file. Show the user the result; do not commit unless asked. + +## Output checklist + +- [ ] Frontmatter has title (quoted), ring, quadrant, featured. No stray fields (no `tags:`, no `date:`). +- [ ] Slug filename matches the title's kebab-case form. +- [ ] At least one "Why" bullet and one "Considerations at INFO" bullet. +- [ ] Bullets separated by blank lines (paragraph-bullet style). +- [ ] Internal links use `/[quadrant]/[slug]` form, consistent within the file. +- [ ] No invented rationale — everything ties back to what the user said or to facts about the tech. + +## Anti-patterns + +- Don't write marketing prose. The radar is a decision log, not a brochure. +- Don't list every feature of the tech. Focus on *why this, why now, here at INFO*. +- Don't add `featured: true` to a tech that's already an industry default — set `false` and note "this is now a given" in the body, like `react.md` in 2023-03-01. diff --git a/.claude/skills/radar-entry-new/evals/evals.json b/.claude/skills/radar-entry-new/evals/evals.json new file mode 100644 index 0000000..24409d7 --- /dev/null +++ b/.claude/skills/radar-entry-new/evals/evals.json @@ -0,0 +1,26 @@ +{ + "skill_name": "radar-entry-new", + "evals": [ + { + "id": 0, + "name": "vitest-add", + "prompt": "We're starting to use Vitest as our JS test runner across new projects, replacing Jest. Add it to the radar.", + "expected_output": "A new markdown file under radar//vitest.md with valid frontmatter (title, ring, quadrant=tools, featured), Why/Considerations sections, internal links to related entries.", + "files": [] + }, + { + "id": 1, + "name": "bun-assess", + "prompt": "Add Bun to the radar — we want to evaluate it for tooling, not committed yet.", + "expected_output": "A new markdown file at radar//bun.md with ring=assess, appropriate quadrant (tools or platforms-and-operations), ADR-shape body emphasizing evaluation context.", + "files": [] + }, + { + "id": 2, + "name": "ddd-method", + "prompt": "Document our decision to use Domain-Driven Design as our primary methodology for new services.", + "expected_output": "A new markdown file under radar//domain-driven-design.md with quadrant=methods-and-patterns, ring=adopt, ADR-style decision body referencing context and trade-offs.", + "files": [] + } + ] +} diff --git a/.gitignore b/.gitignore index 4e540fd..5822597 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,8 @@ techradar # bin src/rd.json .next -.techradar \ No newline at end of file +.techradar + + +# Eval outputs +*-workspace diff --git a/radar/2026-05-13/cloud-vendor-native-services.md b/radar/2026-05-13/cloud-vendor-native-services.md new file mode 100644 index 0000000..6385615 --- /dev/null +++ b/radar/2026-05-13/cloud-vendor-native-services.md @@ -0,0 +1,23 @@ +--- +title: "Cloud Vendor Native Services" +ring: adopt +quadrant: platforms-and-operations +featured: true +--- + +In the context of a cloud-native infrastructure for new client solutions, we generally prefer cloud-vendor native services (specific to [AWS](/platforms-and-operations/aws/) or [Azure](/platforms-and-operations/azure/)) over cloud-vendor agnostic alternatives, to use more functionality and integrate better with the rest of the cloud vendor's ecosystem, accepting an increased vendor lock-in, because native services typically expose capabilities that the abstracted alternatives flatten away. + +### Why native over agnostic? + +- **More functionality:** vendor-agnostic abstractions tend to surface a lowest-common-denominator feature set. +- **Better integration:** native services line up with vendor IAM, observability and networking out of the box. +- **Vendor lock-in is already partial:** many services we use have no agnostic equivalent — accepting native services elsewhere doesn't dramatically change the lock-in posture. + +### Considerations at INFO + +- Applies to new client solutions; existing solutions are not migrated for the sake of this preference. +- Deviation is fine when an agnostic open-market standard is a genuinely better fit, or when the client requires it. +- Specific refinements override this default — see [PostgreSQL](/platforms-and-operations/postgresql/), where we explicitly prefer the open standard over cloud-native relational alternatives. +- Cross-team skill transfer is harder when services are vendor-specific; the broader concepts (containers, queues, object storage) still translate. + +For new INFO cloud solutions, native services are the default unless a vendor-agnostic option clearly wins on the specifics. diff --git a/radar/2026-05-13/dependabot.md b/radar/2026-05-13/dependabot.md new file mode 100644 index 0000000..b882ac4 --- /dev/null +++ b/radar/2026-05-13/dependabot.md @@ -0,0 +1,22 @@ +--- +title: "GitHub Dependabot" +ring: adopt +quadrant: tools +featured: true +--- + +[Dependabot](https://github.com/dependabot) is GitHub's built-in automated dependency update tool — it scans repositories and opens pull requests when newer versions of declared dependencies become available. + +### Why Dependabot? + +- **Security responsibility:** keeping libraries up to date is part of how we deliver secure software for our clients, and the volume of updates across services makes manual tracking impractical. +- **Native to our SCM:** we've standardised on [GitHub](/tools/github/), so Dependabot needs no extra infrastructure and integrates with the existing PR / CI flow. +- **Covers the languages we use:** first-class support for the [Node.js](/languages-and-frameworks/nodejs/) / [TypeScript](/languages-and-frameworks/typescript/) stack and the other ecosystems we touch. + +### Considerations at INFO + +- Every team is expected to have an automated dependency update process; Dependabot is our default mechanism for that on GitHub. +- The principle is "automated dependency updates," not "Dependabot specifically" — projects with a strong reason can use an alternative, but they have to actually run *something*. +- **Current Focus:** Renovate was previously our recommendation but is now on [hold](/tools/renovate/); new projects start with Dependabot. + +For GitHub-hosted INFO projects, Dependabot is our default automated dependency-update tool. diff --git a/radar/2026-05-13/fastify.md b/radar/2026-05-13/fastify.md new file mode 100644 index 0000000..0f66ec4 --- /dev/null +++ b/radar/2026-05-13/fastify.md @@ -0,0 +1,23 @@ +--- +title: "Fastify" +ring: adopt +quadrant: languages-and-frameworks +featured: true +--- + +[Fastify](https://www.fastify.io/) is a fast, low-overhead web framework for [Node.js](/languages-and-frameworks/nodejs/). + +### Why Fastify? + +- **Performance and security defaults:** built around schema-based validation and a plugin architecture that keeps the request lifecycle predictable. +- **First-class [TypeScript](/languages-and-frameworks/typescript/) support:** types ship with the framework rather than as a community add-on. +- **Familiar Express-style API:** developers coming from [Express](/languages-and-frameworks/express/) recognise the routing model without inheriting Express's middleware limitations. +- **Growing ecosystem:** high-quality plugins for auth, OpenAPI, GraphQL and the integrations we typically need. + +### Considerations at INFO + +- Preferred Node.js web framework when we don't have a project-specific reason to pick something else. +- We accept that Fastify is younger than Express and Koa, and that its track record is shorter — the benefits in performance, types and ergonomics outweigh that trade-off. +- **Current Focus:** new [Node.js](/languages-and-frameworks/nodejs/) services default to Fastify; existing Express services are not migrated unless there's a separate reason to touch them. + +For Node.js services at INFO, Fastify is our default web framework.