@pithyjs/codex 0.1.0-beta.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4401 -0
  3. package/dist/annotations.d.ts +106 -0
  4. package/dist/annotations.js +306 -0
  5. package/dist/apply.d.ts +2 -0
  6. package/dist/apply.js +80 -0
  7. package/dist/changed-scope.d.ts +23 -0
  8. package/dist/changed-scope.js +31 -0
  9. package/dist/check.d.ts +2 -0
  10. package/dist/check.js +117 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/env.d.ts +5 -0
  14. package/dist/env.js +35 -0
  15. package/dist/extract-cli.d.ts +23 -0
  16. package/dist/extract-cli.js +192 -0
  17. package/dist/extraction/breaking-changes.d.ts +66 -0
  18. package/dist/extraction/breaking-changes.js +352 -0
  19. package/dist/extraction/example-extractor.d.ts +55 -0
  20. package/dist/extraction/example-extractor.js +272 -0
  21. package/dist/extraction/index.d.ts +27 -0
  22. package/dist/extraction/index.js +31 -0
  23. package/dist/extraction/jsdoc-parser.d.ts +43 -0
  24. package/dist/extraction/jsdoc-parser.js +274 -0
  25. package/dist/extraction/pipeline.d.ts +54 -0
  26. package/dist/extraction/pipeline.js +526 -0
  27. package/dist/extraction/readme-sync.d.ts +108 -0
  28. package/dist/extraction/readme-sync.js +592 -0
  29. package/dist/extraction/snapshot-store.d.ts +40 -0
  30. package/dist/extraction/snapshot-store.js +153 -0
  31. package/dist/extraction/source-linker.d.ts +80 -0
  32. package/dist/extraction/source-linker.js +316 -0
  33. package/dist/extraction/test-example-extractor.d.ts +69 -0
  34. package/dist/extraction/test-example-extractor.js +400 -0
  35. package/dist/extraction/test-pattern-extractor.d.ts +68 -0
  36. package/dist/extraction/test-pattern-extractor.js +261 -0
  37. package/dist/extraction/testing-pyramid.d.ts +44 -0
  38. package/dist/extraction/testing-pyramid.js +163 -0
  39. package/dist/extraction/type-extractor.d.ts +34 -0
  40. package/dist/extraction/type-extractor.js +494 -0
  41. package/dist/extraction/types.d.ts +401 -0
  42. package/dist/extraction/types.js +34 -0
  43. package/dist/indexer.d.ts +1 -0
  44. package/dist/indexer.js +107 -0
  45. package/dist/llm.d.ts +8 -0
  46. package/dist/llm.js +75 -0
  47. package/dist/readme-sync-cli.d.ts +20 -0
  48. package/dist/readme-sync-cli.js +167 -0
  49. package/dist/review.d.ts +2 -0
  50. package/dist/review.js +93 -0
  51. package/dist/scan.d.ts +29 -0
  52. package/dist/scan.js +221 -0
  53. package/dist/schema.d.ts +169 -0
  54. package/dist/schema.js +70 -0
  55. package/dist/snapshot-cli.d.ts +45 -0
  56. package/dist/snapshot-cli.js +217 -0
  57. package/dist/sync-cli.d.ts +22 -0
  58. package/dist/sync-cli.js +154 -0
  59. package/dist/sync-pipeline.d.ts +58 -0
  60. package/dist/sync-pipeline.js +104 -0
  61. package/dist/sync.d.ts +2 -0
  62. package/dist/sync.js +318 -0
  63. package/dist/validate-cli.d.ts +20 -0
  64. package/dist/validate-cli.js +144 -0
  65. package/dist/validate.d.ts +76 -0
  66. package/dist/validate.js +183 -0
  67. package/dist/watch-cli.d.ts +21 -0
  68. package/dist/watch-cli.js +220 -0
  69. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,4401 @@
1
+ # @pithyjs/codex
2
+
3
+ AI-powered documentation generation and maintenance for PithyJS. Codex scans source files for structured annotations, extracts API metadata, syncs READMEs with auto-generated content, validates documentation integrity, and detects breaking API changes via snapshot diffing.
4
+
5
+ ## Installation
6
+
7
+ <!-- @codex:auto install="@pithyjs/codex" -->
8
+ ```bash
9
+ npm install @pithyjs/codex
10
+ ```
11
+
12
+ ```bash
13
+ pnpm add @pithyjs/codex
14
+ ```
15
+
16
+ ```bash
17
+ yarn add @pithyjs/codex
18
+ ```
19
+ <!-- @codex:end -->
20
+
21
+ ```typescript
22
+ import { runExtractionPipeline } from '@pithyjs/codex/extraction';
23
+ ```
24
+
25
+ ## Overview
26
+
27
+ Codex is the documentation backbone of PithyJS. It works by:
28
+
29
+ 1. **Scanning** source files for `@codex`, `@codexApi`, and `@codex:example` annotations
30
+ 2. **Extracting** JSDoc comments, TypeScript type signatures, and test examples
31
+ 3. **Enriching** codex entries with linked API metadata, test coverage, and source locations
32
+ 4. **Syncing** README files using `@codex:auto` markers that auto-populate install commands, API tables, examples, and testing matrices
33
+ 5. **Validating** that all codex entries have consistent metadata, examples, and test references
34
+ 6. **Snapshotting** API surfaces to detect breaking changes between releases
35
+ 7. **Generating** changelogs and migration guides via LLM integration (OpenAI)
36
+
37
+ ## CLI Commands
38
+
39
+ All commands are available as standalone binaries after building the package:
40
+
41
+ | Command | Description |
42
+ | --- | --- |
43
+ | `pithy-codex` | Main CLI entry point for the sync workflow |
44
+ | `pithy-codex-sync` | Run the full sync pipeline (scan → extract → enrich → validate) |
45
+ | `pithy-codex-check` | Validate codex entry integrity and consistency |
46
+ | `pithy-codex-index` | Regenerate codex index files |
47
+ | `pithy-codex-extract` | Run the 7-phase extraction pipeline |
48
+ | `pithy-codex-readme-sync` | Sync `@codex:auto` markers in README files |
49
+ | `pithy-codex-validate` | Validate extraction results against schemas |
50
+ | `pithy-codex-watch` | Watch mode for automatic re-sync on file changes |
51
+ | `pithy-codex-snapshot` | Save, list, or diff API snapshots for breaking change detection |
52
+
53
+ ### Via pnpm scripts
54
+
55
+ ```bash
56
+ pnpm --filter @pithyjs/codex build # Build codex tools (required first)
57
+ pnpm --filter @pithyjs/codex sync # Smart sync (only changed entries)
58
+ pnpm --filter @pithyjs/codex review # Generate review.md
59
+ pnpm --filter @pithyjs/codex apply # Apply approved entries
60
+ pnpm --filter @pithyjs/codex check # Validate integrity
61
+ pnpm --filter @pithyjs/codex index # Regenerate indexes
62
+ pnpm --filter @pithyjs/codex readme-sync # Update READMEs with @codex:auto markers
63
+ pnpm --filter @pithyjs/codex snapshot # Save/list/diff API snapshots
64
+ pnpm --filter @pithyjs/codex extract # Run extraction pipeline
65
+ pnpm --filter @pithyjs/codex validate # Validate extraction results
66
+ pnpm --filter @pithyjs/codex watch # Watch mode for auto-sync
67
+ ```
68
+
69
+ ## Extraction Pipeline
70
+
71
+ The extraction pipeline runs in 7 phases to transform source annotations into rich documentation entries:
72
+
73
+ 1. **Scan** — Discover source files matching configured glob patterns
74
+ 2. **Parse** — Extract `@codex` and `@codexApi` annotations from source files
75
+ 3. **JSDoc** — Link JSDoc comments to their corresponding declarations
76
+ 4. **Types** — Extract TypeScript type signatures and interfaces
77
+ 5. **Examples** — Collect `@codex:example` markers from test files
78
+ 6. **Tests** — Analyze test coverage and match tests to codex entries
79
+ 7. **Enrich** — Combine all extracted data into enriched codex entries with source links
80
+
81
+ The pipeline is configurable via `ExtractionConfig` and outputs an `ExtractionResult` with stats, enriched components, API entries, test references, and README sections.
82
+
83
+ ## API Reference
84
+
85
+ <!-- @codex:auto api="pithy.codex.**" format="table" -->
86
+ | API | Component | Signature | Stability | Description |
87
+ | --- | --- | --- | --- | --- |
88
+ | parseCodexAnnotations | annotations | `(content: string) => CodexAnnotation[]` | stable | Parses @codex annotations from source code |
89
+ | parseCodexApiAnnotations | annotations | `(content: string) => CodexApiAnnotation[]` | stable | Parses @codexApi annotations from source code |
90
+ | createSourceDigest | annotations | `(content: string) => string` | stable | Creates a stable content hash for a source file |
91
+ | createCanonicalFingerprint | annotations | `(component: DiscoveredComponent) => string` | stable | Creates a canonical fingerprint for a discovered component |
92
+ | discoverComponentsFromFile | annotations | `(filePath: string, content?: string) => Promise<DiscoveredComponent[]>` | stable | Discovers components from a single source file using annotations |
93
+ | validateAnnotation | annotations | `(annotation: unknown, type: 'codex' | 'codexApi') => boolean` | stable | Validates annotation format and logs issues in DEV mode |
94
+ | CHANGED_ONLY_PATHSPEC | changed-scope | `readonly string[]` | stable | - |
95
+ | loadEnv | env | `() => void` | stable | Loads environment variables from .env files using dotenv. |
96
+ | askLLM | llm | `(messages: { role: 'system' | 'user'; content: string }[]) => Promise<string>` | stable | Sends a prompt to the OpenAI API and returns the response text. |
97
+ | scan | scan | `(root: string, files?: string[]) => Promise<Candidate[]>` | stable | Scans source files for @codex annotations and returns discovered components/APIs. |
98
+ | combineFingerprints | scan | `(fingerprints: string[]) => string` | internal | Fold the per-file fingerprints of one `@codex` id into a single stable one. Sorts the COMPLETE list before hashing, which is what makes the result independent of scan order. Folding pairwise — `hash(sort([acc, next]))` — looks equivalent and is not: SHA-256 is not associative, so sorting at each step only commutes for two inputs. With three or more (and `pithy.doctor.rules` is declared by 22 files) a different scan order yields a different fingerprint for identical sources, which reports every entry as changed at random. |
99
+ | ApiSig | schema | `z.ZodObject<{ name: string; stability: string; signature?: string }>` | stable | Zod schema for an API signature entry with name, stability, and optional signature. |
100
+ | codexSchema | schema | `z.ZodObject<CodexEntry>` | stable | Zod schema defining the full structure of a codex entry. |
101
+ | CodexEntry | schema | `z.infer<typeof codexSchema>` | stable | Inferred TypeScript type from the codex schema. |
102
+ | consumeValue | snapshot-cli | `(args: string[], index: number, flag: string) => string` | stable | Consume the next argument as a value for an option flag. Throws if the value is missing or looks like another flag. |
103
+ | parseArgs | snapshot-cli | `(args: string[]) => CliOptions` | stable | - |
104
+ | validatePath | snapshot-cli | `(basePath: string, inputPath: string) => string` | stable | Validates and normalizes a path to prevent path traversal attacks |
105
+ | validatePath | sync-pipeline | `(basePath: string, inputPath: string) => string` | stable | Validates and normalizes a path to prevent path traversal attacks |
106
+ | runSyncPipeline | sync-pipeline | `(options: SyncPipelineOptions, logger?: CodexLogger) => Promise<SyncPipelineResult>` | stable | - |
107
+ | validateExampleSyntax | validate | `(code: string, componentId?: string, apiName?: string, language?: string) => ExampleValidationResult` | stable | - |
108
+ | validateExtractionResult | validate | `(result: ExtendedExtractionResult) => ValidationResult` | stable | - |
109
+ | formatValidationReport | validate | `(result: ValidationResult) => string` | stable | - |
110
+ | createApiSnapshot | breaking-changes | `(result: ExtendedExtractionResult, version: string, timestamp?: string) => ApiSnapshot` | stable | Create an API snapshot from extraction results at a given version. Captures all enriched API entries across all components. |
111
+ | snapshotFromTypeDefinitions | breaking-changes | `(types: ExtractedTypeDefinition[], version: string, timestamp?: string) => ApiSnapshot` | stable | Create an API snapshot from extracted type definitions. Only includes exported types (public API surface). |
112
+ | diffSnapshots | breaking-changes | `(before: ApiSnapshot, after: ApiSnapshot) => SnapshotDiff` | stable | Compare two API snapshots and produce a structured diff. Matches entries by qualifiedName (parent.name). |
113
+ | classifyChange | breaking-changes | `(change: ApiChange) => ChangeClassification` | stable | Classify a single API change as major, minor, or patch according to semver conventions. - **major**: removal or signature/member change of a stable API - **minor**: addition, removal of experimental/deprecated, or changes to experimental APIs - **patch**: stability level changes |
114
+ | detectBreakingChanges | breaking-changes | `(diff: SnapshotDiff) => ApiChange[]` | stable | Filter a snapshot diff down to only breaking changes (those classified as "major"). |
115
+ | generateMigrationGuide | breaking-changes | `(changes: ApiChange[]) => string` | stable | Generate a human-readable migration guide for a set of breaking changes. Groups changes by parent component and provides before/after comparisons. |
116
+ | generateChangelog | breaking-changes | `(diff: SnapshotDiff) => string` | stable | Generate a changelog in Markdown from a snapshot diff. Sections: Breaking Changes, Added, Changed. Omits empty sections. |
117
+ | extractExamples | example-extractor | `(content: string, baseLocation: SourceLocation) => ExtractedExample[]` | stable | Extracts all code examples from a markdown/documentation string |
118
+ | validateExample | example-extractor | `(code: string, language: string) => { valid: boolean; errors: string[] }` | stable | Validates that a code example is syntactically valid |
119
+ | wrapDoctestInHarness | example-extractor | `(example: ExtractedExample, entryId: string) => string` | stable | Wraps a doctest example in a test harness |
120
+ | extractExamplesFromSource | example-extractor | `(content: string, filePath: string) => { functionName: string; examples: ExtractedExample[] }[]` | stable | Extracts examples from JSDoc @example tags in source code |
121
+ | generateRunnableExampleFile | example-extractor | `(examples: ExtractedExample[], imports: string[]) => string` | stable | Generates a runnable example file from extracted examples |
122
+ | parseJSDocComment | jsdoc-parser | `(comment: string, location: SourceLocation) => ParsedJSDoc` | stable | Parses a single JSDoc comment block |
123
+ | extractJSDocBlocks | jsdoc-parser | `(content: string, filePath: string) => { jsdoc: ParsedJSDoc; followingCode: string }[]` | stable | Extracts all JSDoc blocks from a source file |
124
+ | linkJSDocToDeclarations | jsdoc-parser | `(content: string, filePath: string) => Map<string, ParsedJSDoc>` | stable | Links JSDoc blocks to their associated function/class declarations |
125
+ | extractSignature | jsdoc-parser | `(code: string) => string | undefined` | stable | Extracts function signature from source code |
126
+ | runExtractionPipeline | pipeline | `(config?: Partial<ExtractionConfig>) => Promise<ExtendedExtractionResult>` | stable | - |
127
+ | extractSingleFile | pipeline | `(filePath: string, content?: string) => Promise<EnrichedComponent | null>` | stable | - |
128
+ | generateExtractionReport | pipeline | `(result: ExtractionResult) => string` | stable | - |
129
+ | exportToCodexFormat | pipeline | `(result: ExtractionResult) => object` | stable | - |
130
+ | parseMarkerAttributes | readme-sync | `(attrString: string) => Record<string, string>` | stable | Parses HTML-style attributes from a marker opening tag string. Only quoted attribute values are supported (double or single quotes). Unquoted values like `install=pkg` are ignored by design. |
131
+ | resolveMarkerType | readme-sync | `(attributes: Record<string, string>) => ReadmeMarkerType | null` | stable | Determines marker type from its attributes. |
132
+ | parseReadmeMarkers | readme-sync | `(content: string) => ReadmeMarker[]` | stable | Parses all @codex:auto markers from README content. |
133
+ | matchIdPattern | readme-sync | `(pattern: string, entryId: string) => boolean` | stable | Matches codex entry IDs against a glob-style pattern. Supports: exact match, trailing `*` (one segment), trailing `**` (any depth). |
134
+ | filterByPattern | readme-sync | `(components: EnrichedComponent[], pattern: string) => EnrichedComponent[]` | stable | Filters components by an ID pattern. |
135
+ | stripTestBoilerplate | readme-sync | `(code: string) => string` | stable | Strips test framework boilerplate from example code extracted from tests. Removes it()/test() wrapper, dedents, converts expect() to value comments. |
136
+ | generateInstallContent | readme-sync | `(packageName: string) => string` | stable | Generates install command content for a package. |
137
+ | generateExamplesContent | readme-sync | `(components: EnrichedComponent[], limit?: number, testExamples?: TestExample[], pattern?: string) => string` | stable | Generates examples content from extraction data. Prefers API-level JSDoc examples; falls back to @codex:example test examples when no API examples are found (test examples are safe from feedback loops). |
138
+ | escapeTableCell | readme-sync | `(str: string) => string` | stable | Escapes pipe characters and collapses newlines for use in markdown table cells. |
139
+ | escapeMd | readme-sync | `(str: string) => string` | stable | Escapes markdown special characters in inline text (bold markers, brackets, pipes, backticks). |
140
+ | sanitizeForInlineCode | readme-sync | `(str: string) => string` | stable | Strips backticks from a string so it can safely be wrapped in inline code. |
141
+ | longestBacktickRun | readme-sync | `(str: string) => number` | stable | Returns the length of the longest consecutive run of backticks in a string. |
142
+ | sanitizeLanguage | readme-sync | `(lang: string) => string` | stable | Sanitizes a language identifier for use in fenced code blocks. |
143
+ | generateApiContent | readme-sync | `(components: EnrichedComponent[], format?: string) => string` | stable | Generates API reference table from extraction data. |
144
+ | generateTestingContent | readme-sync | `(components: EnrichedComponent[]) => string` | stable | Generates testing pyramid table from extraction data. Includes a Status column showing coverage completeness based on test references and category-aware testing pyramid requirements. |
145
+ | generateBundleContent | readme-sync | `(_packageName: string) => string` | stable | Placeholder for bundle content generation (not yet implemented). |
146
+ | syncReadmeContent | readme-sync | `(content: string, filePath: string, components: EnrichedComponent[], testExamples?: TestExample[]) => ReadmeSyncResult` | stable | Syncs a single README's content: parses markers, generates content, replaces. |
147
+ | syncAllReadmes | readme-sync | `(extractionResult: ExtendedExtractionResult, config?: ReadmeSyncConfig) => Promise<ReadmeSyncResult[]>` | stable | Syncs all README files matching configured patterns. |
148
+ | getSnapshotPath | snapshot-store | `(dir: string, version: string) => string` | stable | Get the file path for a snapshot of a given version. |
149
+ | saveSnapshot | snapshot-store | `(snapshot: ApiSnapshot, dir: string) => Promise<void>` | stable | Save an API snapshot to disk as a JSON file named `{version}.json`. Creates the target directory if it doesn't exist. |
150
+ | loadSnapshot | snapshot-store | `(version: string, dir: string) => Promise<ApiSnapshot | null>` | stable | Load an API snapshot from disk by version. Returns `null` if the snapshot file does not exist. Throws if the file exists but cannot be parsed. |
151
+ | listSnapshots | snapshot-store | `(dir: string) => Promise<string[]>` | stable | List all stored snapshot versions, sorted by semver (with lexicographic fallback). Returns an empty array if the directory does not exist. |
152
+ | DeclarationLocation | source-linker | `interface DeclarationLocation { name: string; type: string; location: SourceLocation; signature?: string; exported: boolean }` | stable | Result of finding a declaration in source code |
153
+ | findDeclarationLocation | source-linker | `(content: string, filePath: string, declarationName: string) => DeclarationLocation | undefined` | stable | Finds the source location of a declaration by name |
154
+ | extractAllDeclarations | source-linker | `(content: string, filePath: string) => DeclarationLocation[]` | stable | Extracts all declarations from a source file |
155
+ | parseReadmeSections | source-linker | `(content: string, filePath: string) => ReadmeSection[]` | stable | Parses README content into sections |
156
+ | linkReadmeToEntries | source-linker | `(sections: ReadmeSection[], entryIds: string[]) => ReadmeSection[]` | stable | Links README sections to codex entry IDs based on heading matching |
157
+ | generateSourceLink | source-linker | `(location: SourceLocation, repoUrl: string, branch?: string) => string` | stable | Generates source links for display (GitHub-style URLs) |
158
+ | toRelativePath | source-linker | `(absolutePath: string, projectRoot: string) => string` | stable | Creates a relative path from project root |
159
+ | enrichSourceLocation | source-linker | `(location: SourceLocation, content: string) => SourceLocation & { context?: string }` | stable | Enriches source locations with additional context |
160
+ | extractTestExamples | test-example-extractor | `(content: string, filePath: string) => TestExample[]` | stable | - |
161
+ | parseVitestOutput | test-example-extractor | `(output: string) => TestStatus[]` | stable | - |
162
+ | matchExamplesToStatuses | test-example-extractor | `(examples: TestExample[], statuses: TestStatus[]) => TestExample[]` | stable | - |
163
+ | generateExampleReport | test-example-extractor | `(examples: TestExample[]) => string` | stable | - |
164
+ | validateTestExamples | test-example-extractor | `(examples: TestExample[]) => { valid: TestExample[]; invalid: TestExample[] }` | experimental | - |
165
+ | extractTestCases | test-pattern-extractor | `(content: string, filePath: string) => ExtractedTestCase[]` | stable | Extracts test cases from a single test file |
166
+ | extractTestImports | test-pattern-extractor | `(content: string) => string[]` | stable | Extracts imports from a test file to identify tested modules |
167
+ | identifyCoveredApis | test-pattern-extractor | `(content: string, knownApis: string[]) => string[]` | stable | Identifies which APIs are likely covered by a test file |
168
+ | analyzeTestFile | test-pattern-extractor | `(content: string, filePath: string, knownApis?: string[]) => TestFileAnalysis` | stable | Analyzes a test file and returns structured analysis |
169
+ | testCasesToReferences | test-pattern-extractor | `(testCases: ExtractedTestCase[], filePath: string) => TestReference[]` | stable | Converts test cases to test references for linking to codex entries |
170
+ | matchTestToEntry | test-pattern-extractor | `(testFilePath: string, entryIds: string[]) => string | undefined` | stable | Matches test files to codex entry IDs based on naming conventions |
171
+ | generateCoverageSummary | test-pattern-extractor | `(analyses: TestFileAnalysis[]) => { totalTests: number; byType: Record<string, number>; coveredApis: string[] }` | stable | Generates a test coverage summary for display |
172
+ | getTestingDefaults | testing-pyramid | `(category: string) => TestingRequirements` | stable | Get the default testing requirements for a given codex category. Returns feature defaults for unknown categories. Returns a fresh copy — callers may mutate freely. |
173
+ | applyRiskModifiers | testing-pyramid | `(base: TestingRequirements, risk: RiskLevel | undefined) => TestingRequirements` | stable | Apply risk-level modifiers to testing requirements. Returns a new object — does not mutate the input. |
174
+ | resolveTestingRequirements | testing-pyramid | `(category: string, risk?: RiskLevel, override?: TestingRequirements) => TestingRequirements` | stable | Resolve the final testing requirements for a codex entry. Merges annotation overrides with category defaults and applies risk modifiers. |
175
+ | computeTestingStatus | testing-pyramid | `(reqs: TestingRequirements) => string` | stable | Compute human-readable testing status from requirements. Returns "Complete" if all required levels are covered, "N/A" if no levels are defined, or "Missing X, Y" listing uncovered levels. |
176
+ | extractTypesFromFile | type-extractor | `(filePath: string, content: string) => ExtractedTypeDefinition[]` | stable | - |
177
+ | getTypeSignature | type-extractor | `(def: ExtractedTypeDefinition) => string` | stable | - |
178
+ | generateMethodTable | type-extractor | `(def: ExtractedTypeDefinition) => string` | stable | - |
179
+ <!-- @codex:end -->
180
+
181
+ ## Examples
182
+
183
+ <!-- @codex:auto examples="pithy.codex.**" -->
184
+ **parseCodexAnnotations**
185
+
186
+ ```typescript
187
+ describe("parseCodexAnnotations", () => {
188
+ it("parses a simple flat annotation", () => {
189
+ const source = `/** ${CX}{ "id": "pithy.signals.signal", "title": "Signal", "category": "feature" } */`;
190
+ const result = parseCodexAnnotations(source);
191
+ result; // → 1
192
+ result[0].id; // → "pithy.signals.signal"
193
+ result[0].title; // → "Signal"
194
+ result[0].category; // → "feature"
195
+ });
196
+
197
+ it("parses annotation with risk field", () => {
198
+ const source = `/** ${CX}{ "id": "pithy.core.html", "title": "HTML", "category": "runtime", "risk": "high" } */`;
199
+ const result = parseCodexAnnotations(source);
200
+ result; // → 1
201
+ result[0].risk; // → "high"
202
+ });
203
+
204
+ it("parses annotation with nested testing field (2 levels of nesting)", () => {
205
+ const source = `/** ${CX}{ "id": "pithy.signals.signal", "title": "Signal", "category": "feature", "testing": { "unit": { "required": true, "covered": true, "file": "signal.test.ts" } } } */`;
206
+ const result = parseCodexAnnotations(source);
207
+ result; // → 1
208
+ result[0].testing; // → defined
209
+ result[0].testing!.unit; // → defined
210
+ result[0].testing!.unit!.required; // → true
211
+ result[0].testing!.unit!.covered; // → true
212
+ result[0].testing!.unit!.file; // → "signal.test.ts"
213
+ });
214
+
215
+ it("parses annotation with multiple nested testing levels", () => {
216
+ const source = `/** ${CX}{ "id": "x", "title": "X", "category": "feature", "testing": { "unit": { "required": true }, "integration": { "required": true }, "e2e": { "required": false } } } */`;
217
+ const result = parseCodexAnnotations(source);
218
+ result; // → 1
219
+ result[0].testing!.unit!.required; // → true
220
+ result[0].testing!.integration!.required; // → true
221
+ result[0].testing!.e2e!.required; // → false
222
+ });
223
+
224
+ it("parses multi-line annotation with nested testing", () => {
225
+ const source = `/**
226
+ * ${CX}
227
+ * { "id": "x", "title": "X", "category": "feature", "risk": "critical", "testing": { "unit": { "required": true, "covered": false } } }
228
+ */`;
229
+ const result = parseCodexAnnotations(source);
230
+ result; // → 1
231
+ result[0].risk; // → "critical"
232
+ result[0].testing!.unit!.required; // → true
233
+ });
234
+
235
+ it("returns empty array for invalid JSON", () => {
236
+ const source = `/** ${CX}{ not valid json } */`;
237
+ const result = parseCodexAnnotations(source);
238
+ result; // → []
239
+ });
240
+
241
+ it("returns empty array for annotation missing required fields", () => {
242
+ const source = `/** ${CX}{ "id": "x" } */`;
243
+ const result = parseCodexAnnotations(source);
244
+ result; // → []
245
+ });
246
+
247
+ it("rejects annotation with invalid risk via validateAnnotation", () => {
248
+ const source = `/** ${CX}{ "id": "x", "title": "X", "category": "feature", "risk": "extreme" } */`;
249
+ const result = parseCodexAnnotations(source);
250
+ result; // → []
251
+ });
252
+
253
+ it("rejects annotation with invalid testing shape via validateAnnotation", () => {
254
+ const source = `/** ${CX}{ "id": "x", "title": "X", "category": "feature", "testing": { "unit": "yes" } } */`;
255
+ const result = parseCodexAnnotations(source);
256
+ result; // → []
257
+ });
258
+
259
+ it("parses multiple annotations in same file", () => {
260
+ const source = `
261
+ /** ${CX}{ "id": "a", "title": "A", "category": "feature" } */
262
+ export function a() {}
263
+
264
+ /** ${CX}{ "id": "b", "title": "B", "category": "runtime" } */
265
+ export function b() {}
266
+ `;
267
+ const result = parseCodexAnnotations(source);
268
+ result; // → 2
269
+ result[0].id; // → "a"
270
+ result[1].id; // → "b"
271
+ });
272
+
273
+ it("parses annotation embedded in surrounding code", () => {
274
+ const source = `
275
+ import { signal } from "./signal.js";
276
+
277
+ /** ${CX}{ "id": "pithy.core.html", "title": "HTML", "category": "runtime" } */
278
+ export function html(template: string) {
279
+ return document.createElement("div");
280
+ }
281
+
282
+ export function other() {}
283
+ `;
284
+ const result = parseCodexAnnotations(source);
285
+ result; // → 1
286
+ result[0].id; // → "pithy.core.html"
287
+ });
288
+
289
+ it("returns empty array for empty content", () => {
290
+ parseCodexAnnotations(""); // → []
291
+ });
292
+ });
293
+ ```
294
+
295
+ **parseCodexApiAnnotations**
296
+
297
+ ```typescript
298
+ describe("parseCodexApiAnnotations", () => {
299
+ it("parses a simple API annotation", () => {
300
+ const source = `/** ${CXA}{"parent":"pithy.signals","name":"signal","stability":"stable","signature":"<T>(v: T) => Signal<T>"} */`;
301
+ const result = parseCodexApiAnnotations(source);
302
+ result; // → 1
303
+ result[0].parent; // → "pithy.signals"
304
+ result[0].name; // → "signal"
305
+ result[0].stability; // → "stable"
306
+ });
307
+
308
+ it("defaults stability to stable", () => {
309
+ const source = `/** ${CXA}{"parent":"x","name":"fn"} */`;
310
+ const result = parseCodexApiAnnotations(source);
311
+ result[0].stability; // → "stable"
312
+ });
313
+
314
+ it("returns empty array for missing required fields", () => {
315
+ const source = `/** ${CXA}{"parent":"x"} */`;
316
+ const result = parseCodexApiAnnotations(source);
317
+ result; // → []
318
+ });
319
+
320
+ it("returns empty array for invalid JSON", () => {
321
+ const source = `/** ${CXA}{ bad json } */`;
322
+ const result = parseCodexApiAnnotations(source);
323
+ result; // → []
324
+ });
325
+
326
+ it("parses multiple API annotations in one file", () => {
327
+ const source = `
328
+ /** ${CXA}{"parent":"pithy.signals","name":"signal","stability":"stable"} */
329
+ export function signal() {}
330
+
331
+ /** ${CXA}{"parent":"pithy.signals","name":"effect","stability":"stable"} */
332
+ export function effect() {}
333
+
334
+ /** ${CXA}{"parent":"pithy.signals","name":"computed","stability":"experimental"} */
335
+ export function computed() {}
336
+ `;
337
+ const result = parseCodexApiAnnotations(source);
338
+ result; // → 3
339
+ result[0].name; // → "signal"
340
+ result[1].name; // → "effect"
341
+ result[2].name; // → "computed"
342
+ result[2].stability; // → "experimental"
343
+ });
344
+
345
+ it("preserves deprecated stability", () => {
346
+ const source = `/** ${CXA}{"parent":"x","name":"old","stability":"deprecated"} */`;
347
+ const result = parseCodexApiAnnotations(source);
348
+ result[0].stability; // → "deprecated"
349
+ });
350
+
351
+ it("returns empty array for empty content", () => {
352
+ parseCodexApiAnnotations(""); // → []
353
+ });
354
+ });
355
+ ```
356
+
357
+ **validateAnnotation**
358
+
359
+ ```typescript
360
+ describe("validateAnnotation", () => {
361
+ it("validates a correct codex annotation", () => {
362
+ validateAnnotation({ id: "x", title: "X", category: "feature" }, "codex"); // → true
363
+ });
364
+
365
+ it("rejects missing required fields", () => {
366
+ validateAnnotation({ id: "x" }, "codex"); // → false
367
+ });
368
+
369
+ it("rejects invalid category", () => {
370
+ validateAnnotation({ id: "x", title: "X", category: "invalid" }, "codex"); // → false
371
+ });
372
+
373
+ it("validates valid risk level", () => {
374
+ validateAnnotation({ id: "x", title: "X", category: "feature", risk: "critical" }, "codex"); // → true
375
+ validateAnnotation({ id: "x", title: "X", category: "feature", risk: "low" }, "codex"); // → true
376
+ });
377
+
378
+ it("rejects invalid risk level", () => {
379
+ validateAnnotation({ id: "x", title: "X", category: "feature", risk: "ultra" }, "codex"); // → false
380
+ });
381
+
382
+ it("validates valid testing field", () => {
383
+ const annotation = {
384
+ id: "x", title: "X", category: "feature",
385
+ testing: { unit: { required: true }, integration: { required: false } },
386
+ };
387
+ validateAnnotation(annotation, "codex"); // → true
388
+ });
389
+
390
+ it("rejects non-object testing field", () => {
391
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: "yes" }, "codex"); // → false
392
+ });
393
+
394
+ it("rejects unknown testing levels", () => {
395
+ const annotation = {
396
+ id: "x", title: "X", category: "feature",
397
+ testing: { performance: { required: true } },
398
+ };
399
+ validateAnnotation(annotation, "codex"); // → false
400
+ });
401
+
402
+ it("validates correct codexApi annotation", () => {
403
+ validateAnnotation({ parent: "x", name: "fn" }, "codexApi"); // → true
404
+ });
405
+
406
+ it("rejects codexApi missing parent", () => {
407
+ validateAnnotation({ name: "fn" }, "codexApi"); // → false
408
+ });
409
+
410
+ it("rejects codexApi missing name", () => {
411
+ validateAnnotation({ parent: "x" }, "codexApi"); // → false
412
+ });
413
+
414
+ it("validates all four risk levels", () => {
415
+ for (const risk of ["critical", "high", "medium", "low"]) {
416
+ validateAnnotation({ id: "x", title: "X", category: "feature", risk }, "codex"); // → true
417
+ }
418
+ });
419
+
420
+ it("rejects testing: null", () => {
421
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: null }, "codex"); // → false
422
+ });
423
+
424
+ it("accepts empty testing object", () => {
425
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: {} }, "codex"); // → true
426
+ });
427
+
428
+ it("rejects non-object testing level value (string)", () => {
429
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: "yes" } }, "codex"); // → false
430
+ });
431
+
432
+ it("rejects testing level with non-boolean required", () => {
433
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: { required: "yes" } } }, "codex"); // → false
434
+ });
435
+
436
+ it("rejects testing level with coverage > 100", () => {
437
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: { coverage: 150 } } }, "codex"); // → false
438
+ });
439
+
440
+ it("rejects testing level with negative coverage", () => {
441
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: { coverage: -1 } } }, "codex"); // → false
442
+ });
443
+
444
+ it("accepts valid testing level with required and coverage", () => {
445
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: { required: true, coverage: 80 } } }, "codex"); // → true
446
+ });
447
+
448
+ it("rejects testing level with non-boolean covered", () => {
449
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: { covered: "yes" } } }, "codex"); // → false
450
+ });
451
+
452
+ it("accepts testing level with boolean covered", () => {
453
+ validateAnnotation({ id: "x", title: "X", category: "feature", testing: { unit: { required: true, covered: true } } }, "codex"); // → true
454
+ });
455
+ });
456
+ ```
457
+
458
+ **normalizeSourceContent**
459
+
460
+ ```typescript
461
+ describe("normalizeSourceContent", () => {
462
+ it("removes block comments", () => {
463
+ const result = normalizeSourceContent("const a = 1; /* comment */ const b = 2;");
464
+ result; // → "comment"
465
+ result; // → "const a = 1;"
466
+ result; // → "const b = 2;"
467
+ });
468
+
469
+ it("removes line comments", () => {
470
+ const result = normalizeSourceContent("const a = 1; // line comment\nconst b = 2;");
471
+ result; // → "line comment"
472
+ result; // → "const a = 1;"
473
+ result; // → "const b = 2;"
474
+ });
475
+
476
+ it("preserves @codex annotations", () => {
477
+ const source = `/** ${CX}{ "id": "x" } */\nconst a = 1;`;
478
+ const result = normalizeSourceContent(source);
479
+ result; // → "ANNOTATION:"
480
+ result; // → "const a = 1;"
481
+ });
482
+
483
+ it("preserves @codexApi annotations", () => {
484
+ const source = `/** ${CXA}{"parent":"x","name":"fn"} */\nconst a = 1;`;
485
+ const result = normalizeSourceContent(source);
486
+ result; // → "ANNOTATION:"
487
+ });
488
+
489
+ it("normalizes whitespace to single spaces", () => {
490
+ const result = normalizeSourceContent("const a = 1;\n\n\n const b = 2;");
491
+ result; // → "const a = 1; const b = 2;"
492
+ });
493
+
494
+ it("trims leading and trailing whitespace", () => {
495
+ const result = normalizeSourceContent(" \n const a = 1; \n ");
496
+ result; // → "const a = 1;"
497
+ });
498
+
499
+ it("normalizes CRLF to LF", () => {
500
+ const lf = normalizeSourceContent("const a = 1;\nconst b = 2;");
501
+ const crlf = normalizeSourceContent("const a = 1;\r\nconst b = 2;");
502
+ crlf; // → lf
503
+ });
504
+
505
+ it("handles empty content", () => {
506
+ normalizeSourceContent(""); // → ""
507
+ });
508
+
509
+ it("handles content with only comments", () => {
510
+ const result = normalizeSourceContent("/* only a comment */\n// and a line comment");
511
+ result; // → ""
512
+ });
513
+ });
514
+ ```
515
+
516
+ **createSourceDigest**
517
+
518
+ ```typescript
519
+ describe("createSourceDigest", () => {
520
+ it("returns a 64-character hex string (SHA-256)", () => {
521
+ const digest = createSourceDigest("const a = 1;");
522
+ digest; // → /^[0-9a-f]{64}$/
523
+ });
524
+
525
+ it("is deterministic (same content produces same digest)", () => {
526
+ const content = "const a = signal(0);";
527
+ createSourceDigest(content); // → createSourceDigest(content)
528
+ });
529
+
530
+ it("produces different digests for different content", () => {
531
+ const d1 = createSourceDigest("const a = 1;");
532
+ const d2 = createSourceDigest("const b = 2;");
533
+ d1; // → d2
534
+ });
535
+
536
+ it("produces same digest for CRLF vs LF", () => {
537
+ const lf = createSourceDigest("const a = 1;\nconst b = 2;");
538
+ const crlf = createSourceDigest("const a = 1;\r\nconst b = 2;");
539
+ crlf; // → lf
540
+ });
541
+
542
+ it("ignores comment differences", () => {
543
+ const withComment = createSourceDigest("const a = 1; /* hello */");
544
+ const withoutComment = createSourceDigest("const a = 1;");
545
+ withComment; // → withoutComment
546
+ });
547
+
548
+ it("handles empty content", () => {
549
+ const digest = createSourceDigest("");
550
+ digest; // → /^[0-9a-f]{64}$/
551
+ });
552
+ });
553
+ ```
554
+
555
+ **createCanonicalFingerprint**
556
+
557
+ ```typescript
558
+ describe("createCanonicalFingerprint", () => {
559
+ const baseComponent: DiscoveredComponent = {
560
+ id: "pithy.signals.signal",
561
+ title: "Signal",
562
+ category: "feature",
563
+ apis: [
564
+ { parent: "pithy.signals", name: "signal", stability: "stable" },
565
+ { parent: "pithy.signals", name: "effect", stability: "stable" },
566
+ ],
567
+ files: ["signal.ts"],
568
+ sourceDigest: "abc123",
569
+ };
570
+
571
+ it("returns a 64-character hex string (SHA-256)", () => {
572
+ const fp = createCanonicalFingerprint(baseComponent);
573
+ fp; // → /^[0-9a-f]{64}$/
574
+ });
575
+
576
+ it("is deterministic", () => {
577
+ const fp1 = createCanonicalFingerprint(baseComponent);
578
+ const fp2 = createCanonicalFingerprint(baseComponent);
579
+ fp1; // → fp2
580
+ });
581
+
582
+ it("produces same fingerprint regardless of API order", () => {
583
+ const reversed: DiscoveredComponent = {
584
+ ...baseComponent,
585
+ apis: [
586
+ { parent: "pithy.signals", name: "effect", stability: "stable" },
587
+ { parent: "pithy.signals", name: "signal", stability: "stable" },
588
+ ],
589
+ };
590
+ createCanonicalFingerprint(baseComponent); // → createCanonicalFingerprint(reversed)
591
+ });
592
+
593
+ it("produces different fingerprints for different components", () => {
594
+ const other: DiscoveredComponent = {
595
+ ...baseComponent,
596
+ id: "pithy.signals.computed",
597
+ title: "Computed",
598
+ };
599
+ createCanonicalFingerprint(baseComponent); // → createCanonicalFingerprint(other)
600
+ });
601
+
602
+ it("produces different fingerprints when sourceDigest differs", () => {
603
+ const other: DiscoveredComponent = { ...baseComponent, sourceDigest: "different" };
604
+ createCanonicalFingerprint(baseComponent); // → createCanonicalFingerprint(other)
605
+ });
606
+
607
+ it("ignores files array (not part of canonical form)", () => {
608
+ const other: DiscoveredComponent = { ...baseComponent, files: ["other.ts", "extra.ts"] };
609
+ createCanonicalFingerprint(baseComponent); // → createCanonicalFingerprint(other)
610
+ });
611
+ });
612
+ ```
613
+
614
+ **discoverComponentsFromFile**
615
+
616
+ ```typescript
617
+ describe("discoverComponentsFromFile", () => {
618
+ it("discovers a component with matching API annotations", async () => {
619
+ const source = `
620
+ /** ${CX}{ "id": "pithy.signals", "title": "Signals", "category": "feature" } */
621
+
622
+ /** ${CXA}{"parent":"pithy.signals","name":"signal","stability":"stable"} */
623
+ export function signal() {}
624
+
625
+ /** ${CXA}{"parent":"pithy.signals","name":"effect","stability":"stable"} */
626
+ export function effect() {}
627
+ `;
628
+ const result = await discoverComponentsFromFile("signals.ts", source);
629
+ result; // → 1
630
+ result[0].id; // → "pithy.signals"
631
+ result[0].title; // → "Signals"
632
+ result[0].category; // → "feature"
633
+ result[0].apis; // → 2
634
+ result[0].apis[0].name; // → "signal"
635
+ result[0].apis[1].name; // → "effect"
636
+ result[0].files; // → ["signals.ts"]
637
+ result[0].sourceDigest; // → /^[0-9a-f]{64}$/
638
+ });
639
+
640
+ it("discovers multiple components from one file", async () => {
641
+ const source = `
642
+ /** ${CX}{ "id": "a", "title": "A", "category": "feature" } */
643
+ /** ${CX}{ "id": "b", "title": "B", "category": "runtime" } */
644
+ `;
645
+ const result = await discoverComponentsFromFile("multi.ts", source);
646
+ result; // → 2
647
+ result[0].id; // → "a"
648
+ result[1].id; // → "b"
649
+ });
650
+
651
+ it("assigns APIs only to their parent component", async () => {
652
+ const source = `
653
+ /** ${CX}{ "id": "comp-a", "title": "A", "category": "feature" } */
654
+ /** ${CX}{ "id": "comp-b", "title": "B", "category": "runtime" } */
655
+
656
+ /** ${CXA}{"parent":"comp-a","name":"fn1"} */
657
+ /** ${CXA}{"parent":"comp-b","name":"fn2"} */
658
+ /** ${CXA}{"parent":"comp-b","name":"fn3"} */
659
+ `;
660
+ const result = await discoverComponentsFromFile("multi.ts", source);
661
+ result[0].apis; // → 1
662
+ result[0].apis[0].name; // → "fn1"
663
+ result[1].apis; // → 2
664
+ result[1].apis[0].name; // → "fn2"
665
+ });
666
+
667
+ it("returns empty array for content with no annotations", async () => {
668
+ const result = await discoverComponentsFromFile("plain.ts", "const a = 1;");
669
+ result; // → []
670
+ });
671
+
672
+ it("returns empty array for empty content", async () => {
673
+ const result = await discoverComponentsFromFile("empty.ts", "");
674
+ result; // → []
675
+ });
676
+
677
+ it("returns empty APIs when no @codexApi matches component", async () => {
678
+ const source = `
679
+ /** ${CX}{ "id": "lonely", "title": "Lonely", "category": "feature" } */
680
+ /** ${CXA}{"parent":"other-component","name":"fn"} */
681
+ `;
682
+ const result = await discoverComponentsFromFile("lonely.ts", source);
683
+ result; // → 1
684
+ result[0].apis; // → []
685
+ });
686
+
687
+ it("carries risk and testing fields from annotation to component", async () => {
688
+ const source = `
689
+ /** ${CX}{ "id": "pithy.core.html", "title": "HTML", "category": "runtime", "risk": "high", "testing": { "unit": { "required": true } } } */
690
+ `;
691
+ const result = await discoverComponentsFromFile("html.ts", source);
692
+ result; // → 1
693
+ result[0].risk; // → "high"
694
+ result[0].testing; // → { unit: { required: true } }
695
+ });
696
+
697
+ it("leaves risk and testing undefined when annotation omits them", async () => {
698
+ const source = `
699
+ /** ${CX}{ "id": "pithy.core.simple", "title": "Simple", "category": "feature" } */
700
+ `;
701
+ const result = await discoverComponentsFromFile("simple.ts", source);
702
+ result; // → 1
703
+ result[0].risk; // → undefined
704
+ result[0].testing; // → undefined
705
+ });
706
+ });
707
+ ```
708
+
709
+ **parseArgs**
710
+
711
+ ```typescript
712
+ describe("parseArgs", () => {
713
+ it("defaults to help command with no args", () => {
714
+ const opts = parseArgs([]);
715
+ opts.command; // → "help"
716
+ });
717
+
718
+ it("parses save command with version", () => {
719
+ const opts = parseArgs(["save", "--version", "1.0.0"]);
720
+ opts.command; // → "save"
721
+ opts.version; // → "1.0.0"
722
+ });
723
+
724
+ it("parses diff command with from and to", () => {
725
+ const opts = parseArgs(["diff", "--from", "1.0.0", "--to", "2.0.0"]);
726
+ opts.command; // → "diff"
727
+ opts.fromVersion; // → "1.0.0"
728
+ opts.toVersion; // → "2.0.0"
729
+ });
730
+
731
+ it("parses list command", () => {
732
+ const opts = parseArgs(["list"]);
733
+ opts.command; // → "list"
734
+ });
735
+
736
+ it("parses verbose flag", () => {
737
+ const opts = parseArgs(["save", "-v", "--version", "1.0.0"]);
738
+ opts.verbose; // → true
739
+ });
740
+
741
+ it("parses --help flag overriding command", () => {
742
+ const opts = parseArgs(["save", "--help"]);
743
+ opts.command; // → "help"
744
+ });
745
+
746
+ it("parses custom root and dir", () => {
747
+ const opts = parseArgs(["list", "--root", "/my/project", "--dir", "versions"]);
748
+ opts.root; // → "/my/project"
749
+ opts.versionsDir; // → "versions"
750
+ });
751
+
752
+ it("throws when --root has no value", () => {
753
+ expect(() => parseArgs(["list", "--root"])).toThrow(/Missing value for --root/);
754
+ });
755
+
756
+ it("throws when --version value looks like a flag", () => {
757
+ expect(() => parseArgs(["save", "--version", "--from"])).toThrow(/Missing value for --version/);
758
+ });
759
+
760
+ it("throws when --from has no value", () => {
761
+ expect(() => parseArgs(["diff", "--from"])).toThrow(/Missing value for --from/);
762
+ });
763
+
764
+ it("throws when --dir value looks like a flag", () => {
765
+ expect(() => parseArgs(["list", "--dir", "--verbose"])).toThrow(/Missing value for --dir/);
766
+ });
767
+ });
768
+ ```
769
+
770
+ **validatePath**
771
+
772
+ ```typescript
773
+ describe("validatePath", () => {
774
+ it("accepts a relative path within the base", () => {
775
+ const result = validatePath("/project", "codex.versions");
776
+ result; // → resolve("/project", "codex.versions")
777
+ });
778
+
779
+ it("accepts a nested relative path", () => {
780
+ const result = validatePath("/project", "data/versions");
781
+ result; // → resolve("/project", "data/versions")
782
+ });
783
+
784
+ it("rejects a path that escapes the base via ..", () => {
785
+ expect(() => validatePath("/project", "../../etc/passwd")).toThrow(/Security error/);
786
+ });
787
+
788
+ it("rejects an absolute path outside the base", () => {
789
+ expect(() => validatePath("/project", "/etc/passwd")).toThrow(/Security error/);
790
+ });
791
+
792
+ it("accepts an absolute path within the base", () => {
793
+ const result = validatePath("/project", resolve("/project", "codex.versions"));
794
+ result; // → resolve("/project", "codex.versions")
795
+ });
796
+
797
+ it("accepts a directory name starting with .. (e.g. ..foo)", () => {
798
+ const result = validatePath("/project", "..foo");
799
+ result; // → resolve("/project", "..foo")
800
+ });
801
+ });
802
+ ```
803
+
804
+ **consumeValue**
805
+
806
+ ```typescript
807
+ describe("consumeValue", () => {
808
+ it("returns the value at the given index", () => {
809
+ consumeValue(["--root", "/foo"], 1, "--root"); // → "/foo"
810
+ });
811
+
812
+ it("throws for undefined value (past end of array)", () => {
813
+ expect(() => consumeValue(["--root"], 1, "--root")).toThrow(/Missing value/);
814
+ });
815
+
816
+ it("throws for flag-like value", () => {
817
+ expect(() => consumeValue(["--root", "--dir"], 1, "--root")).toThrow(/Missing value/);
818
+ });
819
+ });
820
+ ```
821
+
822
+ **allows paths within the base directory**
823
+
824
+ ```typescript
825
+ const result = validatePath(base, 'src/file.ts');
826
+ result; // → join(base, 'src/file.ts')
827
+ ```
828
+
829
+ **rejects path traversal attempts**
830
+
831
+ ```typescript
832
+ expect(() => validatePath(base, '../../../etc/passwd')).toThrow(
833
+ 'Security error'
834
+ );
835
+ ```
836
+
837
+ **validateExampleSyntax**
838
+
839
+ ```typescript
840
+ describe("validateExampleSyntax", () => {
841
+ it("accepts valid TypeScript code", () => {
842
+ const result = validateExampleSyntax(
843
+ 'const x: number = 42;\nconsole.log(x);',
844
+ "test.component"
845
+ );
846
+ result.valid; // → true
847
+ result.diagnostics; // → 0
848
+ });
849
+
850
+ it("detects syntax errors in TypeScript", () => {
851
+ const result = validateExampleSyntax(
852
+ "const x: number = ;",
853
+ "test.component"
854
+ );
855
+ result.valid; // → false
856
+ expect(result.diagnostics.length).toBeGreaterThan(0);
857
+ result.diagnostics[0]; // → "Line"
858
+ });
859
+
860
+ it("accepts valid JavaScript code", () => {
861
+ const result = validateExampleSyntax(
862
+ 'const x = 42;\nconsole.log(x);',
863
+ "test.component",
864
+ undefined,
865
+ "javascript"
866
+ );
867
+ result.valid; // → true
868
+ });
869
+
870
+ it("treats unknown languages as valid (skip)", () => {
871
+ const result = validateExampleSyntax(
872
+ "this is not code at all {}[]",
873
+ "test.component",
874
+ undefined,
875
+ "markdown"
876
+ );
877
+ result.valid; // → true
878
+ result.diagnostics; // → 0
879
+ });
880
+
881
+ it("preserves componentId and apiName in result", () => {
882
+ const result = validateExampleSyntax(
883
+ "const x = 1;",
884
+ "my.component",
885
+ "myApi"
886
+ );
887
+ result.componentId; // → "my.component"
888
+ result.apiName; // → "myApi"
889
+ });
890
+
891
+ it("handles empty code string", () => {
892
+ const result = validateExampleSyntax("", "test.component");
893
+ result.valid; // → true
894
+ });
895
+
896
+ it("handles multiline code with imports", () => {
897
+ const code = `
898
+ import { signal } from '@pithyjs/signals';
899
+
900
+ const count = signal(0);
901
+ const doubled = computed(() => count() * 2);
902
+ console.log(doubled());
903
+ `;
904
+ const result = validateExampleSyntax(code, "test.component");
905
+ result.valid; // → true
906
+ });
907
+
908
+ it("detects multiple syntax errors", () => {
909
+ const code = "const x = ;\nconst y = ;";
910
+ const result = validateExampleSyntax(code, "test.component");
911
+ result.valid; // → false
912
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
913
+ });
914
+
915
+ it("accepts async/await syntax", () => {
916
+ const code = `
917
+ async function fetchData() {
918
+ const res = await fetch('/api');
919
+ return res.json();
920
+ }
921
+ `;
922
+ const result = validateExampleSyntax(code, "test.component");
923
+ result.valid; // → true
924
+ });
925
+
926
+ it("handles 'js' as a language alias", () => {
927
+ const result = validateExampleSyntax(
928
+ "const x = 1;",
929
+ "test.component",
930
+ undefined,
931
+ "js"
932
+ );
933
+ result.valid; // → true
934
+ });
935
+
936
+ it("handles 'ts' as a language alias", () => {
937
+ const result = validateExampleSyntax(
938
+ "const x: string = 'hello';",
939
+ "test.component",
940
+ undefined,
941
+ "ts"
942
+ );
943
+ result.valid; // → true
944
+ });
945
+
946
+ it("handles code with Unicode characters", () => {
947
+ const result = validateExampleSyntax(
948
+ 'const msg = "Hello 😀 World";',
949
+ "test.component"
950
+ );
951
+ result.valid; // → true
952
+ });
953
+
954
+ it("accepts type-only errors as valid (only checks syntax)", () => {
955
+ // Type error: assigning string to number — but syntactically valid
956
+ const result = validateExampleSyntax(
957
+ 'const x: number = "not a number";',
958
+ "test.component"
959
+ );
960
+ result.valid; // → true
961
+ });
962
+ });
963
+ ```
964
+
965
+ **validateExtractionResult**
966
+
967
+ ```typescript
968
+ describe("validateExtractionResult", () => {
969
+ it("returns zero counts for empty extraction", () => {
970
+ const result = validateExtractionResult(makeExtractionResult());
971
+ result.totalExamples; // → 0
972
+ result.validCount; // → 0
973
+ result.invalidCount; // → 0
974
+ result.skippedCount; // → 0
975
+ result.invalidExamples; // → 0
976
+ });
977
+
978
+ it("validates component-level examples", () => {
979
+ const extraction = makeExtractionResult({
980
+ components: [
981
+ {
982
+ id: "test.comp",
983
+ title: "Test",
984
+ category: "feature",
985
+ apis: [],
986
+ examples: [
987
+ {
988
+ code: "const x = 42;",
989
+ language: "typescript",
990
+ runnable: true,
991
+ isDoctest: false,
992
+ location: { file: "test.ts", line: 1 },
993
+ },
994
+ ],
995
+ testRefs: [],
996
+ sourceFiles: [{ file: "test.ts", line: 1 }],
997
+ },
998
+ ],
999
+ });
1000
+
1001
+ const result = validateExtractionResult(extraction);
1002
+ result.totalExamples; // → 1
1003
+ result.validCount; // → 1
1004
+ result.invalidCount; // → 0
1005
+ });
1006
+
1007
+ it("validates API-level examples", () => {
1008
+ const extraction = makeExtractionResult({
1009
+ components: [
1010
+ {
1011
+ id: "test.comp",
1012
+ title: "Test",
1013
+ category: "feature",
1014
+ apis: [
1015
+ {
1016
+ id: "test.comp.myFn",
1017
+ name: "myFn",
1018
+ parent: "test.comp",
1019
+ stability: "stable",
1020
+ examples: [
1021
+ {
1022
+ code: "const x: number = ;",
1023
+ language: "typescript",
1024
+ runnable: true,
1025
+ isDoctest: false,
1026
+ location: { file: "test.ts", line: 1 },
1027
+ },
1028
+ ],
1029
+ testRefs: [],
1030
+ sourceLocation: { file: "test.ts", line: 1 },
1031
+ },
1032
+ ],
1033
+ examples: [],
1034
+ testRefs: [],
1035
+ sourceFiles: [{ file: "test.ts", line: 1 }],
1036
+ },
1037
+ ],
1038
+ });
1039
+
1040
+ const result = validateExtractionResult(extraction);
1041
+ result.totalExamples; // → 1
1042
+ result.invalidCount; // → 1
1043
+ result.invalidExamples; // → 1
1044
+ result.invalidExamples[0].apiName; // → "myFn"
1045
+ });
1046
+
1047
+ it("skips non-validatable languages", () => {
1048
+ const extraction = makeExtractionResult({
1049
+ components: [
1050
+ {
1051
+ id: "test.comp",
1052
+ title: "Test",
1053
+ category: "feature",
1054
+ apis: [],
1055
+ examples: [
1056
+ {
1057
+ code: "npm install something",
1058
+ language: "bash",
1059
+ runnable: false,
1060
+ isDoctest: false,
1061
+ location: { file: "test.ts", line: 1 },
1062
+ },
1063
+ ],
1064
+ testRefs: [],
1065
+ sourceFiles: [{ file: "test.ts", line: 1 }],
1066
+ },
1067
+ ],
1068
+ });
1069
+
1070
+ const result = validateExtractionResult(extraction);
1071
+ result.totalExamples; // → 1
1072
+ result.skippedCount; // → 1
1073
+ result.validCount; // → 0
1074
+ result.invalidCount; // → 0
1075
+ });
1076
+
1077
+ it("handles mix of valid, invalid, and skipped", () => {
1078
+ const extraction = makeExtractionResult({
1079
+ components: [
1080
+ {
1081
+ id: "test.comp",
1082
+ title: "Test",
1083
+ category: "feature",
1084
+ apis: [],
1085
+ examples: [
1086
+ {
1087
+ code: "const x = 42;",
1088
+ language: "typescript",
1089
+ runnable: true,
1090
+ isDoctest: false,
1091
+ location: { file: "test.ts", line: 1 },
1092
+ },
1093
+ {
1094
+ code: "const y = ;",
1095
+ language: "typescript",
1096
+ runnable: true,
1097
+ isDoctest: false,
1098
+ location: { file: "test.ts", line: 5 },
1099
+ },
1100
+ {
1101
+ code: "echo hello",
1102
+ language: "bash",
1103
+ runnable: false,
1104
+ isDoctest: false,
1105
+ location: { file: "test.ts", line: 10 },
1106
+ },
1107
+ ],
1108
+ testRefs: [],
1109
+ sourceFiles: [{ file: "test.ts", line: 1 }],
1110
+ },
1111
+ ],
1112
+ });
1113
+
1114
+ const result = validateExtractionResult(extraction);
1115
+ result.totalExamples; // → 3
1116
+ result.validCount; // → 1
1117
+ result.invalidCount; // → 1
1118
+ result.skippedCount; // → 1
1119
+ });
1120
+
1121
+ it("records totalTimeMs", () => {
1122
+ const result = validateExtractionResult(makeExtractionResult());
1123
+ expect(result.totalTimeMs).toBeGreaterThanOrEqual(0);
1124
+ });
1125
+ });
1126
+ ```
1127
+
1128
+ **formatValidationReport**
1129
+
1130
+ ```typescript
1131
+ describe("formatValidationReport", () => {
1132
+ it("formats empty result", () => {
1133
+ const report = formatValidationReport({
1134
+ totalExamples: 0,
1135
+ validCount: 0,
1136
+ invalidCount: 0,
1137
+ skippedCount: 0,
1138
+ invalidExamples: [],
1139
+ totalTimeMs: 5,
1140
+ });
1141
+ report; // → "Total examples: 0"
1142
+ report; // → "✅ Valid: 0"
1143
+ report; // → "❌ Invalid: 0"
1144
+ });
1145
+
1146
+ it("formats result with invalid examples", () => {
1147
+ const report = formatValidationReport({
1148
+ totalExamples: 3,
1149
+ validCount: 1,
1150
+ invalidCount: 1,
1151
+ skippedCount: 1,
1152
+ invalidExamples: [
1153
+ {
1154
+ code: "const x = ;",
1155
+ componentId: "test.comp",
1156
+ apiName: "myFn",
1157
+ valid: false,
1158
+ diagnostics: ["Line 1: Expression expected"],
1159
+ },
1160
+ ],
1161
+ totalTimeMs: 42,
1162
+ });
1163
+ report; // → "Total examples: 3"
1164
+ report; // → "Invalid Examples:"
1165
+ report; // → "test.comp.myFn"
1166
+ report; // → "Expression expected"
1167
+ });
1168
+
1169
+ it("shows component ID without apiName", () => {
1170
+ const report = formatValidationReport({
1171
+ totalExamples: 1,
1172
+ validCount: 0,
1173
+ invalidCount: 1,
1174
+ skippedCount: 0,
1175
+ invalidExamples: [
1176
+ {
1177
+ code: "bad code;",
1178
+ componentId: "test.comp",
1179
+ valid: false,
1180
+ diagnostics: ["Line 1: Error"],
1181
+ },
1182
+ ],
1183
+ totalTimeMs: 10,
1184
+ });
1185
+ report; // → "test.comp"
1186
+ report; // → "test.comp."
1187
+ });
1188
+
1189
+ it("includes timing information", () => {
1190
+ const report = formatValidationReport({
1191
+ totalExamples: 0,
1192
+ validCount: 0,
1193
+ invalidCount: 0,
1194
+ skippedCount: 0,
1195
+ invalidExamples: [],
1196
+ totalTimeMs: 123,
1197
+ });
1198
+ report; // → "123ms"
1199
+ });
1200
+ });
1201
+ ```
1202
+
1203
+ **createApiSnapshot**
1204
+
1205
+ ```typescript
1206
+ describe("createApiSnapshot", () => {
1207
+ it("creates a snapshot from extraction results", () => {
1208
+ const snap = makeSnapshot("1.0.0", [
1209
+ { name: "signal", signature: "<T>(initial: T) => Signal<T>" },
1210
+ { name: "effect", signature: "(fn: () => void) => () => void" },
1211
+ ]);
1212
+
1213
+ snap.version; // → "1.0.0"
1214
+ snap.entries; // → 2
1215
+ snap.entries[0].name; // → "signal"
1216
+ snap.entries[0].signature; // → "<T>(initial: T) => Signal<T>"
1217
+ snap.entries[1].name; // → "effect"
1218
+ snap.timestamp; // → /^\d{4}-\d{2}-\d{2}/
1219
+ });
1220
+
1221
+ it("captures stability for each API entry", () => {
1222
+ const snap = makeSnapshot("1.0.0", [
1223
+ { name: "oldFn", stability: "deprecated", signature: "() => void" },
1224
+ { name: "newFn", stability: "experimental", signature: "() => void" },
1225
+ ]);
1226
+
1227
+ snap.entries[0].stability; // → "deprecated"
1228
+ snap.entries[1].stability; // → "experimental"
1229
+ });
1230
+
1231
+ it("creates an empty snapshot when no APIs exist", () => {
1232
+ const snap = makeSnapshot("0.0.1", []);
1233
+ snap.entries; // → 0
1234
+ snap.version; // → "0.0.1"
1235
+ });
1236
+
1237
+ it("preserves parent component id for each entry", () => {
1238
+ const snap = makeSnapshot("1.0.0", [{ name: "fn", signature: "()" }]);
1239
+ snap.entries[0].parent; // → "test"
1240
+ });
1241
+ });
1242
+ ```
1243
+
1244
+ **snapshotFromTypeDefinitions**
1245
+
1246
+ ```typescript
1247
+ describe("snapshotFromTypeDefinitions", () => {
1248
+ it("creates entries from exported type definitions", () => {
1249
+ const types: ExtractedTypeDefinition[] = [
1250
+ {
1251
+ name: "Signal",
1252
+ kind: "interface",
1253
+ exported: true,
1254
+ members: [
1255
+ { name: "set", type: "method", optional: false, readonly: false },
1256
+ { name: "subscribe", type: "method", optional: false, readonly: false },
1257
+ ],
1258
+ location: loc,
1259
+ },
1260
+ ];
1261
+
1262
+ const snap = snapshotFromTypeDefinitions(types, "1.0.0");
1263
+ snap.version; // → "1.0.0"
1264
+ snap.entries; // → 1
1265
+ snap.entries[0].name; // → "Signal"
1266
+ snap.entries[0].kind; // → "interface"
1267
+ snap.entries[0].signature; // → "export interface Signal"
1268
+ });
1269
+
1270
+ it("skips non-exported type definitions", () => {
1271
+ const types: ExtractedTypeDefinition[] = [
1272
+ {
1273
+ name: "Internal",
1274
+ kind: "interface",
1275
+ exported: false,
1276
+ members: [],
1277
+ location: loc,
1278
+ },
1279
+ ];
1280
+
1281
+ const snap = snapshotFromTypeDefinitions(types, "1.0.0");
1282
+ snap.entries; // → 0
1283
+ });
1284
+
1285
+ it("captures member signatures for interfaces", () => {
1286
+ const types: ExtractedTypeDefinition[] = [
1287
+ {
1288
+ name: "Router",
1289
+ kind: "interface",
1290
+ exported: true,
1291
+ members: [
1292
+ {
1293
+ name: "navigate",
1294
+ type: "method",
1295
+ optional: false,
1296
+ readonly: false,
1297
+ signature: "(path: string) => void",
1298
+ },
1299
+ ],
1300
+ location: loc,
1301
+ },
1302
+ ];
1303
+
1304
+ const snap = snapshotFromTypeDefinitions(types, "1.0.0");
1305
+ snap.entries[0].members; // → 1
1306
+ snap.entries[0].members![0].name; // → "navigate"
1307
+ });
1308
+ });
1309
+ ```
1310
+
1311
+ **diffSnapshots**
1312
+
1313
+ ```typescript
1314
+ describe("diffSnapshots", () => {
1315
+ it("detects added APIs", () => {
1316
+ const before = makeSnapshot("1.0.0", [{ name: "signal", signature: "()" }]);
1317
+ const after = makeSnapshot("2.0.0", [
1318
+ { name: "signal", signature: "()" },
1319
+ { name: "effect", signature: "(fn: () => void) => void" },
1320
+ ]);
1321
+
1322
+ const diff = diffSnapshots(before, after);
1323
+ diff.added; // → 1
1324
+ diff.added[0].name; // → "effect"
1325
+ diff.removed; // → 0
1326
+ diff.changed; // → 0
1327
+ });
1328
+
1329
+ it("detects removed APIs", () => {
1330
+ const before = makeSnapshot("1.0.0", [
1331
+ { name: "signal", signature: "()" },
1332
+ { name: "obsolete", signature: "()" },
1333
+ ]);
1334
+ const after = makeSnapshot("2.0.0", [{ name: "signal", signature: "()" }]);
1335
+
1336
+ const diff = diffSnapshots(before, after);
1337
+ diff.removed; // → 1
1338
+ diff.removed[0].name; // → "obsolete"
1339
+ diff.added; // → 0
1340
+ });
1341
+
1342
+ it("detects signature changes", () => {
1343
+ const before = makeSnapshot("1.0.0", [
1344
+ { name: "signal", signature: "<T>(v: T) => Signal<T>" },
1345
+ ]);
1346
+ const after = makeSnapshot("2.0.0", [
1347
+ { name: "signal", signature: "<T>(v: T, opts?: Options) => Signal<T>" },
1348
+ ]);
1349
+
1350
+ const diff = diffSnapshots(before, after);
1351
+ diff.changed; // → 1
1352
+ diff.changed[0].name; // → "signal"
1353
+ diff.changed[0].changeKind; // → "signature-changed"
1354
+ expect(diff.changed[0].before?.signature).toBe(
1355
+ "<T>(v: T) => Signal<T>",
1356
+ );
1357
+ expect(diff.changed[0].after?.signature).toBe(
1358
+ "<T>(v: T, opts?: Options) => Signal<T>",
1359
+ );
1360
+ });
1361
+
1362
+ it("detects stability changes", () => {
1363
+ const before = makeSnapshot("1.0.0", [
1364
+ { name: "fn", stability: "stable", signature: "()" },
1365
+ ]);
1366
+ const after = makeSnapshot("2.0.0", [
1367
+ { name: "fn", stability: "deprecated", signature: "()" },
1368
+ ]);
1369
+
1370
+ const diff = diffSnapshots(before, after);
1371
+ diff.changed; // → 1
1372
+ diff.changed[0].changeKind; // → "stability-changed"
1373
+ });
1374
+
1375
+ it("reports unchanged APIs count", () => {
1376
+ const before = makeSnapshot("1.0.0", [
1377
+ { name: "signal", signature: "()" },
1378
+ { name: "effect", signature: "(fn: () => void) => void" },
1379
+ ]);
1380
+ const after = makeSnapshot("2.0.0", [
1381
+ { name: "signal", signature: "()" },
1382
+ { name: "effect", signature: "(fn: () => void) => void" },
1383
+ ]);
1384
+
1385
+ const diff = diffSnapshots(before, after);
1386
+ diff.unchanged; // → 2
1387
+ diff.added; // → 0
1388
+ diff.removed; // → 0
1389
+ diff.changed; // → 0
1390
+ });
1391
+
1392
+ it("handles empty snapshots", () => {
1393
+ const empty = makeSnapshot("0.0.0", []);
1394
+ const filled = makeSnapshot("1.0.0", [
1395
+ { name: "signal", signature: "()" },
1396
+ ]);
1397
+
1398
+ const diff = diffSnapshots(empty, filled);
1399
+ diff.added; // → 1
1400
+ diff.removed; // → 0
1401
+ });
1402
+
1403
+ it("handles both snapshots empty", () => {
1404
+ const diff = diffSnapshots(
1405
+ makeSnapshot("0.0.0", []),
1406
+ makeSnapshot("1.0.0", []),
1407
+ );
1408
+ diff.added; // → 0
1409
+ diff.removed; // → 0
1410
+ diff.changed; // → 0
1411
+ diff.unchanged; // → 0
1412
+ });
1413
+
1414
+ it("uses qualified name (parent.name) for matching", () => {
1415
+ const before = createApiSnapshot(
1416
+ {
1417
+ components: [
1418
+ makeComponent("signals", [
1419
+ makeApi({ name: "signal", signature: "()" }),
1420
+ ]),
1421
+ makeComponent("router", [
1422
+ makeApi({ name: "signal", signature: "(path: string) => void" }),
1423
+ ]),
1424
+ ],
1425
+ testAnalysis: [],
1426
+ readmeSections: [],
1427
+ stats: {
1428
+ filesScanned: 0,
1429
+ componentsFound: 2,
1430
+ apisExtracted: 2,
1431
+ examplesExtracted: 0,
1432
+ testsAnalyzed: 0,
1433
+ readmeSectionsLinked: 0,
1434
+ processingTimeMs: 0,
1435
+ },
1436
+ typeDefinitions: [],
1437
+ testExamples: [],
1438
+ testStatuses: [],
1439
+ },
1440
+ "1.0.0",
1441
+ );
1442
+
1443
+ const after = createApiSnapshot(
1444
+ {
1445
+ components: [
1446
+ makeComponent("signals", [
1447
+ makeApi({ name: "signal", signature: "(changed: boolean) => void" }),
1448
+ ]),
1449
+ makeComponent("router", [
1450
+ makeApi({ name: "signal", signature: "(path: string) => void" }),
1451
+ ]),
1452
+ ],
1453
+ testAnalysis: [],
1454
+ readmeSections: [],
1455
+ stats: {
1456
+ filesScanned: 0,
1457
+ componentsFound: 2,
1458
+ apisExtracted: 2,
1459
+ examplesExtracted: 0,
1460
+ testsAnalyzed: 0,
1461
+ readmeSectionsLinked: 0,
1462
+ processingTimeMs: 0,
1463
+ },
1464
+ typeDefinitions: [],
1465
+ testExamples: [],
1466
+ testStatuses: [],
1467
+ },
1468
+ "2.0.0",
1469
+ );
1470
+
1471
+ const diff = diffSnapshots(before, after);
1472
+ // Only the signals.signal changed, router.signal stayed
1473
+ diff.changed; // → 1
1474
+ diff.changed[0].name; // → "signal"
1475
+ diff.changed[0].parent; // → "signals"
1476
+ diff.unchanged; // → 1
1477
+ });
1478
+
1479
+ it("detects combined signature and stability changes", () => {
1480
+ const before = makeSnapshot("1.0.0", [
1481
+ { name: "fn", stability: "stable", signature: "(a: number) => void" },
1482
+ ]);
1483
+ const after = makeSnapshot("2.0.0", [
1484
+ {
1485
+ name: "fn",
1486
+ stability: "deprecated",
1487
+ signature: "(a: number, b: string) => void",
1488
+ },
1489
+ ]);
1490
+
1491
+ const diff = diffSnapshots(before, after);
1492
+ // Both changes detected; signature change takes priority
1493
+ diff.changed; // → 1
1494
+ diff.changed[0].changeKind; // → "signature-changed"
1495
+ });
1496
+ });
1497
+ ```
1498
+
1499
+ **classifyChange**
1500
+
1501
+ ```typescript
1502
+ describe("classifyChange", () => {
1503
+ it("classifies removal as major (breaking)", () => {
1504
+ const change: ApiChange = {
1505
+ name: "obsolete",
1506
+ parent: "test",
1507
+ qualifiedName: "test.obsolete",
1508
+ changeKind: "removed",
1509
+ before: { name: "obsolete", signature: "()", stability: "stable", parent: "test", qualifiedName: "test.obsolete" },
1510
+ };
1511
+ classifyChange(change); // → "major"
1512
+ });
1513
+
1514
+ it("classifies signature change as major (breaking)", () => {
1515
+ const change: ApiChange = {
1516
+ name: "signal",
1517
+ parent: "test",
1518
+ qualifiedName: "test.signal",
1519
+ changeKind: "signature-changed",
1520
+ before: { name: "signal", signature: "(a: T) => S", stability: "stable", parent: "test", qualifiedName: "test.signal" },
1521
+ after: { name: "signal", signature: "(a: T, b: U) => S", stability: "stable", parent: "test", qualifiedName: "test.signal" },
1522
+ };
1523
+ classifyChange(change); // → "major"
1524
+ });
1525
+
1526
+ it("classifies addition as minor (non-breaking)", () => {
1527
+ const change: ApiChange = {
1528
+ name: "effect",
1529
+ parent: "test",
1530
+ qualifiedName: "test.effect",
1531
+ changeKind: "added",
1532
+ after: { name: "effect", signature: "(fn: () => void) => void", stability: "stable", parent: "test", qualifiedName: "test.effect" },
1533
+ };
1534
+ classifyChange(change); // → "minor"
1535
+ });
1536
+
1537
+ it("classifies stable->deprecated as patch", () => {
1538
+ const change: ApiChange = {
1539
+ name: "fn",
1540
+ parent: "test",
1541
+ qualifiedName: "test.fn",
1542
+ changeKind: "stability-changed",
1543
+ before: { name: "fn", signature: "()", stability: "stable", parent: "test", qualifiedName: "test.fn" },
1544
+ after: { name: "fn", signature: "()", stability: "deprecated", parent: "test", qualifiedName: "test.fn" },
1545
+ };
1546
+ classifyChange(change); // → "patch"
1547
+ });
1548
+
1549
+ it("classifies experimental->stable as patch", () => {
1550
+ const change: ApiChange = {
1551
+ name: "fn",
1552
+ parent: "test",
1553
+ qualifiedName: "test.fn",
1554
+ changeKind: "stability-changed",
1555
+ before: { name: "fn", signature: "()", stability: "experimental", parent: "test", qualifiedName: "test.fn" },
1556
+ after: { name: "fn", signature: "()", stability: "stable", parent: "test", qualifiedName: "test.fn" },
1557
+ };
1558
+ classifyChange(change); // → "patch"
1559
+ });
1560
+
1561
+ it("classifies removal of experimental API as minor", () => {
1562
+ const change: ApiChange = {
1563
+ name: "beta",
1564
+ parent: "test",
1565
+ qualifiedName: "test.beta",
1566
+ changeKind: "removed",
1567
+ before: { name: "beta", signature: "()", stability: "experimental", parent: "test", qualifiedName: "test.beta" },
1568
+ };
1569
+ classifyChange(change); // → "minor"
1570
+ });
1571
+
1572
+ it("classifies removal of deprecated API as minor", () => {
1573
+ const change: ApiChange = {
1574
+ name: "old",
1575
+ parent: "test",
1576
+ qualifiedName: "test.old",
1577
+ changeKind: "removed",
1578
+ before: { name: "old", signature: "()", stability: "deprecated", parent: "test", qualifiedName: "test.old" },
1579
+ };
1580
+ classifyChange(change); // → "minor"
1581
+ });
1582
+ });
1583
+ ```
1584
+
1585
+ **detectBreakingChanges**
1586
+
1587
+ ```typescript
1588
+ describe("detectBreakingChanges", () => {
1589
+ it("returns only breaking changes from a diff", () => {
1590
+ const before = makeSnapshot("1.0.0", [
1591
+ { name: "signal", signature: "()" },
1592
+ { name: "removed", signature: "()" },
1593
+ { name: "changed", signature: "(a: number) => void" },
1594
+ ]);
1595
+ const after = makeSnapshot("2.0.0", [
1596
+ { name: "signal", signature: "()" },
1597
+ { name: "added", signature: "()" },
1598
+ { name: "changed", signature: "(a: string) => void" },
1599
+ ]);
1600
+
1601
+ const diff = diffSnapshots(before, after);
1602
+ const breaking = detectBreakingChanges(diff);
1603
+
1604
+ breaking; // → 2
1605
+ const names = breaking.map((c) => c.name);
1606
+ names; // → "removed"
1607
+ names; // → "changed"
1608
+ });
1609
+
1610
+ it("returns empty array when no breaking changes exist", () => {
1611
+ const before = makeSnapshot("1.0.0", [{ name: "signal", signature: "()" }]);
1612
+ const after = makeSnapshot("2.0.0", [
1613
+ { name: "signal", signature: "()" },
1614
+ { name: "effect", signature: "()" },
1615
+ ]);
1616
+
1617
+ const diff = diffSnapshots(before, after);
1618
+ const breaking = detectBreakingChanges(diff);
1619
+ breaking; // → 0
1620
+ });
1621
+
1622
+ it("does not flag removal of experimental APIs as breaking", () => {
1623
+ const before = makeSnapshot("1.0.0", [
1624
+ { name: "beta", stability: "experimental", signature: "()" },
1625
+ ]);
1626
+ const after = makeSnapshot("2.0.0", []);
1627
+
1628
+ const diff = diffSnapshots(before, after);
1629
+ const breaking = detectBreakingChanges(diff);
1630
+ breaking; // → 0
1631
+ });
1632
+
1633
+ it("does not flag removal of deprecated APIs as breaking", () => {
1634
+ const before = makeSnapshot("1.0.0", [
1635
+ { name: "old", stability: "deprecated", signature: "()" },
1636
+ ]);
1637
+ const after = makeSnapshot("2.0.0", []);
1638
+
1639
+ const diff = diffSnapshots(before, after);
1640
+ const breaking = detectBreakingChanges(diff);
1641
+ breaking; // → 0
1642
+ });
1643
+ });
1644
+ ```
1645
+
1646
+ **generateMigrationGuide**
1647
+
1648
+ ```typescript
1649
+ describe("generateMigrationGuide", () => {
1650
+ it("generates guide for removed APIs", () => {
1651
+ const changes: ApiChange[] = [
1652
+ {
1653
+ name: "obsolete",
1654
+ parent: "signals",
1655
+ qualifiedName: "signals.obsolete",
1656
+ changeKind: "removed",
1657
+ before: { name: "obsolete", signature: "(val: T) => void", stability: "stable", parent: "signals", qualifiedName: "signals.obsolete" },
1658
+ },
1659
+ ];
1660
+
1661
+ const guide = generateMigrationGuide(changes);
1662
+ guide; // → "obsolete"
1663
+ guide; // → "removed"
1664
+ guide; // → "Migration Guide"
1665
+ });
1666
+
1667
+ it("generates guide for changed signatures", () => {
1668
+ const changes: ApiChange[] = [
1669
+ {
1670
+ name: "signal",
1671
+ parent: "signals",
1672
+ qualifiedName: "signals.signal",
1673
+ changeKind: "signature-changed",
1674
+ before: { name: "signal", signature: "<T>(v: T) => Signal<T>", stability: "stable", parent: "signals", qualifiedName: "signals.signal" },
1675
+ after: { name: "signal", signature: "<T>(v: T, opts?: Options) => Signal<T>", stability: "stable", parent: "signals", qualifiedName: "signals.signal" },
1676
+ },
1677
+ ];
1678
+
1679
+ const guide = generateMigrationGuide(changes);
1680
+ guide; // → "signal"
1681
+ guide; // → "signature"
1682
+ guide; // → "<T>(v: T) => Signal<T>"
1683
+ guide; // → "<T>(v: T, opts?: Options) => Signal<T>"
1684
+ });
1685
+
1686
+ it("returns empty string for empty changes list", () => {
1687
+ const guide = generateMigrationGuide([]);
1688
+ guide; // → ""
1689
+ });
1690
+
1691
+ it("groups changes by component/parent", () => {
1692
+ const changes: ApiChange[] = [
1693
+ {
1694
+ name: "a",
1695
+ parent: "signals",
1696
+ qualifiedName: "signals.a",
1697
+ changeKind: "removed",
1698
+ before: { name: "a", signature: "()", stability: "stable", parent: "signals", qualifiedName: "signals.a" },
1699
+ },
1700
+ {
1701
+ name: "b",
1702
+ parent: "router",
1703
+ qualifiedName: "router.b",
1704
+ changeKind: "removed",
1705
+ before: { name: "b", signature: "()", stability: "stable", parent: "router", qualifiedName: "router.b" },
1706
+ },
1707
+ ];
1708
+
1709
+ const guide = generateMigrationGuide(changes);
1710
+ guide; // → "signals"
1711
+ guide; // → "router"
1712
+ });
1713
+ });
1714
+ ```
1715
+
1716
+ **generateChangelog**
1717
+
1718
+ ```typescript
1719
+ describe("generateChangelog", () => {
1720
+ it("generates changelog with breaking changes section", () => {
1721
+ const before = makeSnapshot("1.0.0", [
1722
+ { name: "removed", signature: "()" },
1723
+ ]);
1724
+ const after = makeSnapshot("2.0.0", [
1725
+ { name: "added", signature: "()" },
1726
+ ]);
1727
+
1728
+ const diff = diffSnapshots(before, after);
1729
+ const log = generateChangelog(diff);
1730
+
1731
+ log; // → "Breaking Changes"
1732
+ log; // → "removed"
1733
+ log; // → "added"
1734
+ });
1735
+
1736
+ it("generates changelog with only additions (no breaking section)", () => {
1737
+ const before = makeSnapshot("1.0.0", [{ name: "signal", signature: "()" }]);
1738
+ const after = makeSnapshot("2.0.0", [
1739
+ { name: "signal", signature: "()" },
1740
+ { name: "effect", signature: "()" },
1741
+ ]);
1742
+
1743
+ const diff = diffSnapshots(before, after);
1744
+ const log = generateChangelog(diff);
1745
+
1746
+ log; // → "Breaking Changes"
1747
+ log; // → "Added"
1748
+ log; // → "effect"
1749
+ });
1750
+
1751
+ it("generates changelog for stability changes", () => {
1752
+ const before = makeSnapshot("1.0.0", [
1753
+ { name: "fn", stability: "experimental", signature: "()" },
1754
+ ]);
1755
+ const after = makeSnapshot("2.0.0", [
1756
+ { name: "fn", stability: "stable", signature: "()" },
1757
+ ]);
1758
+
1759
+ const diff = diffSnapshots(before, after);
1760
+ const log = generateChangelog(diff);
1761
+
1762
+ log; // → "Changed"
1763
+ log; // → "fn"
1764
+ });
1765
+
1766
+ it("returns empty string for no changes", () => {
1767
+ const before = makeSnapshot("1.0.0", [{ name: "signal", signature: "()" }]);
1768
+ const after = makeSnapshot("2.0.0", [{ name: "signal", signature: "()" }]);
1769
+
1770
+ const diff = diffSnapshots(before, after);
1771
+ const log = generateChangelog(diff);
1772
+ log; // → ""
1773
+ });
1774
+
1775
+ it("includes version information in header", () => {
1776
+ const before = makeSnapshot("1.0.0", [
1777
+ { name: "signal", signature: "()" },
1778
+ ]);
1779
+ const after = makeSnapshot("2.0.0", [
1780
+ { name: "signal", signature: "()" },
1781
+ { name: "effect", signature: "()" },
1782
+ ]);
1783
+
1784
+ const diff = diffSnapshots(before, after);
1785
+ const log = generateChangelog(diff);
1786
+ log; // → "1.0.0"
1787
+ log; // → "2.0.0"
1788
+ });
1789
+
1790
+ it("generates changelog with mixed breaking and non-breaking removals", () => {
1791
+ const before = makeSnapshot("1.0.0", [
1792
+ { name: "stableRemoved", stability: "stable", signature: "()" },
1793
+ { name: "deprecatedRemoved", stability: "deprecated", signature: "()" },
1794
+ ]);
1795
+ const after = makeSnapshot("2.0.0", []);
1796
+
1797
+ const diff = diffSnapshots(before, after);
1798
+ const log = generateChangelog(diff);
1799
+
1800
+ log; // → "Breaking Changes"
1801
+ log; // → "stableRemoved"
1802
+ log; // → "Changed"
1803
+ log; // → "deprecatedRemoved"
1804
+ log; // → "was deprecated"
1805
+ });
1806
+ });
1807
+ ```
1808
+
1809
+ **parseMarkerAttributes**
1810
+
1811
+ ```typescript
1812
+ describe("parseMarkerAttributes", () => {
1813
+ it("parses single attribute with double quotes", () => {
1814
+ const result = parseMarkerAttributes('install="@pithyjs/signals"');
1815
+ result; // → { install: "@pithyjs/signals" }
1816
+ });
1817
+
1818
+ it("parses multiple attributes", () => {
1819
+ const result = parseMarkerAttributes('examples="pithy.signals.*" limit="3"');
1820
+ result; // → { examples: "pithy.signals.*", limit: "3" }
1821
+ });
1822
+
1823
+ it("parses attribute with single quotes", () => {
1824
+ const result = parseMarkerAttributes("api='pithy.signals.*' format='table'");
1825
+ result; // → { api: "pithy.signals.*", format: "table" }
1826
+ });
1827
+
1828
+ it("returns empty object for empty string", () => {
1829
+ parseMarkerAttributes(""); // → {}
1830
+ });
1831
+
1832
+ it("handles whitespace between attributes", () => {
1833
+ const result = parseMarkerAttributes(' install = "@pithyjs/core" ');
1834
+ result; // → { install: "@pithyjs/core" }
1835
+ });
1836
+ });
1837
+ ```
1838
+
1839
+ **resolveMarkerType**
1840
+
1841
+ ```typescript
1842
+ describe("resolveMarkerType", () => {
1843
+ it("resolves 'install' type", () => {
1844
+ resolveMarkerType({ install: "@pithyjs/signals" }); // → "install"
1845
+ });
1846
+
1847
+ it("resolves 'examples' type", () => {
1848
+ resolveMarkerType({ examples: "pithy.signals.*" }); // → "examples"
1849
+ });
1850
+
1851
+ it("resolves 'api' type", () => {
1852
+ resolveMarkerType({ api: "pithy.signals.*" }); // → "api"
1853
+ });
1854
+
1855
+ it("resolves 'bundle' type", () => {
1856
+ resolveMarkerType({ bundle: "@pithyjs/signals" }); // → "bundle"
1857
+ });
1858
+
1859
+ it("resolves 'testing' type", () => {
1860
+ resolveMarkerType({ testing: "pithy.signals.*" }); // → "testing"
1861
+ });
1862
+
1863
+ it("returns null for unknown attributes", () => {
1864
+ resolveMarkerType({ foo: "bar" }); // → null
1865
+ });
1866
+ });
1867
+ ```
1868
+
1869
+ **parseReadmeMarkers**
1870
+
1871
+ ```typescript
1872
+ describe("parseReadmeMarkers", () => {
1873
+ it("parses a single install marker", () => {
1874
+ const content = `# Package
1875
+
1876
+ <!-- @codex:auto install="@pithyjs/signals" -->
1877
+ ```bash
1878
+ npm install @pithyjs/signals
1879
+ ```
1880
+
1881
+ ```bash
1882
+ pnpm add @pithyjs/signals
1883
+ ```
1884
+
1885
+ ```bash
1886
+ yarn add @pithyjs/signals
1887
+ ```
1888
+ <!-- @codex:end -->
1889
+
1890
+ ## More`;
1891
+
1892
+ const markers = parseReadmeMarkers(content);
1893
+ markers; // → 1
1894
+ markers[0].type; // → "install"
1895
+ markers[0].attributes; // → { install: "@pithyjs/signals" }
1896
+ markers[0].innerContent; // → "\nold content\n"
1897
+ });
1898
+
1899
+ it("parses multiple markers in one file", () => {
1900
+ const content = `<!-- @codex:auto install="@pithyjs/core" -->
1901
+ ```bash
1902
+ npm install @pithyjs/core
1903
+ ```
1904
+
1905
+ ```bash
1906
+ pnpm add @pithyjs/core
1907
+ ```
1908
+
1909
+ ```bash
1910
+ yarn add @pithyjs/core
1911
+ ```
1912
+ <!-- @codex:end -->
1913
+
1914
+ <!-- @codex:auto api="pithy.core.*" format="table" -->
1915
+ | API | Component | Signature | Stability | Description |
1916
+ | --- | --- | --- | --- | --- |
1917
+ | LazyState | lazy | `type LazyState = 'idle' | 'loading' | 'loaded' | 'error'` | stable | Possible states for a lazy component |
1918
+ | LazyOptions | lazy | `interface LazyOptions` | stable | Options for configuring lazy loading behavior |
1919
+ | LazyHandle | lazy | `interface LazyHandle<T>` | stable | Handle returned by lazy() for controlling lazy component loading |
1920
+ | lazy | lazy | `<T>(loader: () => Promise<T>, options?: LazyOptions) => LazyHandle<T>` | stable | - |
1921
+ | isLazyHandle | lazy | `(value: unknown) => value is LazyHandle<unknown>` | stable | - |
1922
+ | preloadLazy | lazy | `<T>(handle: LazyHandle<T>) => Promise<T>` | stable | - |
1923
+ | ModuleRecord | module-cache | `interface ModuleRecord<T>` | stable | - |
1924
+ | loadModule | module-cache | `<T>(key: string, loader: () => Promise<T>) => Promise<T>` | stable | - |
1925
+ | getModuleRecord | module-cache | `<T>(key: string) => ModuleRecord<T> | undefined` | stable | - |
1926
+ | invalidateModule | module-cache | `(key: string) => boolean` | stable | - |
1927
+ | getResolvedModuleKeys | module-cache | `() => string[]` | stable | - |
1928
+ | clearModuleCache | module-cache | `() => void` | stable | - |
1929
+ | bindDeferDirectives | bind-defer-directives | `(fragment: DocumentFragment, context: Record<string, unknown>, disposers: Array<() => void>) => void` | stable | - |
1930
+ | registerDirectiveBinder | directive-registry | `(binder: DirectiveBinder) => void` | stable | - |
1931
+ | unregisterDirectiveBinder | directive-registry | `(binder: DirectiveBinder) => void` | stable | - |
1932
+ | executeCustomDirectiveBinders | directive-registry | `(root: Element | DocumentFragment, context: Record<string, unknown>, disposers: Array<() => void>) => void` | stable | - |
1933
+ | createResource | resource | `<T, E = unknown>(fetcher: ResourceFetcher<T>, options?: ResourceOptions) => ResourceHandle<T, E>` | stable | - |
1934
+ <!-- @codex:end -->`;
1935
+
1936
+ const markers = parseReadmeMarkers(content);
1937
+ markers; // → 2
1938
+ markers[0].type; // → "install"
1939
+ markers[1].type; // → "api"
1940
+ });
1941
+
1942
+ it("handles markers with no inner content", () => {
1943
+ const content = `<!-- @codex:auto install="@pithyjs/signals" -->
1944
+ ```bash
1945
+ npm install @pithyjs/signals
1946
+ ```
1947
+
1948
+ ```bash
1949
+ pnpm add @pithyjs/signals
1950
+ ```
1951
+
1952
+ ```bash
1953
+ yarn add @pithyjs/signals
1954
+ ```
1955
+ <!-- @codex:end -->`;
1956
+
1957
+ const markers = parseReadmeMarkers(content);
1958
+ markers; // → 1
1959
+ markers[0].innerContent; // → "\n"
1960
+ });
1961
+
1962
+ it("preserves content outside markers", () => {
1963
+ const content = `# Title
1964
+
1965
+ Some manual content.
1966
+
1967
+ <!-- @codex:auto install="pkg" -->
1968
+ ```bash
1969
+ npm install pkg
1970
+ ```
1971
+
1972
+ ```bash
1973
+ pnpm add pkg
1974
+ ```
1975
+
1976
+ ```bash
1977
+ yarn add pkg
1978
+ ```
1979
+ <!-- @codex:end -->
1980
+
1981
+ More manual content.`;
1982
+
1983
+ const markers = parseReadmeMarkers(content);
1984
+ markers; // → 1
1985
+ // Markers should have correct indices that don't span the whole file
1986
+ expect(markers[0].startIndex).toBeGreaterThan(0);
1987
+ expect(markers[0].endIndex).toBeLessThan(content.length);
1988
+ });
1989
+
1990
+ it("returns empty array when no markers found", () => {
1991
+ parseReadmeMarkers("# Just a regular README\n\nNo markers here."); // → []
1992
+ });
1993
+
1994
+ it("handles malformed marker (missing @codex:end) gracefully", () => {
1995
+ const content = `<!-- @codex:auto install="@pithyjs/signals" -->
1996
+ some content without closing tag`;
1997
+
1998
+ const markers = parseReadmeMarkers(content);
1999
+ markers; // → 0
2000
+ });
2001
+
2002
+ it("assigns correct startIndex and endIndex", () => {
2003
+ const content = `before\n<!-- @codex:auto install="pkg" -->
2004
+ ```bash
2005
+ npm install pkg
2006
+ ```
2007
+
2008
+ ```bash
2009
+ pnpm add pkg
2010
+ ```
2011
+
2012
+ ```bash
2013
+ yarn add pkg
2014
+ ```
2015
+ <!-- @codex:end -->\nafter`;
2016
+ const markers = parseReadmeMarkers(content);
2017
+ markers; // → 1
2018
+
2019
+ const reconstructed =
2020
+ content.slice(0, markers[0].startIndex) +
2021
+ markers[0].originalContent +
2022
+ content.slice(markers[0].endIndex);
2023
+ reconstructed; // → content
2024
+ });
2025
+
2026
+ it("stores the original open tag string", () => {
2027
+ const content = `<!-- @codex:auto examples="pithy.signals.*" limit="3" -->
2028
+ **creates an effect**
2029
+
2030
+ ```typescript
2031
+ const count = signal(0);
2032
+ count(); // → 0
2033
+ ```
2034
+ <!-- @codex:end -->`;
2035
+ const markers = parseReadmeMarkers(content);
2036
+ markers; // → 1
2037
+ markers[0].openTag; // → `<!-- @codex:auto examples="pithy.signals.*" limit="3" -->`
2038
+ });
2039
+ });
2040
+ ```
2041
+
2042
+ **generateInstallContent**
2043
+
2044
+ ````typescript
2045
+ describe("generateInstallContent", () => {
2046
+ it("generates npm/pnpm/yarn commands", () => {
2047
+ const content = generateInstallContent("@pithyjs/signals");
2048
+ content; // → "npm install @pithyjs/signals"
2049
+ content; // → "pnpm add @pithyjs/signals"
2050
+ content; // → "yarn add @pithyjs/signals"
2051
+ });
2052
+
2053
+ it("wraps in bash code blocks", () => {
2054
+ const content = generateInstallContent("@pithyjs/signals");
2055
+ content; // → "```bash"
2056
+ content; // → "```"
2057
+ });
2058
+ });
2059
+ ````
2060
+
2061
+ **generateExamplesContent**
2062
+
2063
+ `````typescript
2064
+ describe("generateExamplesContent", () => {
2065
+ it("generates markdown with code blocks from component examples", () => {
2066
+ const comp = makeComponentWithApis();
2067
+ const content = generateExamplesContent([comp]);
2068
+ content; // → "```ts"
2069
+ content; // → "const count = signal(0)"
2070
+ });
2071
+
2072
+ it("respects limit parameter", () => {
2073
+ const comp = makeComponentWithApis();
2074
+ // Add multiple API-level examples (generateExamplesContent reads api.examples, not comp.examples)
2075
+ comp.apis[0].examples = [
2076
+ { code: "example1", language: "ts", runnable: true, isDoctest: false, location: { file: "f", line: 1 } },
2077
+ { code: "example2", language: "ts", runnable: true, isDoctest: false, location: { file: "f", line: 2 } },
2078
+ { code: "example3", language: "ts", runnable: true, isDoctest: false, location: { file: "f", line: 3 } },
2079
+ ];
2080
+ const content = generateExamplesContent([comp], 1);
2081
+ // Should only show 1 example total
2082
+ const codeBlockCount = (content.match(/```ts/g) || []).length;
2083
+ codeBlockCount; // → 1
2084
+ });
2085
+
2086
+ it("treats limit=0 as zero (not unlimited)", () => {
2087
+ const comp = makeComponentWithApis();
2088
+ const content = generateExamplesContent([comp], 0);
2089
+ // limit=0 should select zero examples, not all of them
2090
+ content; // → ""
2091
+ // Verify without limit it would have returned examples
2092
+ const unlimited = generateExamplesContent([comp]);
2093
+ unlimited; // → "```ts"
2094
+ });
2095
+
2096
+ it("uses dynamic fence length when code contains triple backticks", () => {
2097
+ const comp = makeComponent({
2098
+ apis: [{
2099
+ id: "test.api",
2100
+ name: "fn",
2101
+ parent: "test",
2102
+ stability: "stable",
2103
+ examples: [{
2104
+ code: 'const md = `\n```js\nconsole.log("hi");\n```\n`;',
2105
+ language: "ts",
2106
+ runnable: false,
2107
+ isDoctest: false,
2108
+ location: { file: "f.ts", line: 1 },
2109
+ }],
2110
+ testRefs: [],
2111
+ sourceLocation: { file: "f.ts", line: 1 },
2112
+ }],
2113
+ });
2114
+ const content = generateExamplesContent([comp]);
2115
+ // Should use 4 backticks since content contains triple backticks
2116
+ content; // → "````ts"
2117
+ content; // → /````\s*$/m
2118
+ // Should NOT contain a bare triple-backtick fence
2119
+ content; // → /^```ts$/m
2120
+ });
2121
+
2122
+ it("returns placeholder when no examples available", () => {
2123
+ const comp = makeComponent({ apis: [] });
2124
+ const content = generateExamplesContent([comp]);
2125
+ content; // → "No examples"
2126
+ });
2127
+
2128
+ it("includes example descriptions when available", () => {
2129
+ const comp = makeComponentWithApis();
2130
+ const content = generateExamplesContent([comp]);
2131
+ content; // → "Basic usage"
2132
+ });
2133
+
2134
+ it("falls back to test examples when no API examples exist", () => {
2135
+ const comp = makeComponent({ id: "pithy.signals", apis: [] });
2136
+ const testExamples: TestExample[] = [
2137
+ {
2138
+ entryId: "pithy.signals",
2139
+ name: "creates a signal",
2140
+ code: 'const count = signal(0);\nexpect(count()).toBe(0);',
2141
+ location: { file: "signal.test.ts", line: 5 },
2142
+ testFile: "signal.test.ts",
2143
+ status: "passing",
2144
+ },
2145
+ ];
2146
+ const content = generateExamplesContent([comp], undefined, testExamples, "pithy.signals");
2147
+ content; // → "creates a signal"
2148
+ content; // → "const count = signal(0)"
2149
+ // Verify expect() was stripped by stripTestBoilerplate
2150
+ content; // → "expect("
2151
+ content; // → "count(); // → 0"
2152
+ });
2153
+
2154
+ it("prefers API examples over test examples", () => {
2155
+ const comp = makeComponentWithApis(); // has API-level example "Basic usage"
2156
+ const testExamples: TestExample[] = [
2157
+ {
2158
+ entryId: "pithy.signals",
2159
+ name: "test example",
2160
+ code: 'test code',
2161
+ location: { file: "signal.test.ts", line: 5 },
2162
+ testFile: "signal.test.ts",
2163
+ status: "passing",
2164
+ },
2165
+ ];
2166
+ const content = generateExamplesContent([comp], undefined, testExamples, "pithy.signals.*");
2167
+ // Should use API examples, not test examples
2168
+ content; // → "Basic usage"
2169
+ content; // → "test example"
2170
+ });
2171
+
2172
+ it("filters test examples by pattern", () => {
2173
+ const comp = makeComponent({ id: "pithy.signals", apis: [] });
2174
+ const testExamples: TestExample[] = [
2175
+ {
2176
+ entryId: "pithy.signals",
2177
+ name: "signal test",
2178
+ code: 'signal code',
2179
+ location: { file: "signal.test.ts", line: 5 },
2180
+ testFile: "signal.test.ts",
2181
+ status: "passing",
2182
+ },
2183
+ {
2184
+ entryId: "pithy.router",
2185
+ name: "router test",
2186
+ code: 'router code',
2187
+ location: { file: "router.test.ts", line: 5 },
2188
+ testFile: "router.test.ts",
2189
+ status: "passing",
2190
+ },
2191
+ ];
2192
+ const content = generateExamplesContent([comp], undefined, testExamples, "pithy.signals");
2193
+ content; // → "signal code"
2194
+ content; // → "router code"
2195
+ });
2196
+ });
2197
+ `````
2198
+
2199
+ **generateApiContent**
2200
+
2201
+ ```typescript
2202
+ describe("generateApiContent", () => {
2203
+ it("generates markdown table with correct columns", () => {
2204
+ const comp = makeComponentWithApis();
2205
+ const content = generateApiContent([comp]);
2206
+ content; // → "| API |"
2207
+ content; // → "| Signature |"
2208
+ content; // → "| Stability |"
2209
+ });
2210
+
2211
+ it("renders all APIs from matched components", () => {
2212
+ const comp = makeComponentWithApis();
2213
+ const content = generateApiContent([comp]);
2214
+ content; // → "signal"
2215
+ content; // → "effect"
2216
+ content; // → "computed"
2217
+ });
2218
+
2219
+ it("preserves pipe characters in signatures (inside backticks)", () => {
2220
+ const comp = makeComponent({
2221
+ apis: [{
2222
+ id: "test.api",
2223
+ name: "fn",
2224
+ parent: "test",
2225
+ signature: "(a: A | B) => C",
2226
+ stability: "stable",
2227
+ examples: [],
2228
+ testRefs: [],
2229
+ sourceLocation: { file: "f.ts", line: 1 },
2230
+ }],
2231
+ });
2232
+ const content = generateApiContent([comp]);
2233
+ // Signature is inside backticks — pipes don't need escaping in GFM inline code
2234
+ content; // → "`(a: A | B) => C`"
2235
+ });
2236
+
2237
+ it("renders list format when format='list'", () => {
2238
+ const comp = makeComponentWithApis();
2239
+ const content = generateApiContent([comp], "list");
2240
+ content; // → "- **signal**"
2241
+ content; // → "- **effect**"
2242
+ content; // → "[stable]"
2243
+ content; // → "[experimental]"
2244
+ // Should NOT have table syntax
2245
+ content; // → "| API |"
2246
+ });
2247
+
2248
+ it("includes descriptions in list format", () => {
2249
+ const comp = makeComponentWithApis();
2250
+ const content = generateApiContent([comp], "list");
2251
+ content; // → "Creates a reactive signal"
2252
+ });
2253
+
2254
+ it("returns placeholder when no APIs found", () => {
2255
+ const content = generateApiContent([]);
2256
+ content; // → "No API"
2257
+ });
2258
+ });
2259
+ ```
2260
+
2261
+ **syncReadmeContent**
2262
+
2263
+ ```typescript
2264
+ describe("syncReadmeContent", () => {
2265
+ it("replaces install marker content", () => {
2266
+ const readme = `# Package
2267
+
2268
+ <!-- @codex:auto install="@pithyjs/signals" -->
2269
+ ```bash
2270
+ npm install @pithyjs/signals
2271
+ ```
2272
+
2273
+ ```bash
2274
+ pnpm add @pithyjs/signals
2275
+ ```
2276
+
2277
+ ```bash
2278
+ yarn add @pithyjs/signals
2279
+ ```
2280
+ <!-- @codex:end -->`;
2281
+
2282
+ const result = syncReadmeContent(readme, "README.md", []);
2283
+ result.changed; // → true
2284
+ result.markersProcessed; // → 1
2285
+ result.updatedContent; // → "npm install @pithyjs/signals"
2286
+ });
2287
+
2288
+ it("replaces API marker content", () => {
2289
+ const comp = makeComponentWithApis();
2290
+ const readme = `# API
2291
+
2292
+ <!-- @codex:auto api="pithy.signals.*" format="table" -->
2293
+ _No API entries found._
2294
+ <!-- @codex:end -->`;
2295
+
2296
+ const result = syncReadmeContent(readme, "README.md", [comp]);
2297
+ result.changed; // → true
2298
+ result.updatedContent; // → "signal"
2299
+ result.updatedContent; // → "effect"
2300
+ });
2301
+
2302
+ it("handles multiple markers in one file", () => {
2303
+ const comp = makeComponentWithApis();
2304
+ const readme = `<!-- @codex:auto install="@pithyjs/signals" -->
2305
+ ```bash
2306
+ npm install @pithyjs/signals
2307
+ ```
2308
+
2309
+ ```bash
2310
+ pnpm add @pithyjs/signals
2311
+ ```
2312
+
2313
+ ```bash
2314
+ yarn add @pithyjs/signals
2315
+ ```
2316
+ <!-- @codex:end -->
2317
+
2318
+ <!-- @codex:auto api="pithy.signals.*" -->
2319
+ _No API entries found._
2320
+ <!-- @codex:end -->`;
2321
+
2322
+ const result = syncReadmeContent(readme, "README.md", [comp]);
2323
+ result.markersProcessed; // → 2
2324
+ result.markersUpdated; // → 2
2325
+ });
2326
+
2327
+ it("preserves content outside markers", () => {
2328
+ const readme = `# Title
2329
+
2330
+ Manual content here.
2331
+
2332
+ <!-- @codex:auto install="@pithyjs/signals" -->
2333
+ ```bash
2334
+ npm install @pithyjs/signals
2335
+ ```
2336
+
2337
+ ```bash
2338
+ pnpm add @pithyjs/signals
2339
+ ```
2340
+
2341
+ ```bash
2342
+ yarn add @pithyjs/signals
2343
+ ```
2344
+ <!-- @codex:end -->
2345
+
2346
+ More manual content.`;
2347
+
2348
+ const result = syncReadmeContent(readme, "README.md", []);
2349
+ result.updatedContent; // → "# Title"
2350
+ result.updatedContent; // → "Manual content here."
2351
+ result.updatedContent; // → "More manual content."
2352
+ });
2353
+
2354
+ it("reports changed: false when content is identical after sync", () => {
2355
+ // First sync to get the generated content
2356
+ const readme = `<!-- @codex:auto install="@pithyjs/signals" -->
2357
+ ```bash
2358
+ npm install @pithyjs/signals
2359
+ ```
2360
+
2361
+ ```bash
2362
+ pnpm add @pithyjs/signals
2363
+ ```
2364
+
2365
+ ```bash
2366
+ yarn add @pithyjs/signals
2367
+ ```
2368
+ <!-- @codex:end -->`;
2369
+ const first = syncReadmeContent(readme, "README.md", []);
2370
+
2371
+ // Second sync with the already-synced content
2372
+ const second = syncReadmeContent(first.updatedContent!, "README.md", []);
2373
+ second.changed; // → false
2374
+ });
2375
+
2376
+ it("reports warnings for unsupported marker types", () => {
2377
+ const readme = `<!-- @codex:auto bundle="@pithyjs/signals" -->
2378
+ <!-- Bundle size tracking not yet available -->
2379
+ <!-- @codex:end -->`;
2380
+
2381
+ const result = syncReadmeContent(readme, "README.md", []);
2382
+ expect(result.warnings.length).toBeGreaterThan(0);
2383
+ result.warnings[0]; // → "bundle"
2384
+ });
2385
+
2386
+ it("uses test examples for examples markers when no API examples exist", () => {
2387
+ const comp = makeComponent({
2388
+ id: "pithy.feature.signals",
2389
+ apis: [
2390
+ {
2391
+ id: "pithy.feature.signals.signal",
2392
+ name: "signal",
2393
+ parent: "pithy.feature.signals",
2394
+ stability: "stable",
2395
+ examples: [],
2396
+ testRefs: [],
2397
+ sourceLocation: { file: "signal.ts", line: 1 },
2398
+ },
2399
+ ],
2400
+ });
2401
+ const testExamples: TestExample[] = [
2402
+ {
2403
+ entryId: "pithy.feature.signals",
2404
+ name: "creates a reactive signal",
2405
+ code: 'const count = signal(0);\nexpect(count()).toBe(0);',
2406
+ location: { file: "signal.test.ts", line: 5 },
2407
+ testFile: "signal.test.ts",
2408
+ status: "passing",
2409
+ },
2410
+ ];
2411
+ const readme = `<!-- @codex:auto examples="pithy.feature.signals" -->
2412
+ **creates a reactive signal with initial value**
2413
+
2414
+ ```typescript
2415
+ const count = signal(0);
2416
+ count(); // → 0
2417
+ count.set(1);
2418
+ count(); // → 1
2419
+ ```
2420
+
2421
+ **supports functional updates via .set()**
2422
+
2423
+ ```typescript
2424
+ const count = signal(0);
2425
+ count.set(n => n + 1);
2426
+ count(); // → 1
2427
+ count.set(n => n * 10);
2428
+ count(); // → 10
2429
+ ```
2430
+
2431
+ **tracks signal dependencies and re-runs**
2432
+
2433
+ ```typescript
2434
+ const count = signal(0);
2435
+ let observed = -1;
2436
+ effect(() => {
2437
+ observed = count();
2438
+ });
2439
+ observed; // → 0
2440
+ count.set(5);
2441
+ observed; // → 5
2442
+ ```
2443
+
2444
+ **derives a value from other signals**
2445
+
2446
+ ```typescript
2447
+ const a = signal(2);
2448
+ const b = signal(3);
2449
+ const sum = computed(() => a() + b());
2450
+ sum(); // → 5
2451
+ a.set(10);
2452
+ sum(); // → 13
2453
+ ```
2454
+
2455
+ **defers effect execution until batch completes**
2456
+
2457
+ ```typescript
2458
+ const a = signal(1);
2459
+ const b = signal(2);
2460
+ let runCount = 0;
2461
+ effect(() => {
2462
+ a();
2463
+ b();
2464
+ runCount++;
2465
+ });
2466
+ runCount; // → 1
2467
+
2468
+ batch(() => {
2469
+ a.set(10);
2470
+ b.set(20);
2471
+ });
2472
+ // Effect should have run only once after batch, not twice
2473
+ runCount; // → 2
2474
+ ```
2475
+
2476
+ **runs cleanup on disposal**
2477
+
2478
+ ```typescript
2479
+ const count = signal(0);
2480
+ let cleaned = false;
2481
+
2482
+ const dispose = effect(() => {
2483
+ count();
2484
+ onCleanup(() => {
2485
+ cleaned = true;
2486
+ });
2487
+ });
2488
+
2489
+ cleaned; // → false
2490
+ dispose();
2491
+ cleaned; // → true
2492
+ ```
2493
+
2494
+ **detects signal values at runtime**
2495
+
2496
+ ```typescript
2497
+ const count = signal(0);
2498
+ isSignal(count); // → true
2499
+ isSignal(42); // → false
2500
+ isSignal(() => {}); // → false
2501
+ isSignal(null); // → false
2502
+ ```
2503
+
2504
+ **unsubscribes from a dependency it stopped reading**
2505
+
2506
+ ```typescript
2507
+ const useB = signal(true);
2508
+ const b = signal(2);
2509
+
2510
+ let runs = 0;
2511
+ const dispose = effect(() => {
2512
+ runs += 1;
2513
+ if (useB()) b();
2514
+ });
2515
+
2516
+ useB.set(false);
2517
+ const afterSwitch = runs;
2518
+
2519
+ b.set(99);
2520
+ expect(runs, 'live effect re-ran for a dependency it stopped reading').toBe(
2521
+ afterSwitch
2522
+ );
2523
+
2524
+ dispose();
2525
+ ```
2526
+ <!-- @codex:end -->`;
2527
+
2528
+ const result = syncReadmeContent(readme, "README.md", [comp], testExamples);
2529
+ result.changed; // → true
2530
+ result.updatedContent; // → "creates a reactive signal"
2531
+ result.updatedContent; // → "const count = signal(0)"
2532
+ // Verify test boilerplate was stripped
2533
+ result.updatedContent; // → "expect("
2534
+ result.updatedContent; // → "count(); // → 0"
2535
+ });
2536
+
2537
+ it("skips outer marker when nested markers are detected", () => {
2538
+ const readme = `<!-- @codex:auto install="@pithyjs/signals" -->
2539
+ <!-- @codex:auto api="pithy.signals.*" -->
2540
+ _No API entries found._
2541
+ <!-- @codex:end -->
2542
+ <!-- @codex:end -->`;
2543
+
2544
+ const result = syncReadmeContent(readme, "README.md", []);
2545
+ // The parser skips the outer marker (its inner content contains another @codex:auto).
2546
+ // Only the inner marker is processed — the outer is treated as malformed.
2547
+ result.markersProcessed; // → 1
2548
+ // The extra <!-- @codex:end --> remains in the file as-is (not corrupted)
2549
+ result.updatedContent ?? readme; // → "<!-- @codex:end -->"
2550
+ });
2551
+
2552
+ it("preserves original open tag attributes (no reordering)", () => {
2553
+ const readme = `<!-- @codex:auto examples="pithy.signals.*" limit="3" -->
2554
+ **creates an effect**
2555
+
2556
+ ```typescript
2557
+ const count = signal(0);
2558
+ count(); // → 0
2559
+ ```
2560
+ <!-- @codex:end -->`;
2561
+
2562
+ const first = syncReadmeContent(readme, "README.md", []);
2563
+ // The open tag should be preserved exactly as written
2564
+ first.updatedContent; // → `<!-- @codex:auto examples="pithy.signals.*" limit="3" -->`
2565
+ });
2566
+
2567
+ it("warns on empty pattern value", () => {
2568
+ const readme = `<!-- @codex:auto api="" -->
2569
+ _No API entries found._
2570
+ <!-- @codex:end -->`;
2571
+
2572
+ const result = syncReadmeContent(readme, "README.md", []);
2573
+ result.warnings.some((w) => w.includes("Empty pattern")); // → true
2574
+ });
2575
+
2576
+ it("warns on unknown api format", () => {
2577
+ const comp = makeComponentWithApis();
2578
+ const readme = `<!-- @codex:auto api="pithy.signals.*" format="csv" -->
2579
+ _No API entries found._
2580
+ <!-- @codex:end -->`;
2581
+
2582
+ const result = syncReadmeContent(readme, "README.md", [comp]);
2583
+ result.warnings.some((w) => w.includes('Unknown format "csv"')); // → true
2584
+ });
2585
+
2586
+ it("warns on negative limit", () => {
2587
+ const comp = makeComponentWithApis();
2588
+ const readme = `<!-- @codex:auto examples="pithy.signals.*" limit="-1" -->
2589
+
2590
+ <!-- @codex:end -->`;
2591
+
2592
+ const result = syncReadmeContent(readme, "README.md", [comp]);
2593
+ result.warnings.some((w) => w.includes("Negative")); // → true
2594
+ });
2595
+
2596
+ it("returns correct markersProcessed and markersUpdated counts", () => {
2597
+ const readme = `<!-- @codex:auto install="@pithyjs/signals" -->
2598
+ ```bash
2599
+ npm install @pithyjs/signals
2600
+ ```
2601
+
2602
+ ```bash
2603
+ pnpm add @pithyjs/signals
2604
+ ```
2605
+
2606
+ ```bash
2607
+ yarn add @pithyjs/signals
2608
+ ```
2609
+ <!-- @codex:end -->
2610
+
2611
+ <!-- @codex:auto bundle="@pithyjs/signals" -->
2612
+ <!-- Bundle size tracking not yet available -->
2613
+ <!-- @codex:end -->`;
2614
+
2615
+ const result = syncReadmeContent(readme, "README.md", []);
2616
+ result.markersProcessed; // → 2
2617
+ // Both markers generate content (bundle generates a placeholder)
2618
+ result.markersUpdated; // → 2
2619
+ });
2620
+
2621
+ it("replaces testing marker content", () => {
2622
+ const comp = makeComponentWithApis();
2623
+ const readme = `# Testing
2624
+
2625
+ <!-- @codex:auto testing="pithy.signals.*" -->
2626
+ _No test data available._
2627
+ <!-- @codex:end -->`;
2628
+
2629
+ const result = syncReadmeContent(readme, "README.md", [comp]);
2630
+ result.changed; // → true
2631
+ result.markersProcessed; // → 1
2632
+ result.updatedContent; // → "signal"
2633
+ result.updatedContent; // → "effect"
2634
+ // Should contain a markdown table
2635
+ result.updatedContent; // → "|"
2636
+ });
2637
+
2638
+ it("warns on non-numeric limit value", () => {
2639
+ const comp = makeComponentWithApis();
2640
+ const readme = `<!-- @codex:auto examples="pithy.signals.*" limit="abc" -->
2641
+ **creates an effect**
2642
+
2643
+ ```typescript
2644
+ const count = signal(0);
2645
+ count(); // → 0
2646
+ ```
2647
+ <!-- @codex:end -->`;
2648
+
2649
+ const result = syncReadmeContent(readme, "README.md", [comp]);
2650
+ result.warnings.some((w) => w.toLowerCase().includes("limit")); // → true
2651
+ });
2652
+ });
2653
+ ```
2654
+
2655
+ **syncAllReadmes**
2656
+
2657
+ ```typescript
2658
+ describe("syncAllReadmes", () => {
2659
+ beforeEach(() => {
2660
+ vi.clearAllMocks();
2661
+ });
2662
+
2663
+ it("returns empty results when no README files match", async () => {
2664
+ const { globby } = await import("globby");
2665
+ vi.mocked(globby).mockResolvedValue([]);
2666
+
2667
+ const extractionResult = makeExtractionResult();
2668
+ const results = await syncAllReadmes(extractionResult, { root: "/project" });
2669
+ results; // → []
2670
+ });
2671
+
2672
+ it("processes matched README files and returns results", async () => {
2673
+ const { globby } = await import("globby");
2674
+ const { readFile } = await import("node:fs/promises");
2675
+
2676
+ vi.mocked(globby).mockResolvedValue(["/project/README.md"]);
2677
+ vi.mocked(readFile).mockResolvedValue(
2678
+ `<!-- @codex:auto install="@pithyjs/signals" -->
2679
+ ```bash
2680
+ npm install @pithyjs/signals
2681
+ ```
2682
+
2683
+ ```bash
2684
+ pnpm add @pithyjs/signals
2685
+ ```
2686
+
2687
+ ```bash
2688
+ yarn add @pithyjs/signals
2689
+ ```
2690
+ <!-- @codex:end -->`
2691
+ );
2692
+
2693
+ const extractionResult = makeExtractionResult();
2694
+ const results = await syncAllReadmes(extractionResult, { root: "/project", dryRun: true });
2695
+
2696
+ results; // → 1
2697
+ results[0].filePath; // → "/project/README.md"
2698
+ results[0].changed; // → true
2699
+ results[0].markersProcessed; // → 1
2700
+ });
2701
+
2702
+ it("does not write files in dry-run mode", async () => {
2703
+ const { globby } = await import("globby");
2704
+ const { readFile, writeFile } = await import("node:fs/promises");
2705
+
2706
+ vi.mocked(globby).mockResolvedValue(["/project/README.md"]);
2707
+ vi.mocked(readFile).mockResolvedValue(
2708
+ `<!-- @codex:auto install="@pithyjs/signals" -->
2709
+ ```bash
2710
+ npm install @pithyjs/signals
2711
+ ```
2712
+
2713
+ ```bash
2714
+ pnpm add @pithyjs/signals
2715
+ ```
2716
+
2717
+ ```bash
2718
+ yarn add @pithyjs/signals
2719
+ ```
2720
+ <!-- @codex:end -->`
2721
+ );
2722
+
2723
+ const extractionResult = makeExtractionResult();
2724
+ await syncAllReadmes(extractionResult, { root: "/project", dryRun: true });
2725
+
2726
+ expect(writeFile).not.toHaveBeenCalled();
2727
+ });
2728
+
2729
+ it("writes files when not in dry-run mode", async () => {
2730
+ const { globby } = await import("globby");
2731
+ const { readFile, writeFile } = await import("node:fs/promises");
2732
+
2733
+ vi.mocked(globby).mockResolvedValue(["/project/README.md"]);
2734
+ vi.mocked(readFile).mockResolvedValue(
2735
+ `<!-- @codex:auto install="@pithyjs/signals" -->
2736
+ ```bash
2737
+ npm install @pithyjs/signals
2738
+ ```
2739
+
2740
+ ```bash
2741
+ pnpm add @pithyjs/signals
2742
+ ```
2743
+
2744
+ ```bash
2745
+ yarn add @pithyjs/signals
2746
+ ```
2747
+ <!-- @codex:end -->`
2748
+ );
2749
+ vi.mocked(writeFile).mockResolvedValue(undefined);
2750
+
2751
+ const extractionResult = makeExtractionResult();
2752
+ await syncAllReadmes(extractionResult, { root: "/project", dryRun: false });
2753
+
2754
+ expect(writeFile).toHaveBeenCalledWith(
2755
+ "/project/README.md",
2756
+ expect.stringContaining("npm install @pithyjs/signals"),
2757
+ "utf-8"
2758
+ );
2759
+ });
2760
+
2761
+ it("passes test examples from extraction result to syncReadmeContent", async () => {
2762
+ const { globby } = await import("globby");
2763
+ const { readFile } = await import("node:fs/promises");
2764
+
2765
+ const comp = makeComponent({
2766
+ id: "pithy.feature.signals",
2767
+ apis: [{
2768
+ id: "pithy.feature.signals.signal",
2769
+ name: "signal",
2770
+ parent: "pithy.feature.signals",
2771
+ stability: "stable",
2772
+ examples: [],
2773
+ testRefs: [],
2774
+ sourceLocation: { file: "signal.ts", line: 1 },
2775
+ }],
2776
+ });
2777
+
2778
+ vi.mocked(globby).mockResolvedValue(["/project/README.md"]);
2779
+ vi.mocked(readFile).mockResolvedValue(
2780
+ `<!-- @codex:auto examples="pithy.feature.signals" -->
2781
+ **creates a reactive signal with initial value**
2782
+
2783
+ ```typescript
2784
+ const count = signal(0);
2785
+ count(); // → 0
2786
+ count.set(1);
2787
+ count(); // → 1
2788
+ ```
2789
+
2790
+ **supports functional updates via .set()**
2791
+
2792
+ ```typescript
2793
+ const count = signal(0);
2794
+ count.set(n => n + 1);
2795
+ count(); // → 1
2796
+ count.set(n => n * 10);
2797
+ count(); // → 10
2798
+ ```
2799
+
2800
+ **tracks signal dependencies and re-runs**
2801
+
2802
+ ```typescript
2803
+ const count = signal(0);
2804
+ let observed = -1;
2805
+ effect(() => {
2806
+ observed = count();
2807
+ });
2808
+ observed; // → 0
2809
+ count.set(5);
2810
+ observed; // → 5
2811
+ ```
2812
+
2813
+ **derives a value from other signals**
2814
+
2815
+ ```typescript
2816
+ const a = signal(2);
2817
+ const b = signal(3);
2818
+ const sum = computed(() => a() + b());
2819
+ sum(); // → 5
2820
+ a.set(10);
2821
+ sum(); // → 13
2822
+ ```
2823
+
2824
+ **defers effect execution until batch completes**
2825
+
2826
+ ```typescript
2827
+ const a = signal(1);
2828
+ const b = signal(2);
2829
+ let runCount = 0;
2830
+ effect(() => {
2831
+ a();
2832
+ b();
2833
+ runCount++;
2834
+ });
2835
+ runCount; // → 1
2836
+
2837
+ batch(() => {
2838
+ a.set(10);
2839
+ b.set(20);
2840
+ });
2841
+ // Effect should have run only once after batch, not twice
2842
+ runCount; // → 2
2843
+ ```
2844
+
2845
+ **runs cleanup on disposal**
2846
+
2847
+ ```typescript
2848
+ const count = signal(0);
2849
+ let cleaned = false;
2850
+
2851
+ const dispose = effect(() => {
2852
+ count();
2853
+ onCleanup(() => {
2854
+ cleaned = true;
2855
+ });
2856
+ });
2857
+
2858
+ cleaned; // → false
2859
+ dispose();
2860
+ cleaned; // → true
2861
+ ```
2862
+
2863
+ **detects signal values at runtime**
2864
+
2865
+ ```typescript
2866
+ const count = signal(0);
2867
+ isSignal(count); // → true
2868
+ isSignal(42); // → false
2869
+ isSignal(() => {}); // → false
2870
+ isSignal(null); // → false
2871
+ ```
2872
+
2873
+ **unsubscribes from a dependency it stopped reading**
2874
+
2875
+ ```typescript
2876
+ const useB = signal(true);
2877
+ const b = signal(2);
2878
+
2879
+ let runs = 0;
2880
+ const dispose = effect(() => {
2881
+ runs += 1;
2882
+ if (useB()) b();
2883
+ });
2884
+
2885
+ useB.set(false);
2886
+ const afterSwitch = runs;
2887
+
2888
+ b.set(99);
2889
+ expect(runs, 'live effect re-ran for a dependency it stopped reading').toBe(
2890
+ afterSwitch
2891
+ );
2892
+
2893
+ dispose();
2894
+ ```
2895
+ <!-- @codex:end -->`
2896
+ );
2897
+
2898
+ const extractionResult = makeExtractionResult({
2899
+ components: [comp],
2900
+ testExamples: [{
2901
+ entryId: "pithy.feature.signals",
2902
+ name: "basic usage",
2903
+ code: 'const s = signal(1);\nexpect(s()).toBe(1);',
2904
+ location: { file: "signal.test.ts", line: 5 },
2905
+ testFile: "signal.test.ts",
2906
+ status: "passing",
2907
+ }],
2908
+ });
2909
+
2910
+ const results = await syncAllReadmes(extractionResult, { root: "/project", dryRun: true });
2911
+ results[0].updatedContent; // → "basic usage"
2912
+ results[0].updatedContent; // → "s(); // → 1"
2913
+ });
2914
+ });
2915
+ ```
2916
+
2917
+ **should escape single quotes in entryId**
2918
+
2919
+ ```typescript
2920
+ const example = createExample('console.log("test")');
2921
+ const entryId = "test'injection";
2922
+
2923
+ const result = wrapDoctestInHarness(example, entryId);
2924
+
2925
+ // Should contain escaped single quote
2926
+ result; // → "test\\'injection"
2927
+ ```
2928
+
2929
+ **should escape regex metacharacters in API names**
2930
+
2931
+ ```typescript
2932
+ const content = 'import { set } from "./store"; set(value);';
2933
+ const apisWithMetachars = ['set(value)', 'get.*', 'test[0]', 'func$'];
2934
+
2935
+ // Should not throw and should handle metacharacters safely
2936
+ expect(() => identifyCoveredApis(content, apisWithMetachars)).not.toThrow();
2937
+ ```
2938
+
2939
+ **getSnapshotPath**
2940
+
2941
+ ```typescript
2942
+ describe("getSnapshotPath", () => {
2943
+ it("generates path for a simple version", () => {
2944
+ const path = getSnapshotPath(TEST_DIR, "1.0.0");
2945
+ path; // → join(TEST_DIR, "1.0.0.json")
2946
+ });
2947
+
2948
+ it("generates path for a prerelease version", () => {
2949
+ const path = getSnapshotPath(TEST_DIR, "2.0.0-beta.1");
2950
+ path; // → join(TEST_DIR, "2.0.0-beta.1.json")
2951
+ });
2952
+
2953
+ it("rejects version strings with path separators", () => {
2954
+ expect(() => getSnapshotPath(TEST_DIR, "../../etc/passwd")).toThrow(/Invalid version/);
2955
+ });
2956
+
2957
+ it("rejects version strings with slashes", () => {
2958
+ expect(() => getSnapshotPath(TEST_DIR, "v1/exploit")).toThrow(/Invalid version/);
2959
+ });
2960
+
2961
+ it("rejects version strings with backslashes", () => {
2962
+ expect(() => getSnapshotPath(TEST_DIR, "v1\\exploit")).toThrow(/Invalid version/);
2963
+ });
2964
+
2965
+ it("rejects empty version string", () => {
2966
+ expect(() => getSnapshotPath(TEST_DIR, "")).toThrow(/Invalid version/);
2967
+ });
2968
+ });
2969
+ ```
2970
+
2971
+ **saveSnapshot**
2972
+
2973
+ ```typescript
2974
+ describe("saveSnapshot", () => {
2975
+ it("saves a snapshot to disk as JSON", async () => {
2976
+ const snapshot = makeTestSnapshot("1.0.0");
2977
+ await saveSnapshot(snapshot, TEST_DIR);
2978
+
2979
+ const filePath = join(TEST_DIR, "1.0.0.json");
2980
+ const content = await readFile(filePath, "utf8");
2981
+ const parsed = JSON.parse(content);
2982
+
2983
+ parsed.version; // → "1.0.0"
2984
+ parsed.entries; // → 2
2985
+ parsed.timestamp; // → "2025-01-01T00:00:00.000Z"
2986
+ });
2987
+
2988
+ it("creates the directory if it does not exist", async () => {
2989
+ const nestedDir = join(TEST_DIR, "nested", "versions");
2990
+ const snapshot = makeTestSnapshot("1.0.0");
2991
+ await saveSnapshot(snapshot, nestedDir);
2992
+
2993
+ const filePath = join(nestedDir, "1.0.0.json");
2994
+ const content = await readFile(filePath, "utf8");
2995
+ JSON.parse(content).version; // → "1.0.0"
2996
+ });
2997
+
2998
+ it("overwrites an existing snapshot for the same version", async () => {
2999
+ const v1 = makeTestSnapshot("1.0.0");
3000
+ await saveSnapshot(v1, TEST_DIR);
3001
+
3002
+ const v1Updated: ApiSnapshot = {
3003
+ ...v1,
3004
+ entries: [
3005
+ ...v1.entries,
3006
+ {
3007
+ name: "computed",
3008
+ parent: "pithy.signals",
3009
+ qualifiedName: "pithy.signals.computed",
3010
+ signature: "<T>(fn: () => T) => () => T",
3011
+ stability: "stable",
3012
+ },
3013
+ ],
3014
+ };
3015
+ await saveSnapshot(v1Updated, TEST_DIR);
3016
+
3017
+ const content = await readFile(join(TEST_DIR, "1.0.0.json"), "utf8");
3018
+ JSON.parse(content).entries; // → 3
3019
+ });
3020
+
3021
+ it("handles prerelease version strings in filenames", async () => {
3022
+ const snapshot = makeTestSnapshot("2.0.0-beta.1");
3023
+ await saveSnapshot(snapshot, TEST_DIR);
3024
+
3025
+ const filePath = join(TEST_DIR, "2.0.0-beta.1.json");
3026
+ const content = await readFile(filePath, "utf8");
3027
+ JSON.parse(content).version; // → "2.0.0-beta.1"
3028
+ });
3029
+
3030
+ it("produces valid JSON with 2-space indentation", async () => {
3031
+ const snapshot = makeTestSnapshot("1.0.0");
3032
+ await saveSnapshot(snapshot, TEST_DIR);
3033
+
3034
+ const content = await readFile(join(TEST_DIR, "1.0.0.json"), "utf8");
3035
+ // Check indentation (2 spaces)
3036
+ content; // → ' "version"'
3037
+ // Should end with a newline
3038
+ content.endsWith("\n"); // → true
3039
+ });
3040
+ });
3041
+ ```
3042
+
3043
+ **loadSnapshot**
3044
+
3045
+ ```typescript
3046
+ describe("loadSnapshot", () => {
3047
+ it("loads a saved snapshot by version", async () => {
3048
+ const original = makeTestSnapshot("1.0.0");
3049
+ await saveSnapshot(original, TEST_DIR);
3050
+
3051
+ const loaded = await loadSnapshot("1.0.0", TEST_DIR);
3052
+ loaded; // → null
3053
+ loaded!.version; // → "1.0.0"
3054
+ loaded!.entries; // → 2
3055
+ loaded!.entries[0].name; // → "signal"
3056
+ });
3057
+
3058
+ it("returns null when snapshot does not exist", async () => {
3059
+ const loaded = await loadSnapshot("99.99.99", TEST_DIR);
3060
+ loaded; // → null
3061
+ });
3062
+
3063
+ it("returns null when directory does not exist", async () => {
3064
+ const loaded = await loadSnapshot("1.0.0", join(TEST_DIR, "nonexistent"));
3065
+ loaded; // → null
3066
+ });
3067
+
3068
+ it("preserves all snapshot fields through save/load cycle", async () => {
3069
+ const original = makeTestSnapshot("1.0.0");
3070
+ await saveSnapshot(original, TEST_DIR);
3071
+ const loaded = await loadSnapshot("1.0.0", TEST_DIR);
3072
+
3073
+ loaded; // → original
3074
+ });
3075
+
3076
+ it("loads snapshot with type members correctly", async () => {
3077
+ const snapshot: ApiSnapshot = {
3078
+ version: "1.0.0",
3079
+ timestamp: "2025-01-01T00:00:00.000Z",
3080
+ entries: [
3081
+ {
3082
+ name: "Signal",
3083
+ parent: "pithy.signals",
3084
+ qualifiedName: "pithy.signals.Signal",
3085
+ stability: "stable",
3086
+ kind: "interface",
3087
+ members: [
3088
+ { name: "set", type: "method", optional: false, readonly: false },
3089
+ { name: "subscribe", type: "method", optional: false, readonly: false },
3090
+ ],
3091
+ },
3092
+ ],
3093
+ };
3094
+ await saveSnapshot(snapshot, TEST_DIR);
3095
+ const loaded = await loadSnapshot("1.0.0", TEST_DIR);
3096
+
3097
+ loaded!.entries[0].kind; // → "interface"
3098
+ loaded!.entries[0].members; // → 2
3099
+ });
3100
+
3101
+ it("throws on corrupted JSON file instead of returning null", async () => {
3102
+ await mkdir(TEST_DIR, { recursive: true });
3103
+ await writeFile(join(TEST_DIR, "corrupt.json"), "not valid json{{{", "utf8");
3104
+ await expect(loadSnapshot("corrupt", TEST_DIR)).rejects.toThrow(/Failed to parse snapshot/);
3105
+ });
3106
+
3107
+ it("throws on non-ENOENT filesystem errors instead of returning null", async () => {
3108
+ // Trying to read a version that resolves to a directory path triggers a non-ENOENT error
3109
+ const dirAsFile = join(TEST_DIR, "not-a-file");
3110
+ await mkdir(join(dirAsFile + ".json"), { recursive: true }); // Create dir where file expected
3111
+ await expect(loadSnapshot("not-a-file", TEST_DIR)).rejects.toThrow();
3112
+ });
3113
+ });
3114
+ ```
3115
+
3116
+ **listSnapshots**
3117
+
3118
+ ```typescript
3119
+ describe("listSnapshots", () => {
3120
+ it("lists all saved snapshot versions sorted by version", async () => {
3121
+ await saveSnapshot(makeTestSnapshot("1.0.0"), TEST_DIR);
3122
+ await saveSnapshot(makeTestSnapshot("2.0.0"), TEST_DIR);
3123
+ await saveSnapshot(makeTestSnapshot("1.1.0"), TEST_DIR);
3124
+
3125
+ const versions = await listSnapshots(TEST_DIR);
3126
+ versions; // → ["1.0.0", "1.1.0", "2.0.0"]
3127
+ });
3128
+
3129
+ it("sorts by semver, not lexicographically (1.2.0 before 1.10.0)", async () => {
3130
+ await saveSnapshot(makeTestSnapshot("1.10.0"), TEST_DIR);
3131
+ await saveSnapshot(makeTestSnapshot("1.2.0"), TEST_DIR);
3132
+ await saveSnapshot(makeTestSnapshot("1.1.0"), TEST_DIR);
3133
+
3134
+ const versions = await listSnapshots(TEST_DIR);
3135
+ versions; // → ["1.1.0", "1.2.0", "1.10.0"]
3136
+ });
3137
+
3138
+ it("returns empty array when no snapshots exist", async () => {
3139
+ const versions = await listSnapshots(TEST_DIR);
3140
+ versions; // → []
3141
+ });
3142
+
3143
+ it("returns empty array when directory does not exist", async () => {
3144
+ const versions = await listSnapshots(join(TEST_DIR, "nonexistent"));
3145
+ versions; // → []
3146
+ });
3147
+
3148
+ it("ignores non-JSON files", async () => {
3149
+ await saveSnapshot(makeTestSnapshot("1.0.0"), TEST_DIR);
3150
+ await writeFile(join(TEST_DIR, "README.md"), "# Versions\n");
3151
+
3152
+ const versions = await listSnapshots(TEST_DIR);
3153
+ versions; // → ["1.0.0"]
3154
+ });
3155
+
3156
+ it("handles prerelease versions", async () => {
3157
+ await saveSnapshot(makeTestSnapshot("1.0.0"), TEST_DIR);
3158
+ await saveSnapshot(makeTestSnapshot("2.0.0-beta.1"), TEST_DIR);
3159
+
3160
+ const versions = await listSnapshots(TEST_DIR);
3161
+ versions; // → "1.0.0"
3162
+ versions; // → "2.0.0-beta.1"
3163
+ });
3164
+
3165
+ it("sorts prereleases before their release (semver precedence)", async () => {
3166
+ await saveSnapshot(makeTestSnapshot("1.0.0"), TEST_DIR);
3167
+ await saveSnapshot(makeTestSnapshot("1.0.0-beta.1"), TEST_DIR);
3168
+ await saveSnapshot(makeTestSnapshot("1.0.0-alpha.1"), TEST_DIR);
3169
+
3170
+ const versions = await listSnapshots(TEST_DIR);
3171
+ versions; // → ["1.0.0-alpha.1", "1.0.0-beta.1", "1.0.0"]
3172
+ });
3173
+
3174
+ it("sorts prerelease numeric identifiers numerically", async () => {
3175
+ await saveSnapshot(makeTestSnapshot("1.0.0-beta.2"), TEST_DIR);
3176
+ await saveSnapshot(makeTestSnapshot("1.0.0-beta.10"), TEST_DIR);
3177
+ await saveSnapshot(makeTestSnapshot("1.0.0-beta.1"), TEST_DIR);
3178
+
3179
+ const versions = await listSnapshots(TEST_DIR);
3180
+ versions; // → ["1.0.0-beta.1", "1.0.0-beta.2", "1.0.0-beta.10"]
3181
+ });
3182
+ });
3183
+ ```
3184
+
3185
+ **extractTestExamples**
3186
+
3187
+ ```typescript
3188
+ describe("extractTestExamples", () => {
3189
+ it("extracts examples with plain marker format", () => {
3190
+ const content = `
3191
+ // @codex:example pithy.signals.effect
3192
+ it("creates an effect", () => {
3193
+ const count = signal(0);
3194
+ count(); // → 0
3195
+ });
3196
+ `;
3197
+ const examples = extractTestExamples(content, "/test/signal.test.ts");
3198
+ examples; // → 1
3199
+ examples[0].entryId; // → "pithy.signals.effect"
3200
+ examples[0].name; // → "creates an effect"
3201
+ examples[0].code; // → "signal(0)"
3202
+ examples[0].testFile; // → "/test/signal.test.ts"
3203
+ examples[0].status; // → "unknown"
3204
+ });
3205
+
3206
+ it("extracts examples with parenthesized marker format", () => {
3207
+ const content = `
3208
+ // @codex:example("pithy.core.compile")
3209
+ test("compiles template", () => {
3210
+ compile("<div>hello</div>");
3211
+ });
3212
+ `;
3213
+ const examples = extractTestExamples(content, "test.ts");
3214
+ examples; // → 1
3215
+ examples[0].entryId; // → "pithy.core.compile"
3216
+ });
3217
+
3218
+ it("extracts examples with object marker format", () => {
3219
+ const content = `
3220
+ // @codex:example { entry: "pithy.router.navigate" }
3221
+ it("navigates to route", () => {
3222
+ router.navigate("/home");
3223
+ });
3224
+ `;
3225
+ const examples = extractTestExamples(content, "test.ts");
3226
+ examples; // → 1
3227
+ examples[0].entryId; // → "pithy.router.navigate"
3228
+ });
3229
+
3230
+ it("extracts multiple examples from one file", () => {
3231
+ const content = `
3232
+ // @codex:example pithy.a
3233
+ it("first", () => { expect(1).toBe(1); });
3234
+
3235
+ // @codex:example pithy.b
3236
+ it("second", () => { expect(2).toBe(2); });
3237
+ `;
3238
+ const examples = extractTestExamples(content, "test.ts");
3239
+ examples; // → 2
3240
+ examples[0].entryId; // → "pithy.a"
3241
+ examples[1].entryId; // → "pithy.b"
3242
+ });
3243
+
3244
+ it("skips markers not followed by a test block", () => {
3245
+ const content = `
3246
+ // @codex:example pithy.orphan
3247
+ const helper = () => {};
3248
+ `;
3249
+ const examples = extractTestExamples(content, "test.ts");
3250
+ examples; // → 0
3251
+ });
3252
+
3253
+ it("handles braces inside strings correctly", () => {
3254
+ const content = `
3255
+ // @codex:example pithy.strings
3256
+ it("handles object { with braces }", () => {
3257
+ const obj = { key: "value with }" };
3258
+ obj.key; // → defined
3259
+ });
3260
+ `;
3261
+ const examples = extractTestExamples(content, "test.ts");
3262
+ examples; // → 1
3263
+ // Should capture the full test block including the closing });
3264
+ examples[0].code; // → "expect(obj.key).toBeDefined()"
3265
+ });
3266
+
3267
+ it("handles braces inside comments", () => {
3268
+ const content = `
3269
+ // @codex:example pithy.comments
3270
+ it("works with comments", () => {
3271
+ // This { brace should be ignored }
3272
+ const x = 1;
3273
+ x; // → 1
3274
+ });
3275
+ `;
3276
+ const examples = extractTestExamples(content, "test.ts");
3277
+ examples; // → 1
3278
+ examples[0].code; // → "expect(x).toBe(1)"
3279
+ });
3280
+
3281
+ it("handles braces inside multi-line block comments", () => {
3282
+ const content = `
3283
+ // @codex:example pithy.multiline
3284
+ it("multi-line comment", () => {
3285
+ /*
3286
+ * This { brace } should be ignored
3287
+ * and this one too {
3288
+ */
3289
+ const x = 1;
3290
+ x; // → 1
3291
+ });
3292
+ `;
3293
+ const examples = extractTestExamples(content, "test.ts");
3294
+ examples; // → 1
3295
+ examples[0].code; // → "expect(x).toBe(1)"
3296
+ examples[0].code; // → "});"
3297
+ });
3298
+
3299
+ it("provides correct 1-indexed line numbers", () => {
3300
+ const content = `line1
3301
+ line2
3302
+ // @codex:example pithy.loc
3303
+ it("has location", () => {
3304
+ true; // → true
3305
+ });`;
3306
+ const examples = extractTestExamples(content, "test.ts");
3307
+ examples[0].location.line; // → 4); // it() is on line 4 (1-indexed
3308
+ });
3309
+
3310
+ it("handles escaped quotes in test names", () => {
3311
+ const content = `
3312
+ // @codex:example pithy.escape
3313
+ it("handles \\"quoted\\" text", () => {
3314
+ true; // → true
3315
+ });
3316
+ `;
3317
+ const examples = extractTestExamples(content, "test.ts");
3318
+ examples; // → 1
3319
+ examples[0].name; // → 'handles \\"quoted\\" text'
3320
+ });
3321
+
3322
+ it("handles describe blocks", () => {
3323
+ const content = `
3324
+ // @codex:example pithy.describe
3325
+ describe("my suite", () => {
3326
+ it("inner test", () => {
3327
+ 1; // → 1
3328
+ });
3329
+ });
3330
+ `;
3331
+ const examples = extractTestExamples(content, "test.ts");
3332
+ examples; // → 1
3333
+ examples[0].name; // → "my suite"
3334
+ examples[0].code; // → "inner test"
3335
+ });
3336
+
3337
+ it("does not match marker-like text that isn't a dotted identifier", () => {
3338
+ const content = `
3339
+ // This text mentions @codex:example but has no id after it
3340
+ it("test", () => { expect(1).toBe(1); });
3341
+ `;
3342
+ const examples = extractTestExamples(content, "test.ts");
3343
+ examples; // → 0
3344
+ });
3345
+ });
3346
+ ```
3347
+
3348
+ **parseVitestOutput**
3349
+
3350
+ ```typescript
3351
+ describe("parseVitestOutput", () => {
3352
+ it("parses testResults format (jest-like)", () => {
3353
+ const output = JSON.stringify({
3354
+ testResults: [
3355
+ {
3356
+ name: "packages/signals/src/signal.test.ts",
3357
+ assertionResults: [
3358
+ { fullName: "creates a signal", status: "passed", duration: 5 },
3359
+ {
3360
+ fullName: "updates signal",
3361
+ status: "failed",
3362
+ duration: 10,
3363
+ failureMessages: ["Expected 1 to be 2"],
3364
+ },
3365
+ ],
3366
+ },
3367
+ ],
3368
+ });
3369
+
3370
+ const statuses = parseVitestOutput(output);
3371
+ statuses; // → 2
3372
+ expect(statuses[0]).toMatchObject({
3373
+ file: "packages/signals/src/signal.test.ts",
3374
+ testName: "creates a signal",
3375
+ status: "passed",
3376
+ });
3377
+ expect(statuses[1]).toMatchObject({
3378
+ status: "failed",
3379
+ error: "Expected 1 to be 2",
3380
+ });
3381
+ });
3382
+
3383
+ it("parses results/tasks format (vitest native)", () => {
3384
+ const output = JSON.stringify({
3385
+ success: true,
3386
+ results: [
3387
+ {
3388
+ file: "src/foo.test.ts",
3389
+ tasks: [
3390
+ {
3391
+ name: "suite",
3392
+ type: "suite",
3393
+ tasks: [
3394
+ {
3395
+ name: "test one",
3396
+ type: "test",
3397
+ result: { state: "pass", duration: 3 },
3398
+ },
3399
+ ],
3400
+ },
3401
+ ],
3402
+ },
3403
+ ],
3404
+ });
3405
+
3406
+ const statuses = parseVitestOutput(output);
3407
+ statuses; // → 1
3408
+ expect(statuses[0]).toMatchObject({
3409
+ file: "src/foo.test.ts",
3410
+ testName: "suite > test one",
3411
+ status: "passed",
3412
+ });
3413
+ });
3414
+
3415
+ it("returns empty array for invalid JSON", () => {
3416
+ parseVitestOutput("not json"); // → []
3417
+ });
3418
+
3419
+ it("returns empty array for empty object", () => {
3420
+ parseVitestOutput("{}"); // → []
3421
+ });
3422
+
3423
+ it("maps pending/skipped/todo to skipped", () => {
3424
+ const output = JSON.stringify({
3425
+ testResults: [
3426
+ {
3427
+ name: "test.ts",
3428
+ assertionResults: [
3429
+ { title: "pending", status: "pending" },
3430
+ { title: "skipped", status: "skipped" },
3431
+ { title: "todo", status: "todo" },
3432
+ ],
3433
+ },
3434
+ ],
3435
+ });
3436
+
3437
+ const statuses = parseVitestOutput(output);
3438
+ statuses.every((s) => s.status === "skipped"); // → true
3439
+ });
3440
+ });
3441
+ ```
3442
+
3443
+ **matchExamplesToStatuses**
3444
+
3445
+ ```typescript
3446
+ describe("matchExamplesToStatuses", () => {
3447
+ function makeExample(overrides: Partial<TestExample> = {}): TestExample {
3448
+ return {
3449
+ entryId: "pithy.test",
3450
+ name: "my test",
3451
+ code: 'it("my test", () => {})',
3452
+ location: { file: "test.ts", line: 1 },
3453
+ testFile: "packages/signals/src/signal.test.ts",
3454
+ status: "unknown",
3455
+ ...overrides,
3456
+ };
3457
+ }
3458
+
3459
+ it("matches by exact name and file path suffix", () => {
3460
+ const examples = [makeExample()];
3461
+ const statuses: TestStatus[] = [
3462
+ {
3463
+ file: "packages/signals/src/signal.test.ts",
3464
+ testName: "my test",
3465
+ status: "passed",
3466
+ },
3467
+ ];
3468
+
3469
+ const result = matchExamplesToStatuses(examples, statuses);
3470
+ result[0].status; // → "passing"
3471
+ });
3472
+
3473
+ it("matches when status testName includes describe prefix", () => {
3474
+ const examples = [makeExample({ name: "creates signal" })];
3475
+ const statuses: TestStatus[] = [
3476
+ {
3477
+ file: "packages/signals/src/signal.test.ts",
3478
+ testName: "signal > creates signal",
3479
+ status: "passed",
3480
+ },
3481
+ ];
3482
+
3483
+ const result = matchExamplesToStatuses(examples, statuses);
3484
+ result[0].status; // → "passing"
3485
+ });
3486
+
3487
+ it("does not false-match short names across different files", () => {
3488
+ const examples = [
3489
+ makeExample({
3490
+ name: "works",
3491
+ testFile: "packages/foo/src/utils.test.ts",
3492
+ }),
3493
+ ];
3494
+ const statuses: TestStatus[] = [
3495
+ {
3496
+ file: "packages/bar/src/utils.test.ts",
3497
+ testName: "works",
3498
+ status: "passed",
3499
+ },
3500
+ ];
3501
+
3502
+ // Different packages — should NOT match
3503
+ const result = matchExamplesToStatuses(examples, statuses);
3504
+ result[0].status; // → "unknown"
3505
+ });
3506
+
3507
+ it("maps failed status correctly", () => {
3508
+ const examples = [makeExample()];
3509
+ const statuses: TestStatus[] = [
3510
+ {
3511
+ file: "packages/signals/src/signal.test.ts",
3512
+ testName: "my test",
3513
+ status: "failed",
3514
+ error: "Assertion failed",
3515
+ },
3516
+ ];
3517
+
3518
+ const result = matchExamplesToStatuses(examples, statuses);
3519
+ result[0].status; // → "failing"
3520
+ result[0].error; // → "Assertion failed"
3521
+ });
3522
+
3523
+ it("maps skipped status correctly", () => {
3524
+ const examples = [makeExample()];
3525
+ const statuses: TestStatus[] = [
3526
+ {
3527
+ file: "packages/signals/src/signal.test.ts",
3528
+ testName: "my test",
3529
+ status: "skipped",
3530
+ },
3531
+ ];
3532
+
3533
+ const result = matchExamplesToStatuses(examples, statuses);
3534
+ result[0].status; // → "skipped"
3535
+ });
3536
+
3537
+ it("leaves unmatched examples as unknown", () => {
3538
+ const examples = [makeExample({ name: "no match here" })];
3539
+ const statuses: TestStatus[] = [];
3540
+
3541
+ const result = matchExamplesToStatuses(examples, statuses);
3542
+ result[0].status; // → "unknown"
3543
+ });
3544
+ });
3545
+ ```
3546
+
3547
+ **getTestingDefaults**
3548
+
3549
+ ```typescript
3550
+ describe("getTestingDefaults", () => {
3551
+ it("returns directive defaults", () => {
3552
+ const defaults = getTestingDefaults("directive");
3553
+ defaults.unit; // → { required: true, coverage: 90 }
3554
+ defaults.integration; // → { required: true, coverage: 80 }
3555
+ defaults.e2e; // → { required: false }
3556
+ });
3557
+
3558
+ it("returns feature defaults", () => {
3559
+ const defaults = getTestingDefaults("feature");
3560
+ defaults.unit; // → { required: true, coverage: 85 }
3561
+ defaults.integration; // → { required: true, coverage: 70 }
3562
+ defaults.e2e; // → { required: false }
3563
+ });
3564
+
3565
+ it("returns runtime defaults", () => {
3566
+ const defaults = getTestingDefaults("runtime");
3567
+ defaults.unit; // → { required: true, coverage: 90 }
3568
+ defaults.integration; // → { required: true, coverage: 80 }
3569
+ defaults.e2e; // → { required: true }
3570
+ });
3571
+
3572
+ it("returns compiler defaults", () => {
3573
+ const defaults = getTestingDefaults("compiler");
3574
+ defaults.unit; // → { required: true, coverage: 95 }
3575
+ defaults.integration; // → { required: true, coverage: 70 }
3576
+ defaults.e2e; // → { required: false }
3577
+ });
3578
+
3579
+ it("returns plugin defaults", () => {
3580
+ const defaults = getTestingDefaults("plugin");
3581
+ defaults.unit; // → { required: true, coverage: 70 }
3582
+ defaults.integration; // → { required: true, coverage: 85 }
3583
+ defaults.e2e; // → { required: true }
3584
+ });
3585
+
3586
+ it("returns empty requirements for types category", () => {
3587
+ const defaults = getTestingDefaults("types");
3588
+ defaults.unit; // → undefined
3589
+ defaults.integration; // → undefined
3590
+ defaults.e2e; // → undefined
3591
+ });
3592
+
3593
+ it("returns feature defaults for unknown category", () => {
3594
+ const defaults = getTestingDefaults("unknown-category");
3595
+ defaults; // → getTestingDefaults("feature")
3596
+ });
3597
+
3598
+ it("returns independent copies (mutations don't affect defaults)", () => {
3599
+ const first = getTestingDefaults("feature");
3600
+ first.unit!.coverage = 999;
3601
+ const second = getTestingDefaults("feature");
3602
+ second.unit!.coverage; // → 85
3603
+ });
3604
+ });
3605
+ ```
3606
+
3607
+ **applyRiskModifiers**
3608
+
3609
+ ```typescript
3610
+ describe("applyRiskModifiers", () => {
3611
+ it("applies critical risk modifiers", () => {
3612
+ const base: TestingRequirements = {
3613
+ unit: { required: true, coverage: 85 },
3614
+ integration: { required: true, coverage: 70 },
3615
+ e2e: { required: false },
3616
+ };
3617
+
3618
+ const result = applyRiskModifiers(base, "critical");
3619
+ expect(result.unit!.coverage).toBe(90); // +5
3620
+ expect(result.integration!.coverage).toBe(80); // +10
3621
+ expect(result.e2e!.required).toBe(true); // Required
3622
+ });
3623
+
3624
+ it("applies high risk modifiers", () => {
3625
+ const base: TestingRequirements = {
3626
+ unit: { required: true, coverage: 85 },
3627
+ integration: { required: true, coverage: 70 },
3628
+ e2e: { required: false },
3629
+ };
3630
+
3631
+ const result = applyRiskModifiers(base, "high");
3632
+ expect(result.unit!.coverage).toBe(85); // +0
3633
+ expect(result.integration!.coverage).toBe(75); // +5
3634
+ expect(result.e2e!.required).toBe(false); // Unchanged
3635
+ });
3636
+
3637
+ it("applies medium risk modifiers", () => {
3638
+ const base: TestingRequirements = {
3639
+ unit: { required: true, coverage: 85 },
3640
+ integration: { required: true, coverage: 70 },
3641
+ e2e: { required: false },
3642
+ };
3643
+
3644
+ const result = applyRiskModifiers(base, "medium");
3645
+ expect(result.unit!.coverage).toBe(80); // -5
3646
+ expect(result.integration!.coverage).toBe(70); // +0
3647
+ result.e2e!.required; // → false
3648
+ });
3649
+
3650
+ it("applies low risk modifiers", () => {
3651
+ const base: TestingRequirements = {
3652
+ unit: { required: true, coverage: 85 },
3653
+ integration: { required: true, coverage: 70 },
3654
+ e2e: { required: false },
3655
+ };
3656
+
3657
+ const result = applyRiskModifiers(base, "low");
3658
+ expect(result.unit!.coverage).toBe(75); // -10
3659
+ expect(result.integration!.coverage).toBe(60); // -10
3660
+ result.e2e!.required; // → false
3661
+ });
3662
+
3663
+ it("does not modify base object (returns new object)", () => {
3664
+ const base: TestingRequirements = {
3665
+ unit: { required: true, coverage: 85 },
3666
+ integration: { required: true, coverage: 70 },
3667
+ e2e: { required: false },
3668
+ };
3669
+ const original = JSON.parse(JSON.stringify(base));
3670
+
3671
+ applyRiskModifiers(base, "critical");
3672
+ base; // → original
3673
+ });
3674
+
3675
+ it("handles undefined test levels gracefully", () => {
3676
+ const base: TestingRequirements = {};
3677
+ const result = applyRiskModifiers(base, "critical");
3678
+ result; // → {}
3679
+ });
3680
+
3681
+ it("clamps coverage to minimum 0", () => {
3682
+ const base: TestingRequirements = {
3683
+ unit: { required: true, coverage: 5 },
3684
+ };
3685
+ const result = applyRiskModifiers(base, "low");
3686
+ result.unit!.coverage; // → 0
3687
+ });
3688
+
3689
+ it("clamps coverage to maximum 100", () => {
3690
+ const base: TestingRequirements = {
3691
+ unit: { required: true, coverage: 98 },
3692
+ };
3693
+ const result = applyRiskModifiers(base, "critical");
3694
+ result.unit!.coverage; // → 100
3695
+ });
3696
+
3697
+ it("returns base unchanged when no risk level provided", () => {
3698
+ const base: TestingRequirements = {
3699
+ unit: { required: true, coverage: 85 },
3700
+ };
3701
+ const result = applyRiskModifiers(base, undefined);
3702
+ result; // → base
3703
+ });
3704
+
3705
+ it("returns a deep copy when no risk level provided (no shared references)", () => {
3706
+ const base: TestingRequirements = {
3707
+ unit: { required: true, coverage: 85 },
3708
+ };
3709
+ const result = applyRiskModifiers(base, undefined);
3710
+ result.unit!.coverage = 999;
3711
+ base.unit!.coverage; // → 85
3712
+ });
3713
+
3714
+ it("returns clone for unknown risk string (fallback path)", () => {
3715
+ const base: TestingRequirements = {
3716
+ unit: { required: true, coverage: 85 },
3717
+ };
3718
+ const result = applyRiskModifiers(base, "unknown" as RiskLevel);
3719
+ result; // → base
3720
+ // Verify it's a clone, not the same object
3721
+ result.unit!.coverage = 999;
3722
+ base.unit!.coverage; // → 85
3723
+ });
3724
+
3725
+ it("handles level with coverage undefined", () => {
3726
+ const base: TestingRequirements = {
3727
+ unit: { required: true },
3728
+ };
3729
+ const result = applyRiskModifiers(base, "critical");
3730
+ result.unit!.required; // → true
3731
+ result.unit!.coverage; // → undefined
3732
+ });
3733
+
3734
+ it("handles e2e-only base with critical risk", () => {
3735
+ const base: TestingRequirements = {
3736
+ e2e: { required: false },
3737
+ };
3738
+ const result = applyRiskModifiers(base, "critical");
3739
+ result.e2e!.required; // → true
3740
+ result.unit; // → undefined
3741
+ result.integration; // → undefined
3742
+ });
3743
+ });
3744
+ ```
3745
+
3746
+ **resolveTestingRequirements**
3747
+
3748
+ ```typescript
3749
+ describe("resolveTestingRequirements", () => {
3750
+ it("uses category defaults when no annotation testing exists", () => {
3751
+ const result = resolveTestingRequirements("feature");
3752
+ result.unit; // → defined
3753
+ result.unit!.required; // → true
3754
+ result.unit!.coverage; // → 85
3755
+ });
3756
+
3757
+ it("applies risk modifier on top of category defaults", () => {
3758
+ const result = resolveTestingRequirements("feature", "critical");
3759
+ expect(result.unit!.coverage).toBe(90); // 85 + 5
3760
+ expect(result.integration!.coverage).toBe(80); // 70 + 10
3761
+ result.e2e!.required; // → true
3762
+ });
3763
+
3764
+ it("annotation testing overrides defaults", () => {
3765
+ const override: TestingRequirements = {
3766
+ unit: { required: true, coverage: 100 },
3767
+ };
3768
+ const result = resolveTestingRequirements("feature", undefined, override);
3769
+ result.unit!.coverage; // → 100
3770
+ // Non-overridden levels use defaults
3771
+ result.integration; // → defined
3772
+ result.integration!.coverage; // → 70
3773
+ });
3774
+
3775
+ it("annotation testing with risk modifier", () => {
3776
+ const override: TestingRequirements = {
3777
+ unit: { required: true, coverage: 80 },
3778
+ };
3779
+ const result = resolveTestingRequirements("feature", "high", override);
3780
+ // Override unit is used as base, then risk applied: 80 + 0 = 80
3781
+ result.unit!.coverage; // → 80
3782
+ // Integration from defaults + risk: 70 + 5 = 75
3783
+ result.integration!.coverage; // → 75
3784
+ });
3785
+
3786
+ it("returns empty requirements for types category", () => {
3787
+ const result = resolveTestingRequirements("types");
3788
+ result.unit; // → undefined
3789
+ result.integration; // → undefined
3790
+ result.e2e; // → undefined
3791
+ });
3792
+
3793
+ it("full override replaces all levels", () => {
3794
+ const override: TestingRequirements = {
3795
+ unit: { required: true, coverage: 100 },
3796
+ integration: { required: false, coverage: 50 },
3797
+ e2e: { required: true },
3798
+ };
3799
+ const result = resolveTestingRequirements("feature", undefined, override);
3800
+ result.unit!.coverage; // → 100
3801
+ result.integration!.coverage; // → 50
3802
+ result.integration!.required; // → false
3803
+ result.e2e!.required; // → true
3804
+ });
3805
+
3806
+ it("unknown category with risk modifier uses feature fallback", () => {
3807
+ const result = resolveTestingRequirements("unknown", "critical");
3808
+ // feature defaults: unit 85, integration 70, e2e false
3809
+ // critical: +5 unit, +10 integration, e2e required
3810
+ result.unit!.coverage; // → 90
3811
+ result.integration!.coverage; // → 80
3812
+ result.e2e!.required; // → true
3813
+ });
3814
+
3815
+ it("deep-merges partial override so defaults are preserved", () => {
3816
+ // Override only sets covered/file, should retain required/coverage from defaults
3817
+ const override: TestingRequirements = {
3818
+ unit: { required: true, covered: true, file: "signal.test.ts" },
3819
+ };
3820
+ const result = resolveTestingRequirements("feature", undefined, override);
3821
+ // Default feature unit: required=true, coverage=85
3822
+ result.unit!.required; // → true
3823
+ result.unit!.covered; // → true
3824
+ result.unit!.file; // → "signal.test.ts"
3825
+ expect(result.unit!.coverage).toBe(85); // retained from defaults
3826
+ // Integration should still have defaults
3827
+ result.integration!.required; // → true
3828
+ result.integration!.coverage; // → 70
3829
+ });
3830
+ });
3831
+ ```
3832
+
3833
+ **computeTestingStatus**
3834
+
3835
+ ```typescript
3836
+ describe("computeTestingStatus", () => {
3837
+ it("returns 'Complete' when all required levels are covered", () => {
3838
+ const reqs: TestingRequirements = {
3839
+ unit: { required: true, covered: true },
3840
+ integration: { required: true, covered: true },
3841
+ e2e: { required: false },
3842
+ };
3843
+ computeTestingStatus(reqs); // → "Complete"
3844
+ });
3845
+
3846
+ it("returns 'Missing unit' when unit is required but not covered", () => {
3847
+ const reqs: TestingRequirements = {
3848
+ unit: { required: true, covered: false },
3849
+ integration: { required: true, covered: true },
3850
+ e2e: { required: false },
3851
+ };
3852
+ computeTestingStatus(reqs); // → "Missing unit"
3853
+ });
3854
+
3855
+ it("returns 'Missing integration' when integration is required but not covered", () => {
3856
+ const reqs: TestingRequirements = {
3857
+ unit: { required: true, covered: true },
3858
+ integration: { required: true, covered: false },
3859
+ e2e: { required: false },
3860
+ };
3861
+ computeTestingStatus(reqs); // → "Missing integration"
3862
+ });
3863
+
3864
+ it("returns 'Missing e2e' when e2e is required but not covered", () => {
3865
+ const reqs: TestingRequirements = {
3866
+ unit: { required: true, covered: true },
3867
+ integration: { required: true, covered: true },
3868
+ e2e: { required: true, covered: false },
3869
+ };
3870
+ computeTestingStatus(reqs); // → "Missing e2e"
3871
+ });
3872
+
3873
+ it("lists multiple missing levels", () => {
3874
+ const reqs: TestingRequirements = {
3875
+ unit: { required: true, covered: false },
3876
+ integration: { required: true, covered: false },
3877
+ e2e: { required: false },
3878
+ };
3879
+ const status = computeTestingStatus(reqs);
3880
+ status; // → "unit"
3881
+ status; // → "integration"
3882
+ });
3883
+
3884
+ it("returns 'N/A' when no levels are defined", () => {
3885
+ const reqs: TestingRequirements = {};
3886
+ computeTestingStatus(reqs); // → "N/A"
3887
+ });
3888
+
3889
+ it("returns 'Complete' when required levels are covered and e2e is optional", () => {
3890
+ const reqs: TestingRequirements = {
3891
+ unit: { required: true, covered: true },
3892
+ e2e: { required: false, covered: false },
3893
+ };
3894
+ computeTestingStatus(reqs); // → "Complete"
3895
+ });
3896
+
3897
+ it("lists all three levels when all missing", () => {
3898
+ const reqs: TestingRequirements = {
3899
+ unit: { required: true, covered: false },
3900
+ integration: { required: true, covered: false },
3901
+ e2e: { required: true, covered: false },
3902
+ };
3903
+ computeTestingStatus(reqs); // → "Missing unit, integration, e2e"
3904
+ });
3905
+
3906
+ it("ignores levels with required: false even when not covered", () => {
3907
+ const reqs: TestingRequirements = {
3908
+ unit: { required: false, covered: false },
3909
+ integration: { required: false, covered: false },
3910
+ };
3911
+ computeTestingStatus(reqs); // → "Complete"
3912
+ });
3913
+ });
3914
+ ```
3915
+
3916
+ **extractTypesFromFile**
3917
+
3918
+ ```typescript
3919
+ describe("extractTypesFromFile", () => {
3920
+ it("extracts an exported interface", () => {
3921
+ const code = `
3922
+ export interface Foo {
3923
+ bar: string;
3924
+ baz?: number;
3925
+ }`;
3926
+ const types = extractTypesFromFile("test.ts", code);
3927
+ types; // → 1
3928
+ types[0].name; // → "Foo"
3929
+ types[0].kind; // → "interface"
3930
+ types[0].exported; // → true
3931
+ types[0].members; // → 2
3932
+ expect(types[0].members[0]).toMatchObject({
3933
+ name: "bar",
3934
+ type: "string",
3935
+ optional: false,
3936
+ });
3937
+ expect(types[0].members[1]).toMatchObject({
3938
+ name: "baz",
3939
+ type: "number",
3940
+ optional: true,
3941
+ });
3942
+ });
3943
+
3944
+ it("extracts a non-exported interface", () => {
3945
+ const code = `interface Internal { x: number; }`;
3946
+ const types = extractTypesFromFile("test.ts", code);
3947
+ types[0].exported; // → false
3948
+ });
3949
+
3950
+ it("extracts interface with generics and extends", () => {
3951
+ const code = `
3952
+ export interface Container<T extends Base> extends Parent {
3953
+ value: T;
3954
+ }`;
3955
+ const types = extractTypesFromFile("test.ts", code);
3956
+ types[0].generics; // → ["T extends Base"]
3957
+ types[0].extends; // → ["Parent"]
3958
+ });
3959
+
3960
+ it("extracts interface with method signatures", () => {
3961
+ const code = `
3962
+ export interface Handler {
3963
+ handle(req: Request): Response;
3964
+ optional?(): void;
3965
+ }`;
3966
+ const types = extractTypesFromFile("test.ts", code);
3967
+ types[0].members; // → 2
3968
+ types[0].members[0].name; // → "handle"
3969
+ types[0].members[0].type; // → "method"
3970
+ types[0].members[1].optional; // → true
3971
+ });
3972
+
3973
+ it("extracts interface with readonly and index signatures", () => {
3974
+ const code = `
3975
+ export interface Config {
3976
+ readonly name: string;
3977
+ [key: string]: unknown;
3978
+ }`;
3979
+ const types = extractTypesFromFile("test.ts", code);
3980
+ types[0].members[0].readonly; // → true
3981
+ types[0].members[1].name; // → "[index]"
3982
+ });
3983
+
3984
+ it("extracts a type alias with object literal", () => {
3985
+ const code = `
3986
+ export type Options = {
3987
+ verbose: boolean;
3988
+ count?: number;
3989
+ };`;
3990
+ const types = extractTypesFromFile("test.ts", code);
3991
+ types[0].kind; // → "type"
3992
+ types[0].name; // → "Options"
3993
+ types[0].members; // → 2
3994
+ });
3995
+
3996
+ it("extracts a union type alias as __type member", () => {
3997
+ const code = `export type Status = "active" | "inactive";`;
3998
+ const types = extractTypesFromFile("test.ts", code);
3999
+ types[0].kind; // → "type"
4000
+ types[0].members; // → 1
4001
+ types[0].members[0].name; // → "__type"
4002
+ types[0].members[0].type; // → "active"
4003
+ });
4004
+
4005
+ it("extracts a type alias with generics", () => {
4006
+ const code = `export type Wrapper<T> = { inner: T };`;
4007
+ const types = extractTypesFromFile("test.ts", code);
4008
+ types[0].generics; // → ["T"]
4009
+ });
4010
+
4011
+ it("extracts an enum", () => {
4012
+ const code = `
4013
+ export enum Direction {
4014
+ Up = "UP",
4015
+ Down = "DOWN",
4016
+ Left,
4017
+ }`;
4018
+ const types = extractTypesFromFile("test.ts", code);
4019
+ types[0].kind; // → "enum"
4020
+ types[0].name; // → "Direction"
4021
+ types[0].members; // → 3
4022
+ expect(types[0].members[0]).toMatchObject({
4023
+ name: "Up",
4024
+ type: '"UP"',
4025
+ readonly: true,
4026
+ });
4027
+ expect(types[0].members[2].type).toBe("auto"); // no initializer
4028
+ });
4029
+
4030
+ it("extracts a class with public members only", () => {
4031
+ const code = `
4032
+ export class MyService {
4033
+ public name: string = "";
4034
+ private secret: string = "";
4035
+ protected internal: number = 0;
4036
+
4037
+ constructor() {}
4038
+
4039
+ public greet(): string { return this.name; }
4040
+ private hash(): void {}
4041
+ protected compute(): void {}
4042
+
4043
+ get value(): number { return 1; }
4044
+ set value(v: number) {}
4045
+ }`;
4046
+ const types = extractTypesFromFile("test.ts", code);
4047
+ types[0].kind; // → "class"
4048
+ types[0].exported; // → true
4049
+
4050
+ const memberNames = types[0].members.map((m) => m.name);
4051
+ // Should include: name, greet, value (getter), value (setter)
4052
+ memberNames; // → "name"
4053
+ memberNames; // → "greet"
4054
+ memberNames; // → "value"
4055
+ // Should NOT include: secret, internal, hash, compute
4056
+ memberNames; // → "secret"
4057
+ memberNames; // → "internal"
4058
+ memberNames; // → "hash"
4059
+ memberNames; // → "compute"
4060
+
4061
+ // Method signatures should not include the body
4062
+ const greet = types[0].members.find((m) => m.name === "greet");
4063
+ greet?.signature; // → "greet(): string"
4064
+ greet?.signature; // → "return"
4065
+
4066
+ // Getter/setter signatures should be declaration-only
4067
+ const getter = types[0].members.find((m) => m.name === "value" && m.type === "getter");
4068
+ getter?.signature; // → "get value(): number"
4069
+ const setter = types[0].members.find((m) => m.name === "value" && m.type === "setter");
4070
+ setter?.signature; // → "set value(v: number)"
4071
+ });
4072
+
4073
+ it("skips ECMAScript #private fields", () => {
4074
+ const code = `
4075
+ export class Vault {
4076
+ #data: string = "";
4077
+ public label: string = "";
4078
+ #compute(): void {}
4079
+ }`;
4080
+ const types = extractTypesFromFile("test.ts", code);
4081
+ const memberNames = types[0].members.map((m) => m.name);
4082
+ memberNames; // → ["label"]
4083
+ });
4084
+
4085
+ it("extracts class with generics, extends, and implements", () => {
4086
+ const code = `
4087
+ export class Store<T> extends BaseStore implements Disposable {
4088
+ public items: T[] = [];
4089
+ }`;
4090
+ const types = extractTypesFromFile("test.ts", code);
4091
+ types[0].generics; // → ["T"]
4092
+ types[0].extends; // → ["BaseStore"]
4093
+ types[0].implements; // → ["Disposable"]
4094
+ });
4095
+
4096
+ it("extracts JSDoc description from types", () => {
4097
+ const code = `
4098
+ /** A documented interface */
4099
+ export interface Documented {
4100
+ value: string;
4101
+ }`;
4102
+ const types = extractTypesFromFile("test.ts", code);
4103
+ types[0].jsdoc; // → defined
4104
+ types[0].jsdoc!.description; // → "A documented interface"
4105
+ });
4106
+
4107
+ it("extracts JSDoc @param and @returns tags", () => {
4108
+ const code = `
4109
+ /**
4110
+ * A service class
4111
+ * @param config - The configuration
4112
+ * @returns The instance
4113
+ * @since 1.0.0
4114
+ * @deprecated Use NewService instead
4115
+ */
4116
+ export interface Service {
4117
+ run(): void;
4118
+ }`;
4119
+ const types = extractTypesFromFile("test.ts", code);
4120
+ const jsdoc = types[0].jsdoc!;
4121
+ jsdoc.params; // → 1
4122
+ jsdoc.params[0].name; // → "config"
4123
+ jsdoc.returns?.description; // → "The instance"
4124
+ jsdoc.since; // → "1.0.0"
4125
+ jsdoc.deprecated; // → truthy
4126
+ });
4127
+
4128
+ it("extracts multiple types from a single file", () => {
4129
+ const code = `
4130
+ export interface A { x: number; }
4131
+ export type B = string;
4132
+ export enum C { X, Y }
4133
+ export class D { public z = 1; }`;
4134
+ const types = extractTypesFromFile("test.ts", code);
4135
+ types; // → 4
4136
+ types.map((t) => t.name); // → ["A", "B", "C", "D"]
4137
+ types.map((t) => t.kind); // → ["interface", "type", "enum", "class"]
4138
+ });
4139
+
4140
+ it("returns empty array for files with no type definitions", () => {
4141
+ const code = `const x = 5; function foo() { return x; }`;
4142
+ const types = extractTypesFromFile("test.ts", code);
4143
+ types; // → []
4144
+ });
4145
+
4146
+ it("provides correct source locations", () => {
4147
+ const code = `\nexport interface Foo {\n bar: string;\n}\n`;
4148
+ const types = extractTypesFromFile("test.ts", code);
4149
+ types[0].location.file; // → "test.ts"
4150
+ types[0].location.line; // → 2
4151
+ });
4152
+ });
4153
+ ```
4154
+
4155
+ **getTypeSignature**
4156
+
4157
+ ```typescript
4158
+ describe("getTypeSignature", () => {
4159
+ it("generates interface signature", () => {
4160
+ const sig = getTypeSignature({
4161
+ name: "Foo",
4162
+ kind: "interface",
4163
+ exported: true,
4164
+ generics: ["T"],
4165
+ extends: ["Bar"],
4166
+ members: [],
4167
+ location: { file: "test.ts", line: 1 },
4168
+ });
4169
+ sig; // → "export interface Foo<T> extends Bar"
4170
+ });
4171
+
4172
+ it("generates type signature", () => {
4173
+ const sig = getTypeSignature({
4174
+ name: "Options",
4175
+ kind: "type",
4176
+ exported: true,
4177
+ generics: ["K", "V"],
4178
+ members: [],
4179
+ location: { file: "test.ts", line: 1 },
4180
+ });
4181
+ sig; // → "export type Options<K, V>"
4182
+ });
4183
+
4184
+ it("generates enum signature", () => {
4185
+ const sig = getTypeSignature({
4186
+ name: "Status",
4187
+ kind: "enum",
4188
+ exported: false,
4189
+ members: [],
4190
+ location: { file: "test.ts", line: 1 },
4191
+ });
4192
+ sig; // → "enum Status"
4193
+ });
4194
+
4195
+ it("generates class signature with implements", () => {
4196
+ const sig = getTypeSignature({
4197
+ name: "Service",
4198
+ kind: "class",
4199
+ exported: true,
4200
+ extends: ["Base"],
4201
+ implements: ["Disposable", "Serializable"],
4202
+ members: [],
4203
+ location: { file: "test.ts", line: 1 },
4204
+ });
4205
+ expect(sig).toBe(
4206
+ "export class Service extends Base implements Disposable, Serializable"
4207
+ );
4208
+ });
4209
+ });
4210
+ ```
4211
+
4212
+ **generateMethodTable**
4213
+
4214
+ ```typescript
4215
+ describe("generateMethodTable", () => {
4216
+ it("returns empty string for no members", () => {
4217
+ expect(
4218
+ generateMethodTable({
4219
+ name: "Empty",
4220
+ kind: "interface",
4221
+ exported: true,
4222
+ members: [],
4223
+ location: { file: "test.ts", line: 1 },
4224
+ })
4225
+ ).toBe("");
4226
+ });
4227
+
4228
+ it("generates markdown table", () => {
4229
+ const table = generateMethodTable({
4230
+ name: "Foo",
4231
+ kind: "interface",
4232
+ exported: true,
4233
+ members: [
4234
+ { name: "bar", type: "string", optional: false, readonly: false, description: "The bar" },
4235
+ { name: "baz", type: "number", optional: true, readonly: false },
4236
+ { name: "id", type: "string", optional: false, readonly: true },
4237
+ ],
4238
+ location: { file: "test.ts", line: 1 },
4239
+ });
4240
+
4241
+ table; // → "| Member | Type | Description |"
4242
+ table; // → "| `bar` | `string` | The bar |"
4243
+ table; // → "| `baz?` | `number` | |"
4244
+ table; // → "| `readonly id` | `string` | |"
4245
+ });
4246
+
4247
+ it("renders both readonly and optional when both are set", () => {
4248
+ const table = generateMethodTable({
4249
+ name: "Foo",
4250
+ kind: "interface",
4251
+ exported: true,
4252
+ members: [
4253
+ { name: "version", type: "string", optional: true, readonly: true },
4254
+ ],
4255
+ location: { file: "test.ts", line: 1 },
4256
+ });
4257
+
4258
+ table; // → "| `readonly version?` | `string` | |"
4259
+ });
4260
+
4261
+ it("escapes pipes and newlines in descriptions", () => {
4262
+ const table = generateMethodTable({
4263
+ name: "Foo",
4264
+ kind: "interface",
4265
+ exported: true,
4266
+ members: [
4267
+ { name: "bar", type: "string", optional: false, readonly: false, description: "A | B choice" },
4268
+ { name: "baz", type: "number", optional: false, readonly: false, description: "Line1\nLine2" },
4269
+ ],
4270
+ location: { file: "test.ts", line: 1 },
4271
+ });
4272
+
4273
+ table; // → "A \\| B choice"
4274
+ table; // → "Line1\n"
4275
+ table; // → "Line1<br/>Line2"
4276
+ });
4277
+
4278
+ it("uses signature over type when available", () => {
4279
+ const table = generateMethodTable({
4280
+ name: "Foo",
4281
+ kind: "interface",
4282
+ exported: true,
4283
+ members: [
4284
+ {
4285
+ name: "handle",
4286
+ type: "method",
4287
+ optional: false,
4288
+ readonly: false,
4289
+ signature: "handle(req: Request): Response",
4290
+ },
4291
+ ],
4292
+ location: { file: "test.ts", line: 1 },
4293
+ });
4294
+
4295
+ table; // → "handle(req: Request): Response"
4296
+ });
4297
+ });
4298
+ ```
4299
+ <!-- @codex:end -->
4300
+
4301
+ ## Testing
4302
+
4303
+ <!-- @codex:auto testing="pithy.codex.**" -->
4304
+ | Feature | Unit | Integration | E2E | Status |
4305
+ | --- | --- | --- | --- | --- |
4306
+ | parseCodexAnnotations | ✓ | - | - | Missing integration |
4307
+ | parseCodexApiAnnotations | ✓ | - | - | Missing integration |
4308
+ | createSourceDigest | ✓ | - | - | Missing integration |
4309
+ | createCanonicalFingerprint | ✓ | - | - | Missing integration |
4310
+ | discoverComponentsFromFile | ✓ | - | - | Missing integration |
4311
+ | validateAnnotation | ✓ | - | - | Missing integration |
4312
+ | CHANGED_ONLY_PATHSPEC | - | - | - | N/A |
4313
+ | loadEnv | - | - | - | Missing unit, integration |
4314
+ | askLLM | - | - | - | Missing unit, integration |
4315
+ | scan | - | - | - | Missing unit, integration |
4316
+ | combineFingerprints | - | - | - | Missing unit, integration |
4317
+ | ApiSig | - | - | - | Missing unit, integration |
4318
+ | codexSchema | - | - | - | Missing unit, integration |
4319
+ | CodexEntry | - | - | - | Missing unit, integration |
4320
+ | consumeValue | ✓ | - | - | Missing integration, e2e |
4321
+ | parseArgs | ✓ | - | - | Missing integration, e2e |
4322
+ | validatePath | ✓ | - | - | Missing integration, e2e |
4323
+ | validatePath | ✓ | - | - | Missing integration |
4324
+ | runSyncPipeline | - | - | - | Missing unit, integration |
4325
+ | validateExampleSyntax | - | - | - | Missing unit, integration |
4326
+ | validateExtractionResult | - | - | - | Missing unit, integration |
4327
+ | formatValidationReport | - | - | - | Missing unit, integration |
4328
+ | createApiSnapshot | ✓ | - | - | Missing integration |
4329
+ | snapshotFromTypeDefinitions | ✓ | - | - | Missing integration |
4330
+ | diffSnapshots | ✓ | - | - | Missing integration |
4331
+ | classifyChange | ✓ | - | - | Missing integration |
4332
+ | detectBreakingChanges | ✓ | - | - | Missing integration |
4333
+ | generateMigrationGuide | ✓ | - | - | Missing integration |
4334
+ | generateChangelog | ✓ | - | - | Missing integration |
4335
+ | extractExamples | - | - | - | Missing unit, integration |
4336
+ | validateExample | - | - | - | Missing unit, integration |
4337
+ | wrapDoctestInHarness | - | - | - | Missing unit, integration |
4338
+ | extractExamplesFromSource | - | - | - | Missing unit, integration |
4339
+ | generateRunnableExampleFile | - | - | - | Missing unit, integration |
4340
+ | parseJSDocComment | - | - | - | Missing unit, integration |
4341
+ | extractJSDocBlocks | - | - | - | Missing unit, integration |
4342
+ | linkJSDocToDeclarations | - | - | - | Missing unit, integration |
4343
+ | extractSignature | - | - | - | Missing unit, integration |
4344
+ | runExtractionPipeline | - | - | - | Missing unit, integration |
4345
+ | extractSingleFile | - | - | - | Missing unit, integration |
4346
+ | generateExtractionReport | - | - | - | Missing unit, integration |
4347
+ | exportToCodexFormat | - | - | - | Missing unit, integration |
4348
+ | parseMarkerAttributes | - | - | - | Missing unit, integration |
4349
+ | resolveMarkerType | - | - | - | Missing unit, integration |
4350
+ | parseReadmeMarkers | - | - | - | Missing unit, integration |
4351
+ | matchIdPattern | - | - | - | Missing unit, integration |
4352
+ | filterByPattern | - | - | - | Missing unit, integration |
4353
+ | stripTestBoilerplate | - | - | - | Missing unit, integration |
4354
+ | generateInstallContent | - | - | - | Missing unit, integration |
4355
+ | generateExamplesContent | - | - | - | Missing unit, integration |
4356
+ | escapeTableCell | - | - | - | Missing unit, integration |
4357
+ | escapeMd | - | - | - | Missing unit, integration |
4358
+ | sanitizeForInlineCode | - | - | - | Missing unit, integration |
4359
+ | longestBacktickRun | - | - | - | Missing unit, integration |
4360
+ | sanitizeLanguage | - | - | - | Missing unit, integration |
4361
+ | generateApiContent | - | - | - | Missing unit, integration |
4362
+ | generateTestingContent | - | - | - | Missing unit, integration |
4363
+ | generateBundleContent | - | - | - | Missing unit, integration |
4364
+ | syncReadmeContent | - | - | - | Missing unit, integration |
4365
+ | syncAllReadmes | - | - | - | Missing unit, integration |
4366
+ | getSnapshotPath | ✓ | - | - | Missing integration |
4367
+ | saveSnapshot | ✓ | - | - | Missing integration |
4368
+ | loadSnapshot | ✓ | - | - | Missing integration |
4369
+ | listSnapshots | ✓ | - | - | Missing integration |
4370
+ | DeclarationLocation | - | - | - | Missing unit, integration |
4371
+ | findDeclarationLocation | - | - | - | Missing unit, integration |
4372
+ | extractAllDeclarations | - | - | - | Missing unit, integration |
4373
+ | parseReadmeSections | - | - | - | Missing unit, integration |
4374
+ | linkReadmeToEntries | - | - | - | Missing unit, integration |
4375
+ | generateSourceLink | - | - | - | Missing unit, integration |
4376
+ | toRelativePath | - | - | - | Missing unit, integration |
4377
+ | enrichSourceLocation | - | - | - | Missing unit, integration |
4378
+ | extractTestExamples | ✓ | - | - | Missing integration |
4379
+ | parseVitestOutput | ✓ | - | - | Missing integration |
4380
+ | matchExamplesToStatuses | ✓ | - | - | Missing integration |
4381
+ | generateExampleReport | ✓ | - | - | Missing integration |
4382
+ | validateTestExamples | ✓ | - | - | Missing integration |
4383
+ | extractTestCases | - | - | - | Missing unit, integration |
4384
+ | extractTestImports | - | - | - | Missing unit, integration |
4385
+ | identifyCoveredApis | - | - | - | Missing unit, integration |
4386
+ | analyzeTestFile | - | - | - | Missing unit, integration |
4387
+ | testCasesToReferences | - | - | - | Missing unit, integration |
4388
+ | matchTestToEntry | - | - | - | Missing unit, integration |
4389
+ | generateCoverageSummary | - | - | - | Missing unit, integration |
4390
+ | getTestingDefaults | ✓ | - | - | Missing integration |
4391
+ | applyRiskModifiers | ✓ | - | - | Missing integration |
4392
+ | resolveTestingRequirements | ✓ | - | - | Missing integration |
4393
+ | computeTestingStatus | ✓ | - | - | Missing integration |
4394
+ | extractTypesFromFile | ✓ | - | - | Missing integration |
4395
+ | getTypeSignature | ✓ | - | - | Missing integration |
4396
+ | generateMethodTable | ✓ | - | - | Missing integration |
4397
+ <!-- @codex:end -->
4398
+
4399
+ ## License
4400
+
4401
+ MIT