From ae2968ffc07bb295910bf616e3e0912a2acacfd5 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:20:36 +0000 Subject: [PATCH 1/8] chore(ai): add development guide for @targetd project --- .github/copilot-instructions.md | 264 ++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..6b7d8160 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,264 @@ +# @targetd Development Guide + +## Project Overview + +This is a **Deno-first monorepo** for a type-safe targeting and feature flag +system. The architecture enables dynamic content delivery based on query +conditions, with packages distributed via JSR (JavaScript Registry). + +**Core concept**: Store flat key-value rules with targeting conditions, query at +runtime to get the matching payload. Think feature flags meets content +management. + +## Architecture + +### Package Structure + +- **@targetd/api** - Core in-memory data store with targeting engine. All other + packages depend on this. +- **@targetd/server** - Express-based HTTP server exposing api via REST + endpoints +- **@targetd/client** - Type-safe HTTP client for querying server instances +- **@targetd/fs** - File-based rule loading (JSON/YAML) with hot-reloading +- **@targetd/explode** - Utility to transform flat keys (`app.title`) into + nested objects +- **@targetd/date-range** - Built-in targeting descriptor for date range + matching +- **@targetd/json-schema** - JSON Schema generation from Zod schemas + +### Key Patterns + +**Targeting Descriptors**: Objects with `predicate`, `queryParser`, and +`targetingParser` that define how to match query values against targeting rules. +See +[createTargetingDescriptor.ts](packages/api/src/createTargetingDescriptor.ts) +and built-in predicates in +[packages/api/src/predicates/](packages/api/src/predicates/). + +**PromisedData Pattern**: The `Data.create()` builder uses +[PromisedData](packages/api/src/PromisedData.ts) to enable fluent async +configuration before resolving to a `Data` instance. + +**Type Inference**: Heavy use of TypeScript's type system with Zod schemas. The +`Data` class uses complex type inference (`DT.Meta`) to ensure payloads, +targeting, and queries are type-safe across the API. + +### Creating Custom Targeting Descriptors + +Targeting descriptors define how query parameters match against targeting rules. +Create custom ones for domain-specific logic: + +```typescript +import { createTargetingDescriptor } from '@targetd/api' +import { z } from 'zod' + +// Simple equality check with optional query +const customEquals = createTargetingDescriptor({ + predicate: (query) => (target) => !query || query === target, + queryParser: z.string(), + targetingParser: z.string(), + requiresQuery: false, // Evaluates even without query parameter +}) + +// Complex async predicate (e.g., database lookup) +const userSegment = createTargetingDescriptor({ + predicate: (userId) => async (segment) => { + if (!userId) return false + const user = await fetchUser(userId) + return user.segments.includes(segment) + }, + queryParser: z.string(), + targetingParser: z.string(), +}) + +// Multiple value matching with negation +const platformMatch = { + predicate: (query) => (targets) => { + if (!query) return true + return targets.some((t) => + t.startsWith('!') ? t.slice(1) !== query : t === query + ) + }, + queryParser: z.string(), + targetingParser: z.array(z.string()), +} +``` + +See [@targetd/date-range](packages/date-range/src/index.ts) for a real-world +example that evaluates against current time when no query is provided. + +## Development Workflow + +### Commands + +```bash +# Run all tests (from root) +deno task test + +# Run tests in watch mode +deno task test:dev + +# Type check all packages +deno task check + +# Format code +deno fmt + +# Lint code +deno lint + +# Run package-specific tests +cd packages/api && deno task test +``` + +### Testing + +- Uses **Deno's built-in test runner** (`Deno.test`) +- Snapshot testing with `@std/testing/snapshot` +- Tests live in `test/` directories alongside `src/` +- Async predicates are common - see + [Data.test.ts](packages/api/test/Data.test.ts#L16-L20) for examples + +### Adding New Packages + +1. Create `packages/[name]/` directory +2. Add `deno.json` with package metadata, exports, and workspace imports +3. Add to workspace array in root `deno.json` +4. Add to `release-please-config.json` for automated releases +5. Add to `.release-please-manifest.json` with initial version (e.g., + `"packages/[name]": "0.0.0"`) +6. Structure: `src/index.ts` (exports), `test/*.test.ts`, `README.md`, + `LICENSE`, `CHANGELOG.md` + +## Important Conventions + +**Import Paths**: Use `.ts` extensions in imports, even though it's Deno. Use +workspace references like `@targetd/api` in package dependencies. + +**Zod Schemas**: Import from `zod/mini` for core schemas (faster), `zod/v4/core` +for types. See [packages/api/src/Data.ts](packages/api/src/Data.ts#L22-L25). + +**Exports**: Each package has `src/index.ts` as the main export in `deno.json`. +Re-export types with `export type *` and utilities appropriately. + +**Formatting**: Single quotes, no semicolons (enforced by `deno fmt` config). + +**Commits**: Conventional commits enforced via commitlint. Use `fix:`, `feat:`, +`chore:`, etc. + +## Release Process + +- **Automated via Release Please** on pushes to `master` +- Each package has independent versioning +- Tags follow pattern: `@targetd/[package]-v[version]` +- Published to **JSR** (not npm), accessible via `jsr:@targetd/[package]` +- GitHub Actions workflow: test → release-please → publish to JSR + +## Common Pitfalls + +1. **Targeting predicates must handle undefined queries**: When query param is + missing, predicate still evaluates. Return appropriate default (often `true` + or `false`). + +2. **Rules order matters**: First matching rule wins. Always add fallback rules + (no targeting) last. + +3. **Type inference can be tricky**: The `Data<$>` generic carries + payload/targeting/query types. Use helper types in `types/` directories when + extending. + +4. **File-based rules structure**: Files must export objects where keys are + payload names containing `rules` arrays. See + [packages/fs/test/fixtures/rules/](packages/fs/test/fixtures/rules/). + +5. **Express middleware order**: In `@targetd/server`, query casting happens + before error handling. Custom middleware should follow this pattern. + +## Debugging Type Inference Issues + +The `Data<$>` generic uses complex type inference that can sometimes be +challenging: + +**Strategy 1: Extract intermediate types** + +```typescript +// Instead of chaining everything +const data = await Data.create() + .usePayload({ foo: z.string() }) + .useTargeting({ bar: targetIncludes(z.string()) }) + .addRules('foo', rules) + +// Break it down to inspect types +const step1 = Data.create() +const step2 = step1.usePayload({ foo: z.string() }) +const step3 = step2.useTargeting({ bar: targetIncludes(z.string()) }) +type Step3Type = Awaited // Inspect in IDE +``` + +**Strategy 2: Use helper types from `types/` directories** + +```typescript +import type { DT, PT, QT, TT } from '@targetd/api' + +// Access inferred types explicitly +type MyPayloads = PT.InferPayloads +type MyQuery = QT.InferQuery +type MyTargeting = TT.InferTargeting +``` + +**Strategy 3: Check Zod import paths** + +- Use `zod/mini` for schemas: `import { z } from 'zod/mini'` +- Use `zod/v4/core` for types: `import type { output } from 'zod/v4/core'` +- Mixing imports can break type inference + +**Strategy 4: Verify predicate signatures** Predicates must return +`boolean | Promise`. Async predicates need `await` or return promises: + +```typescript +// ✅ Correct +predicate: ; +;((q) => (t) => q === t || fetchValue(q, t)) // Returns Promise +predicate: ; +;((q) => async (t) => await check(q, t)) // Returns Promise + +// ❌ Wrong +predicate: ; +;(async (q) => (t) => check(q, t)) // Outer async breaks signature +``` + +**Common error: "Type instantiation is excessively deep"** + +- Usually means Zod schema is too complex or circular +- Break complex schemas into smaller named schemas +- Check for accidental circular references in targeting descriptors + +## External Dependencies + +- **Zod v4** - Schema validation and type inference (core dependency) +- **Express** - HTTP server (server package only) +- **@johngw/fs** - Author's file system utilities (fs package) +- **fast-deep-equal** - Deep equality checks in targeting +- **YAML** - File format support in fs loader + +## Quick Reference + +**Add a new targeting predicate**: Create in `packages/api/src/predicates/`, +follow pattern from +[targetIncludes.ts](packages/api/src/predicates/targetIncludes.ts). + +**Add middleware to server**: See +[packages/server/src/middleware/](packages/server/src/middleware/) for examples. + +**Debug targeting**: Use `data.getMatchingItem()` to see which rule matched and +access metadata. + +**Test async predicates**: Wrap predicates that return promises in `await` or +use `setTimeout` helper - see +[Data.test.ts](packages/api/test/Data.test.ts#L16-L20). + +**Transform flat keys to nested**: Use `@targetd/explode` with `.` separator to +convert `app.title` → `{ app: { title: ... } }`. + +**Type-safe client queries**: Pass your `Data` instance to `Client` constructor +for full type inference on queries and responses. From d97777c42ada7a1ea4e56b65038674c9d7d7bf22 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:30:02 +0000 Subject: [PATCH 2/8] docs: update zod imports for targeting descriptor query and targeting parsers --- .github/copilot-instructions.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6b7d8160..7ab8464d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -50,13 +50,13 @@ Create custom ones for domain-specific logic: ```typescript import { createTargetingDescriptor } from '@targetd/api' -import { z } from 'zod' +import { array, string } from 'zod/mini' // Simple equality check with optional query const customEquals = createTargetingDescriptor({ predicate: (query) => (target) => !query || query === target, - queryParser: z.string(), - targetingParser: z.string(), + queryParser: string(), + targetingParser: string(), requiresQuery: false, // Evaluates even without query parameter }) @@ -67,8 +67,8 @@ const userSegment = createTargetingDescriptor({ const user = await fetchUser(userId) return user.segments.includes(segment) }, - queryParser: z.string(), - targetingParser: z.string(), + queryParser: string(), + targetingParser: string(), }) // Multiple value matching with negation @@ -79,8 +79,8 @@ const platformMatch = { t.startsWith('!') ? t.slice(1) !== query : t === query ) }, - queryParser: z.string(), - targetingParser: z.array(z.string()), + queryParser: string(), + targetingParser: array(string()), } ``` From 866ff4aa6377df075bfb2333c94811c4592fe035 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:31:17 +0000 Subject: [PATCH 3/8] docs: rephrasing to incoorporate requiresquery Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6b7d8160..c9e2cf81 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -156,9 +156,10 @@ Re-export types with `export type *` and utilities appropriately. ## Common Pitfalls -1. **Targeting predicates must handle undefined queries**: When query param is - missing, predicate still evaluates. Return appropriate default (often `true` - or `false`). +1. **Targeting predicates may receive undefined queries**: Predicates can see + `undefined` when `requiresQuery: false` or when a query key is present but + parsed/cast to `undefined` (for example, an empty string). Return an + appropriate default (often `true` or `false`) in these cases. 2. **Rules order matters**: First matching rule wins. Always add fallback rules (no targeting) last. From c330a191bf490763019848282916ae78173f2db0 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:33:43 +0000 Subject: [PATCH 4/8] docs: remove outdated strategy for verifying predicate signatures in copilot instructions --- .github/copilot-instructions.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7ab8464d..240285f5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -212,21 +212,6 @@ type MyTargeting = TT.InferTargeting - Use `zod/v4/core` for types: `import type { output } from 'zod/v4/core'` - Mixing imports can break type inference -**Strategy 4: Verify predicate signatures** Predicates must return -`boolean | Promise`. Async predicates need `await` or return promises: - -```typescript -// ✅ Correct -predicate: ; -;((q) => (t) => q === t || fetchValue(q, t)) // Returns Promise -predicate: ; -;((q) => async (t) => await check(q, t)) // Returns Promise - -// ❌ Wrong -predicate: ; -;(async (q) => (t) => check(q, t)) // Outer async breaks signature -``` - **Common error: "Type instantiation is excessively deep"** - Usually means Zod schema is too complex or circular From 0c1db088abda7bbd36ba7d6344c342e395c9aa40 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:35:01 +0000 Subject: [PATCH 5/8] docs: remove debug targeting instructions from copilot guide This was an hallucination. --- .github/copilot-instructions.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 240285f5..ad940037 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -235,9 +235,6 @@ follow pattern from **Add middleware to server**: See [packages/server/src/middleware/](packages/server/src/middleware/) for examples. -**Debug targeting**: Use `data.getMatchingItem()` to see which rule matched and -access metadata. - **Test async predicates**: Wrap predicates that return promises in `await` or use `setTimeout` helper - see [Data.test.ts](packages/api/test/Data.test.ts#L16-L20). From da3772e02a6900abd4e52dcf4b81a901d8b1d58e Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:36:44 +0000 Subject: [PATCH 6/8] docs(copilot-instructions): consisten zod importing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f336fb43..06216034 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -135,8 +135,12 @@ cd packages/api && deno task test **Import Paths**: Use `.ts` extensions in imports, even though it's Deno. Use workspace references like `@targetd/api` in package dependencies. -**Zod Schemas**: Import from `zod/mini` for core schemas (faster), `zod/v4/core` -for types. See [packages/api/src/Data.ts](packages/api/src/Data.ts#L22-L25). +**Zod Schemas**: For new code and public examples, prefer `import { z } from 'zod'` +for consistency with the root README and package READMEs. Some internal, +performance-sensitive modules may instead use `zod/mini` for schemas and +`zod/v4/core` for types; when editing those files (e.g. +[packages/api/src/Data.ts](packages/api/src/Data.ts#L22-L25)), follow the +existing local style. **Exports**: Each package has `src/index.ts` as the main export in `deno.json`. Re-export types with `export type *` and utilities appropriately. From 554b23e7188fbf719287ea2bc1e1f9258d432abe Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:38:24 +0000 Subject: [PATCH 7/8] docs(copilot-instructions): correct test:dev description --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 06216034..c652d7d2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -95,7 +95,7 @@ example that evaluates against current time when no query is provided. # Run all tests (from root) deno task test -# Run tests in watch mode +# Run tests and potentially install development dependencies deno task test:dev # Type check all packages From ea0c212268f16831a44b20eea5e6062db958a525 Mon Sep 17 00:00:00 2001 From: John Wright Date: Tue, 24 Feb 2026 10:45:31 +0000 Subject: [PATCH 8/8] docs(copilot): add requiresQuery property to targeting descriptors --- .github/copilot-instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f336fb43..46332201 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -68,6 +68,7 @@ const userSegment = createTargetingDescriptor({ return user.segments.includes(segment) }, queryParser: string(), + requiresQuery: false, targetingParser: string(), }) @@ -80,6 +81,7 @@ const platformMatch = { ) }, queryParser: string(), + requiresQuery: false, targetingParser: array(string()), } ```