From 21990414b278c90b4e7067caebe09fae2bc7f548 Mon Sep 17 00:00:00 2001 From: Charles Wiltgen Date: Sat, 25 Jul 2026 11:09:25 -0700 Subject: [PATCH] refactor(scripts): generate audit-area surfaces from one registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/audit-areas.json is now the single source for /axiom:audit areas. Three surfaces are generated from it into marker-delimited regions, leaving the surrounding hand-authored prose untouched: commands/audit.md argument: list (hash markers, inside frontmatter) commands/audit.md body table (model-facing "Detects" text) docs/commands/utility/audit.md (human-facing prose) The registry keeps `detects` and `docsDetects` as separate fields rather than reconciling them. They differ for 22 of 32 areas because one is routing text the model reads and the other is prose a person reads — the docs-are-human / source-is-for-LLM split, not drift. The sidebar is deliberately NOT generated: each of its groups interleaves audit entries with unrelated commands (Build holds audit-build beside fix-build, optimize-build, resolve-deps), so the audit rows are not a spliceable region and generating the groups would delete the others. It stays hand-maintained and is now checked directly against the registry, which is what the old docs-vs-sidebar grouped parity was approximating. Retires validateParity and validateGroupedParity: with three surfaces generated from one source they cannot disagree by construction, so the set-parity and grouped-parity comparisons are replaced by one staleness check — the same generate-and-diff pattern already used for the inlined auditors (12d-bis) and the Codex variant (12f). validateRegistry gates the source itself (duplicate areas, unknown groups, empty fields, a `|` that would break a table cell, a docPath the sidebar parser could not read the area back out of). Also collapses the three arbitrary orderings that had accumulated across the surfaces into one: group order, then alphabetical within group. Row order in a for-LLM table carries no meaning, and docs and sidebar already used exactly this order. Verified no data was lost: every body-table row and every docs description is byte-identical to the previous committed content compared order-independently; only ordering changed. Mutation-tested by adding an area to the registry alone — all three generated regions plus the sidebar check reported it. 126 unit tests pass. --- .../plugins/axiom/commands/audit.md | 54 ++-- axiom-mcp/dist/bundle-stats.json | 6 +- axiom-mcp/dist/bundle.json | 4 +- docs/commands/utility/audit.md | 3 +- package.json | 3 +- scripts/audit-areas.json | 273 ++++++++++++++++++ scripts/audit-areas.test.ts | 271 +++++++++++++++++ scripts/audit-areas.ts | 228 +++++++++++++++ scripts/build-audit-areas.ts | 151 ++++++++++ scripts/pre-deploy.ts | 84 ++++-- 10 files changed, 1023 insertions(+), 54 deletions(-) create mode 100644 scripts/audit-areas.json create mode 100644 scripts/audit-areas.test.ts create mode 100644 scripts/audit-areas.ts create mode 100644 scripts/build-audit-areas.ts diff --git a/.claude-plugin/plugins/axiom/commands/audit.md b/.claude-plugin/plugins/axiom/commands/audit.md index 9ada277c..17f2ef72 100644 --- a/.claude-plugin/plugins/axiom/commands/audit.md +++ b/.claude-plugin/plugins/axiom/commands/audit.md @@ -1,10 +1,12 @@ --- description: Smart audit selector - analyzes your project and suggests relevant audits argument-hint: "[area]" -# Not a Claude Code field — Axiom's own canonical audit-area registry, -# validated against the body table, docs page, and sidebar by -# scripts/audit-parity.ts. `argument-hint` above is the real user-facing hint. -argument: "area (optional) - Which audit to run: all, memory, concurrency, accessibility, energy, swiftui-performance, swiftui-architecture, swiftui-nav, swiftui-layout, swift-performance, swift-simplify, core-data, swiftdata, database-schema, grdb-performance, networking, codable, icloud, storage, liquid-glass, textkit, testing, test-failures, build, spritekit, security, modernization, camera, foundation-models, iap, screenshots, ux-flow, resize" +# Not a Claude Code field — Axiom's own audit-area registry. GENERATED from +# scripts/audit-areas.json by scripts/build-audit-areas.ts; do not hand-edit. +# `argument-hint` above is the real user-facing hint. +# AXIOM_AUDIT_ARGUMENT_BEGIN +argument: "area (optional) - Which audit to run: all, build, codable, core-data, energy, memory, modernization, swift-performance, swift-simplify, test-failures, testing, concurrency, liquid-glass, resize, swiftui-architecture, swiftui-layout, swiftui-nav, swiftui-performance, textkit, ux-flow, camera, foundation-models, iap, networking, database-schema, grdb-performance, icloud, storage, swiftdata, accessibility, spritekit, screenshots, security" +# AXIOM_AUDIT_ARGUMENT_END disable-model-invocation: true --- @@ -17,40 +19,42 @@ If no area specified → analyze project and suggest relevant audits ## Available Audits + | Area | Agent | Detects | |------|-------|---------| -| accessibility | accessibility-auditor | VoiceOver labels, Dynamic Type, color contrast, WCAG compliance | -| concurrency | concurrency-auditor | Swift 6 data races, unsafe Task captures, actor isolation | +| build | build-optimizer | Build time optimization opportunities | +| codable | codable-auditor | JSON serialization issues, Sendable violations | +| core-data | core-data-auditor | Thread safety, schema migrations, N+1 queries | | energy | energy-auditor | Timer abuse, polling patterns, continuous location, animation leaks, background mode misuse | | memory | memory-auditor | Retain cycles, leaks, Timer/observer patterns | -| swiftui-performance | swiftui-performance-analyzer | Expensive body, formatters, whole-collection dependencies, missing lazy | -| swiftui-architecture | swiftui-architecture-auditor | Logic in view, MVVM/TCA patterns, boundary violations | -| swiftui-nav | swiftui-nav-auditor | NavigationStack issues, path management, deep linking | +| modernization | modernization-helper | ObservableObject→@Observable, @StateObject→@State, deprecated APIs | | swift-performance | swift-performance-analyzer | ARC issues, allocation patterns, generic specialization | | swift-simplify | swift-simplifier | Behavior-preserving Swift simplifications — guard/optional cleanups, if/switch expressions, collection idioms, redundant boilerplate, dead availability guards | -| core-data | core-data-auditor | Thread safety, schema migrations, N+1 queries | -| networking | networking-auditor | Deprecated APIs (SCNetworkReachability), anti-patterns | -| codable | codable-auditor | JSON serialization issues, Sendable violations | -| icloud | icloud-auditor | iCloud integration issues, entitlements | -| storage | storage-auditor | File protection, storage strategies, data management | -| liquid-glass | liquid-glass-auditor | iOS 26 adoption opportunities, toolbar improvements | -| textkit | textkit-auditor | TextKit issues, text rendering problems | -| testing | testing-auditor | Flaky tests, slow tests, Swift Testing migration, test quality | | test-failures | test-failure-analyzer | Root-cause diagnosis for a failing or intermittent test — missing await confirmation, @MainActor gaps, shared suite state, missing .serialized | -| build | build-optimizer | Build time optimization opportunities | -| spritekit | spritekit-auditor | Physics bitmask issues, draw call waste, node accumulation, action leaks | -| security | security-privacy-scanner | API keys in code, insecure storage, Privacy Manifests, ATS violations | -| modernization | modernization-helper | ObservableObject→@Observable, @StateObject→@State, deprecated APIs | +| testing | testing-auditor | Flaky tests, slow tests, Swift Testing migration, test quality | +| concurrency | concurrency-auditor | Swift 6 data races, unsafe Task captures, actor isolation | +| liquid-glass | liquid-glass-auditor | iOS 26 adoption opportunities, toolbar improvements | +| resize | resize-auditor | Scene-lifecycle gaps, UIScreen.main, UIRequiresFullScreen, orientation-derived layout, fixed-canvas rendering, Mirroring input | +| swiftui-architecture | swiftui-architecture-auditor | Logic in view, MVVM/TCA patterns, boundary violations | +| swiftui-layout | swiftui-layout-auditor | GeometryReader misuse, deprecated screen APIs, hardcoded breakpoints, identity loss | +| swiftui-nav | swiftui-nav-auditor | NavigationStack issues, path management, deep linking | +| swiftui-performance | swiftui-performance-analyzer | Expensive body, formatters, whole-collection dependencies, missing lazy | +| textkit | textkit-auditor | TextKit issues, text rendering problems | +| ux-flow | ux-flow-auditor | Dead-end views, dismiss traps, buried CTAs, missing empty/loading/error states, accessibility dead ends | | camera | camera-auditor | Deprecated camera APIs, missing interruption handlers, threading violations | -| swiftdata | swiftdata-auditor | @Model struct, missing VersionedSchema models, relationship defaults, migration timing, N+1 | | foundation-models | foundation-models-auditor | Missing availability checks, main thread blocking, manual JSON parsing, guardrail handling | | iap | iap-auditor | Missing transaction.finish(), weak receipt validation, missing restore, subscription status tracking, StoreKit test config gaps | -| swiftui-layout | swiftui-layout-auditor | GeometryReader misuse, deprecated screen APIs, hardcoded breakpoints, identity loss | -| resize | resize-auditor | Scene-lifecycle gaps, UIScreen.main, UIRequiresFullScreen, orientation-derived layout, fixed-canvas rendering, Mirroring input | +| networking | networking-auditor | Deprecated APIs (SCNetworkReachability), anti-patterns | | database-schema | database-schema-auditor | Unsafe ALTER TABLE, DROP operations, missing idempotency, FK misuse, transaction safety | | grdb-performance | grdb-performance-auditor | Raw SQL string interpolation, missing FK indexes, missing PRAGMA optimize, app-group WAL and suspension defense, INSERT OR REPLACE misused as upsert, observation on WITHOUT ROWID tables | +| icloud | icloud-auditor | iCloud integration issues, entitlements | +| storage | storage-auditor | File protection, storage strategies, data management | +| swiftdata | swiftdata-auditor | @Model struct, missing VersionedSchema models, relationship defaults, migration timing, N+1 | +| accessibility | accessibility-auditor | VoiceOver labels, Dynamic Type, color contrast, WCAG compliance | +| spritekit | spritekit-auditor | Physics bitmask issues, draw call waste, node accumulation, action leaks | | screenshots | screenshot-validator | Placeholder text, wrong dimensions, debug indicators, broken UI, competitor references | -| ux-flow | ux-flow-auditor | Dead-end views, dismiss traps, buried CTAs, missing empty/loading/error states, accessibility dead ends | +| security | security-privacy-scanner | API keys in code, insecure storage, Privacy Manifests, ATS violations | + ## Direct Dispatch diff --git a/axiom-mcp/dist/bundle-stats.json b/axiom-mcp/dist/bundle-stats.json index 89a36d46..b8a86520 100644 --- a/axiom-mcp/dist/bundle-stats.json +++ b/axiom-mcp/dist/bundle-stats.json @@ -1,12 +1,12 @@ { - "totalBytes": 12663907, + "totalBytes": 12664017, "skills": { "count": 296, "bytes": 6916351 }, "commands": { "count": 15, - "bytes": 41000 + "bytes": 41110 }, "agents": { "count": 42, @@ -15,5 +15,5 @@ "searchIndex": { "bytes": 5055648 }, - "generatedAt": "2026-07-25T17:18:03.321Z" + "generatedAt": "2026-07-25T17:51:46.409Z" } \ No newline at end of file diff --git a/axiom-mcp/dist/bundle.json b/axiom-mcp/dist/bundle.json index 0566e0ed..84f413c9 100644 --- a/axiom-mcp/dist/bundle.json +++ b/axiom-mcp/dist/bundle.json @@ -1,6 +1,6 @@ { "version": "0.2.0", - "generatedAt": "2026-07-25T17:18:03.321Z", + "generatedAt": "2026-07-25T17:51:46.409Z", "skills": { "axiom-accessibility": { "name": "axiom-accessibility", @@ -43026,7 +43026,7 @@ "audit": { "name": "audit", "description": "Smart audit selector - analyzes your project and suggests relevant audits", - "content": "\nYou are an iOS project auditor with access to specialized Axiom audit agents.\n\n## Your Task\n\nIf user specified an area → launch that specific audit agent\nIf no area specified → analyze project and suggest relevant audits\n\n## Available Audits\n\n| Area | Agent | Detects |\n|------|-------|---------|\n| accessibility | accessibility-auditor | VoiceOver labels, Dynamic Type, color contrast, WCAG compliance |\n| concurrency | concurrency-auditor | Swift 6 data races, unsafe Task captures, actor isolation |\n| energy | energy-auditor | Timer abuse, polling patterns, continuous location, animation leaks, background mode misuse |\n| memory | memory-auditor | Retain cycles, leaks, Timer/observer patterns |\n| swiftui-performance | swiftui-performance-analyzer | Expensive body, formatters, whole-collection dependencies, missing lazy |\n| swiftui-architecture | swiftui-architecture-auditor | Logic in view, MVVM/TCA patterns, boundary violations |\n| swiftui-nav | swiftui-nav-auditor | NavigationStack issues, path management, deep linking |\n| swift-performance | swift-performance-analyzer | ARC issues, allocation patterns, generic specialization |\n| swift-simplify | swift-simplifier | Behavior-preserving Swift simplifications — guard/optional cleanups, if/switch expressions, collection idioms, redundant boilerplate, dead availability guards |\n| core-data | core-data-auditor | Thread safety, schema migrations, N+1 queries |\n| networking | networking-auditor | Deprecated APIs (SCNetworkReachability), anti-patterns |\n| codable | codable-auditor | JSON serialization issues, Sendable violations |\n| icloud | icloud-auditor | iCloud integration issues, entitlements |\n| storage | storage-auditor | File protection, storage strategies, data management |\n| liquid-glass | liquid-glass-auditor | iOS 26 adoption opportunities, toolbar improvements |\n| textkit | textkit-auditor | TextKit issues, text rendering problems |\n| testing | testing-auditor | Flaky tests, slow tests, Swift Testing migration, test quality |\n| test-failures | test-failure-analyzer | Root-cause diagnosis for a failing or intermittent test — missing await confirmation, @MainActor gaps, shared suite state, missing .serialized |\n| build | build-optimizer | Build time optimization opportunities |\n| spritekit | spritekit-auditor | Physics bitmask issues, draw call waste, node accumulation, action leaks |\n| security | security-privacy-scanner | API keys in code, insecure storage, Privacy Manifests, ATS violations |\n| modernization | modernization-helper | ObservableObject→@Observable, @StateObject→@State, deprecated APIs |\n| camera | camera-auditor | Deprecated camera APIs, missing interruption handlers, threading violations |\n| swiftdata | swiftdata-auditor | @Model struct, missing VersionedSchema models, relationship defaults, migration timing, N+1 |\n| foundation-models | foundation-models-auditor | Missing availability checks, main thread blocking, manual JSON parsing, guardrail handling |\n| iap | iap-auditor | Missing transaction.finish(), weak receipt validation, missing restore, subscription status tracking, StoreKit test config gaps |\n| swiftui-layout | swiftui-layout-auditor | GeometryReader misuse, deprecated screen APIs, hardcoded breakpoints, identity loss |\n| resize | resize-auditor | Scene-lifecycle gaps, UIScreen.main, UIRequiresFullScreen, orientation-derived layout, fixed-canvas rendering, Mirroring input |\n| database-schema | database-schema-auditor | Unsafe ALTER TABLE, DROP operations, missing idempotency, FK misuse, transaction safety |\n| grdb-performance | grdb-performance-auditor | Raw SQL string interpolation, missing FK indexes, missing PRAGMA optimize, app-group WAL and suspension defense, INSERT OR REPLACE misused as upsert, observation on WITHOUT ROWID tables |\n| screenshots | screenshot-validator | Placeholder text, wrong dimensions, debug indicators, broken UI, competitor references |\n| ux-flow | ux-flow-auditor | Dead-end views, dismiss traps, buried CTAs, missing empty/loading/error states, accessibility dead ends |\n\n## Direct Dispatch\n\nIf area argument provided ($ARGUMENTS contains an area):\n\nIf $ARGUMENTS is \"all\" → Launch the `health-check` agent instead. This runs all relevant auditors in parallel with a unified report.\n\nIf $ARGUMENTS is a filename (contains `.swift`, `.m`, etc.) rather than an audit area name → treat it as a scoped audit request. Acknowledge you're selecting the most relevant audit(s) for that file, then analyze the file to pick appropriate auditor(s). Don't frame this as a user error — it's a valid shorthand.\n\n1. Look up the agent name from the table above\n2. Launch that agent using the Task tool with subagent_type set to the agent name\n3. Pass the current directory path to the agent\n\n**Example:**\n- User runs `/axiom:audit memory` → Launch memory-auditor agent\n- User runs `/axiom:audit concurrency` → Launch concurrency-auditor agent\n- User runs `/axiom:audit MyService.swift` → Pick relevant auditor(s) for that file and run them\n\n## Batch Execution Guidance\n\nWhen running multiple audits (either user-requested or from smart suggestions):\n\n**Priority Order:**\n1. **CRITICAL audits** (data corruption/loss risk):\n - core-data → Schema safety, thread violations\n - swiftdata → @Model correctness, migration safety, relationship defaults\n - database-schema → Unsafe ALTER TABLE, DROP operations, FK integrity\n - storage → Files in wrong locations\n - icloud → NSFileCoordinator violations\n\n2. **HIGH audits** (production crashes, App Store rejection):\n - concurrency → Swift 6 data races\n - memory → Retain cycles, leaks\n - energy → Timer abuse, polling, continuous location\n - networking → Deprecated APIs, ANR risk\n - security → Hardcoded credentials, Privacy Manifests, ATS\n - testing → Flaky tests, slow CI\n\n3. **MEDIUM audits** (architecture, performance, UX):\n - swiftui-architecture → Logic in views, testability\n - ux-flow → Dead ends, dismiss traps, missing states, UX defects\n - swiftui-performance → Expensive operations, missing lazy\n - swiftui-layout → GeometryReader misuse, hardcoded breakpoints, identity loss\n - resize → scene-lifecycle gaps, UIScreen.main, fixed-canvas assumptions\n - swift-performance → ARC overhead, allocations\n - foundation-models → Availability checks, error handling, session lifecycle\n\n4. **LOW audits** (enhancement opportunities):\n - accessibility → WCAG compliance, VoiceOver\n - liquid-glass → iOS 26 adoption\n - codable → JSON best practices\n - modernization → Legacy API migration\n - swift-simplify → Behavior-preserving Swift clarity simplifications\n - camera → Deprecated capture APIs\n - screenshots → App Store screenshot compliance\n\n**Batch Recommendations:**\n- For pre-release: Run CRITICAL + HIGH audits\n- For architecture review: Run swiftui-architecture + swiftui-nav + swiftui-layout + swiftui-performance\n- For UX review: Run ux-flow + swiftui-nav + accessibility\n- For performance tuning: Run swift-performance + swiftui-performance + memory + energy\n- For App Store prep: Run accessibility + networking + storage + security + screenshots\n- For CI reliability: Run testing + concurrency + memory\n- For battery optimization: Run energy + memory + networking\n- For data layer review: Run swiftdata + database-schema + core-data + storage\n- For AI integration: Run foundation-models + concurrency\n\n**Note:** Agents have built-in output limits (>50 issues → top 10 shown) to prevent overwhelming output on large codebases.\n\n## Multi-Audit Execution\n\nWhen running multiple audits (user selected 2+ areas):\n\n1. **Launch each agent in background**: Use the Task tool with `run_in_background: true` parameter\n2. **Instruct each agent to write full results to file**:\n - Path: `scratch/audit-{area}-{date}.md`\n - Example: `scratch/audit-memory-2025-01-01.md`\n - Include in agent prompt: \"Write your full detailed report to {path}. Return only a summary with issue counts. Skip any files in scratch/ — these are previous audit reports, not source code.\"\n3. **Collect results**: Use TaskOutput tool to retrieve each agent's summary\n4. **Present combined summary table**:\n | Audit | Status | CRITICAL | HIGH | MEDIUM | LOW | File |\n |-------|--------|----------|------|--------|-----|------|\n | memory | ✓ | 1 | 3 | 5 | 2 | scratch/audit-memory-2025-01-01.md |\n | concurrency | ✓ | 0 | 2 | 8 | 0 | scratch/audit-concurrency-2025-01-01.md |\n5. **User reviews files** for full details\n\n**Why this approach:**\n- Each audit remains fully thorough (no shortcuts)\n- Combined output doesn't exceed token limits\n- User gets quick summary + detailed files for review\n\n**Single audit**: When only one audit is requested, run it normally (foreground, full output to conversation).\n\n## Regression Tracking\n\nWhen writing results to `scratch/audit-{area}-{date}.md`:\n\n1. Check for most recent previous file for that area (`scratch/audit-{area}-*.md`)\n2. If found, include a \"Regression Check\" section in output comparing:\n - **New issues** (not in previous run)\n - **Fixed issues** (in previous but not current)\n - **Persistent issues** (in both runs)\n3. Summary line: \"3 new, 5 fixed, 12 persistent since last audit on YYYY-MM-DD\"\n\nNo new files, no YAML — markdown in, markdown out. The `scratch/` directory IS the persistence layer.\n\n## Enhanced Rating Table\n\nFor CRITICAL and HIGH findings, agents should include an enhanced rating table:\n\n```markdown\n| Finding | Urgency | Blast Radius | Fix Effort | ROI |\n|---------|---------|-------------|-----------|-----|\n| Dead-end after payment | Ship-blocker | All users | 30 min | Critical |\n| Missing empty state | Next release | Users who search | 15 min | High |\n```\n\n**Columns**:\n- **Urgency**: Ship-blocker / Next release / Backlog\n- **Blast Radius**: All users / Specific flow / Edge case\n- **Fix Effort**: Time estimate for the fix\n- **ROI**: Computed from urgency x blast radius / effort\n\nIndividual agents adopt this format incrementally — no requirement to update all agents at once. The `ux-flow-auditor` uses this format natively. When updating other agents, prioritize those with high-stakes findings: `security-privacy-scanner`, `core-data-auditor`, `database-schema-auditor`, `concurrency-auditor`.\n\n## Project Analysis (No Area Specified)\n\nIf no area argument:\n1. Analyze project structure:\n - Check for .xcodeproj/.xcworkspace → suggest build audit\n - Find SwiftUI files (*.swift with \"import SwiftUI\") → suggest swiftui-performance, swiftui-architecture\n - Find Swift files → suggest swift-simplify audit\n - Find .xcdatamodeld → suggest core-data audit\n - Check deployment target in .xcodeproj → suggest modernization audit\n - Find CloudKit entitlements → suggest icloud audit\n - Find async/await usage → suggest concurrency audit\n - Find Timer/NotificationCenter → suggest memory audit\n - Find Timer.scheduledTimer or CLLocationManager → suggest energy audit\n - Find URLSession or polling patterns → suggest energy audit\n - Find *Tests.swift files → suggest testing audit\n - Find SpriteKit imports (import SpriteKit, SKScene, SKSpriteNode) → suggest spritekit audit\n - Find hardcoded strings matching API key patterns → suggest security audit\n - Find ObservableObject/StateObject usage → suggest modernization audit\n - Find AVCaptureSession imports → suggest camera audit\n - Find @Model classes → suggest swiftdata audit\n - Find LanguageModelSession / @Generable / FoundationModels imports → suggest foundation-models audit\n - Find GeometryReader / layout patterns → suggest swiftui-layout audit\n - Find UIScreen.main / UIRequiresFullScreen / app-delegate-only lifecycle → suggest resize audit\n - Find registerMigration / ALTER TABLE / DatabaseMigrator → suggest database-schema audit\n - Find screenshots folder (Screenshots/, screenshots/, marketing/) → suggest screenshots audit\n - Find NavigationStack/sheet/TabView → suggest ux-flow audit\n\n2. Present findings and ask: \"Based on your project, I suggest these audits: [list]. Which would you like to run?\"\n\n3. After user selects, launch the corresponding agent(s)\n\n$ARGUMENTS\n" + "content": "\nYou are an iOS project auditor with access to specialized Axiom audit agents.\n\n## Your Task\n\nIf user specified an area → launch that specific audit agent\nIf no area specified → analyze project and suggest relevant audits\n\n## Available Audits\n\n\n| Area | Agent | Detects |\n|------|-------|---------|\n| build | build-optimizer | Build time optimization opportunities |\n| codable | codable-auditor | JSON serialization issues, Sendable violations |\n| core-data | core-data-auditor | Thread safety, schema migrations, N+1 queries |\n| energy | energy-auditor | Timer abuse, polling patterns, continuous location, animation leaks, background mode misuse |\n| memory | memory-auditor | Retain cycles, leaks, Timer/observer patterns |\n| modernization | modernization-helper | ObservableObject→@Observable, @StateObject→@State, deprecated APIs |\n| swift-performance | swift-performance-analyzer | ARC issues, allocation patterns, generic specialization |\n| swift-simplify | swift-simplifier | Behavior-preserving Swift simplifications — guard/optional cleanups, if/switch expressions, collection idioms, redundant boilerplate, dead availability guards |\n| test-failures | test-failure-analyzer | Root-cause diagnosis for a failing or intermittent test — missing await confirmation, @MainActor gaps, shared suite state, missing .serialized |\n| testing | testing-auditor | Flaky tests, slow tests, Swift Testing migration, test quality |\n| concurrency | concurrency-auditor | Swift 6 data races, unsafe Task captures, actor isolation |\n| liquid-glass | liquid-glass-auditor | iOS 26 adoption opportunities, toolbar improvements |\n| resize | resize-auditor | Scene-lifecycle gaps, UIScreen.main, UIRequiresFullScreen, orientation-derived layout, fixed-canvas rendering, Mirroring input |\n| swiftui-architecture | swiftui-architecture-auditor | Logic in view, MVVM/TCA patterns, boundary violations |\n| swiftui-layout | swiftui-layout-auditor | GeometryReader misuse, deprecated screen APIs, hardcoded breakpoints, identity loss |\n| swiftui-nav | swiftui-nav-auditor | NavigationStack issues, path management, deep linking |\n| swiftui-performance | swiftui-performance-analyzer | Expensive body, formatters, whole-collection dependencies, missing lazy |\n| textkit | textkit-auditor | TextKit issues, text rendering problems |\n| ux-flow | ux-flow-auditor | Dead-end views, dismiss traps, buried CTAs, missing empty/loading/error states, accessibility dead ends |\n| camera | camera-auditor | Deprecated camera APIs, missing interruption handlers, threading violations |\n| foundation-models | foundation-models-auditor | Missing availability checks, main thread blocking, manual JSON parsing, guardrail handling |\n| iap | iap-auditor | Missing transaction.finish(), weak receipt validation, missing restore, subscription status tracking, StoreKit test config gaps |\n| networking | networking-auditor | Deprecated APIs (SCNetworkReachability), anti-patterns |\n| database-schema | database-schema-auditor | Unsafe ALTER TABLE, DROP operations, missing idempotency, FK misuse, transaction safety |\n| grdb-performance | grdb-performance-auditor | Raw SQL string interpolation, missing FK indexes, missing PRAGMA optimize, app-group WAL and suspension defense, INSERT OR REPLACE misused as upsert, observation on WITHOUT ROWID tables |\n| icloud | icloud-auditor | iCloud integration issues, entitlements |\n| storage | storage-auditor | File protection, storage strategies, data management |\n| swiftdata | swiftdata-auditor | @Model struct, missing VersionedSchema models, relationship defaults, migration timing, N+1 |\n| accessibility | accessibility-auditor | VoiceOver labels, Dynamic Type, color contrast, WCAG compliance |\n| spritekit | spritekit-auditor | Physics bitmask issues, draw call waste, node accumulation, action leaks |\n| screenshots | screenshot-validator | Placeholder text, wrong dimensions, debug indicators, broken UI, competitor references |\n| security | security-privacy-scanner | API keys in code, insecure storage, Privacy Manifests, ATS violations |\n\n\n## Direct Dispatch\n\nIf area argument provided ($ARGUMENTS contains an area):\n\nIf $ARGUMENTS is \"all\" → Launch the `health-check` agent instead. This runs all relevant auditors in parallel with a unified report.\n\nIf $ARGUMENTS is a filename (contains `.swift`, `.m`, etc.) rather than an audit area name → treat it as a scoped audit request. Acknowledge you're selecting the most relevant audit(s) for that file, then analyze the file to pick appropriate auditor(s). Don't frame this as a user error — it's a valid shorthand.\n\n1. Look up the agent name from the table above\n2. Launch that agent using the Task tool with subagent_type set to the agent name\n3. Pass the current directory path to the agent\n\n**Example:**\n- User runs `/axiom:audit memory` → Launch memory-auditor agent\n- User runs `/axiom:audit concurrency` → Launch concurrency-auditor agent\n- User runs `/axiom:audit MyService.swift` → Pick relevant auditor(s) for that file and run them\n\n## Batch Execution Guidance\n\nWhen running multiple audits (either user-requested or from smart suggestions):\n\n**Priority Order:**\n1. **CRITICAL audits** (data corruption/loss risk):\n - core-data → Schema safety, thread violations\n - swiftdata → @Model correctness, migration safety, relationship defaults\n - database-schema → Unsafe ALTER TABLE, DROP operations, FK integrity\n - storage → Files in wrong locations\n - icloud → NSFileCoordinator violations\n\n2. **HIGH audits** (production crashes, App Store rejection):\n - concurrency → Swift 6 data races\n - memory → Retain cycles, leaks\n - energy → Timer abuse, polling, continuous location\n - networking → Deprecated APIs, ANR risk\n - security → Hardcoded credentials, Privacy Manifests, ATS\n - testing → Flaky tests, slow CI\n\n3. **MEDIUM audits** (architecture, performance, UX):\n - swiftui-architecture → Logic in views, testability\n - ux-flow → Dead ends, dismiss traps, missing states, UX defects\n - swiftui-performance → Expensive operations, missing lazy\n - swiftui-layout → GeometryReader misuse, hardcoded breakpoints, identity loss\n - resize → scene-lifecycle gaps, UIScreen.main, fixed-canvas assumptions\n - swift-performance → ARC overhead, allocations\n - foundation-models → Availability checks, error handling, session lifecycle\n\n4. **LOW audits** (enhancement opportunities):\n - accessibility → WCAG compliance, VoiceOver\n - liquid-glass → iOS 26 adoption\n - codable → JSON best practices\n - modernization → Legacy API migration\n - swift-simplify → Behavior-preserving Swift clarity simplifications\n - camera → Deprecated capture APIs\n - screenshots → App Store screenshot compliance\n\n**Batch Recommendations:**\n- For pre-release: Run CRITICAL + HIGH audits\n- For architecture review: Run swiftui-architecture + swiftui-nav + swiftui-layout + swiftui-performance\n- For UX review: Run ux-flow + swiftui-nav + accessibility\n- For performance tuning: Run swift-performance + swiftui-performance + memory + energy\n- For App Store prep: Run accessibility + networking + storage + security + screenshots\n- For CI reliability: Run testing + concurrency + memory\n- For battery optimization: Run energy + memory + networking\n- For data layer review: Run swiftdata + database-schema + core-data + storage\n- For AI integration: Run foundation-models + concurrency\n\n**Note:** Agents have built-in output limits (>50 issues → top 10 shown) to prevent overwhelming output on large codebases.\n\n## Multi-Audit Execution\n\nWhen running multiple audits (user selected 2+ areas):\n\n1. **Launch each agent in background**: Use the Task tool with `run_in_background: true` parameter\n2. **Instruct each agent to write full results to file**:\n - Path: `scratch/audit-{area}-{date}.md`\n - Example: `scratch/audit-memory-2025-01-01.md`\n - Include in agent prompt: \"Write your full detailed report to {path}. Return only a summary with issue counts. Skip any files in scratch/ — these are previous audit reports, not source code.\"\n3. **Collect results**: Use TaskOutput tool to retrieve each agent's summary\n4. **Present combined summary table**:\n | Audit | Status | CRITICAL | HIGH | MEDIUM | LOW | File |\n |-------|--------|----------|------|--------|-----|------|\n | memory | ✓ | 1 | 3 | 5 | 2 | scratch/audit-memory-2025-01-01.md |\n | concurrency | ✓ | 0 | 2 | 8 | 0 | scratch/audit-concurrency-2025-01-01.md |\n5. **User reviews files** for full details\n\n**Why this approach:**\n- Each audit remains fully thorough (no shortcuts)\n- Combined output doesn't exceed token limits\n- User gets quick summary + detailed files for review\n\n**Single audit**: When only one audit is requested, run it normally (foreground, full output to conversation).\n\n## Regression Tracking\n\nWhen writing results to `scratch/audit-{area}-{date}.md`:\n\n1. Check for most recent previous file for that area (`scratch/audit-{area}-*.md`)\n2. If found, include a \"Regression Check\" section in output comparing:\n - **New issues** (not in previous run)\n - **Fixed issues** (in previous but not current)\n - **Persistent issues** (in both runs)\n3. Summary line: \"3 new, 5 fixed, 12 persistent since last audit on YYYY-MM-DD\"\n\nNo new files, no YAML — markdown in, markdown out. The `scratch/` directory IS the persistence layer.\n\n## Enhanced Rating Table\n\nFor CRITICAL and HIGH findings, agents should include an enhanced rating table:\n\n```markdown\n| Finding | Urgency | Blast Radius | Fix Effort | ROI |\n|---------|---------|-------------|-----------|-----|\n| Dead-end after payment | Ship-blocker | All users | 30 min | Critical |\n| Missing empty state | Next release | Users who search | 15 min | High |\n```\n\n**Columns**:\n- **Urgency**: Ship-blocker / Next release / Backlog\n- **Blast Radius**: All users / Specific flow / Edge case\n- **Fix Effort**: Time estimate for the fix\n- **ROI**: Computed from urgency x blast radius / effort\n\nIndividual agents adopt this format incrementally — no requirement to update all agents at once. The `ux-flow-auditor` uses this format natively. When updating other agents, prioritize those with high-stakes findings: `security-privacy-scanner`, `core-data-auditor`, `database-schema-auditor`, `concurrency-auditor`.\n\n## Project Analysis (No Area Specified)\n\nIf no area argument:\n1. Analyze project structure:\n - Check for .xcodeproj/.xcworkspace → suggest build audit\n - Find SwiftUI files (*.swift with \"import SwiftUI\") → suggest swiftui-performance, swiftui-architecture\n - Find Swift files → suggest swift-simplify audit\n - Find .xcdatamodeld → suggest core-data audit\n - Check deployment target in .xcodeproj → suggest modernization audit\n - Find CloudKit entitlements → suggest icloud audit\n - Find async/await usage → suggest concurrency audit\n - Find Timer/NotificationCenter → suggest memory audit\n - Find Timer.scheduledTimer or CLLocationManager → suggest energy audit\n - Find URLSession or polling patterns → suggest energy audit\n - Find *Tests.swift files → suggest testing audit\n - Find SpriteKit imports (import SpriteKit, SKScene, SKSpriteNode) → suggest spritekit audit\n - Find hardcoded strings matching API key patterns → suggest security audit\n - Find ObservableObject/StateObject usage → suggest modernization audit\n - Find AVCaptureSession imports → suggest camera audit\n - Find @Model classes → suggest swiftdata audit\n - Find LanguageModelSession / @Generable / FoundationModels imports → suggest foundation-models audit\n - Find GeometryReader / layout patterns → suggest swiftui-layout audit\n - Find UIScreen.main / UIRequiresFullScreen / app-delegate-only lifecycle → suggest resize audit\n - Find registerMigration / ALTER TABLE / DatabaseMigrator → suggest database-schema audit\n - Find screenshots folder (Screenshots/, screenshots/, marketing/) → suggest screenshots audit\n - Find NavigationStack/sheet/TabView → suggest ux-flow audit\n\n2. Present findings and ask: \"Based on your project, I suggest these audits: [list]. Which would you like to run?\"\n\n3. After user selects, launch the corresponding agent(s)\n\n$ARGUMENTS\n" }, "compare-traces": { "name": "compare-traces", diff --git a/docs/commands/utility/audit.md b/docs/commands/utility/audit.md index c826ece2..8025143b 100644 --- a/docs/commands/utility/audit.md +++ b/docs/commands/utility/audit.md @@ -30,6 +30,7 @@ When run without arguments, analyzes your project and recommends relevant audits Grouped to mirror the sidebar exactly — same group names, same group order, same items in the same order within each group. + ### Build | Area | What It Checks | |------|----------------| @@ -101,7 +102,7 @@ Grouped to mirror the sidebar exactly — same group names, same group order, sa |------|----------------| | `screenshots` | Placeholder text, wrong dimensions, debug indicators | | `security` | API keys in code, insecure storage, Privacy Manifests, ATS violations | - + ## Priority Levels 1. **CRITICAL** – `core-data`, `swiftdata`, `database-schema`, `storage`, `icloud` (data corruption/loss risk) diff --git a/package.json b/package.json index 352412be..80d4feb9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ ] }, "scripts": { - "build": "npm run build:auditors && npm run build:codex && npm run build:mcp && npm run build:docs", + "build": "npm run build:audit-areas && npm run build:auditors && npm run build:codex && npm run build:mcp && npm run build:docs", + "build:audit-areas": "node scripts/build-audit-areas.ts", "build:auditors": "node scripts/build-inlined-auditors.ts", "build:codex": "node scripts/build-codex.ts", "build:mcp": "cd axiom-mcp && pnpm run build:bundle", diff --git a/scripts/audit-areas.json b/scripts/audit-areas.json new file mode 100644 index 00000000..0e0d9fe8 --- /dev/null +++ b/scripts/audit-areas.json @@ -0,0 +1,273 @@ +{ + "//": "Canonical registry of /axiom:audit areas. Edit here, then run `npm run build:audit-areas`. Four surfaces are generated from this file: the argument list and body table in commands/audit.md (for-LLM), the docs page table (for-humans), and the VitePress sidebar. `detects` is model-facing routing text; `docsDetects` is human prose — they differ on purpose per the docs-are-human / source-is-for-LLM split.", + "groupOrder": [ + "Build", + "Debugging", + "Testing", + "Concurrency", + "UI & Design", + "Integration", + "Storage", + "Accessibility", + "Games", + "Shipping" + ], + "areas": [ + { + "area": "build", + "agent": "build-optimizer", + "group": "Build", + "detects": "Build time optimization opportunities", + "docsDetects": "Build time optimization opportunities", + "docPath": "/commands/build/audit-build" + }, + { + "area": "codable", + "agent": "codable-auditor", + "group": "Debugging", + "detects": "JSON serialization issues, Sendable violations", + "docsDetects": "Manual JSON building, error swallowing, Sendable violations", + "docPath": "/commands/debugging/audit-codable" + }, + { + "area": "core-data", + "agent": "core-data-auditor", + "group": "Debugging", + "detects": "Thread safety, schema migrations, N+1 queries", + "docsDetects": "Thread safety, schema migrations, N+1 queries", + "docPath": "/commands/debugging/audit-core-data" + }, + { + "area": "energy", + "agent": "energy-auditor", + "group": "Debugging", + "detects": "Timer abuse, polling patterns, continuous location, animation leaks, background mode misuse", + "docsDetects": "Timer abuse, polling patterns, continuous location, animation leaks", + "docPath": "/commands/debugging/audit-energy" + }, + { + "area": "memory", + "agent": "memory-auditor", + "group": "Debugging", + "detects": "Retain cycles, leaks, Timer/observer patterns", + "docsDetects": "Retain cycles, Timer/observer leaks, closure captures", + "docPath": "/commands/debugging/audit-memory" + }, + { + "area": "modernization", + "agent": "modernization-helper", + "group": "Debugging", + "detects": "ObservableObject→@Observable, @StateObject→@State, deprecated APIs", + "docsDetects": "ObservableObject→@Observable, @StateObject→@State, deprecated APIs", + "docPath": "/commands/debugging/audit-modernization" + }, + { + "area": "swift-performance", + "agent": "swift-performance-analyzer", + "group": "Debugging", + "detects": "ARC issues, allocation patterns, generic specialization", + "docsDetects": "ARC issues, allocation patterns, generic specialization", + "docPath": "/commands/debugging/audit-swift-performance" + }, + { + "area": "swift-simplify", + "agent": "swift-simplifier", + "group": "Debugging", + "detects": "Behavior-preserving Swift simplifications — guard/optional cleanups, if/switch expressions, collection idioms, redundant boilerplate, dead availability guards", + "docsDetects": "Behavior-preserving Swift clarity — guard/optional cleanups, if/switch expressions, collection idioms, boilerplate", + "docPath": "/commands/debugging/audit-swift-simplify" + }, + { + "area": "test-failures", + "agent": "test-failure-analyzer", + "group": "Testing", + "detects": "Root-cause diagnosis for a failing or intermittent test — missing await confirmation, @MainActor gaps, shared suite state, missing .serialized", + "docsDetects": "Why a specific test fails or flakes — missing `await confirmation`, `@MainActor` gaps, shared suite state", + "docPath": "/commands/testing/audit-test-failures" + }, + { + "area": "testing", + "agent": "testing-auditor", + "group": "Testing", + "detects": "Flaky tests, slow tests, Swift Testing migration, test quality", + "docsDetects": "Flaky tests, slow tests, Swift Testing migration", + "docPath": "/commands/testing/audit-testing" + }, + { + "area": "concurrency", + "agent": "concurrency-auditor", + "group": "Concurrency", + "detects": "Swift 6 data races, unsafe Task captures, actor isolation", + "docsDetects": "Swift 6 data races, unsafe Task captures, actor isolation", + "docPath": "/commands/concurrency/audit-concurrency" + }, + { + "area": "liquid-glass", + "agent": "liquid-glass-auditor", + "group": "UI & Design", + "detects": "iOS 26 adoption opportunities, toolbar improvements", + "docsDetects": "iOS 26 adoption opportunities, toolbar improvements", + "docPath": "/commands/ui-design/audit-liquid-glass" + }, + { + "area": "resize", + "agent": "resize-auditor", + "group": "UI & Design", + "detects": "Scene-lifecycle gaps, UIScreen.main, UIRequiresFullScreen, orientation-derived layout, fixed-canvas rendering, Mirroring input", + "docsDetects": "Scene-lifecycle gaps, UIScreen.main, fixed-canvas rendering, iPhone Mirroring input", + "docPath": "/commands/ui-design/audit-resize" + }, + { + "area": "swiftui-architecture", + "agent": "swiftui-architecture-auditor", + "group": "UI & Design", + "detects": "Logic in view, MVVM/TCA patterns, boundary violations", + "docsDetects": "Logic in views, MVVM/TCA boundary violations", + "docPath": "/commands/ui-design/audit-swiftui-architecture" + }, + { + "area": "swiftui-layout", + "agent": "swiftui-layout-auditor", + "group": "UI & Design", + "detects": "GeometryReader misuse, deprecated screen APIs, hardcoded breakpoints, identity loss", + "docsDetects": "GeometryReader misuse, deprecated screen APIs, hardcoded breakpoints", + "docPath": "/commands/ui-design/audit-swiftui-layout" + }, + { + "area": "swiftui-nav", + "agent": "swiftui-nav-auditor", + "group": "UI & Design", + "detects": "NavigationStack issues, path management, deep linking", + "docsDetects": "NavigationStack issues, path management, deep linking", + "docPath": "/commands/ui-design/audit-swiftui-nav" + }, + { + "area": "swiftui-performance", + "agent": "swiftui-performance-analyzer", + "group": "UI & Design", + "detects": "Expensive body, formatters, whole-collection dependencies, missing lazy", + "docsDetects": "Expensive body, formatters, missing lazy containers", + "docPath": "/commands/ui-design/audit-swiftui-performance" + }, + { + "area": "textkit", + "agent": "textkit-auditor", + "group": "UI & Design", + "detects": "TextKit issues, text rendering problems", + "docsDetects": "TextKit issues, text rendering problems", + "docPath": "/commands/ui-design/audit-textkit" + }, + { + "area": "ux-flow", + "agent": "ux-flow-auditor", + "group": "UI & Design", + "detects": "Dead-end views, dismiss traps, buried CTAs, missing empty/loading/error states, accessibility dead ends", + "docsDetects": "Dead-end views, dismiss traps, missing empty/loading/error states", + "docPath": "/commands/ui-design/audit-ux-flow" + }, + { + "area": "camera", + "agent": "camera-auditor", + "group": "Integration", + "detects": "Deprecated camera APIs, missing interruption handlers, threading violations", + "docsDetects": "Deprecated camera APIs, missing interruption handlers", + "docPath": "/commands/integration/audit-camera" + }, + { + "area": "foundation-models", + "agent": "foundation-models-auditor", + "group": "Integration", + "detects": "Missing availability checks, main thread blocking, manual JSON parsing, guardrail handling", + "docsDetects": "Availability checks, main-thread blocking, guardrail handling", + "docPath": "/commands/integration/audit-foundation-models" + }, + { + "area": "iap", + "agent": "iap-auditor", + "group": "Integration", + "detects": "Missing transaction.finish(), weak receipt validation, missing restore, subscription status tracking, StoreKit test config gaps", + "docsDetects": "Missing `transaction.finish()`, weak receipt validation, missing restore, subscription tracking", + "docPath": "/commands/integration/audit-iap" + }, + { + "area": "networking", + "agent": "networking-auditor", + "group": "Integration", + "detects": "Deprecated APIs (SCNetworkReachability), anti-patterns", + "docsDetects": "Deprecated APIs (SCNetworkReachability), anti-patterns", + "docPath": "/commands/integration/audit-networking" + }, + { + "area": "database-schema", + "agent": "database-schema-auditor", + "group": "Storage", + "detects": "Unsafe ALTER TABLE, DROP operations, missing idempotency, FK misuse, transaction safety", + "docsDetects": "Unsafe ALTER TABLE, DROP operations, FK integrity", + "docPath": "/commands/storage/audit-database-schema" + }, + { + "area": "grdb-performance", + "agent": "grdb-performance-auditor", + "group": "Storage", + "detects": "Raw SQL string interpolation, missing FK indexes, missing PRAGMA optimize, app-group WAL and suspension defense, INSERT OR REPLACE misused as upsert, observation on WITHOUT ROWID tables", + "docsDetects": "SQL string interpolation, missing FK indexes, `PRAGMA optimize`, app-group WAL and suspension defense, silent observation staleness", + "docPath": "/commands/storage/audit-grdb-performance" + }, + { + "area": "icloud", + "agent": "icloud-auditor", + "group": "Storage", + "detects": "iCloud integration issues, entitlements", + "docsDetects": "iCloud entitlements, file coordination, CloudKit errors", + "docPath": "/commands/storage/audit-icloud" + }, + { + "area": "storage", + "agent": "storage-auditor", + "group": "Storage", + "detects": "File protection, storage strategies, data management", + "docsDetects": "File protection, backup exclusions, storage strategies", + "docPath": "/commands/storage/audit-storage" + }, + { + "area": "swiftdata", + "agent": "swiftdata-auditor", + "group": "Storage", + "detects": "@Model struct, missing VersionedSchema models, relationship defaults, migration timing, N+1", + "docsDetects": "@Model correctness, VersionedSchema, relationship defaults", + "docPath": "/commands/storage/audit-swiftdata" + }, + { + "area": "accessibility", + "agent": "accessibility-auditor", + "group": "Accessibility", + "detects": "VoiceOver labels, Dynamic Type, color contrast, WCAG compliance", + "docsDetects": "VoiceOver, Dynamic Type, WCAG compliance", + "docPath": "/commands/accessibility/audit-accessibility" + }, + { + "area": "spritekit", + "agent": "spritekit-auditor", + "group": "Games", + "detects": "Physics bitmask issues, draw call waste, node accumulation, action leaks", + "docsDetects": "Physics bitmask issues, draw call waste, action leaks", + "docPath": "/commands/games/audit-spritekit" + }, + { + "area": "screenshots", + "agent": "screenshot-validator", + "group": "Shipping", + "detects": "Placeholder text, wrong dimensions, debug indicators, broken UI, competitor references", + "docsDetects": "Placeholder text, wrong dimensions, debug indicators", + "docPath": "/commands/shipping/audit-screenshots" + }, + { + "area": "security", + "agent": "security-privacy-scanner", + "group": "Shipping", + "detects": "API keys in code, insecure storage, Privacy Manifests, ATS violations", + "docsDetects": "API keys in code, insecure storage, Privacy Manifests, ATS violations", + "docPath": "/commands/shipping/audit-security" + } + ] +} diff --git a/scripts/audit-areas.test.ts b/scripts/audit-areas.test.ts new file mode 100644 index 00000000..90cfd730 --- /dev/null +++ b/scripts/audit-areas.test.ts @@ -0,0 +1,271 @@ +/** + * Tests for scripts/audit-areas.ts. + * + * Run via `node --test scripts/audit-areas.test.ts`. Wired into predeploy + * so every release gates on these passing. + * + * Hermetic — synthetic registries only, never the real audit-areas.json. + * The real file is exercised by the round-trip check in + * build-audit-areas.ts --check, which predeploy also runs. + */ + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + orderAreas, + validateRegistry, + renderArgumentList, + renderBodyTable, + renderDocsTables, + expectedSidebarGroups, + validateSidebarAgainstRegistry, + spliceRegion, + type AuditRegistry, +} from "./audit-areas.ts"; + +const registry: AuditRegistry = { + groupOrder: ["Build", "Storage"], + areas: [ + { + area: "swiftdata", + agent: "swiftdata-auditor", + group: "Storage", + detects: "@Model struct, VersionedSchema, relationship defaults", + docsDetects: "@Model correctness, VersionedSchema, relationship defaults", + docPath: "/commands/storage/audit-swiftdata", + }, + { + area: "build", + agent: "build-optimizer", + group: "Build", + detects: "Build time optimization opportunities", + docsDetects: "Build time optimization opportunities", + docPath: "/commands/build/audit-build", + }, + { + area: "icloud", + agent: "icloud-auditor", + group: "Storage", + detects: "iCloud integration issues, entitlements", + docsDetects: "iCloud entitlements, file coordination, CloudKit errors", + docPath: "/commands/storage/audit-icloud", + }, + ], +}; + +describe("orderAreas", () => { + it("sorts by group order, then alphabetically within group", () => { + assert.deepEqual(orderAreas(registry).map((a) => a.area), [ + "build", + "icloud", + "swiftdata", + ]); + }); + + it("does not mutate the registry", () => { + const before = registry.areas.map((a) => a.area); + orderAreas(registry); + assert.deepEqual(registry.areas.map((a) => a.area), before); + }); +}); + +describe("validateRegistry", () => { + it("accepts a well-formed registry", () => { + assert.deepEqual(validateRegistry(registry), []); + }); + + it("rejects an area whose group is not in groupOrder", () => { + const bad = { ...registry, areas: [{ ...registry.areas[0], group: "Ghost" }] }; + const errs = validateRegistry(bad); + assert.equal(errs.filter((e) => /not in groupOrder/.test(e)).length, 1); + }); + + it("rejects a duplicate area", () => { + const bad = { ...registry, areas: [...registry.areas, registry.areas[0]] }; + assert.ok(validateRegistry(bad).some((e) => /duplicate area 'swiftdata'/.test(e))); + }); + + it("rejects an empty required field", () => { + const bad = { ...registry, areas: [{ ...registry.areas[0], detects: " " }] }; + assert.ok(validateRegistry(bad).some((e) => /empty 'detects'/.test(e))); + }); + + it("rejects a pipe in description text, which would break the table cell", () => { + const bad = { + ...registry, + areas: [{ ...registry.areas[0], docsDetects: "a | b" }], + }; + assert.ok(validateRegistry(bad).some((e) => /breaks table rendering/.test(e))); + }); + + it("rejects a docPath the sidebar parser could not derive the area from", () => { + // parseSidebarAreas reads the area back out of /audit-; a + // mismatch would make the generated sidebar disagree with itself. + const bad = { + ...registry, + areas: [{ ...registry.areas[0], docPath: "/commands/storage/audit-swift-data" }], + }; + assert.ok(validateRegistry(bad).some((e) => /must end with '\/audit-swiftdata'/.test(e))); + }); + + it("rejects 'all', which is the meta-target rather than an area", () => { + const bad = { + ...registry, + areas: [{ ...registry.areas[0], area: "all", docPath: "/commands/storage/audit-all" }], + }; + assert.ok(validateRegistry(bad).some((e) => /meta-target/.test(e))); + }); + + it("rejects a group declared in groupOrder but used by no area", () => { + const bad = { ...registry, groupOrder: [...registry.groupOrder, "Games"] }; + assert.ok(validateRegistry(bad).some((e) => /group 'Games'.*no areas/.test(e))); + }); +}); + +describe("renderArgumentList", () => { + it("renders the meta-target first, then areas in canonical order", () => { + assert.equal( + renderArgumentList(registry), + 'argument: "area (optional) - Which audit to run: all, build, icloud, swiftdata"', + ); + }); +}); + +describe("renderBodyTable", () => { + it("renders the model-facing detects text in canonical order", () => { + assert.equal( + renderBodyTable(registry), + [ + "| Area | Agent | Detects |", + "|------|-------|---------|", + "| build | build-optimizer | Build time optimization opportunities |", + "| icloud | icloud-auditor | iCloud integration issues, entitlements |", + "| swiftdata | swiftdata-auditor | @Model struct, VersionedSchema, relationship defaults |", + ].join("\n"), + ); + }); +}); + +describe("renderDocsTables", () => { + it("renders one table per group, using the human-facing text", () => { + assert.equal( + renderDocsTables(registry), + [ + "### Build", + "| Area | What It Checks |", + "|------|----------------|", + "| `build` | Build time optimization opportunities |", + "", + "### Storage", + "| Area | What It Checks |", + "|------|----------------|", + "| `icloud` | iCloud entitlements, file coordination, CloudKit errors |", + "| `swiftdata` | @Model correctness, VersionedSchema, relationship defaults |", + ].join("\n"), + ); + }); + + it("uses docsDetects, not detects — the two differ on purpose", () => { + const out = renderDocsTables(registry); + assert.match(out, /iCloud entitlements, file coordination, CloudKit errors/); + assert.doesNotMatch(out, /iCloud integration issues, entitlements/); + }); +}); + +describe("expectedSidebarGroups", () => { + it("returns audit areas per group, in registry order", () => { + assert.deepEqual(expectedSidebarGroups(registry), [ + { group: "Build", areas: ["build"] }, + { group: "Storage", areas: ["icloud", "swiftdata"] }, + ]); + }); + + it("omits groups that have no areas", () => { + const withEmpty = { ...registry, groupOrder: ["Build", "Games", "Storage"] }; + assert.deepEqual(expectedSidebarGroups(withEmpty).map((g) => g.group), ["Build", "Storage"]); + }); +}); + +describe("validateSidebarAgainstRegistry", () => { + it("accepts a sidebar matching the registry", () => { + assert.deepEqual( + validateSidebarAgainstRegistry(registry, expectedSidebarGroups(registry)), + [], + ); + }); + + it("flags an area present in the registry but missing from the sidebar", () => { + const actual = [ + { group: "Build", areas: ["build"] }, + { group: "Storage", areas: ["icloud"] }, + ]; + const errs = validateSidebarAgainstRegistry(registry, actual); + assert.equal(errs.length, 1); + assert.match(errs[0], /Storage/); + assert.match(errs[0], /swiftdata/); + }); + + it("flags within-group order drift", () => { + const actual = [ + { group: "Build", areas: ["build"] }, + { group: "Storage", areas: ["swiftdata", "icloud"] }, + ]; + assert.equal(validateSidebarAgainstRegistry(registry, actual).length, 1); + }); + + it("reports group misalignment once, not per item", () => { + const actual = [{ group: "Storage", areas: ["icloud", "swiftdata"] }]; + const errs = validateSidebarAgainstRegistry(registry, actual); + assert.equal(errs.length, 1); + assert.match(errs[0], /groups differ/); + }); +}); + +describe("spliceRegion", () => { + const doc = [ + "before", + "", + "old content", + "still old", + "", + "after", + ].join("\n"); + + it("replaces only the content between the markers", () => { + assert.equal( + spliceRegion(doc, "X", "NEW"), + ["before", "", "NEW", "", "after"].join("\n"), + ); + }); + + it("is idempotent — splicing the same content twice is a fixed point", () => { + const once = spliceRegion(doc, "X", "NEW"); + assert.equal(spliceRegion(once!, "X", "NEW"), once); + }); + + it("supports line-comment markers for TypeScript config files", () => { + const ts = ["a", "// Y_BEGIN", "old", "// Y_END", "b"].join("\n"); + assert.equal( + spliceRegion(ts, "Y", "new", "line"), + ["a", "// Y_BEGIN", "new", "// Y_END", "b"].join("\n"), + ); + }); + + it("supports hash markers for YAML frontmatter, where HTML comments are not comments", () => { + const yaml = ["description: x", "# Z_BEGIN", "old: y", "# Z_END", "other: z"].join("\n"); + assert.equal( + spliceRegion(yaml, "Z", "new: y", "hash"), + ["description: x", "# Z_BEGIN", "new: y", "# Z_END", "other: z"].join("\n"), + ); + }); + + it("returns null when a marker is missing, rather than writing garbage", () => { + assert.equal(spliceRegion("no markers here", "X", "NEW"), null); + assert.equal(spliceRegion("\nonly open", "X", "NEW"), null); + }); + + it("returns null when the markers are inverted", () => { + const inverted = ["", ""].join("\n"); + assert.equal(spliceRegion(inverted, "X", "NEW"), null); + }); +}); diff --git a/scripts/audit-areas.ts b/scripts/audit-areas.ts new file mode 100644 index 00000000..272d2e1c --- /dev/null +++ b/scripts/audit-areas.ts @@ -0,0 +1,228 @@ +/** + * Audit-area registry rendering — pure, I/O free. + * + * `scripts/audit-areas.json` is the single source for the /axiom:audit + * area list. Four surfaces are rendered from it: + * + * 1. the `argument:` list in commands/audit.md (Axiom's own registry) + * 2. the `## Available Audits` body table there (for-LLM routing text) + * 3. the grouped tables in docs/commands/utility/audit.md (human prose) + * 4. the audit links in the docs sidebar (navigation) + * + * Surfaces 1-2 and 3-4 deliberately carry DIFFERENT description text: + * `detects` is what the model reads to pick an agent, `docsDetects` is + * what a person reads on the docs site. That split is the project's + * docs-are-human / source-is-for-LLM convention, not drift — so the + * registry holds both fields rather than reconciling them. + * + * Callers (scripts/build-audit-areas.ts) do the file I/O and the + * marker-region splicing. Tests in audit-areas.test.ts. + */ + +export interface AuditArea { + area: string; + agent: string; + group: string; + /** Model-facing routing text — the body table's "Detects" column. */ + detects: string; + /** Human-facing prose — the docs page's "What It Checks" column. */ + docsDetects: string; + /** Sidebar link target, e.g. `/commands/storage/audit-swiftdata`. */ + docPath: string; +} + +export interface AuditRegistry { + groupOrder: string[]; + areas: AuditArea[]; +} + +/** Begin/end markers delimiting each generated region. */ +export const MARKERS = { + argument: "AXIOM_AUDIT_ARGUMENT", + bodyTable: "AXIOM_AUDIT_TABLE", + docsTable: "AXIOM_AUDIT_DOCS", + sidebar: "AXIOM_AUDIT_SIDEBAR", +} as const; + +/** + * Order areas canonically: by group order, then alphabetically within + * each group. This is the order docs and sidebar already used; applying + * it to audit.md too collapses the three arbitrary orderings that had + * accumulated across the four surfaces into one. + */ +export function orderAreas(registry: AuditRegistry): AuditArea[] { + const rank = new Map(registry.groupOrder.map((g, i) => [g, i])); + return [...registry.areas].sort( + (a, b) => + (rank.get(a.group) ?? Infinity) - (rank.get(b.group) ?? Infinity) || + a.area.localeCompare(b.area), + ); +} + +/** + * Validate the registry before anything is rendered from it. A bad + * registry would otherwise propagate silently into all four surfaces. + */ +export function validateRegistry(registry: AuditRegistry): string[] { + const errors: string[] = []; + const groups = new Set(registry.groupOrder); + const seenAreas = new Set(); + const seenPaths = new Set(); + + if (registry.groupOrder.length === 0) errors.push("groupOrder is empty"); + if (registry.areas.length === 0) errors.push("areas is empty"); + + for (const a of registry.areas) { + for (const field of [ + "area", + "agent", + "group", + "detects", + "docsDetects", + "docPath", + ] as const) { + if (!a[field] || a[field].trim() === "") { + errors.push(`area '${a.area || "(unnamed)"}' has empty '${field}'`); + } + } + if (!groups.has(a.group)) { + errors.push(`area '${a.area}' has group '${a.group}' which is not in groupOrder`); + } + if (seenAreas.has(a.area)) errors.push(`duplicate area '${a.area}'`); + seenAreas.add(a.area); + if (seenPaths.has(a.docPath)) errors.push(`duplicate docPath '${a.docPath}'`); + seenPaths.add(a.docPath); + // A `|` would break out of the markdown table cell it is rendered into. + if (a.detects.includes("|") || a.docsDetects.includes("|")) { + errors.push(`area '${a.area}' has a '|' in description text — breaks table rendering`); + } + if (!a.docPath.endsWith(`/audit-${a.area}`)) { + errors.push( + `area '${a.area}' has docPath '${a.docPath}' — must end with '/audit-${a.area}' so parseSidebarAreas can derive the area back`, + ); + } + } + + // 'all' is the meta-target dispatching to health-check; it is never an area. + if (seenAreas.has("all")) errors.push("'all' is the meta-target and must not be an area"); + + const unused = registry.groupOrder.filter( + (g) => !registry.areas.some((a) => a.group === g), + ); + for (const g of unused) errors.push(`group '${g}' in groupOrder has no areas`); + + return errors; +} + +/** Surface 1 — the comma-separated list in the `argument:` frontmatter line. */ +export function renderArgumentList(registry: AuditRegistry): string { + const areas = orderAreas(registry).map((a) => a.area); + return `argument: "area (optional) - Which audit to run: all, ${areas.join(", ")}"`; +} + +/** Surface 2 — the `## Available Audits` body table (model-facing text). */ +export function renderBodyTable(registry: AuditRegistry): string { + const lines = ["| Area | Agent | Detects |", "|------|-------|---------|"]; + for (const a of orderAreas(registry)) { + lines.push(`| ${a.area} | ${a.agent} | ${a.detects} |`); + } + return lines.join("\n"); +} + +/** Surface 3 — the grouped docs tables (human-facing prose). */ +export function renderDocsTables(registry: AuditRegistry): string { + const ordered = orderAreas(registry); + const out: string[] = []; + for (const group of registry.groupOrder) { + const inGroup = ordered.filter((a) => a.group === group); + if (inGroup.length === 0) continue; + out.push(`### ${group}`); + out.push("| Area | What It Checks |"); + out.push("|------|----------------|"); + for (const a of inGroup) out.push(`| \`${a.area}\` | ${a.docsDetects} |`); + out.push(""); + } + // Trailing blank line is supplied by the marker splice. + return out.join("\n").replace(/\n$/, ""); +} + +/** + * Surface 4 — the docs sidebar. NOT generated, and deliberately so: each + * sidebar group interleaves audit entries with unrelated commands + * (`Build` holds audit-build alongside fix-build, optimize-build, and + * resolve-deps), so the audit rows are not a whole region that can be + * spliced. Generating the groups would delete the non-audit entries. + * + * Instead the sidebar stays hand-maintained and is CHECKED against the + * registry. That still collapses the old four hand-maintained copies to + * one source plus one checked projection. + * + * Returns the audit groups the sidebar should contain, in order, for + * comparison against `parseSidebarGroups` from audit-parity.ts. + */ +export function expectedSidebarGroups( + registry: AuditRegistry, +): Array<{ group: string; areas: string[] }> { + const ordered = orderAreas(registry); + return registry.groupOrder + .map((group) => ({ + group, + areas: ordered.filter((a) => a.group === group).map((a) => a.area), + })) + .filter((g) => g.areas.length > 0); +} + +/** + * Compare the sidebar as parsed from config.ts against what the registry + * says it should hold. Reports group-level and item-level divergence. + */ +export function validateSidebarAgainstRegistry( + registry: AuditRegistry, + actual: Array<{ group: string; areas: string[] }>, +): string[] { + const expected = expectedSidebarGroups(registry); + const errors: string[] = []; + + const eNames = expected.map((g) => g.group); + const aNames = actual.map((g) => g.group); + if (JSON.stringify(eNames) !== JSON.stringify(aNames)) { + errors.push( + `sidebar audit groups differ from the registry: expected [${eNames.join(", ")}], found [${aNames.join(", ")}]`, + ); + return errors; // Item comparison is noise once groups are misaligned. + } + + for (let i = 0; i < expected.length; i++) { + if (JSON.stringify(expected[i].areas) !== JSON.stringify(actual[i].areas)) { + errors.push( + `sidebar group '${expected[i].group}': expected [${expected[i].areas.join(", ")}], found [${actual[i].areas.join(", ")}]`, + ); + } + } + return errors; +} + +/** + * Replace the content between `` / `` + * style markers. Returns null when the markers are absent or malformed, + * so the caller can report a precise error instead of writing garbage. + */ +export function spliceRegion( + content: string, + marker: string, + replacement: string, + commentStyle: "html" | "line" | "hash" = "html", +): string | null { + // `hash` is for YAML frontmatter, where an HTML comment is not a comment. + const prefix = { html: "