diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e8e130..66bd6c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,11 @@ jobs: - name: Test run: npm test + + - name: Install Next.js example + working-directory: examples/nextjs-basic + run: npm ci + + - name: Build Next.js example + working-directory: examples/nextjs-basic + run: npm run build diff --git a/.gitignore b/.gitignore index ede5259..2d8ebd3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules/ # build output dist/ coverage/ +.next/ +**/.next/ # env files .env diff --git a/README.md b/README.md index 681ee5e..1a75d99 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # @rlippmann/context-compiler TypeScript port of the Context Compiler core. +Deterministic control layer for LLM applications. +Compiles explicit user directives into authoritative context state before model calls. +Helps hosts enforce premise and policy guardrails consistently across turns. Reference implementation (Python): https://github.com/rlippmann/context-compiler @@ -31,6 +34,13 @@ Behavioral conformance is defined by the upstream Python fixture corpus and dire npm install @rlippmann/context-compiler ``` +## Examples + +- `examples/nextjs-basic/` — minimal Next.js App Router integration + - compiler-mediated request flow + - `clarify` blocks LLM calls + - per-session state via `exportJson()` / `importJson()` + ## Quick Start ```ts @@ -55,5 +65,7 @@ if (decision.kind === 'update') { - `engine.step(input)` -> apply one user input and return a `Decision`. - `engine.state` -> authoritative current state snapshot. - `engine.exportJson()` / `engine.importJson(payload)` -> state serialization utilities. +- Note: in 0.5.x, export/import serialize authoritative state only (`premise`, `policies`), not pending clarify/confirm interaction state. +- For stateless HTTP integrations, hosts must persist pending clarification context separately when needed; a checkpoint-style full-resume API is planned for 0.6. - `compile_transcript(messages)` -> replay user messages and return `state` or `confirm`. - `getPremiseValue(state)` / `getPolicyItems(state, value?)` -> read helpers for state. diff --git a/examples/06_transcript_replay.ts b/examples/06_transcript_replay.ts index f2418db..8ef1c04 100644 --- a/examples/06_transcript_replay.ts +++ b/examples/06_transcript_replay.ts @@ -1,4 +1,4 @@ -import { compile_transcript, createEngine, type TranscriptResult } from '../src/index.js'; +import { compile_transcript, createEngine, getPolicyItems, type TranscriptResult } from '../src/index.js'; declare const process: { argv: string[] }; @@ -51,11 +51,8 @@ export function runExample06(): { const currentReplay = applyTranscriptOnCurrentEngine(engine, transcript); const freshPolicies = - freshReplay.kind === 'state' ? Object.keys(freshReplay.state.policies).sort((a, b) => a.localeCompare(b)) : []; - const currentPolicies = - currentReplay.kind === 'state' - ? Object.keys(currentReplay.state.policies).sort((a, b) => a.localeCompare(b)) - : []; + freshReplay.kind === 'state' ? getPolicyItems(freshReplay.state) : []; + const currentPolicies = currentReplay.kind === 'state' ? getPolicyItems(currentReplay.state) : []; return { freshReplayKind: freshReplay.kind, diff --git a/examples/07_single_policy_correction.ts b/examples/07_single_policy_correction.ts index 1fc5d64..df52f9c 100644 --- a/examples/07_single_policy_correction.ts +++ b/examples/07_single_policy_correction.ts @@ -1,4 +1,4 @@ -import { createEngine } from '../src/index.js'; +import { createEngine, getPolicyItems } from '../src/index.js'; declare const process: { argv: string[] }; @@ -11,10 +11,12 @@ export function runExample07(): { const decision1 = engine.step('prohibit peanuts'); const decision2 = engine.step('remove policy peanuts'); const decision3 = engine.step('use peanuts'); + const useItems = getPolicyItems(engine.state, 'use'); + const prohibitItems = getPolicyItems(engine.state, 'prohibit'); return { stepKinds: [decision1.kind, decision2.kind, decision3.kind], - finalPolicy: engine.state.policies.peanuts ?? null + finalPolicy: useItems.includes('peanuts') ? 'use' : prohibitItems.includes('peanuts') ? 'prohibit' : null }; } diff --git a/examples/README.md b/examples/README.md index 36795b3..5e4629f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,6 +3,8 @@ TypeScript examples showing host-side usage of the Context Compiler core API. These examples target Python 0.5 semantic compatibility and only use core APIs. +In 0.5.x, `exportJson()` / `importJson()` persist authoritative state only (`premise`, `policies`) and do not persist pending clarify/confirm interaction state. +For stateless HTTP hosts, pending clarification context must be persisted separately if you need request-to-request clarify/confirm continuity. ## 01_persistent_guardrails.ts diff --git a/examples/nextjs-basic/README.md b/examples/nextjs-basic/README.md new file mode 100644 index 0000000..aa59fb6 --- /dev/null +++ b/examples/nextjs-basic/README.md @@ -0,0 +1,8 @@ +# Next.js Basic Integration + +This example uses `engine.exportJson()` / `engine.importJson()` for per-session persistence. +In 0.5.x, that payload stores authoritative state only (`premise` and `policies`), not pending clarify/confirm interaction state. +In stateless HTTP flows, a clarify on request N can lose pending confirmation context on request N+1 after restore. +That means follow-ups like `maybe` may not re-trigger the same clarify prompt unless the host persists pending interaction state separately. +A checkpoint-style resume API is planned for 0.6 to support full conversation resume. + diff --git a/examples/nextjs-basic/app/api/chat/route.ts b/examples/nextjs-basic/app/api/chat/route.ts new file mode 100644 index 0000000..614ef25 --- /dev/null +++ b/examples/nextjs-basic/app/api/chat/route.ts @@ -0,0 +1,125 @@ +import { createEngine, getPolicyItems, getPremiseValue, type EngineState } from '@rlippmann/context-compiler'; +import { loadSessionState, saveSessionState } from '../../../lib/context-sessions'; + +type ChatBody = { + sessionId: string; + input: string; + history?: Array<{ role: string; content: unknown }>; +}; + +type ChatResponse = + | { kind: 'clarify'; prompt_to_user: string | null } + | { kind: 'continue'; output: string }; + +function stateToSystemPrompt(state: EngineState): string { + const useItems = new Set(getPolicyItems(state, 'use')); + const policies = getPolicyItems(state) + .map((item) => `- ${useItems.has(item) ? 'USE' : 'PROHIBIT'}: ${item}`) + .join('\n'); + + return [ + 'You are an assistant operating under compiled context.', + '', + 'PREMISE:', + getPremiseValue(state) ?? '(none)', + '', + 'POLICIES:', + policies || '(none)', + '', + 'Follow these constraints strictly.' + ].join('\n'); +} + +function minimalRecentContext(history: ChatBody['history']) { + if (!history?.length) { + return []; + } + + return history + .filter( + (m): m is { role: 'user' | 'assistant'; content: string } => + (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string' + ) + .slice(-2) + .map((m) => ({ role: m.role, content: m.content })); +} + +export async function POST(req: Request): Promise { + const { sessionId, input, history }: ChatBody = await req.json(); + + if (!sessionId || !input) { + return Response.json({ error: 'sessionId and input are required' }, { status: 400 }); + } + + const engine = createEngine(); + const saved = loadSessionState(sessionId); + + if (saved) { + engine.importJson(saved); + } else if (history?.length) { + for (const m of history) { + if (m.role !== 'user' || typeof m.content !== 'string') { + continue; + } + + const d = engine.step(m.content); + if (d.kind === 'clarify') { + saveSessionState(sessionId, engine.exportJson()); + const payload: ChatResponse = { + kind: 'clarify', + prompt_to_user: d.prompt_to_user + }; + return Response.json(payload); + } + } + + saveSessionState(sessionId, engine.exportJson()); + } + + const decision = engine.step(input); + + if (decision.kind === 'clarify') { + saveSessionState(sessionId, engine.exportJson()); + const payload: ChatResponse = { + kind: 'clarify', + prompt_to_user: decision.prompt_to_user + }; + return Response.json(payload); + } + + saveSessionState(sessionId, engine.exportJson()); + + const usedReplay = !saved && !!history?.length; + const messages = [ + { role: 'system', content: stateToSystemPrompt(engine.state) }, + ...(usedReplay ? [] : minimalRecentContext(history)), + { role: 'user', content: input } + ]; + + const llmRes = await fetch(`${process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.OPENAI_API_KEY}` + }, + body: JSON.stringify({ + model: process.env.OPENAI_MODEL ?? 'gpt-4o-mini', + messages, + temperature: 0.2 + }) + }); + + if (!llmRes.ok) { + const text = await llmRes.text(); + return Response.json({ error: 'llm_failed', details: text }, { status: 502 }); + } + + const data = await llmRes.json(); + const output = data?.choices?.[0]?.message?.content ?? ''; + + const payload: ChatResponse = { + kind: 'continue', + output + }; + return Response.json(payload); +} diff --git a/examples/nextjs-basic/lib/context-sessions.ts b/examples/nextjs-basic/lib/context-sessions.ts new file mode 100644 index 0000000..9b454dc --- /dev/null +++ b/examples/nextjs-basic/lib/context-sessions.ts @@ -0,0 +1,9 @@ +const sessionState = new Map(); + +export function loadSessionState(sessionId: string): string | null { + return sessionState.get(sessionId) ?? null; +} + +export function saveSessionState(sessionId: string, exportedState: string): void { + sessionState.set(sessionId, exportedState); +} diff --git a/examples/nextjs-basic/next-env.d.ts b/examples/nextjs-basic/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/examples/nextjs-basic/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/nextjs-basic/package-lock.json b/examples/nextjs-basic/package-lock.json new file mode 100644 index 0000000..0ed10ae --- /dev/null +++ b/examples/nextjs-basic/package-lock.json @@ -0,0 +1,1037 @@ +{ + "name": "nextjs-basic-context-compiler-example", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextjs-basic-context-compiler-example", + "version": "0.0.1", + "dependencies": { + "@rlippmann/context-compiler": "file:../..", + "next": "^15.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "typescript": "^5.7.2" + } + }, + "../..": { + "name": "@rlippmann/context-compiler", + "version": "0.5.0", + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.9.3", + "vitest": "^3.2.4" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz", + "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz", + "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz", + "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz", + "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz", + "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz", + "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz", + "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz", + "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz", + "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@rlippmann/context-compiler": { + "resolved": "../..", + "link": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", + "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.15", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.15", + "@next/swc-darwin-x64": "15.5.15", + "@next/swc-linux-arm64-gnu": "15.5.15", + "@next/swc-linux-arm64-musl": "15.5.15", + "@next/swc-linux-x64-gnu": "15.5.15", + "@next/swc-linux-x64-musl": "15.5.15", + "@next/swc-win32-arm64-msvc": "15.5.15", + "@next/swc-win32-x64-msvc": "15.5.15", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/nextjs-basic/package.json b/examples/nextjs-basic/package.json new file mode 100644 index 0000000..0757681 --- /dev/null +++ b/examples/nextjs-basic/package.json @@ -0,0 +1,24 @@ +{ + "name": "nextjs-basic-context-compiler-example", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test:integration": "node ../../scripts/test-nextjs-integration.js" + }, + "dependencies": { + "@rlippmann/context-compiler": "file:../..", + "next": "^15.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "typescript": "^5.7.2" + } +} diff --git a/examples/nextjs-basic/tsconfig.json b/examples/nextjs-basic/tsconfig.json new file mode 100644 index 0000000..423f708 --- /dev/null +++ b/examples/nextjs-basic/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package-lock.json b/package-lock.json index 00f2e26..feee4bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rlippmann/context-compiler", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rlippmann/context-compiler", - "version": "0.5.0", + "version": "0.5.1", "devDependencies": { "typescript": "^5.9.3", "vitest": "^3.2.4" diff --git a/package.json b/package.json index 974c287..dad3066 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,19 @@ { "name": "@rlippmann/context-compiler", - "version": "0.5.0", + "version": "0.5.1", + "description": "Deterministic TypeScript control layer for LLM apps. Compiles explicit user directives into authoritative context state before model calls.", + "keywords": [ + "llm", + "prompt", + "guardrails", + "context", + "state", + "agent", + "middleware", + "typescript", + "openai", + "ai" + ], "license": "Apache-2.0", "repository": { "type": "git", diff --git a/scripts/test-nextjs-integration.js b/scripts/test-nextjs-integration.js new file mode 100644 index 0000000..2665b1d --- /dev/null +++ b/scripts/test-nextjs-integration.js @@ -0,0 +1,322 @@ +#!/usr/bin/env node +// Replays demo scenarios against the Next.js API +// Validates clarify/continue behavior and state persistence + +/** + * Black-box behavior tests for the Next.js integration endpoint. + * + * Target: + * POST http://localhost:3000/api/chat + * + * Usage: + * node scripts/test-nextjs-integration.js + * API_URL=http://localhost:3000/api/chat node scripts/test-nextjs-integration.js + * STRICT_CONTENT=1 node scripts/test-nextjs-integration.js + * + * Notes: + * - By default, this validates deterministic compiler behavior via `kind` and + * clarify prompt fragments. + * - Continue-output semantic checks are optional and controlled by STRICT_CONTENT=1 + * because real model outputs can vary. + */ + +const API_URL = process.env.API_URL || 'http://localhost:3000/api/chat'; +const STRICT_CONTENT = process.env.STRICT_CONTENT === '1'; + +function logStep(label, payload, response) { + console.log(`\n[${label}] request:`); + console.log(JSON.stringify(payload, null, 2)); + console.log(`[${label}] response:`); + console.log(JSON.stringify(response, null, 2)); +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function assertKind(response, expectedKind, scenarioName, stepName) { + assert( + response && response.kind === expectedKind, + `${scenarioName} / ${stepName}: expected kind="${expectedKind}", got ${JSON.stringify(response)}` + ); +} + +function assertConstraintHint(output, pattern, scenarioName, hint) { + assert(typeof output === 'string' && output.length > 0, `${scenarioName}: expected non-empty output`); + assert( + pattern.test(output), + `${scenarioName}: output does not appear to reflect constraint (${hint}). Output: ${JSON.stringify(output)}` + ); +} + +function assertClarifyNoOutput(response, scenarioName, stepName) { + assert( + !Object.prototype.hasOwnProperty.call(response, 'output'), + `${scenarioName} / ${stepName}: expected no output field on clarify (indirect no-model-call check)` + ); +} + +function assertPromptIncludes(response, fragment, scenarioName, stepName) { + assert( + typeof response.prompt_to_user === 'string' && response.prompt_to_user.includes(fragment), + `${scenarioName} / ${stepName}: expected prompt to include ${JSON.stringify(fragment)}, got ${JSON.stringify(response)}` + ); +} + +async function postChat(body) { + const res = await fetch(API_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); + + let json; + try { + json = await res.json(); + } catch (err) { + const text = await res.text(); + throw new Error(`Non-JSON response (${res.status}): ${text}`); + } + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${JSON.stringify(json)}`); + } + + return json; +} + +async function runScenario(scenario, sessionId, steps) { + console.log(`\n=== ${scenario} ===`); + for (let i = 0; i < steps.length; i += 1) { + const step = steps[i]; + const label = `s${sessionId}-step${i + 1}`; + const payload = { + sessionId, + input: step.input, + ...(step.history ? { history: step.history } : {}), + }; + const response = await postChat(payload); + logStep(label, payload, response); + + assertKind(response, step.expectKind, scenario, `step${i + 1}`); + if (step.expectKind === 'clarify') { + assertClarifyNoOutput(response, scenario, `step${i + 1}`); + } + if (step.promptIncludes) { + assertPromptIncludes(response, step.promptIncludes, scenario, `step${i + 1}`); + } + if (step.expectOutputNonEmpty) { + assert( + typeof response.output === 'string' && response.output.trim().length > 0, + `${scenario} / step${i + 1}: expected non-empty output` + ); + } + if (step.outputPattern) { + assertConstraintHint(response.output, step.outputPattern, scenario, step.outputPatternHint || 'expected content'); + } + } +} + +async function runAllScenarios() { + await runScenario('1) Passthrough continue', '01-passthrough', [ + { + input: 'hello there', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + ]); + + await runScenario('2) Contradiction clarify', '02-contradiction', [ + { + input: 'prohibit peanuts', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'use peanuts', + expectKind: 'clarify', + promptIncludes: 'already prohibited', + }, + ]); + + await runScenario('3) Premise lifecycle', '03-premise-lifecycle', [ + { + input: 'set premise vegetarian', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'set premise formal tone', + expectKind: 'clarify', + promptIncludes: 'Premise already exists', + }, + { + input: 'change premise to vegan', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'clear premise', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'change premise to pescatarian', + expectKind: 'clarify', + promptIncludes: 'No premise exists yet', + }, + ]); + + await runScenario('4) Near-miss clarify prompts', '04-near-miss', [ + { + input: 'set premise to concise replies', + expectKind: 'clarify', + promptIncludes: "Did you mean 'set premise concise replies'?", + }, + { + input: 'change premise concise replies', + expectKind: 'clarify', + promptIncludes: "Did you mean 'change premise to concise replies'?", + }, + ]); + + await runScenario('5) Policy lifecycle and conflicts', '05-policy-lifecycle', [ + { + input: 'use docker', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'prohibit docker', + expectKind: 'clarify', + promptIncludes: 'already in use', + }, + { + input: 'remove policy docker', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'prohibit docker', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + { + input: 'use docker', + expectKind: 'clarify', + promptIncludes: 'already prohibited', + }, + { + input: 'reset policies', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + ]); + + await runScenario('6) Pending confirmation gating (unmatched + yes)', '06-pending-yes', [ + { + input: 'use podman instead of docker', + expectKind: 'clarify', + promptIncludes: 'No exact policy found for "docker".', + }, + { + input: 'maybe', + expectKind: 'clarify', + promptIncludes: 'No exact policy found for "docker".', + }, + { + input: 'yes', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + ]); + + await runScenario('7) Pending confirmation gating (unmatched + no)', '07-pending-no', [ + { + input: 'use podman instead of docker', + expectKind: 'clarify', + promptIncludes: 'No exact policy found for "docker".', + }, + { + input: 'no', + expectKind: 'continue', + expectOutputNonEmpty: true, + }, + ]); + + await runScenario('8) Replay path', '08-replay', [ + { + history: [{ role: 'user', content: 'prohibit peanuts' }], + input: 'use peanuts', + expectKind: 'clarify', + promptIncludes: 'already prohibited', + }, + ]); + + await runScenario('9) State persistence', '09-persistence', [ + { + input: 'set premise vegan', + expectKind: 'continue', + expectOutputNonEmpty: true, + ...(STRICT_CONTENT + ? { + outputPattern: /\b(vegan|plant[- ]based|dairy[- ]free|egg[- ]free)\b/i, + outputPatternHint: 'vegan', + } + : {}), + }, + { + input: 'give me a recipe idea', + expectKind: 'continue', + expectOutputNonEmpty: true, + ...(STRICT_CONTENT + ? { + outputPattern: /\b(vegan|plant[- ]based|dairy[- ]free|egg[- ]free)\b/i, + outputPatternHint: 'vegan', + } + : {}), + }, + ]); + + await runScenario('10) Premise + continue', '10-premise-continue', [ + { + input: 'set premise vegetarian', + expectKind: 'continue', + expectOutputNonEmpty: true, + ...(STRICT_CONTENT + ? { + outputPattern: /\b(vegetarian|veggie|meatless|plant[- ]based)\b/i, + outputPatternHint: 'vegetarian', + } + : {}), + }, + { + input: 'suggest a dinner', + expectKind: 'continue', + expectOutputNonEmpty: true, + ...(STRICT_CONTENT + ? { + outputPattern: /\b(vegetarian|veggie|meatless|plant[- ]based)\b/i, + outputPatternHint: 'vegetarian', + } + : {}), + }, + ]); +} + +async function main() { + console.log(`Testing Next.js API at: ${API_URL}`); + console.log(`STRICT_CONTENT=${STRICT_CONTENT ? '1' : '0'}`); + + await runAllScenarios(); + + console.log('\nAll scenarios passed.'); +} + +main().catch((err) => { + console.error('\nTest run failed.'); + console.error(err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index c790678..c50b696 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,6 +7,6 @@ "declarationMap": false, "types": [] }, - "include": ["index.ts", "src/**/*.ts", "examples/**/*.ts", "demos/**/*.ts", "demos/**/*.d.ts"], + "include": ["index.ts", "src/**/*.ts", "examples/*.ts", "demos/**/*.ts", "demos/**/*.d.ts"], "exclude": ["tests", "dist", "node_modules"] }