mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
refactor(scripts): generate audit-area surfaces from one registry
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.
This commit is contained in:
@@ -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
|
||||
|
||||
<!-- AXIOM_AUDIT_TABLE_BEGIN — generated from scripts/audit-areas.json -->
|
||||
| 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 |
|
||||
<!-- AXIOM_AUDIT_TABLE_END -->
|
||||
|
||||
## Direct Dispatch
|
||||
|
||||
|
||||
Vendored
+3
-3
@@ -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"
|
||||
}
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
|
||||
<!-- AXIOM_AUDIT_DOCS_BEGIN — generated from scripts/audit-areas.json -->
|
||||
### 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 |
|
||||
|
||||
<!-- AXIOM_AUDIT_DOCS_END -->
|
||||
## Priority Levels
|
||||
|
||||
1. **CRITICAL** – `core-data`, `swiftdata`, `database-schema`, `storage`, `icloud` (data corruption/loss risk)
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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-<area>; 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",
|
||||
"<!-- X_BEGIN — generated -->",
|
||||
"old content",
|
||||
"still old",
|
||||
"<!-- X_END -->",
|
||||
"after",
|
||||
].join("\n");
|
||||
|
||||
it("replaces only the content between the markers", () => {
|
||||
assert.equal(
|
||||
spliceRegion(doc, "X", "NEW"),
|
||||
["before", "<!-- X_BEGIN — generated -->", "NEW", "<!-- X_END -->", "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("<!-- X_BEGIN -->\nonly open", "X", "NEW"), null);
|
||||
});
|
||||
|
||||
it("returns null when the markers are inverted", () => {
|
||||
const inverted = ["<!-- X_END -->", "<!-- X_BEGIN -->"].join("\n");
|
||||
assert.equal(spliceRegion(inverted, "X", "NEW"), null);
|
||||
});
|
||||
});
|
||||
@@ -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<string>();
|
||||
const seenPaths = new Set<string>();
|
||||
|
||||
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 `<!-- BEGIN name -->` / `<!-- END name -->`
|
||||
* 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: "<!-- ", line: "// ", hash: "# " }[commentStyle];
|
||||
const [open, close] = [`${prefix}${marker}_BEGIN`, `${prefix}${marker}_END`];
|
||||
|
||||
const beginIdx = content.indexOf(open);
|
||||
const endIdx = content.indexOf(close);
|
||||
if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) return null;
|
||||
|
||||
const afterBegin = content.indexOf("\n", beginIdx);
|
||||
if (afterBegin === -1 || afterBegin > endIdx) return null;
|
||||
|
||||
return content.slice(0, afterBegin + 1) + replacement + "\n" + content.slice(endIdx);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Generate the /axiom:audit area surfaces from scripts/audit-areas.json.
|
||||
*
|
||||
* npm run build:audit-areas # write
|
||||
* npm run build:audit-areas -- --check # verify only, non-zero on drift
|
||||
*
|
||||
* Three regions, each delimited by BEGIN/END markers so the surrounding
|
||||
* hand-authored prose is untouched:
|
||||
*
|
||||
* commands/audit.md `argument:` line (hash markers, in frontmatter)
|
||||
* commands/audit.md body table (html markers)
|
||||
* docs/commands/utility/audit.md grouped tables (html markers)
|
||||
*
|
||||
* The docs sidebar is NOT generated: each of its groups interleaves audit
|
||||
* entries with unrelated commands, so the audit rows are not a spliceable
|
||||
* region. It stays hand-maintained and is checked against the registry by
|
||||
* validateSidebarAgainstRegistry in pre-deploy.
|
||||
*
|
||||
* Generated output is COMMITTED — same convention as axiom-codex/ and the
|
||||
* inlined auditors. pre-deploy.ts runs this in --check mode and fails the
|
||||
* build if a committed file has drifted from the registry.
|
||||
*
|
||||
* Rendering logic lives in scripts/audit-areas.ts (I/O free, unit tested).
|
||||
* This file is the disk layer.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
MARKERS,
|
||||
renderArgumentList,
|
||||
renderBodyTable,
|
||||
renderDocsTables,
|
||||
spliceRegion,
|
||||
validateRegistry,
|
||||
type AuditRegistry,
|
||||
} from "./audit-areas.ts";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const PLUGIN = path.join(root, ".claude-plugin/plugins/axiom");
|
||||
|
||||
const check = process.argv.includes("--check");
|
||||
|
||||
const registryPath = path.join(root, "scripts/audit-areas.json");
|
||||
const registry: AuditRegistry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
||||
|
||||
const registryErrors = validateRegistry(registry);
|
||||
if (registryErrors.length > 0) {
|
||||
console.error("audit-areas.json is invalid:");
|
||||
for (const e of registryErrors) console.error(` ✗ ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
interface Target {
|
||||
file: string;
|
||||
marker: string;
|
||||
style: "html" | "line" | "hash";
|
||||
render: () => string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const targets: Target[] = [
|
||||
{
|
||||
file: path.join(PLUGIN, "commands/audit.md"),
|
||||
marker: MARKERS.argument,
|
||||
style: "hash",
|
||||
render: () => renderArgumentList(registry),
|
||||
label: "commands/audit.md argument: line",
|
||||
},
|
||||
{
|
||||
file: path.join(PLUGIN, "commands/audit.md"),
|
||||
marker: MARKERS.bodyTable,
|
||||
style: "html",
|
||||
render: () => renderBodyTable(registry),
|
||||
label: "commands/audit.md body table",
|
||||
},
|
||||
{
|
||||
file: path.join(root, "docs/commands/utility/audit.md"),
|
||||
marker: MARKERS.docsTable,
|
||||
style: "html",
|
||||
render: () => renderDocsTables(registry),
|
||||
label: "docs/commands/utility/audit.md grouped tables",
|
||||
},
|
||||
];
|
||||
|
||||
let written = 0;
|
||||
let unchanged = 0;
|
||||
const drift: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
// Group by file so two regions in one file compose rather than clobber.
|
||||
const byFile = new Map<string, Target[]>();
|
||||
for (const t of targets) {
|
||||
const list = byFile.get(t.file) ?? [];
|
||||
list.push(t);
|
||||
byFile.set(t.file, list);
|
||||
}
|
||||
|
||||
for (const [file, fileTargets] of byFile) {
|
||||
if (!fs.existsSync(file)) {
|
||||
errors.push(`${path.relative(root, file)} not found`);
|
||||
continue;
|
||||
}
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
let content = original;
|
||||
|
||||
for (const t of fileTargets) {
|
||||
const next = spliceRegion(content, t.marker, t.render(), t.style);
|
||||
if (next === null) {
|
||||
errors.push(
|
||||
`${t.label}: markers ${t.marker}_BEGIN / ${t.marker}_END not found or malformed`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
content = next;
|
||||
}
|
||||
|
||||
if (content === original) {
|
||||
unchanged += fileTargets.length;
|
||||
continue;
|
||||
}
|
||||
if (check) {
|
||||
drift.push(path.relative(root, file));
|
||||
} else {
|
||||
fs.writeFileSync(file, content);
|
||||
written += fileTargets.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const e of errors) console.error(` ✗ ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (check) {
|
||||
if (drift.length > 0) {
|
||||
console.error(
|
||||
`audit-area surfaces are stale relative to scripts/audit-areas.json: ${drift.join(", ")}`,
|
||||
);
|
||||
console.error(" Run: npm run build:audit-areas");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`audit areas: ${registry.areas.length} areas across ${registry.groupOrder.length} groups; all 3 surfaces up to date`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`audit areas: ${registry.areas.length} areas across ${registry.groupOrder.length} groups; ${written} region(s) written, ${unchanged} unchanged`,
|
||||
);
|
||||
}
|
||||
+62
-22
@@ -36,12 +36,7 @@ import { MCP_TOOL_BINARIES } from "../axiom-mcp/src/tools/binaries.ts";
|
||||
import {
|
||||
parseFrontmatterAreas,
|
||||
parseBodyTable,
|
||||
parseDocAreas,
|
||||
parseSidebarAreas,
|
||||
parseSidebarGroups,
|
||||
parseDocGroups,
|
||||
validateParity,
|
||||
validateGroupedParity,
|
||||
parseInlineAuditReferences,
|
||||
validateInlineReferences,
|
||||
validateAgentDescriptionParity,
|
||||
@@ -50,6 +45,16 @@ import {
|
||||
validateAdvertisedCommands,
|
||||
AGENT_FRONTMATTER_KEYS,
|
||||
} from "./audit-parity.ts";
|
||||
import {
|
||||
MARKERS,
|
||||
renderArgumentList,
|
||||
renderBodyTable,
|
||||
renderDocsTables,
|
||||
spliceRegion,
|
||||
validateRegistry,
|
||||
validateSidebarAgainstRegistry,
|
||||
type AuditRegistry,
|
||||
} from "./audit-areas.ts";
|
||||
import {
|
||||
checkSkillInvocations,
|
||||
findSkillNameCollisions,
|
||||
@@ -1037,27 +1042,61 @@ if (!fs.existsSync(auditCmdPath)) {
|
||||
const docContent = fs.readFileSync(auditDocPath, "utf8");
|
||||
const cfgContent = fs.readFileSync(sidebarConfigPath, "utf8");
|
||||
|
||||
// frontmatter is still parsed (not derived from the registry) so the
|
||||
// advertised-area check below validates against what the file actually
|
||||
// ships, not against what it was supposed to ship.
|
||||
const frontmatter = parseFrontmatterAreas(cmdContent);
|
||||
const bodyRows = parseBodyTable(cmdContent);
|
||||
const body = bodyRows.map((r) => r.area);
|
||||
const docAreas = parseDocAreas(docContent);
|
||||
const sidebar = parseSidebarAreas(cfgContent);
|
||||
|
||||
const parityErrors = validateParity({
|
||||
frontmatter,
|
||||
body,
|
||||
docs: docAreas,
|
||||
sidebar,
|
||||
});
|
||||
// The frontmatter list, the body table, and the docs page are now
|
||||
// GENERATED from scripts/audit-areas.json, so they cannot disagree with
|
||||
// each other by construction — the old A↔B↔C set-parity and grouped-parity
|
||||
// checks are replaced by a single staleness check against the registry.
|
||||
// Same "generate in memory, diff against committed" pattern as the
|
||||
// inlined auditors (12d-bis) and the Codex variant (12f).
|
||||
const registryPath = path.join(root, "scripts/audit-areas.json");
|
||||
let auditRegistry: AuditRegistry | undefined;
|
||||
const parityErrors: string[] = [];
|
||||
try {
|
||||
auditRegistry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
||||
} catch (e: unknown) {
|
||||
parityErrors.push(`scripts/audit-areas.json unreadable: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
if (auditRegistry) {
|
||||
for (const msg of validateRegistry(auditRegistry)) {
|
||||
parityErrors.push(`audit-areas.json: ${msg}`);
|
||||
}
|
||||
|
||||
const generated: Array<[string, string, string, "html" | "hash", string]> = [
|
||||
[auditCmdPath, cmdContent, MARKERS.argument, "hash", renderArgumentList(auditRegistry)],
|
||||
[auditCmdPath, cmdContent, MARKERS.bodyTable, "html", renderBodyTable(auditRegistry)],
|
||||
[auditDocPath, docContent, MARKERS.docsTable, "html", renderDocsTables(auditRegistry)],
|
||||
];
|
||||
for (const [file, content, marker, style, rendered] of generated) {
|
||||
const spliced = spliceRegion(content, marker, rendered, style);
|
||||
const rel = path.relative(root, file);
|
||||
if (spliced === null) {
|
||||
parityErrors.push(`${rel}: ${marker} markers missing or malformed`);
|
||||
} else if (spliced !== content) {
|
||||
parityErrors.push(
|
||||
`${rel} region ${marker} is stale relative to scripts/audit-areas.json — run \`npm run build:audit-areas\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const msg of parityErrors) error("audit-parity", msg);
|
||||
|
||||
// Grouped parity: enforce same group names, same group order, same
|
||||
// items per group, same item order. Catches drifts that set parity
|
||||
// doesn't (e.g. axiom-imz: 27=27 set parity but 5-vs-8 group counts).
|
||||
// The sidebar is NOT generated — each of its groups interleaves audit
|
||||
// entries with unrelated commands, so the audit rows are not a
|
||||
// spliceable region. It stays hand-maintained and is checked against
|
||||
// the registry directly, which is what the old docs↔sidebar grouped
|
||||
// parity was approximating.
|
||||
const sidebarGroups = parseSidebarGroups(cfgContent);
|
||||
const docGroups = parseDocGroups(docContent);
|
||||
const groupedErrors = validateGroupedParity(sidebarGroups, docGroups);
|
||||
for (const msg of groupedErrors) error("audit-parity", `(grouped) ${msg}`);
|
||||
const groupedErrors = auditRegistry
|
||||
? validateSidebarAgainstRegistry(auditRegistry, sidebarGroups)
|
||||
: [];
|
||||
for (const msg of groupedErrors) error("audit-parity", `(sidebar) ${msg}`);
|
||||
|
||||
// E: agent file existence — needs filesystem access so it stays here.
|
||||
// Read agent file contents into a map so the description-parity check
|
||||
@@ -1190,8 +1229,9 @@ if (!fs.existsSync(auditCmdPath)) {
|
||||
advertisedErrors.length === 0
|
||||
) {
|
||||
console.log(
|
||||
` ✓ ${frontmatter.length} audit areas in sync across frontmatter, body table, docs page, sidebar ` +
|
||||
`(${sidebarGroups.length} groups, same order; ${bodyRows.length} agent refs resolve; ` +
|
||||
` ✓ ${frontmatter.length} audit areas generated from scripts/audit-areas.json into ` +
|
||||
`frontmatter + body table + docs page; sidebar checked against it ` +
|
||||
`(${sidebarGroups.length} groups; ${bodyRows.length} agent refs resolve; ` +
|
||||
`${inlineSections.length} prose sections + ${bodyRows.length} agent descriptions verified; ` +
|
||||
`${advertisingAgents} agents advertise a command, ${advertisedExempt.length} exempt)`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user