@pyreon/mcp 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -13,6 +13,10 @@
13
13
  * get_routes — List all routes in the current project
14
14
  * get_components — List all components with their props and signals
15
15
  * get_browser_smoke_status — Report which browser-categorized packages have smoke coverage
16
+ * get_pattern — Fetch a "how do I do X" pattern body from docs/patterns/
17
+ * get_anti_patterns — Browse the anti-patterns catalog, optionally filtered by category
18
+ * get_changelog — Recent release notes for a @pyreon/* package, parsed from CHANGELOG.md
19
+ * audit_test_environment — Scan test files for mock-vnode patterns (PR #197 bug class)
16
20
  *
17
21
  * Usage:
18
22
  * bunx @pyreon/mcp # stdio transport (for IDE integration)
@@ -20,42 +24,75 @@
20
24
 
21
25
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
26
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
23
- import { detectReactPatterns, diagnoseError, migrateReactCode } from '@pyreon/compiler'
27
+ import {
28
+ type AuditRisk,
29
+ auditTestEnvironment,
30
+ detectPyreonPatterns,
31
+ detectReactPatterns,
32
+ diagnoseError,
33
+ formatTestAudit,
34
+ migrateReactCode,
35
+ } from '@pyreon/compiler'
36
+ import { existsSync, readFileSync } from 'node:fs'
37
+ import { dirname, join, resolve } from 'node:path'
24
38
  import { z } from 'zod'
25
39
  import packageJson from '../package.json' with { type: 'json' }
40
+ import {
41
+ ANTI_PATTERN_CATEGORIES,
42
+ type AntiPatternCategory,
43
+ formatAntiPatterns,
44
+ parseAntiPatterns,
45
+ } from './anti-patterns'
26
46
  import { API_REFERENCE } from './api-reference'
47
+ import {
48
+ findChangelog,
49
+ formatChangelog,
50
+ formatChangelogIndex,
51
+ loadChangelogRegistry,
52
+ suggestChangelogs,
53
+ } from './changelog'
54
+ import {
55
+ findPattern,
56
+ formatPatternBody,
57
+ formatPatternIndex,
58
+ loadPatternRegistry,
59
+ suggestPatterns,
60
+ } from './patterns'
27
61
  import { generateContext, type ProjectContext } from './project-scanner'
28
62
 
29
63
  // ═══════════════════════════════════════════════════════════════════════════════
30
- // Server setup
64
+ // Server setup — exported as a factory so tests can stand up a server with an
65
+ // in-memory transport instead of stdio.
31
66
  // ═══════════════════════════════════════════════════════════════════════════════
32
67
 
33
- const server = new McpServer({
34
- name: 'pyreon',
35
- version: packageJson.version,
36
- })
37
-
38
- // Cache project context (regenerated on demand)
39
- let cachedContext: ProjectContext | null = null
40
- let contextCwd = process.cwd()
41
-
42
- function getContext(): ProjectContext {
43
- if (!cachedContext || contextCwd !== process.cwd()) {
44
- contextCwd = process.cwd()
45
- cachedContext = generateContext(contextCwd)
46
- }
47
- return cachedContext
48
- }
49
-
50
68
  function textResult(text: string) {
51
69
  return { content: [{ type: 'text' as const, text }] }
52
70
  }
53
71
 
54
- // ═══════════════════════════════════════════════════════════════════════════════
55
- // Tool: get_api
56
- // ═══════════════════════════════════════════════════════════════════════════════
72
+ export function createServer(): McpServer {
73
+ const server = new McpServer({
74
+ name: 'pyreon',
75
+ version: packageJson.version,
76
+ })
77
+
78
+ // Project context cache is per-server-instance so the test server and the
79
+ // prod server do not share state.
80
+ let cachedContext: ProjectContext | null = null
81
+ let contextCwd = process.cwd()
82
+
83
+ function getContext(): ProjectContext {
84
+ if (!cachedContext || contextCwd !== process.cwd()) {
85
+ contextCwd = process.cwd()
86
+ cachedContext = generateContext(contextCwd)
87
+ }
88
+ return cachedContext
89
+ }
57
90
 
58
- server.tool(
91
+ // ═══════════════════════════════════════════════════════════════════════════════
92
+ // Tool: get_api
93
+ // ═══════════════════════════════════════════════════════════════════════════════
94
+
95
+ server.tool(
59
96
  'get_api',
60
97
  {
61
98
  package: z.string(),
@@ -97,13 +134,33 @@ server.tool(
97
134
  filename: z.string().optional(),
98
135
  },
99
136
  async ({ code, filename }) => {
100
- const diagnostics = detectReactPatterns(code, filename ?? 'snippet.tsx')
101
-
102
- if (diagnostics.length === 0) {
137
+ // Run both detectors. The React detector flags "coming from React"
138
+ // mistakes (useState, className, .value writes) — relevant when the
139
+ // code has not yet committed to Pyreon. The Pyreon detector flags
140
+ // "using Pyreon wrong" mistakes (missing <For by>, destructured
141
+ // props, typeof-process dev gates) — relevant once the imports are
142
+ // Pyreon. A single snippet may trigger both sets, so we merge.
143
+ const fname = filename ?? 'snippet.tsx'
144
+ const reactDiags = detectReactPatterns(code, fname)
145
+ const pyreonDiags = detectPyreonPatterns(code, fname)
146
+
147
+ if (reactDiags.length === 0 && pyreonDiags.length === 0) {
103
148
  return textResult('✓ No issues found. The code follows Pyreon patterns correctly.')
104
149
  }
105
150
 
106
- const issueText = diagnostics
151
+ type Diag = {
152
+ code: string
153
+ message: string
154
+ line: number
155
+ column: number
156
+ current: string
157
+ suggested: string
158
+ fixable: boolean
159
+ }
160
+ const merged: Diag[] = [...reactDiags, ...pyreonDiags]
161
+ merged.sort((a, b) => a.line - b.line || a.column - b.column)
162
+
163
+ const issueText = merged
107
164
  .map(
108
165
  (d, i) =>
109
166
  `${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ? 'yes' : 'no'}`,
@@ -111,7 +168,7 @@ server.tool(
111
168
  .join('\n\n')
112
169
 
113
170
  return textResult(
114
- `Found ${diagnostics.length} issue${diagnostics.length === 1 ? '' : 's'}:\n\n${issueText}`,
171
+ `Found ${merged.length} issue${merged.length === 1 ? '' : 's'}:\n\n${issueText}`,
115
172
  )
116
173
  },
117
174
  )
@@ -393,16 +450,205 @@ server.tool(
393
450
  },
394
451
  )
395
452
 
453
+ // ═══════════════════════════════════════════════════════════════════════════════
454
+ // Tool: get_pattern — serves docs/patterns/<name>.md
455
+ // ═══════════════════════════════════════════════════════════════════════════════
456
+
457
+ server.tool(
458
+ 'get_pattern',
459
+ {
460
+ name: z
461
+ .string()
462
+ .optional()
463
+ .describe(
464
+ 'Pattern slug (e.g. "dev-warnings", "controllable-state"). Omit to list available patterns.',
465
+ ),
466
+ },
467
+ async ({ name }) => {
468
+ const registry = loadPatternRegistry()
469
+ if (!name) return textResult(formatPatternIndex(registry))
470
+
471
+ const pattern = findPattern(registry, name)
472
+ if (pattern) return textResult(formatPatternBody(pattern))
473
+
474
+ const suggestions = suggestPatterns(registry, name)
475
+ const suggestText =
476
+ suggestions.length > 0
477
+ ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join('\n')}\n\nOr call get_pattern() with no arg to see the full list.`
478
+ : 'Call get_pattern() with no arg to see available patterns.'
479
+ return textResult(`Pattern "${name}" not found.\n\n${suggestText}`)
480
+ },
481
+ )
482
+
483
+ // ═══════════════════════════════════════════════════════════════════════════════
484
+ // Tool: get_anti_patterns — parses .claude/rules/anti-patterns.md
485
+ // ═══════════════════════════════════════════════════════════════════════════════
486
+
487
+ server.tool(
488
+ 'get_anti_patterns',
489
+ {
490
+ category: z
491
+ .enum([
492
+ 'reactivity',
493
+ 'jsx',
494
+ 'context',
495
+ 'architecture',
496
+ 'testing',
497
+ 'lifecycle',
498
+ 'documentation',
499
+ 'all',
500
+ ])
501
+ .optional()
502
+ .describe(
503
+ `Category filter: ${ANTI_PATTERN_CATEGORIES.join(', ')}, or "all" (default).`,
504
+ ),
505
+ },
506
+ async ({ category = 'all' }) => {
507
+ const cat = category as AntiPatternCategory | 'all'
508
+ const doc = loadAntiPatternsDoc()
509
+ if (!doc) {
510
+ return textResult(
511
+ 'Could not locate `.claude/rules/anti-patterns.md`. This tool reads the file from the Pyreon monorepo — running in a consumer project without the rules directory surfaces this miss. File issues against pyreon/pyreon if the file exists but is not being found.',
512
+ )
513
+ }
514
+ const all = parseAntiPatterns(doc)
515
+ const filtered = cat === 'all' ? all : all.filter((e) => e.category === cat)
516
+ return textResult(formatAntiPatterns(filtered, cat))
517
+ },
518
+ )
519
+
520
+ // ═══════════════════════════════════════════════════════════════════════════════
521
+ // Tool: get_changelog — recent release notes for @pyreon/* packages
522
+ // ═══════════════════════════════════════════════════════════════════════════════
523
+
524
+ server.tool(
525
+ 'get_changelog',
526
+ {
527
+ package: z
528
+ .string()
529
+ .optional()
530
+ .describe(
531
+ 'Package name, e.g. "query" or "@pyreon/query". Omit to list every package with a CHANGELOG.',
532
+ ),
533
+ limit: z
534
+ .number()
535
+ .int()
536
+ .positive()
537
+ .optional()
538
+ .describe('Maximum number of substantive versions to return. Default 5.'),
539
+ includeDependencyUpdates: z
540
+ .boolean()
541
+ .optional()
542
+ .describe('Include `Updated dependencies` bullets. Default false (usually noise).'),
543
+ since: z
544
+ .string()
545
+ .optional()
546
+ .describe(
547
+ 'Only include versions strictly newer than this one (e.g. "0.12.0"). Useful when an agent knows the version it was trained against and wants just the delta.',
548
+ ),
549
+ },
550
+ async ({ package: pkg, limit, includeDependencyUpdates, since }) => {
551
+ const registry = loadChangelogRegistry()
552
+ if (!pkg) return textResult(formatChangelogIndex(registry))
553
+
554
+ const changelog = findChangelog(registry, pkg)
555
+ if (changelog) {
556
+ return textResult(
557
+ formatChangelog(changelog, {
558
+ limit,
559
+ includeDependencyUpdates,
560
+ since,
561
+ }),
562
+ )
563
+ }
564
+
565
+ const suggestions = suggestChangelogs(registry, pkg)
566
+ const suggestText =
567
+ suggestions.length > 0
568
+ ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join('\n')}\n\nOr call get_changelog() with no arg for the full list.`
569
+ : 'Call get_changelog() with no arg for the list of packages that ship a CHANGELOG.'
570
+ return textResult(`Changelog for "${pkg}" not found.\n\n${suggestText}`)
571
+ },
572
+ )
573
+
574
+ // ═══════════════════════════════════════════════════════════════════════════════
575
+ // Tool: audit_test_environment — mock-vnode pattern audit (T2.5.7)
576
+ // ═══════════════════════════════════════════════════════════════════════════════
577
+
578
+ server.tool(
579
+ 'audit_test_environment',
580
+ {
581
+ minRisk: z
582
+ .enum(['high', 'medium', 'low'])
583
+ .optional()
584
+ .describe(
585
+ 'Minimum risk level to surface. Default "medium" — HIGH + MEDIUM files. Use "high" for only the riskiest, "low" to see everything.',
586
+ ),
587
+ limit: z
588
+ .number()
589
+ .int()
590
+ .positive()
591
+ .optional()
592
+ .describe('Maximum entries to show per risk group. Default 20.'),
593
+ },
594
+ async ({ minRisk, limit }) => {
595
+ const result = auditTestEnvironment(process.cwd())
596
+ return textResult(
597
+ formatTestAudit(result, {
598
+ minRisk: minRisk as AuditRisk | undefined,
599
+ limit,
600
+ }),
601
+ )
602
+ },
603
+ )
604
+
605
+ return server
606
+ }
607
+
608
+ /**
609
+ * Locate `.claude/rules/anti-patterns.md` by walking up from cwd.
610
+ * Returns the file contents or null if not found within 30 levels.
611
+ * Separate from the patterns loader because the doc path is fixed
612
+ * (`.claude/rules/`) — no glob needed.
613
+ */
614
+ function loadAntiPatternsDoc(startDir: string = process.cwd()): string | null {
615
+ let dir = resolve(startDir)
616
+ for (let i = 0; i < 30; i++) {
617
+ const candidate = join(dir, '.claude', 'rules', 'anti-patterns.md')
618
+ if (existsSync(candidate)) {
619
+ try {
620
+ return readFileSync(candidate, 'utf8')
621
+ } catch {
622
+ return null
623
+ }
624
+ }
625
+ const parent = dirname(dir)
626
+ if (parent === dir) return null
627
+ dir = parent
628
+ }
629
+ return null
630
+ }
631
+
396
632
  // ═══════════════════════════════════════════════════════════════════════════════
397
- // Start server
633
+ // Start server (stdio transport) when invoked directly as a binary.
634
+ // Imports for tests do NOT auto-start — the integration test in
635
+ // `tests/validate.test.ts` wires up an in-memory transport instead.
398
636
  // ═══════════════════════════════════════════════════════════════════════════════
399
637
 
400
638
  async function main(): Promise<void> {
639
+ const server = createServer()
401
640
  const transport = new StdioServerTransport()
402
641
  await server.connect(transport)
403
642
  }
404
643
 
405
- main().catch((err) => {
406
- console.error('MCP server error:', err)
407
- process.exit(1)
408
- })
644
+ // `import.meta.main` is Bun's "entry module" flag. The compiled Node bin
645
+ // (via bun build) preserves this — the bunx / tsx invocation of the
646
+ // shebang sets it truthy; `import { createServer } from '...'` does not.
647
+ // Covers both "run as CLI" and "imported by a test" without needing
648
+ // require.main shims.
649
+ if (import.meta.main) {
650
+ main().catch((err) => {
651
+ console.error('MCP server error:', err)
652
+ process.exit(1)
653
+ })
654
+ }
@@ -0,0 +1,187 @@
1
+ import { defineManifest } from '@pyreon/manifest'
2
+
3
+ export default defineManifest({
4
+ name: '@pyreon/mcp',
5
+ title: 'MCP Server',
6
+ tagline:
7
+ 'Model Context Protocol server — live API lookup, validation, migration, anti-pattern catalog, changelog, test-environment audit',
8
+ description:
9
+ 'MCP server (stdio transport) that exposes Pyreon\\\'s structured knowledge to AI coding assistants (Claude Code, Cursor, etc.). Eleven tools: `get_api` (look up any Pyreon API), `validate` (catch React + Pyreon-specific anti-patterns in a snippet), `migrate_react` (auto-convert React code), `diagnose` (parse a Pyreon error into structured fix info), `get_routes` / `get_components` (project introspection), `get_browser_smoke_status` (which packages need a browser smoke test), `get_pattern` (canonical "how do I do X" docs), `get_anti_patterns` (the catalog from `.claude/rules/anti-patterns.md`), `get_changelog` (recent release notes per package), and `audit_test_environment` (mock-vnode test scanner — PR #197 bug class).',
10
+ category: 'server',
11
+ features: [
12
+ 'Eleven tools covering lookup, validation, migration, diagnosis, introspection, audit',
13
+ 'stdio transport — drop-in compatible with every MCP client',
14
+ 'Project context cached per server instance, auto-invalidates on cwd change',
15
+ 'Manifest-driven — `get_api` reads `api-reference.ts`, regenerated from package manifests',
16
+ 'AST-based detectors — `validate` catches React + Pyreon-specific patterns statically',
17
+ 'Real-repo audit tools (`audit_test_environment`, `get_browser_smoke_status`) walk packages/',
18
+ ],
19
+ longExample: `// .mcp/config.json — register the server with any MCP-aware client
20
+ {
21
+ "mcpServers": {
22
+ "pyreon": {
23
+ "command": "bunx",
24
+ "args": ["@pyreon/mcp"]
25
+ }
26
+ }
27
+ }
28
+
29
+ // Then from the client (Claude Code, Cursor, etc.):
30
+ // get_api({ package: 'flow', symbol: 'createFlow' })
31
+ // → signature, example, common mistakes
32
+ // validate({ code: '<MyButton onClick={handler}>...' })
33
+ // → React-pattern + Pyreon-pattern diagnostics with line/col
34
+ // get_pattern({ name: 'controllable-state' })
35
+ // → canonical pattern body from docs/patterns/
36
+ // get_anti_patterns({ category: 'reactivity' })
37
+ // → reactivity foot-guns from .claude/rules/anti-patterns.md
38
+ // get_changelog({ package: 'flow', limit: 5 })
39
+ // → recent release notes filtered through ceremonial-bump removal
40
+ // audit_test_environment({ minRisk: 'medium' })
41
+ // → mock-vnode test files ranked HIGH / MEDIUM / LOW`,
42
+ api: [
43
+ {
44
+ name: 'get_browser_smoke_status',
45
+ kind: 'constant',
46
+ signature: 'tool: get_browser_smoke_status — no args',
47
+ summary:
48
+ "Companion to the `pyreon/require-browser-smoke-test` lint rule. Reports which browser-categorized Pyreon packages have at least one `*.browser.test.{ts,tsx}` file under `src/`. Uses the same `.claude/rules/browser-packages.json` single source of truth as the rule + the CI script. Lets an AI agent check coverage before writing a new browser package (so it adds a smoke test in the same PR) instead of discovering the failure when CI runs. Falls back with a clear message if the JSON isn't present (e.g. consumer apps that don't ship the Pyreon monorepo layout).",
49
+ example: `// Ask the MCP server:
50
+ // "which Pyreon packages are missing browser smoke coverage?"
51
+ // Tool walks packages/, matches against .claude/rules/browser-packages.json,
52
+ // returns a coverage report.`,
53
+ mistakes: [
54
+ "Using the tool's output as a substitute for running the CI script — this tool only checks file existence, not the self-expiring-exemption check that `bun run lint:browser-smoke` performs",
55
+ ],
56
+ seeAlso: ['audit_test_environment'],
57
+ },
58
+ {
59
+ name: 'get_api',
60
+ kind: 'constant',
61
+ signature: 'tool: get_api({ package: string; symbol: string }) → APIEntry',
62
+ summary:
63
+ 'Look up any Pyreon API by `package` (e.g. `"flow"` or `"@pyreon/flow"`) and `symbol` (e.g. `"createFlow"`). Returns the canonical signature, example, foot-gun catalogue, and cross-references — drawn from `api-reference.ts`, which is regenerated from each package\\\'s `manifest.ts`. The single agent-facing entry point for "what does this API do and how do I avoid the common mistakes."',
64
+ example: `// Agent-side
65
+ get_api({ package: 'flow', symbol: 'createFlow' })
66
+ get_api({ package: '@pyreon/router', symbol: 'useTypedSearchParams' })`,
67
+ seeAlso: ['validate', 'get_pattern'],
68
+ },
69
+ {
70
+ name: 'validate',
71
+ kind: 'constant',
72
+ signature: 'tool: validate({ code: string; filename?: string }) → Diagnostics[]',
73
+ summary:
74
+ 'Two AST-based detectors run in parallel: `detectReactPatterns` flags "coming from React" mistakes (`useState`, `useEffect`, `className`, `onChange` on inputs, React-package imports), and `detectPyreonPatterns` flags "using Pyreon wrong" mistakes (`<For>` missing `by`, props destructured at component signature, `typeof process` dev gates, raw `addEventListener`, `Date.now() + Math.random()` IDs). Diagnostics are merged + sorted by line / column for top-down reading.',
75
+ example: `validate({ code: \`
76
+ function MyComp(props) {
77
+ const { value } = props // → props-destructured
78
+ return <For each={items}>{...}</For> // → for-missing-by
79
+ }
80
+ \` })`,
81
+ seeAlso: ['get_anti_patterns', 'migrate_react'],
82
+ },
83
+ {
84
+ name: 'migrate_react',
85
+ kind: 'constant',
86
+ signature: 'tool: migrate_react({ code: string; filename?: string }) → MigrationResult',
87
+ summary:
88
+ 'Convert React code to idiomatic Pyreon. Handles `useState` → `signal()`, `useEffect` → `effect()`, `className` → `class`, `onChange` → `onInput`, `useMemo` → `computed()`, React imports → Pyreon imports. Reports per-edit fixable diagnostics so callers can apply or review.',
89
+ example: `migrate_react({ code: \`
90
+ import { useState, useEffect } from 'react'
91
+ function Counter() {
92
+ const [count, setCount] = useState(0)
93
+ useEffect(() => { console.log(count) }, [count])
94
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
95
+ }
96
+ \` })`,
97
+ seeAlso: ['validate'],
98
+ },
99
+ {
100
+ name: 'diagnose',
101
+ kind: 'constant',
102
+ signature: 'tool: diagnose({ error: string }) → DiagnoseResult',
103
+ summary:
104
+ 'Parse a Pyreon runtime / build error message into structured fix information: probable cause, recommended fix, related docs, and the `.claude/rules/anti-patterns.md` entry (if any) the error matches. Useful when an agent sees a stack trace and wants to skip the "search the codebase for similar errors" step.',
105
+ example: `diagnose({ error: 'Cannot redefine property X on object [object Object]' })
106
+ // → cause: configurable: false on a getter; fix: set configurable: true`,
107
+ seeAlso: ['validate', 'get_anti_patterns'],
108
+ },
109
+ {
110
+ name: 'get_routes',
111
+ kind: 'constant',
112
+ signature: 'tool: get_routes() → Route[]',
113
+ summary:
114
+ 'List every route in the current project — path, loader presence, guards, params, and named-route name. Walks the project source from `process.cwd()` down. Cached per server instance with auto-invalidation on `cwd` change.',
115
+ example: `get_routes()
116
+ // → [{ path: '/', name: 'home', hasLoader: true, params: [] }, ...]`,
117
+ seeAlso: ['get_components'],
118
+ },
119
+ {
120
+ name: 'get_components',
121
+ kind: 'constant',
122
+ signature: 'tool: get_components() → ComponentInfo[]',
123
+ summary:
124
+ 'List every component in the current project with its props and signal usage. Same scanner as `get_routes`. Useful for an agent before generating new code that needs to reference existing components.',
125
+ example: `get_components()
126
+ // → [{ name: 'Button', file: 'src/Button.tsx', props: ['onClick', 'children'], signals: ['count'] }, ...]`,
127
+ seeAlso: ['get_routes'],
128
+ },
129
+ {
130
+ name: 'get_pattern',
131
+ kind: 'constant',
132
+ signature: 'tool: get_pattern({ name?: string }) → PatternBody | string[]',
133
+ summary:
134
+ 'Fetch a canonical "how do I do X" pattern body from `docs/patterns/`. Eight foundational patterns ship: `dev-warnings`, `controllable-state`, `ssr-safe-hooks`, `signal-writes`, `keyed-lists`, `reactive-context`, `event-listeners`, `form-fields`. Omit `name` to list available patterns. Drop a new `docs/patterns/<slug>.md` file to add one — picked up on next call.',
135
+ example: `get_pattern({ name: 'controllable-state' })
136
+ // → full canonical pattern body
137
+ get_pattern({})
138
+ // → [{ name: 'controllable-state', summary: '...' }, ...]`,
139
+ seeAlso: ['get_anti_patterns'],
140
+ },
141
+ {
142
+ name: 'get_anti_patterns',
143
+ kind: 'constant',
144
+ signature:
145
+ "tool: get_anti_patterns({ category?: 'reactivity' | 'jsx' | 'context' | 'architecture' | 'testing' | 'lifecycle' | 'documentation' | 'all' }) → AntiPattern[]",
146
+ summary:
147
+ 'Browse the anti-patterns catalog parsed from `.claude/rules/anti-patterns.md`. Each entry surfaces its `[detector: <code>]` tag inline so an agent can pair the catalog entry with the live static detector exposed by `validate`. Optional `category` filter; default returns all categories.',
148
+ example: `get_anti_patterns({ category: 'reactivity' })
149
+ // → ['Bare signal in JSX text', 'Stale closures', 'Destructuring props', ...]`,
150
+ seeAlso: ['validate', 'get_pattern'],
151
+ },
152
+ {
153
+ name: 'get_changelog',
154
+ kind: 'constant',
155
+ signature:
156
+ 'tool: get_changelog({ package?: string; limit?: number; includeDependencyUpdates?: boolean; since?: string }) → ChangelogEntry[]',
157
+ summary:
158
+ 'Recent release notes for any `@pyreon/*` package without scraping `git log`. Parses `packages/**/CHANGELOG.md` into version entries (`{ version, changes[], dependencyUpdates[], empty }`) and returns the N most recent substantive versions (default 5). Filters out ceremonial version bumps (pure dependency-update releases with no user-facing body) by default — opt back in with `includeDependencyUpdates: true`. `since: "0.12.0"` returns the delta from a known floor — useful when an agent knows the version it was trained against.',
159
+ example: `get_changelog({ package: 'flow', limit: 5 })
160
+ get_changelog({ package: '@pyreon/router', since: '0.12.0' })`,
161
+ seeAlso: ['get_api'],
162
+ },
163
+ {
164
+ name: 'audit_test_environment',
165
+ kind: 'constant',
166
+ signature:
167
+ "tool: audit_test_environment({ minRisk?: 'high' | 'medium' | 'low'; limit?: number }) → AuditReport",
168
+ summary:
169
+ 'Scan every `*.test.{ts,tsx}` under `packages/` for the mock-vnode anti-pattern that caused PR #197\\\'s silent metadata drop. Files are classified HIGH / MEDIUM / LOW based on the balance of mock-vnode literals + helpers + helper-call sites vs real `h()` calls + `@pyreon/core` import. Three context-aware skips (helper-def vs binding discrimination, type-guard call-arg skip, template-string fixture mask) keep the false-positive rate low. Run before merging a new test file or after a framework change.',
170
+ example: `audit_test_environment({ minRisk: 'medium', limit: 10 })
171
+ // → grouped report with HIGH / MEDIUM / LOW sections`,
172
+ seeAlso: ['get_browser_smoke_status'],
173
+ },
174
+ ],
175
+ gotchas: [
176
+ {
177
+ label: 'Project-context caching',
178
+ note:
179
+ 'Each `createServer()` instance maintains its own cached context (routes, components, islands). The cache auto-resets when `process.cwd()` changes between tool invocations, so the same server can operate across multiple projects in one session.',
180
+ },
181
+ {
182
+ label: 'Manifest-driven',
183
+ note:
184
+ '`get_api` reads `api-reference.ts`, which is generated from each package\\\'s `manifest.ts`. The marker-pair protocol (`<gen-docs:api-reference:start @pyreon/<name>>`) lets some packages be generated and others stay hand-written during incremental migration.',
185
+ },
186
+ ],
187
+ })