From da58a623032da3ebb4f2d74022f90e56a6eec855 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 19:56:50 -0700 Subject: [PATCH] feat(growth): add synthetic Dawn research app for LangSmith --- .gitignore | 4 + apps/growth-research/.env.example | 12 + apps/growth-research/README.md | 147 ++ apps/growth-research/dawn.config.ts | 18 + .../deployment-package-lock.json | 1341 ++++++++++++++++ apps/growth-research/eslint.config.mjs | 6 + apps/growth-research/package.json | 26 + apps/growth-research/project.json | 47 + apps/growth-research/scripts/dawn-cli.mts | 15 + .../scripts/langsmith-smoke.mts | 112 ++ apps/growth-research/scripts/memory-probe.mts | 52 + .../scripts/package-langsmith.mts | 167 ++ .../scripts/platform-client.mts | 196 +++ .../scripts/verify-langsmith-artifact.mts | 6 + .../src/app/enrichment/research/index.ts | 12 + .../src/app/enrichment/research/memory.ts | 13 + .../src/app/enrichment/research/plan.md | 5 + .../research/skills/company-evidence/SKILL.md | 12 + .../research/subagents/researcher/index.ts | 11 + .../src/runtime/fixture-contract.ts | 18 + .../src/runtime/memory-store.ts | 50 + .../src/runtime/model-boundary.ts | 47 + .../src/tools/coordinatorSummary.ts | 6 + apps/growth-research/src/tools/readFixture.ts | 15 + .../growth-research/test/capabilities.spec.ts | 121 ++ .../growth-research/test/fixture-tool.spec.ts | 21 + .../growth-research/test/memory-store.spec.ts | 34 + .../test/memory.integration.spec.ts | 68 + .../test/model-boundary.spec.ts | 65 + apps/growth-research/test/packaging.spec.ts | 152 ++ .../test/platform-client.spec.ts | 151 ++ apps/growth-research/test/smoke.spec.ts | 65 + apps/growth-research/tsconfig.json | 16 + apps/growth-research/vitest.config.ts | 12 + .../vitest.memory-integration.config.ts | 3 + package-lock.json | 1401 +++++++++++++++++ 36 files changed, 4447 insertions(+) create mode 100644 apps/growth-research/.env.example create mode 100644 apps/growth-research/README.md create mode 100644 apps/growth-research/dawn.config.ts create mode 100644 apps/growth-research/deployment-package-lock.json create mode 100644 apps/growth-research/eslint.config.mjs create mode 100644 apps/growth-research/package.json create mode 100644 apps/growth-research/project.json create mode 100644 apps/growth-research/scripts/dawn-cli.mts create mode 100644 apps/growth-research/scripts/langsmith-smoke.mts create mode 100644 apps/growth-research/scripts/memory-probe.mts create mode 100644 apps/growth-research/scripts/package-langsmith.mts create mode 100644 apps/growth-research/scripts/platform-client.mts create mode 100644 apps/growth-research/scripts/verify-langsmith-artifact.mts create mode 100644 apps/growth-research/src/app/enrichment/research/index.ts create mode 100644 apps/growth-research/src/app/enrichment/research/memory.ts create mode 100644 apps/growth-research/src/app/enrichment/research/plan.md create mode 100644 apps/growth-research/src/app/enrichment/research/skills/company-evidence/SKILL.md create mode 100644 apps/growth-research/src/app/enrichment/research/subagents/researcher/index.ts create mode 100644 apps/growth-research/src/runtime/fixture-contract.ts create mode 100644 apps/growth-research/src/runtime/memory-store.ts create mode 100644 apps/growth-research/src/runtime/model-boundary.ts create mode 100644 apps/growth-research/src/tools/coordinatorSummary.ts create mode 100644 apps/growth-research/src/tools/readFixture.ts create mode 100644 apps/growth-research/test/capabilities.spec.ts create mode 100644 apps/growth-research/test/fixture-tool.spec.ts create mode 100644 apps/growth-research/test/memory-store.spec.ts create mode 100644 apps/growth-research/test/memory.integration.spec.ts create mode 100644 apps/growth-research/test/model-boundary.spec.ts create mode 100644 apps/growth-research/test/packaging.spec.ts create mode 100644 apps/growth-research/test/platform-client.spec.ts create mode 100644 apps/growth-research/test/smoke.spec.ts create mode 100644 apps/growth-research/tsconfig.json create mode 100644 apps/growth-research/vitest.config.ts create mode 100644 apps/growth-research/vitest.memory-integration.config.ts diff --git a/.gitignore b/.gitignore index 7fc839b60..7981f2dee 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,7 @@ keys/ # Aborted fixture-record runs strand raw recordings under a committed path **/e2e/fixtures/.staging/ + +# Growth research generated deployment artifacts +apps/growth-research/.dawn/ +apps/growth-research/.deployment/ diff --git a/apps/growth-research/.env.example b/apps/growth-research/.env.example new file mode 100644 index 000000000..48baca867 --- /dev/null +++ b/apps/growth-research/.env.example @@ -0,0 +1,12 @@ +# Runtime names only. Never copy environment files into the deployment artifact. +OPENAI_API_KEY= +DAWN_DATABASE_URL= +GROWTH_RESEARCH_TEST_DATABASE_URL= +# Explicit operator-only synthetic invocation gate; blank disables model calls. +GROWTH_RESEARCH_FIXTURE_MODE= +# Trusted synthetic memory slot: atlas or beacon. Not an authenticated tenant ID. +GROWTH_RESEARCH_FIXTURE_SLOT= +# Optional cancellation probe pause; integer 0..5000, default 0. +GROWTH_RESEARCH_FIXTURE_DELAY_MS= +GROWTH_RESEARCH_URL= +LANGSMITH_API_KEY= diff --git a/apps/growth-research/README.md b/apps/growth-research/README.md new file mode 100644 index 000000000..af12b13f8 --- /dev/null +++ b/apps/growth-research/README.md @@ -0,0 +1,147 @@ +# Growth research compatibility application + +Private synthetic Dawn application, separate from lifecycle and the Python cockpit. +Published Dawn packages are pinned to `0.8.24`; the app and deployment require Node 24. +The only public graph ID is `growth_research`, pointing to the unchanged generated +Dawn `/enrichment/research#agent` entry. The safe alias avoids slash/hash routing +failures in the Agent Server's internal per-graph HTTP endpoints. Its registered researcher is +private to coordinator delegation; staging verifies the known generated specialist +entry but removes its standalone public graph key. + +This app exercises authored plans, skills, scoped delegation, candidate memory and +platform thread continuation against a fixed synthetic corpus. It is disabled by +default and has no connection to Growth ingestion or campaign delivery. Configure +a dedicated memory database; do not point it at Growth's canonical database. + +Dawn 0.8.24 sets the child checkpointer to `false`; `task` accepts only `subagent` +and `input`. Each delegation starts a fresh child conversation. Carry relevant +context explicitly through the checkpointed parent when delegating follow-up work. + +From the workspace root on Node 24: + +```sh +npm ci --ignore-scripts +npx nx test growth-research +npx nx run growth-research:check +npx nx lint growth-research +npx nx build growth-research +``` + +The build uses the CLI resolved from this application and checks its version before +execution. Dawn emits a LangSmith entry under `.dawn/build`. Packaging preserves +that entry and the relative `src/` and `dawn.config.ts` layout, stages approved files +under `.deployment`, and normalizes `langgraph.json` to Node 24, `dependencies: ["."]` +and `env: {}`. Configure secret values in the deployment environment. Generated +`.dawn/routes/*/tools.json` schemas are preserved because actual tool execution needs +them; arbitrary build files and all environment files remain excluded. The artifact +pins Agent Server `api_version: "0.13.4"` and contains a standalone NodeNext +`tsconfig.json` for the official server's static schema extractor. It does not inherit +the monorepo's compiler configuration or path aliases. + +`deployment-package-lock.json` is the standalone runtime dependency lock. To update +it after changing direct dependencies, use `deploymentManifest()` from +`scripts/package-langsmith.mts` to write a temporary standalone `package.json`, run +`npm install --package-lock-only --ignore-scripts --workspaces=false` there, and copy +its lock to `deployment-package-lock.json`. The build rejects stale direct dependency +locks. Do not copy the monorepo lock or workspace dependencies into the artifact. +The workspace lock keeps this app's dependency tree nested so its testing helpers +and runtime resolve Dawn 0.8.24 while lifecycle retains Dawn 0.8.21. + +Set `GROWTH_RESEARCH_FIXTURE_MODE=synthetic-only` explicitly to permit model calls. +The default blocks them. The fixed corpus contains `atlas` and `beacon`; tools accept +only those identifiers and cannot fetch URLs, read arbitrary files or execute shell +commands. The specialist is explicitly registered with delegation denied by default +and only that specialist allowed. It can read fixtures but is denied the shared +coordinator summary tool. Planning and skill instructions are authored beside the +coordinator route. + +For a local active-child cancellation probe, the operator may set +`GROWTH_RESEARCH_FIXTURE_DELAY_MS` to an integer from 0 to 5000. It defaults to zero; +the model cannot choose a delay. The fixture tool cooperatively observes cancellation +while paused and rechecks both cancellation and fixture mode before returning data. + +The public Dawn `seedModelImporter` bootstrap installs a process-wide bounded +OpenAI model for this isolated app. Every request is gated, including cached models. +It uses `gpt-4.1-mini`, a 1,024-token output cap, zero provider retries and a 20-second +request timeout. Credential-free schema extraction can construct the model with a +construction-only placeholder; invocation and actual HTTP fetch reject absent real +credentials, so the placeholder is never sent. Route recursion is limited to 12 steps and Dawn retries to one +attempt. These are compatibility-probe bounds, not a shared spending reservation or +production provider selection. Provider-free tests inspect actual request bodies, +verify one request on a retryable failure, and observe a stalled request timing out. + +Candidate memory uses an explicit lazy pgvector store via `DAWN_DATABASE_URL`, with +8-dimensional deterministic synthetic embeddings. Generated `remember` writes are +candidates and normal `recall` excludes them. Missing database configuration fails +when durable memory is accessed; there is no SQLite fallback. The eager prompt index +is explicitly disabled with `indexMaxEntries: 0`; a zero-result search returns an +empty list without opening a connection. This allows credential-free graph import +and packaging while positive-limit recall and all writes still require the database. +This index setting is necessary because Dawn 0.8.24 does not consult `memory.enabled` +when a route-local memory declaration exists. + +Run the separate, uncached integration target only against a disposable database: + +```sh +GROWTH_RESEARCH_TEST_DATABASE_URL='postgres://…' npx nx run growth-research:test-memory-integration +``` + +The probe requires that variable and never falls back to a production URL. It uses +fresh child processes for generated candidate writes, active recall, slot isolation +and deletion, and deletes only the fixture record it created. Memory namespaces use +the stable `growth-research` workspace and route plus a server-owned `GROWTH_RESEARCH_FIXTURE_SLOT` (`atlas` or +`beacon`, default `atlas`). These are trusted synthetic deployment slots, not +authenticated account identities. The explicit workspace remains stable across source, +staging and relocated deployment directories, which the subprocess test verifies. +Dawn's scope callback has no authenticated user; +production tenancy still requires separate application-owned authorization. Synthetic +hash embeddings do not establish semantic retrieval quality for live data. + +Build, staging, standalone installation, and native Node graph import require no +model or database credentials. Native import is only a packaging check; run server +and cloud smoke checks separately to exercise the deployment boundary. The server's static +schema extractor still emits a nonfatal `Unsupported type: never` diagnostic; the +tested runtime operations succeeded despite it. Fast tests use the public Dawn +harness and a local mock model; memory persistence is verified separately against +PostgreSQL. + +Run the fast and database suites sequentially: Dawn's local testing harness uses a +shared checkpoint file, so overlapping those commands can produce a SQLite lock +error. This does not change the deployed graph's LangSmith checkpoint ownership or +its separate pgvector memory store. + +Agent Server `0.13.4-node24` can acknowledge interruption before its JavaScript child +stops, allowing a later result checkpoint. The generated Dawn graph cancels when a +live `config.signal` is supplied; the official JS sidecar does not forward that +signal. No vendor patch is included. Cancellation and protection against writes +after cancellation remain failed live-use gates. The smoke client's cleanup command +refuses interrupted threads; an operator must independently establish worker +quiescence before deleting those records. A terminal run status alone is insufficient. +Deploy the verified artifact with the official CLI `0.4.21` source archive layout +and the LangSmith control-plane source-upload API. Updates should target the existing deployment ID: +request its upload URL, upload only the verified `.deployment` archive, and submit +the returned object path with `revision_source: "internal_source"`, +`langgraph_config_path: "langgraph.json"`, and `install_command: "npm ci --ignore-scripts"`. +The signed upload requires `Content-Type: application/gzip` and +`X-Goog-Content-Length-Range: 0,209715200`. Configure secrets through the deployment +API; never include an environment file in the archive. Re-enabling synthetic model +tests requires both a provider key and the explicit fixture-mode value. Do not wire +real Growth signals into this deployment until its remaining live-use gates pass. + +The uncached platform smoke target takes positional fixture, thread and correlation +identifiers. Set `GROWTH_RESEARCH_URL`, `LANGSMITH_API_KEY` when authentication is +required, and the explicit fixture-mode gate in the operator environment: + +```sh +npx nx run growth-research:smoke-langsmith -- direct THREAD_UUID SMOKE_ID +``` + +Other phases are `delegated`, `memory`, `continuation`, and `cleanup`. Continuation +uses the same thread and smoke ID after a direct run; cleanup verifies ownership +and rejects active or interrupted runs, then deletes the fixture thread and verifies +absence. Interrupted fixtures require the separate operator procedure described above. + +This application is restricted to synthetic compatibility work. It does not collect +real people or companies, publish account facts, or dispatch campaigns. Live use still +requires trusted scopes, source controls, budget enforcement, a durable Growth work +ledger, publication validation and cross-store deletion safeguards. diff --git a/apps/growth-research/dawn.config.ts b/apps/growth-research/dawn.config.ts new file mode 100644 index 000000000..14a989e0c --- /dev/null +++ b/apps/growth-research/dawn.config.ts @@ -0,0 +1,18 @@ +import type { DawnConfig } from '@dawn-ai/core'; +import './src/runtime/model-boundary.js'; +import { candidateMemoryStore, syntheticEmbedder, trustedFixtureScope } from './src/runtime/memory-store.js'; + +export default { + appDir: 'src/app', + build: { targets: ['langsmith'] }, + toolOutput: { noOffloadTools: ['readFixture', 'coordinatorSummary', 'readSkill', 'writeTodos', 'recall', 'remember'] }, + summarization: { enabled: false }, + memory: { + store: candidateMemoryStore, + indexMaxEntries: 0, + writes: 'candidate', + vector: { embedder: syntheticEmbedder }, + resolveScope: trustedFixtureScope, + episodes: { enabled: false }, + }, +} satisfies DawnConfig; diff --git a/apps/growth-research/deployment-package-lock.json b/apps/growth-research/deployment-package-lock.json new file mode 100644 index 000000000..581ab44ea --- /dev/null +++ b/apps/growth-research/deployment-package-lock.json @@ -0,0 +1,1341 @@ +{ + "name": "@threadplane-internal/growth-research", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@threadplane-internal/growth-research", + "version": "0.0.0", + "dependencies": { + "@dawn-ai/cli": "0.8.24", + "@dawn-ai/core": "0.8.24", + "@dawn-ai/langchain": "0.8.24", + "@dawn-ai/memory": "0.8.24", + "@dawn-ai/memory-pgvector": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@langchain/core": "1.2.9", + "@langchain/langgraph-checkpoint": "1.1.5", + "@langchain/openai": "1.5.11", + "@types/node": "25.6.0", + "pg": "8.23.0", + "zod": "4.5.4" + }, + "engines": { + "node": "24" + } + }, + "node_modules/@ag-ui/core": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ag-ui/core/-/core-0.0.59.tgz", + "integrity": "sha512-hDgy4ipTqXieT8YG8Mr917Y+FD/f11VK1GefZ5CwTDCuNqS/oTwjJ5l/DZkicThgS8hQW/Y7wPylPBMBJ8BkUg==", + "license": "MIT", + "dependencies": { + "zod": "^3.22.4" + } + }, + "node_modules/@ag-ui/core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@ag-ui/encoder": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ag-ui/encoder/-/encoder-0.0.59.tgz", + "integrity": "sha512-wQCzBsStyZMm8nzhTdTx1G0B1C8yv5rbzZLnz1ka/l5LcJEsK3KTounU8CR9l+7QtcpOUUDJKd0xAbyI6gNcSw==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@ag-ui/proto": "0.0.59" + } + }, + "node_modules/@ag-ui/proto": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ag-ui/proto/-/proto-0.0.59.tgz", + "integrity": "sha512-X+uvDaegLEHw5kJu8tv2eSqHH8ouat+JCfFokGV1uuMquY8qCEQSlGsM7xzexwX9fujuxgHOWumg5kJDqa0+RA==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@bufbuild/protobuf": "^2.2.5", + "@protobuf-ts/protoc": "^2.11.1" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.14.1.tgz", + "integrity": "sha512-agRJn3+EJDUe8AvxTx/LnHA/GErvLE62pSaSk7+MwFOtOv8eWBu/qCq2qoZjBjVZ3C2aiFJCveuSs17KMkYGOw==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, + "node_modules/@dawn-ai/ag-ui": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/ag-ui/-/ag-ui-0.8.24.tgz", + "integrity": "sha512-7lce3QKiT4ZosMFIju+k1EebeSqxTqvPux+EuSTi/l0YblA1IMxtF+oMIrA0slDxJ3dv5IfRzYNbOcznVnOT/Q==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@ag-ui/encoder": "0.0.59", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@copilotkit/react-core": ">=1.66.0", + "react": ">=19.0.0" + }, + "peerDependenciesMeta": { + "@copilotkit/react-core": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@dawn-ai/cli": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/cli/-/cli-0.8.24.tgz", + "integrity": "sha512-18+jxTh9vjXHNwX4Y+TrM5bYBSK94W1qevuU50BM54NA2ETw9dfUSPYxY2Se2Gffln/u6ubO6UCxChTLgTj+jQ==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@dawn-ai/ag-ui": "0.8.24", + "@dawn-ai/core": "0.8.24", + "@dawn-ai/langchain": "0.8.24", + "@dawn-ai/langgraph": "0.8.24", + "@dawn-ai/memory": "0.8.24", + "@dawn-ai/permissions": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/sqlite-storage": "0.8.24", + "commander": "15.0.0", + "esbuild": "^0.28.1", + "tsx": "^4.23.5" + }, + "bin": { + "dawn": "dist/index.js" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@dawn-ai/core": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/core/-/core-0.8.24.tgz", + "integrity": "sha512-zrx6H1vhFpfvbO9BQIkpKTUymTJPumyMZj/vkd4gWv/3L5iEAS+QOHkdmn8v1nUNPlXzag7W6NovmBIQCC5E0w==", + "license": "MIT", + "dependencies": { + "@dawn-ai/permissions": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/sqlite-storage": "0.8.24", + "@dawn-ai/workspace": "0.8.24", + "@langchain/langgraph": "^1.4.9", + "@typescript/old": "npm:typescript@6.0.2", + "tsx": "^4.23.5", + "typescript": "npm:@typescript/typescript6@6.0.2", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@langchain/langgraph-checkpoint": "^1.1.3" + } + }, + "node_modules/@dawn-ai/langchain": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/langchain/-/langchain-0.8.24.tgz", + "integrity": "sha512-smwRLyflWG4fkvbv8bTXoILlEZX7kYB0NJWCFC4oX1tWf4rjO+cgeohECQgdkhktpVxuj0g34ASe2zOxonQHJQ==", + "license": "MIT", + "dependencies": { + "@dawn-ai/core": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/workspace": "0.8.24", + "@langchain/langgraph": "^1.4.9", + "@langchain/openai": "^1.5.5", + "gpt-tokenizer": "^3.4.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@langchain/anthropic": "^1.5.2", + "@langchain/core": "^1.1.47", + "@langchain/google-genai": "^2.2.0", + "@langchain/groq": "^1.3.1", + "@langchain/langgraph-checkpoint": "^1.1.3", + "@langchain/mistralai": "^1.2.0", + "@langchain/ollama": "^1.3.0", + "@langchain/openrouter": "^0.4.5", + "@langchain/xai": "^1.4.5" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/langgraph-checkpoint": { + "optional": false + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "@langchain/openrouter": { + "optional": true + }, + "@langchain/xai": { + "optional": true + } + } + }, + "node_modules/@dawn-ai/langgraph": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/langgraph/-/langgraph-0.8.24.tgz", + "integrity": "sha512-Tb/gLuuDQOYZJbEf4e6ZqBasvH38OJL9UdaM0jsXRanCFrC5u8mA1hRF9JXLqqfuA1Eyr1zJzDQHqv2/ZDv1HA==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sdk": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@dawn-ai/memory": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/memory/-/memory-0.8.24.tgz", + "integrity": "sha512-yVptc9AeDEGm73j1SysyVDZLJmYriAp5mRBbhdVFzb5cULEI5X3Ysk3sxqlhZoM1z/7VZONhNA8g6zEL8ppRuA==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sqlite-storage": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@dawn-ai/memory-pgvector": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/memory-pgvector/-/memory-pgvector-0.8.24.tgz", + "integrity": "sha512-i6WWyts+Uga/xwRK7nFOEtlKt6fAPL8OgDo5U6m5VzGg0J35Gmv02gHww9PQ70RiyyVn5x8vp+RFCju/FRaQtw==", + "license": "MIT", + "dependencies": { + "@dawn-ai/memory": "0.8.24", + "pg": "^8.22.0", + "pgvector": "^0.3.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@dawn-ai/permissions": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/permissions/-/permissions-0.8.24.tgz", + "integrity": "sha512-PfFQ9rm08TGmTi4twAeaUN+7cZLOB3s+Anfa5isVI/uM0mRKuRFr4hg/WEBQPaZVyDuTrCqiDgOcXTJ1mfySeA==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sdk": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@dawn-ai/sdk": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/sdk/-/sdk-0.8.24.tgz", + "integrity": "sha512-YzBVD53dzPUNTkFwbYYdh/XMCX92wmSwmOmIsxkBgnxc3gX11b5z18F6NO7IeWJdmJbxRaLVBAAcUJvO/9E0qg==", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@dawn-ai/sqlite-storage": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/sqlite-storage/-/sqlite-storage-0.8.24.tgz", + "integrity": "sha512-2fGt9K7PabDpN9KdguYrdzMC6mI+lMud/8chvRriCdIqJYeWGUXfZB+GWihXbTU+dR6o3NE1hyAabl+Sj6upkQ==", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.2.4", + "@langchain/langgraph-checkpoint": "^1.1.3" + } + }, + "node_modules/@dawn-ai/workspace": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/workspace/-/workspace-0.8.24.tgz", + "integrity": "sha512-bW4c1Xj3lLqnbiPSHXkTDxcdJ4py/SGe4aKuPOWMKdxo64qso/2cuRcVh2kCOFP3sUv9KJXuE2RqdeJIV2Rf4Q==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sdk": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.9.tgz", + "integrity": "sha512-conzSEj9Zu1AyXJLXsSbgrtxtxinmI1yGqQ5CIJZSoV5rvv+yvQE/vgBnoySpBQ/bl3YPgj2FL/gbDjWykLSfg==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.4.14.tgz", + "integrity": "sha512-uWAdRYTllfKCnTrlyovExPJCHJwcf3Wl2LzUlnaqsT7Rmoo3aCeYtq/7MV/Pw4q11motG8pR8bjr6T6V8Pe1gQ==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.1.5", + "@langchain/langgraph-sdk": "~1.10.2", + "@langchain/protocol": "^0.0.19", + "@standard-schema/spec": "1.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48", + "zod": "^3.25.32 || ^4.2.0" + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.5.tgz", + "integrity": "sha512-BwDwl5VeTOh6CVuiIPgsUgfK51vTJDMSbFcSCUfjJWsl8/DPdK/mbv+ejxJstkSk/BlSPMP4JfXWcN6jD2ea2Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.10.2.tgz", + "integrity": "sha512-86qsfdBZWu1ZgywLN8AThU/jXi9rjPDZPWcTJp4SA1A/L62ypTNoSXbvtiwZt1odokXccYTxK1XWS8tmVdvEmw==", + "license": "MIT", + "dependencies": { + "@langchain/protocol": "^0.0.19", + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/openai": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.5.11.tgz", + "integrity": "sha512-BvGp5lQk5//0WVwTIepscazFpneT9I9+mc+kp+cLuhGHFb7mc9zGNrusZOXoa3p73SN0i3XqTo8lyIndpVx3Hw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^7.5.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@langchain/core": "^1.2.9" + } + }, + "node_modules/@langchain/protocol": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.19.tgz", + "integrity": "sha512-9hKcRrH7cBX6gfutdfXPoft1OCchHe4FEpALoDJMl5Qu+n/YG5ynZmyu8+8cxORlPwHBoKTxggvXz+76M1yX1Q==", + "license": "MIT" + }, + "node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@typescript/old": { + "name": "typescript", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gpt-tokenizer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", + "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", + "license": "MIT" + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/langsmith": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.10.1.tgz", + "integrity": "sha512-zRDCnLznGdzx1VottX4CWr8v9ZZLRoSql2pbjEXYA1Jeg+NMDdq87x/v0Dk4GNkNZYhE+ZxFX3vTxwWq6W6gVA==", + "license": "MIT", + "dependencies": { + "p-queue": "6.6.2" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*", + "ws": ">=7" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/openai": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.10.0.tgz", + "integrity": "sha512-sn9t2Kls7O52PwuF9BUTYNu4Gk/r0lXJyrgaNht4TNRlZFb3dJIGO0RciSgjARGCBRtWjySubAQFJttlzUvGQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "undici": ">=5 <9", + "ws": "^8.21.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "undici": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgvector": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/pgvector/-/pgvector-0.3.0.tgz", + "integrity": "sha512-+t7qcQD2us8fO8YIq/3lA0gUrD+bVO70MG1MhcDcxJz/OlRGGIIHzFq/4x57Vn/LpzX5wFdfOTLQp9QMPd4ljQ==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "name": "@typescript/typescript6", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript6/-/typescript6-6.0.2.tgz", + "integrity": "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==", + "license": "Apache-2.0", + "dependencies": { + "@typescript/old": "npm:typescript@^6" + }, + "bin": { + "tsc6": "bin/tsc6" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/growth-research/eslint.config.mjs b/apps/growth-research/eslint.config.mjs new file mode 100644 index 000000000..e07eec8db --- /dev/null +++ b/apps/growth-research/eslint.config.mjs @@ -0,0 +1,6 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [ + { ignores: ['**/.dawn/**', '**/.deployment/**'] }, + ...baseConfig, +]; diff --git a/apps/growth-research/package.json b/apps/growth-research/package.json new file mode 100644 index 000000000..eadc11a5f --- /dev/null +++ b/apps/growth-research/package.json @@ -0,0 +1,26 @@ +{ + "name": "@threadplane-internal/growth-research", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { "node": "24" }, + "dependencies": { + "@dawn-ai/cli": "0.8.24", + "@dawn-ai/core": "0.8.24", + "@dawn-ai/langchain": "0.8.24", + "@dawn-ai/memory": "0.8.24", + "@dawn-ai/memory-pgvector": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@langchain/core": "1.2.9", + "@langchain/langgraph-checkpoint": "1.1.5", + "@langchain/openai": "1.5.11", + "@types/node": "25.6.0", + "pg": "8.23.0", + "zod": "4.5.4" + }, + "devDependencies": { + "@dawn-ai/evals": "0.8.24", + "@dawn-ai/testing": "0.8.24", + "@dawn-ai/workspace": "0.8.24" + } +} diff --git a/apps/growth-research/project.json b/apps/growth-research/project.json new file mode 100644 index 000000000..20e4f7c87 --- /dev/null +++ b/apps/growth-research/project.json @@ -0,0 +1,47 @@ +{ + "name": "growth-research", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/growth-research/src", + "projectType": "application", + "tags": ["scope:internal", "scope:growth-research", "type:app", "runtime:node24"], + "targets": { + "smoke-langsmith": { + "executor": "nx:run-commands", + "cache": false, + "options": { + "cwd": "apps/growth-research", + "command": "node scripts/langsmith-smoke.mts", + "forwardAllArgs": true + } + }, + "test-memory-integration": { + "executor": "nx:run-commands", + "cache": false, + "options": { "command": "npx vitest run --config apps/growth-research/vitest.memory-integration.config.ts --reporter=verbose" } + }, + "test": { + "executor": "@nx/vitest:test", + "options": { "configFile": "apps/growth-research/vitest.config.ts" } + }, + "check": { + "executor": "nx:run-commands", + "cache": false, + "options": { + "cwd": "apps/growth-research", + "commands": ["node scripts/dawn-cli.mts check", "node ../../node_modules/typescript/bin/tsc --noEmit -p tsconfig.json"], + "parallel": false + } + }, + "build": { + "executor": "nx:run-commands", + "cache": false, + "outputs": ["{projectRoot}/.deployment"], + "options": { + "cwd": "apps/growth-research", + "commands": ["node scripts/dawn-cli.mts build --clean", "node scripts/package-langsmith.mts", "node scripts/verify-langsmith-artifact.mts"], + "parallel": false + } + }, + "lint": { "executor": "@nx/eslint:lint" } + } +} diff --git a/apps/growth-research/scripts/dawn-cli.mts b/apps/growth-research/scripts/dawn-cli.mts new file mode 100644 index 000000000..65818028b --- /dev/null +++ b/apps/growth-research/scripts/dawn-cli.mts @@ -0,0 +1,15 @@ +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +if (process.versions.node.split('.')[0] !== '24') throw new Error('Growth research requires Node 24'); +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const require = createRequire(resolve(appRoot, 'package.json')); +const cli = require.resolve('@dawn-ai/cli'); +const metadata = JSON.parse(readFileSync(resolve(dirname(cli), '../package.json'), 'utf8')); +if (metadata.version !== '0.8.24') throw new Error(`Expected app-local Dawn CLI 0.8.24, resolved ${metadata.version}`); +const result = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], { cwd: appRoot, stdio: 'inherit' }); +if (result.error) throw result.error; +process.exitCode = result.status ?? 1; diff --git a/apps/growth-research/scripts/langsmith-smoke.mts b/apps/growth-research/scripts/langsmith-smoke.mts new file mode 100644 index 000000000..6cadf3808 --- /dev/null +++ b/apps/growth-research/scripts/langsmith-smoke.mts @@ -0,0 +1,112 @@ +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createPlatformClient, PlatformError, researchGraphId } from './platform-client.mts'; + +export const fixturePrompts = { + direct: 'direct fixture atlas: do not delegate. Load company-evidence with readSkill, read atlas with readFixture, mark your plan completed with writeTodos, and report the fixture source.', + delegated: 'delegate atlas: delegate exactly once to researcher with input "specialist atlas". Return its source citation.', + continuation: 'continuation fixture atlas: use the prior thread evidence and state to report the Atlas fixture source again. Do not delegate.', + memory: 'memory fixture atlas: read atlas with readFixture. Use remember with data {"fixtureId":"atlas","observation":"Synthetic Angular evaluation","source":"fixture:atlas:v1"} and content "Synthetic Angular evaluation". Then recall "Synthetic Angular evaluation". Report the pending candidate and do not approve it.', +} as const; +export type SmokeFixture = keyof typeof fixturePrompts; +export function isSmokeFixture(value: string): value is SmokeFixture { + return Object.hasOwn(fixturePrompts, value); +} + +function record(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new PlatformError('missing_evidence', 'Expected persisted fixture state.'); + return value as Record; +} +function content(message: Record): string { + return typeof message['content'] === 'string' ? message['content'] : JSON.stringify(message['content'] ?? ''); +} + +export function verifyContinuationBase(state: unknown): void { + const values = record(record(state)['values']); + if (!Array.isArray(values['messages'])) throw new PlatformError('missing_evidence', 'Expected prior direct fixture state.'); + const messages = values['messages'].map(record); + const start = messages.findLastIndex(message => ['human', 'user'].includes(String(message['type'] ?? message['role'])) && content(message).startsWith('direct fixture atlas')); + if (start < 0) throw new PlatformError('missing_evidence', 'Expected prior direct fixture state.'); + const end = messages.findIndex((message, index) => index > start && ['human', 'user'].includes(String(message['type'] ?? message['role']))); + verifyFixtureState('direct', { values: { ...values, messages: messages.slice(start, end < 0 ? undefined : end) } }); +} + +export function verifyFixtureState(fixture: SmokeFixture, state: unknown) { + const values = record(record(state)['values']); + if (!Array.isArray(values['messages'])) throw new PlatformError('missing_evidence', 'Expected persisted messages.'); + const messages = values['messages'].map(record); + const fail = () => { throw new PlatformError('missing_evidence', `Persisted ${fixture} evidence did not meet the smoke gate.`); }; + const turnStart = messages.findLastIndex(message => ['human', 'user'].includes(String(message['type'] ?? message['role']))); + const expectedStart = fixture === 'delegated' ? 'delegate atlas' : `${fixture} fixture atlas`; + if (turnStart < 0 || !content(messages[turnStart] ?? {}).startsWith(expectedStart)) fail(); + const current = messages.slice(turnStart); + const tools = current.filter(message => (message['type'] ?? message['role']) === 'tool'); + if (tools.some(tool => tool['status'] === 'error')) fail(); + const final = messages.at(-1); + if (!final || !['ai', 'assistant'].includes(String(final['type'] ?? final['role'])) || !content(final)) fail(); + const hasTool = (name: string, text: string) => tools.some(tool => tool['name'] === name && content(tool).includes(text)); + const todos = Array.isArray(values['todos']) ? values['todos'].map(record) : []; + const planComplete = todos.length > 0 && todos.every(todo => todo['status'] === 'completed'); + let candidateId: string | undefined; + if (fixture === 'continuation') { + verifyContinuationBase(state); + if (!planComplete) fail(); + } else if (fixture === 'direct') { + if (!hasTool('readSkill', 'Never treat candidate memory as an accepted account fact') || !hasTool('readFixture', 'fixture:atlas:v1') || !hasTool('writeTodos', '') || !planComplete) fail(); + } else if (fixture === 'delegated') { + const taskCall = current.some(message => Array.isArray(message['tool_calls']) && message['tool_calls'].some(call => { + const value = record(call); return value['name'] === 'task' && record(value['args'])['subagent'] === 'researcher'; + })); + if (!taskCall || !hasTool('task', 'fixture:atlas:v1')) fail(); + } else if (fixture === 'memory') { + const remembered = tools.find(tool => tool['name'] === 'remember' && /Stored memory candidate memory_[a-f0-9]{16} \(pending approval\)/.test(content(tool))); + candidateId = remembered ? content(remembered).match(/memory_[a-f0-9]{16}/)?.[0] : undefined; + const recalls = tools.filter(tool => tool['name'] === 'recall'); + if (!candidateId || !recalls.length || recalls.some(tool => content(tool).trim() !== '(no memories found)') || tools.indexOf(recalls[0] ?? {}) < tools.indexOf(remembered ?? {})) fail(); + const between = current.slice(current.indexOf(remembered ?? {}) + 1, current.indexOf(recalls[0] ?? {})); + if (!between.some(message => ['ai', 'assistant'].includes(String(message['type'] ?? message['role'])) && Array.isArray(message['tool_calls']) && message['tool_calls'].some(call => record(call)['name'] === 'recall'))) fail(); + } else fail(); + return { fixture, tools: [...new Set(tools.map(tool => String(tool['name'])))], planComplete, messageCount: messages.length, ...(candidateId ? { candidateId } : {}) }; +} + +export async function runFixture(client: ReturnType, fixture: SmokeFixture, threadId: string, smokeId: string) { + if (!isSmokeFixture(fixture)) throw new PlatformError('invalid_arguments', 'Unknown synthetic fixture.'); + const assistants = await client.discover(); + if (!Array.isArray(assistants) || !assistants.length || assistants.length >= 100 || assistants.some(row => record(row)['graph_id'] !== researchGraphId)) { + throw new PlatformError('graph_discovery_failed', 'Expected only the coordinator graph on the research deployment.'); + } + await client.ensureFixtureThread(threadId, smokeId); + if (fixture === 'continuation') verifyContinuationBase(await client.getState(threadId)); + const correlation = `${smokeId}:${fixture}`; + const run = await client.submitRun(threadId, correlation, { messages: [{ role: 'user', content: fixturePrompts[fixture] }] }); + try { + await client.waitForSuccess(threadId, run.run_id); + const state = await client.getState(threadId); + return { ...verifyFixtureState(fixture, state), threadId, runId: run.run_id, smokeId, checkpoint: record(state)['checkpoint_id'] ?? record(state)['checkpoint'] ?? null }; + } catch (error) { + // Preserve failed evidence, but stop a known active run before the operator inspects it. + const current = await client.getRun(threadId, run.run_id).catch(() => null); + if (current?.status === 'running' || current?.status === 'pending') await client.cancelRun(threadId, run.run_id); + throw error; + } +} + +async function main(): Promise { + if (process.env['GROWTH_RESEARCH_FIXTURE_MODE'] !== 'synthetic-only') throw new PlatformError('fixture_disabled', 'Synthetic fixture mode must be explicitly enabled.'); + const [fixture, threadId, smokeId] = process.argv.slice(2); + if (!fixture || !threadId || !smokeId || !(isSmokeFixture(fixture) || fixture === 'cleanup')) throw new PlatformError('invalid_arguments', 'Use: langsmith-smoke.mts direct|delegated|memory|continuation|cleanup THREAD_UUID SMOKE_ID'); + const client = createPlatformClient({ url: process.env['GROWTH_RESEARCH_URL'] ?? '', apiKey: process.env['LANGSMITH_API_KEY'] }); + if (fixture === 'cleanup') { + await client.deleteFixtureThread(threadId, smokeId); + console.log(JSON.stringify({ threadId, smokeId, deletedAndAbsent: true })); + } else { + console.log(JSON.stringify(await runFixture(client, fixture as SmokeFixture, threadId, smokeId))); + } +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch(error => { + console.error(JSON.stringify({ error: error instanceof PlatformError ? error.code : 'smoke_failed', threadId: process.argv[3], smokeId: process.argv[4] })); + process.exitCode = 1; + }); +} diff --git a/apps/growth-research/scripts/memory-probe.mts b/apps/growth-research/scripts/memory-probe.mts new file mode 100644 index 000000000..4f5484db1 --- /dev/null +++ b/apps/growth-research/scripts/memory-probe.mts @@ -0,0 +1,52 @@ +import { randomUUID } from 'node:crypto'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { serializeNamespace } from '@dawn-ai/memory'; +import { createAgentHarness, script } from '@dawn-ai/testing'; +import { candidateMemoryStore as store, syntheticEmbedder, trustedFixtureScope } from '../src/runtime/memory-store.ts'; + +if (!process.env['GROWTH_RESEARCH_TEST_DATABASE_URL']) throw new Error('GROWTH_RESEARCH_TEST_DATABASE_URL is required'); +if (process.env['DAWN_DATABASE_URL'] !== process.env['GROWTH_RESEARCH_TEST_DATABASE_URL']) throw new Error('Memory probe must use the explicit test database'); +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const fixtureId = trustedFixtureScope().agent; +const namespace = serializeNamespace({ ...trustedFixtureScope(), route: '/enrichment/research' }); +const action = process.argv[2]; +let harness: Awaited> | undefined; +try { + if (action === 'write') { + const observation = `Synthetic candidate ${randomUUID()}`; + harness = await createAgentHarness({ appRoot, route: '/enrichment/research#agent' }); + const run = await harness.run({ input: 'write synthetic candidate', fixtures: script().user('write synthetic candidate').callsTool('remember', { + data: { fixtureId, observation, source: `fixture:${fixtureId}:v1` }, content: observation, + }).replies('Candidate proposed.') }); + const candidate = (await store.listCandidates(namespace)).find(record => record.content === observation); + if (!candidate || candidate.status !== 'candidate') throw new Error(`Expected candidate write: ${JSON.stringify(run.toolResults)}`); + console.log(JSON.stringify({ id: candidate.id, pid: process.pid })); + } else if (action === 'seed-active-control') { + // Independent test control: never promote or alter the model-authored candidate. + const id = `synthetic_control_${randomUUID()}`; + const content = `Synthetic active control ${id}`; + const now = new Date().toISOString(); + const [embedding] = await syntheticEmbedder.embed([content]); + await store.put({ + id, namespace, kind: 'semantic', status: 'active', content, + data: { fixtureId, observation: content, source: `fixture:${fixtureId}:v1` }, + source: { type: 'eval', id }, confidence: 1, tags: [id], createdAt: now, updatedAt: now, + }, { embedding, embeddingModel: syntheticEmbedder.id }); + console.log(JSON.stringify({ id, content, pid: process.pid })); + } else if (action === 'read') { + harness = await createAgentHarness({ appRoot, route: '/enrichment/research#agent' }); + const controlId = process.argv[3]; + const run = await harness.run({ input: 'recall synthetic candidates', fixtures: script().user('recall synthetic candidates').callsTool('recall', { query: controlId ?? 'Synthetic candidate', ...(controlId ? { tags: [controlId] } : {}) }).replies('Recall checked.') }); + if (run.toolResults.some(result => result.isError)) throw new Error('Generated memory recall failed'); + console.log(JSON.stringify({ pid: process.pid, candidateIds: (await store.listCandidates(namespace)).map(record => record.id), activeIds: (await store.search({ namespace, status: 'active' })).map(record => record.id), recalled: JSON.stringify(run.toolResults) })); + } else if (action === 'delete' && process.argv[3]) { + const record = await store.get(process.argv[3]); + if (record && record.namespace !== namespace) throw new Error('Cannot delete a record outside this fixture namespace'); + await store.delete(process.argv[3]); + console.log(JSON.stringify({ pid: process.pid })); + } else throw new Error('Unknown memory probe action'); +} finally { + await harness?.close(); + await store.close(); +} diff --git a/apps/growth-research/scripts/package-langsmith.mts b/apps/growth-research/scripts/package-langsmith.mts new file mode 100644 index 000000000..b0e30bd95 --- /dev/null +++ b/apps/growth-research/scripts/package-langsmith.mts @@ -0,0 +1,167 @@ +import { copyFile, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'; +import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const graphId = '/enrichment/research#agent'; +const publicGraphId = 'growth_research'; +const apiVersion = '0.13.4'; +const deploymentTsConfig = { + compilerOptions: { target: 'ES2024', module: 'NodeNext', moduleResolution: 'NodeNext', types: ['node'], skipLibCheck: true, noEmit: true }, + include: ['src/**/*.ts', 'dawn.config.ts', '.dawn/build/**/*.ts'], +}; +type JsonObject = Record; + +function object(value: unknown, label: string): JsonObject { + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Unexpected ${label} shape`); + return value as JsonObject; +} + +async function readObject(path: string): Promise { + return object(JSON.parse(await readFile(path, 'utf8')), path); +} + +export function deploymentManifest(value: unknown): JsonObject { + const manifest = object(value, 'package manifest'); + const dependencies = object(manifest['dependencies'], 'dependencies'); + for (const [name, version] of Object.entries(dependencies)) { + if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)) { + throw new Error(`Deployment dependency ${name} must use an exact registry version`); + } + } + if (manifest['private'] !== true || manifest['type'] !== 'module' || object(manifest['engines'], 'engines')['node'] !== '24') { + throw new Error('Unexpected package manifest: private ESM application on Node 24 required'); + } + return { name: manifest['name'], version: manifest['version'], private: true, type: 'module', engines: { node: '24' }, dependencies }; +} + +async function contained(root: string, path: string): Promise { + const rel = relative(root, await realpath(path)); + if (rel === '..' || rel.startsWith('../') || isAbsolute(rel)) throw new Error(`Outside-root symlink: ${relative(root, path)}`); + if ((await lstat(path)).isSymbolicLink()) throw new Error(`Symlinks are not allowed in deployment inputs: ${relative(root, path)}`); +} + +async function copySource(root: string, path: string, output: string): Promise { + await contained(root, path); + const name = basename(path); + if (name.startsWith('.') || name === 'node_modules' || /\.(spec|test)\.[cm]?ts$/.test(name)) return; + if ((await lstat(path)).isDirectory()) { + await mkdir(output, { recursive: true }); + for (const child of await readdir(path)) await copySource(root, join(path, child), join(output, child)); + } else if (['.ts', '.mts', '.json', '.md'].includes(extname(name))) { + await copyFile(path, output); + } else { + throw new Error(`Unexpected source file type: ${relative(root, path)}`); + } +} + +async function validateReference(root: string, value: unknown, label: string): Promise { + if (typeof value !== 'string' || !/^\.\/(?:\.dawn\/build\/|src\/)[\w./-]+\.[cm]?ts:[A-Za-z_$][\w$]*$/.test(value)) { + throw new Error(`Unexpected ${label} reference`); + } + const path = value.slice(0, value.lastIndexOf(':')); + if (path.split('/').includes('..')) throw new Error(`Unexpected ${label} path traversal`); + try { await contained(root, resolve(root, path)); } catch { throw new Error(`Invalid staged ${label} path: ${path}`); } +} + +function validateLock(lock: JsonObject, manifest: JsonObject): void { + if (lock['lockfileVersion'] !== 3) throw new Error('Unexpected deployment lockfile version'); + const packages = object(lock['packages'], 'lock packages'); + const root = object(packages[''], 'lock root'); + if (JSON.stringify(root['dependencies']) !== JSON.stringify(manifest['dependencies'])) { + throw new Error('Deployment dependency lock is stale; regenerate deployment-package-lock.json'); + } + for (const [path, entry] of Object.entries(packages)) { + const record = object(entry, 'locked dependency'); + if (path && !path.startsWith('node_modules/')) throw new Error('Unexpected workspace dependency in deployment lock'); + if (record['link'] || (typeof record['resolved'] === 'string' && !record['resolved'].startsWith('https://registry.npmjs.org/'))) { + throw new Error('Deployment lock contains a non-registry dependency'); + } + for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) { + for (const version of Object.values(object(record[section] ?? {}, 'locked dependencies'))) { + if (typeof version !== 'string' || /^(workspace:|file:|link:)/.test(version)) throw new Error('Deployment lock contains a local dependency'); + } + } + } +} + +export async function verifyLangSmithArtifact(output: string): Promise { + const root = await realpath(output); + const config = await readObject(join(root, 'langgraph.json')); + const graphs = object(config['graphs'], 'graphs'); + if (Object.keys(graphs).length !== 1 || typeof graphs[publicGraphId] !== 'string' || !/^\.\/\.dawn\/build\/[\w-]+\.ts:graph$/.test(graphs[publicGraphId])) { + throw new Error(`Expected exactly the ${publicGraphId} public graph`); + } + await validateReference(root, graphs[publicGraphId], 'graph'); + if (JSON.stringify(await readObject(join(root, 'tsconfig.json'))) !== JSON.stringify(deploymentTsConfig)) throw new Error('Unexpected standalone TypeScript configuration'); + if (config['api_version'] !== apiVersion) throw new Error(`Expected Agent Server API version ${apiVersion}`); + if (config['node_version'] !== '24' || JSON.stringify(config['env']) !== '{}' || JSON.stringify(config['dependencies']) !== '["."]') { + throw new Error('Unexpected normalized deployment config'); + } + if (config['auth']) await validateReference(root, object(config['auth'], 'auth')['path'], 'auth'); + const visit = async (path: string): Promise => { + await contained(root, path); + if (basename(path).startsWith('.env')) throw new Error('Environment files are forbidden in deployment artifacts'); + if ((await lstat(path)).isDirectory()) for (const child of await readdir(path)) await visit(join(path, child)); + }; + await visit(root); + const manifest = deploymentManifest(await readObject(join(root, 'package.json'))); + validateLock(await readObject(join(root, 'package-lock.json')), manifest); +} + +export async function stageLangSmith(appRoot: string): Promise { + const root = await realpath(appRoot); + for (const path of ['src', 'dawn.config.ts', 'package.json', 'deployment-package-lock.json', '.dawn', '.dawn/build', '.dawn/build/langgraph.json']) { + await contained(root, join(root, path)); + } + const config = await readObject(join(root, '.dawn/build/langgraph.json')); + const generatedGraphs = object(config['graphs'], 'graphs'); + const specialistId = '/enrichment/research/subagents/researcher#agent'; + if (Object.keys(generatedGraphs).some(key => key !== graphId && key !== specialistId)) throw new Error('Unexpected generated graph'); + if (specialistId in generatedGraphs) { + if (generatedGraphs[specialistId] !== './.dawn/build/enrichment-research-subagents-researcher.ts:graph') throw new Error('Unexpected specialist graph entry'); + await validateReference(root, generatedGraphs[specialistId], 'specialist graph'); + } + if (Object.keys(config).some(key => !['graphs', 'env', 'node_version', 'api_version', 'dependencies', 'auth'].includes(key))) throw new Error('Unexpected generated configuration field'); + if ('api_version' in config && config['api_version'] !== apiVersion) throw new Error(`Unexpected Agent Server API version; expected ${apiVersion}`); + if (!['22', '24'].includes(String(config['node_version'])) || JSON.stringify(config['dependencies']) !== '["."]' || !(typeof config['env'] === 'string' || (config['env'] && typeof config['env'] === 'object' && !Array.isArray(config['env'])))) { + throw new Error('Unexpected generated deployment config shape'); + } + if (config['auth']) { + const auth = object(config['auth'], 'auth'); + if (Object.keys(auth).some(key => !['path', 'disable_studio_auth'].includes(key)) || ('disable_studio_auth' in auth && typeof auth['disable_studio_auth'] !== 'boolean')) throw new Error('Unexpected auth configuration'); + } + const manifest = deploymentManifest(await readObject(join(root, 'package.json'))); + const lock = await readObject(join(root, 'deployment-package-lock.json')); + validateLock(lock, manifest); + const output = join(root, '.deployment'); + try { await contained(root, output); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } + await rm(output, { recursive: true, force: true }); + await mkdir(join(output, '.dawn/build'), { recursive: true }); + await copySource(root, join(root, 'src'), join(output, 'src')); + await copyFile(join(root, 'dawn.config.ts'), join(output, 'dawn.config.ts')); + const copySchemas = async (path: string, target: string): Promise => { + await contained(root, path); + if ((await lstat(path)).isDirectory()) { + await mkdir(target, { recursive: true }); + for (const name of await readdir(path)) await copySchemas(join(path, name), join(target, name)); + } else if (basename(path) === 'tools.json') { + await readObject(path); + await copyFile(path, target); + } + }; + await copySchemas(join(root, '.dawn/routes'), join(output, '.dawn/routes')); + for (const name of await readdir(join(root, '.dawn/build'))) { + if (!name.endsWith('.ts')) continue; + await contained(root, join(root, '.dawn/build', name)); + await copyFile(join(root, '.dawn/build', name), join(output, '.dawn/build', name)); + } + for (const [name, value] of Object.entries({ 'package.json': manifest, 'package-lock.json': lock, 'tsconfig.json': deploymentTsConfig, 'langgraph.json': { ...config, graphs: { [publicGraphId]: generatedGraphs[graphId] }, node_version: '24', api_version: apiVersion, dependencies: ['.'], env: {} } })) { + await writeFile(join(output, name), `${JSON.stringify(value, null, 2)}\n`); + } + try { await verifyLangSmithArtifact(output); } catch (error) { await rm(output, { recursive: true, force: true }); throw error; } + return output; +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + console.log(`Staged LangSmith artifact: ${await stageLangSmith(resolve(dirname(fileURLToPath(import.meta.url)), '..'))}`); +} diff --git a/apps/growth-research/scripts/platform-client.mts b/apps/growth-research/scripts/platform-client.mts new file mode 100644 index 000000000..729edfd7c --- /dev/null +++ b/apps/growth-research/scripts/platform-client.mts @@ -0,0 +1,196 @@ +import { setTimeout as delay } from 'node:timers/promises'; + +export const researchGraphId = 'growth_research'; +const uuid = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i; +const statuses = new Set(['pending', 'running', 'success', 'error', 'timeout', 'interrupted']); + +export class PlatformError extends Error { + readonly code: string; + readonly status: number | undefined; + constructor(code: string, message: string, status?: number) { + super(message); + this.code = code; + this.status = status; + } +} + +export interface PlatformRun { + run_id: string; + thread_id: string; + status: string; + metadata: Record; +} + +function object(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new PlatformError('invalid_response', 'Invalid platform response.'); + return value as Record; +} + +function id(value: string): string { + if (!uuid.test(value)) throw new PlatformError('invalid_id', 'Expected a platform UUID.'); + return value; +} + +function parseRun(value: unknown, threadId: string): PlatformRun { + const row = object(value); + if (typeof row['run_id'] !== 'string' || !uuid.test(row['run_id']) || row['thread_id'] !== threadId || typeof row['status'] !== 'string' || !statuses.has(row['status'])) { + throw new PlatformError('invalid_response', 'Invalid platform run.'); + } + return { run_id: row['run_id'], thread_id: threadId, status: row['status'], metadata: object(row['metadata'] ?? {}) }; +} + +/** Internal synthetic smoke client. Persistent work leases and cross-process deduplication belong to Growth. */ +export function createPlatformClient(options: { + url: string; + apiKey?: string; + fetch?: typeof fetch; + requestTimeoutMs?: number; + runTimeoutMs?: number; + pollMs?: number; +}) { + const base = new URL(options.url); + const local = ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname); + if ((base.protocol !== 'https:' && !(local && base.protocol === 'http:')) || base.username || base.password || base.search || base.hash || base.pathname !== '/') { + throw new PlatformError('invalid_url', 'Use a bare HTTPS server origin or local development origin.'); + } + if (!local && !options.apiKey) throw new PlatformError('missing_credential', 'A server-held LangSmith credential is required.'); + const fetcher = options.fetch ?? fetch; + const requestTimeoutMs = options.requestTimeoutMs ?? 15_000; + const runTimeoutMs = options.runTimeoutMs ?? 120_000; + const pollMs = options.pollMs ?? 500; + for (const n of [requestTimeoutMs, runTimeoutMs, pollMs]) { + if (!Number.isFinite(n) || n <= 0 || n > 300_000) throw new PlatformError('invalid_timeout', 'Timeouts must be positive and bounded.'); + } + + async function request(path: string, method = 'GET', body?: unknown, responseOptions: { json?: boolean; timeoutMs?: number } = {}): Promise { + let response: Response; + try { + response = await fetcher(new URL(path, base).href, { + method, redirect: 'error', signal: AbortSignal.timeout(responseOptions.timeoutMs ?? requestTimeoutMs), + headers: { 'content-type': 'application/json', ...(options.apiKey ? { 'x-api-key': options.apiKey } : {}) }, + ...(body !== undefined ? { body: JSON.stringify(body) } : {}), + }); + } catch { + // Transport exceptions and response bodies can contain credentials or fixture input. + throw new PlatformError('transport_error', 'Platform request did not return a usable response.'); + } + if (!response.ok) throw new PlatformError('http_error', `Platform request failed with HTTP ${response.status}.`, response.status); + if (responseOptions.json === false || response.status === 204) return null; + try { return await response.json(); } catch { throw new PlatformError('invalid_response', 'Platform returned invalid JSON.'); } + } + + async function listRuns(threadId: string): Promise { + const runs: PlatformRun[] = []; + for (let offset = 0; offset < 1_000; offset += 100) { + const page = await request(`/threads/${id(threadId)}/runs?limit=100&offset=${offset}`); + if (!Array.isArray(page)) throw new PlatformError('invalid_response', 'Expected a platform run list.'); + runs.push(...page.map(row => parseRun(row, threadId))); + if (page.length < 100) return runs; + } + throw new PlatformError('pagination_limit', 'Run history exceeds the synthetic smoke limit.'); + } + + async function findRun(threadId: string, correlationId: string): Promise { + const matches = new Map((await listRuns(threadId)).filter(run => run.metadata['growth_research_correlation'] === correlationId).map(run => [run.run_id, run])); + if (matches.size > 1) throw new PlatformError('duplicate_runs', 'Multiple runs match this fixture; operator reconciliation is required.'); + return matches.values().next().value; + } + + const submissions = new Map>(); + function submitRun(threadId: string, correlationId: string, input: unknown): Promise { + if (!correlationId || correlationId.length > 160) throw new PlatformError('invalid_correlation', 'A bounded fixture correlation ID is required.'); + const key = `${id(threadId)}:${correlationId}`; + const existing = submissions.get(key); + if (existing) return existing; + const attempt = (async () => { + const prior = await findRun(threadId, correlationId); + if (prior) return prior; + try { + const result = parseRun(await request(`/threads/${threadId}/runs`, 'POST', { + assistant_id: researchGraphId, input, + metadata: { growth_research_correlation: correlationId }, + config: { recursion_limit: 12 }, multitask_strategy: 'reject', durability: 'sync', + }), threadId); + if (result.metadata['growth_research_correlation'] !== correlationId) throw new PlatformError('invalid_response', 'Run correlation was not returned.'); + return result; + } catch (error) { + if (error instanceof PlatformError && error.status && error.status >= 400 && error.status < 500) throw error; + try { + const reconciled = await findRun(threadId, correlationId); + if (reconciled) return reconciled; + } catch { /* Keep the uncertain submission blocked, including when reconciliation fails. */ } + throw new PlatformError('ambiguous_submission', 'Run submission outcome is unknown; reconcile before another attempt.'); + } + })(); + // Retain rejected promises too: a caller retry must not blindly submit again. + submissions.set(key, attempt); + return attempt; + } + + async function readRun(threadId: string, runId: string, timeoutMs = requestTimeoutMs): Promise { + const run = parseRun(await request(`/threads/${id(threadId)}/runs/${id(runId)}`, 'GET', undefined, { timeoutMs }), threadId); + if (run.run_id !== runId) throw new PlatformError('invalid_response', 'Platform returned a different run.'); + return run; + } + + async function waitForTerminal(threadId: string, runId: string): Promise { + const until = Date.now() + runTimeoutMs; + while (Date.now() < until) { + let run: PlatformRun; + try { run = await readRun(threadId, runId, Math.max(1, Math.min(requestTimeoutMs, until - Date.now()))); } catch (error) { + if (Date.now() >= until) break; + throw error; + } + if (Date.now() >= until) break; + if (run.status !== 'pending' && run.status !== 'running') return run; + await delay(Math.max(1, Math.min(pollMs, until - Date.now()))); + } + throw new PlatformError('run_wait_timeout', 'Run did not finish within the smoke deadline; cancel or reconcile it.'); + } + + async function waitForSuccess(threadId: string, runId: string): Promise { + const run = await waitForTerminal(threadId, runId); + if (run.status !== 'success') throw new PlatformError('run_failed', `Synthetic run ended with status ${run.status}.`); + return run; + } + + function assertOwnership(value: unknown, threadId: string, smokeId: string): void { + const thread = object(value); + if (thread['thread_id'] !== threadId || object(thread['metadata'] ?? {})['growth_research_smoke'] !== smokeId) { + throw new PlatformError('foreign_thread', 'Thread does not belong to this synthetic smoke.'); + } + } + + async function ensureFixtureThread(threadId: string, smokeId: string): Promise { + if (!smokeId || smokeId.length > 160) throw new PlatformError('invalid_correlation', 'A bounded smoke ID is required.'); + const thread = await request('/threads', 'POST', { thread_id: id(threadId), if_exists: 'do_nothing', metadata: { growth_research_smoke: smokeId } }); + assertOwnership(thread, threadId, smokeId); + } + + async function deleteFixtureThread(threadId: string, smokeId: string): Promise { + assertOwnership(await request(`/threads/${id(threadId)}`), threadId, smokeId); + const runs = await listRuns(threadId); + if (runs.some(run => run.status === 'pending' || run.status === 'running')) { + throw new PlatformError('active_run', 'Cancel and reconcile active fixture runs before deletion.'); + } + // Agent Server 0.13.4 can report interruption while its JS graph keeps writing. + // Leave these threads for operator cleanup after independently proven quiescence. + if (runs.some(run => run.status === 'interrupted')) throw new PlatformError('quiescence_unverified', 'Interrupted JavaScript runs require verified worker quiescence before operator cleanup.'); + await request(`/threads/${threadId}`, 'DELETE', undefined, { json: false }); + try { await request(`/threads/${threadId}`); } catch (error) { + if (error instanceof PlatformError && error.status === 404) return; + throw error; + } + throw new PlatformError('cleanup_failed', 'Fixture thread remains readable after deletion.'); + } + + async function cancelRun(threadId: string, runId: string): Promise { + await request(`/threads/${id(threadId)}/runs/${id(runId)}/cancel?wait=true&action=interrupt`, 'POST', undefined, { json: false }); + return waitForTerminal(threadId, runId); + } + + return { submitRun, getRun: (threadId: string, runId: string) => readRun(threadId, runId), listRuns, waitForTerminal, waitForSuccess, ensureFixtureThread, deleteFixtureThread, cancelRun, + getState: (threadId: string) => request(`/threads/${id(threadId)}/state`), + discover: () => request('/assistants/search', 'POST', { limit: 100 }), + }; +} diff --git a/apps/growth-research/scripts/verify-langsmith-artifact.mts b/apps/growth-research/scripts/verify-langsmith-artifact.mts new file mode 100644 index 000000000..1ca4833d4 --- /dev/null +++ b/apps/growth-research/scripts/verify-langsmith-artifact.mts @@ -0,0 +1,6 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { verifyLangSmithArtifact } from './package-langsmith.mts'; + +await verifyLangSmithArtifact(resolve(dirname(fileURLToPath(import.meta.url)), '../.deployment')); +console.log('Verified staged configuration, graph paths, dependency lock and absence of environment files.'); diff --git a/apps/growth-research/src/app/enrichment/research/index.ts b/apps/growth-research/src/app/enrichment/research/index.ts new file mode 100644 index 000000000..39909655a --- /dev/null +++ b/apps/growth-research/src/app/enrichment/research/index.ts @@ -0,0 +1,12 @@ +import { agent } from '@dawn-ai/sdk'; +import researcher from './subagents/researcher/index.js'; + +export default agent({ + model: 'gpt-4.1-mini', + systemPrompt: 'Coordinate synthetic fixture research only. Accept atlas or beacon fixture IDs. Load the company-evidence skill, maintain the authored plan, and read the compiled fixture directly or delegate a bounded task to researcher. Cite fixture source identifiers. Do not research real people or companies or promote candidate memories to accepted facts.', + tools: { allow: ['readFixture', 'coordinatorSummary'] }, + subagents: { researcher }, + delegation: { default: 'deny', rules: { researcher: { action: 'allow' } } }, + recursionLimit: 12, + retry: { maxAttempts: 1 }, +}); diff --git a/apps/growth-research/src/app/enrichment/research/memory.ts b/apps/growth-research/src/app/enrichment/research/memory.ts new file mode 100644 index 000000000..d805e7192 --- /dev/null +++ b/apps/growth-research/src/app/enrichment/research/memory.ts @@ -0,0 +1,13 @@ +import { defineMemory } from '@dawn-ai/sdk'; +import { z } from 'zod'; + +export default defineMemory({ + kind: 'semantic', + scope: ['workspace', 'route', 'agent'], + identity: ['fixtureId', 'source'], + schema: z.object({ + fixtureId: z.enum(['atlas', 'beacon']), + observation: z.string().min(1).max(500), + source: z.enum(['fixture:atlas:v1', 'fixture:beacon:v1']), + }), +}); diff --git a/apps/growth-research/src/app/enrichment/research/plan.md b/apps/growth-research/src/app/enrichment/research/plan.md new file mode 100644 index 000000000..de7550dbd --- /dev/null +++ b/apps/growth-research/src/app/enrichment/research/plan.md @@ -0,0 +1,5 @@ +# Synthetic enrichment compatibility + +- [ ] Identify the synthetic fixture +- [ ] Check only the supplied fixture evidence +- [ ] Report the observed compatibility result without real-world account assertions diff --git a/apps/growth-research/src/app/enrichment/research/skills/company-evidence/SKILL.md b/apps/growth-research/src/app/enrichment/research/skills/company-evidence/SKILL.md new file mode 100644 index 000000000..df77f523c --- /dev/null +++ b/apps/growth-research/src/app/enrichment/research/skills/company-evidence/SKILL.md @@ -0,0 +1,12 @@ +--- +name: company-evidence +description: Inspect synthetic company fixtures for the deployment compatibility probe. +--- + +Use only the synthetic fixture corpus. Distinguish observations from candidate claims. +Never collect real subjects or publish account assertions. + +1. Select the supplied atlas or beacon fixture. +2. Read its observation and retain the exact fixture source identifier. +3. State what the fixture supports and what remains unknown. +4. Never treat candidate memory as an accepted account fact. diff --git a/apps/growth-research/src/app/enrichment/research/subagents/researcher/index.ts b/apps/growth-research/src/app/enrichment/research/subagents/researcher/index.ts new file mode 100644 index 000000000..e0709a4d1 --- /dev/null +++ b/apps/growth-research/src/app/enrichment/research/subagents/researcher/index.ts @@ -0,0 +1,11 @@ +import { agent } from '@dawn-ai/sdk'; + +export default agent({ + model: 'gpt-4.1-mini', + description: 'Review a bounded synthetic fixture and return its evidence.', + systemPrompt: 'You are the synthetic evidence specialist. Read only the named atlas or beacon fixture with readFixture. Return evidence with its fixture source. Never access real subjects or promote candidate claims.', + tools: { allow: ['readFixture'], deny: ['coordinatorSummary'] }, + delegation: { default: 'deny' }, + recursionLimit: 12, + retry: { maxAttempts: 1 }, +}); diff --git a/apps/growth-research/src/runtime/fixture-contract.ts b/apps/growth-research/src/runtime/fixture-contract.ts new file mode 100644 index 000000000..2d943fa52 --- /dev/null +++ b/apps/growth-research/src/runtime/fixture-contract.ts @@ -0,0 +1,18 @@ +export type FixtureId = 'atlas' | 'beacon'; + +const corpus = { + atlas: { name: 'Atlas Synthetic', observation: 'Synthetic fixture documents an Angular evaluation.', source: 'fixture:atlas:v1' }, + beacon: { name: 'Beacon Synthetic', observation: 'Synthetic fixture documents a support prototype.', source: 'fixture:beacon:v1' }, +} as const; + +export function assertFixtureMode(): void { + if (process.env['GROWTH_RESEARCH_FIXTURE_MODE'] !== 'synthetic-only') { + throw new Error('Growth research fixture mode is disabled'); + } +} + +export function readSyntheticFixture(fixtureId: FixtureId) { + assertFixtureMode(); + if (fixtureId !== 'atlas' && fixtureId !== 'beacon') throw new Error('Unknown synthetic fixture'); + return { fixtureId, ...corpus[fixtureId] }; +} diff --git a/apps/growth-research/src/runtime/memory-store.ts b/apps/growth-research/src/runtime/memory-store.ts new file mode 100644 index 000000000..4fe230fc7 --- /dev/null +++ b/apps/growth-research/src/runtime/memory-store.ts @@ -0,0 +1,50 @@ +import { createHash } from 'node:crypto'; +import { pgvectorMemoryStore, type PgvectorMemoryStore } from '@dawn-ai/memory-pgvector'; + +export const syntheticEmbedder = { + id: 'growth-synthetic-sha256-v1', + dims: 8, + async embed(texts: readonly string[]): Promise { + return texts.map(text => { + const digest = createHash('sha256').update(text).digest(); + return Float32Array.from({ length: 8 }, (_, index) => digest.readUInt32BE(index * 4) / 0xffffffff); + }); + }, +}; + +// Server-owned fixture slot, never read from a user message or route parameter. +// This proves synthetic addressing only; it is not authenticated tenant scope. +export function trustedFixtureScope(): { workspace: 'growth-research'; agent: 'atlas' | 'beacon' } { + const slot = process.env['GROWTH_RESEARCH_FIXTURE_SLOT'] ?? 'atlas'; + if (slot !== 'atlas' && slot !== 'beacon') throw new Error('Unknown trusted fixture slot'); + return { workspace: 'growth-research', agent: slot }; +} + +export function createDurableMemoryStore(): PgvectorMemoryStore { + let initialized: PgvectorMemoryStore | undefined; + const store = () => { + if (!initialized) { + const connectionString = process.env['DAWN_DATABASE_URL']; + if (!connectionString) throw new Error('DAWN_DATABASE_URL is required for durable Growth research memory'); + initialized = pgvectorMemoryStore({ connectionString, dimensions: syntheticEmbedder.dims, tablePrefix: 'growth_research' }); + } + return initialized; + }; + return { + put: async (...args) => store().put(...args), + get: async (...args) => store().get(...args), + // Disabling Dawn's eager prompt index must not open a database at graph import. + // Explicit recall uses a positive limit and still requires durable storage. + search: async query => query.limit === 0 ? [] : store().search(query), + update: async (...args) => store().update(...args), + supersede: async (...args) => store().supersede(...args), + delete: async (...args) => store().delete(...args), + listCandidates: async (...args) => store().listCandidates(...args), + browse: async (...args) => store().browse(...args), + stats: async (...args) => store().stats(...args), + prune: async (...args) => store().prune(...args), + close: async () => { await initialized?.close(); initialized = undefined; }, + }; +} + +export const candidateMemoryStore = createDurableMemoryStore(); diff --git a/apps/growth-research/src/runtime/model-boundary.ts b/apps/growth-research/src/runtime/model-boundary.ts new file mode 100644 index 000000000..8389627e7 --- /dev/null +++ b/apps/growth-research/src/runtime/model-boundary.ts @@ -0,0 +1,47 @@ +import { seedModelImporter } from '@dawn-ai/langchain'; +import { ChatOpenAI } from '@langchain/openai'; +import { assertFixtureMode } from './fixture-contract.js'; + +export const providerLimits = { maxTokens: 1024, maxRetries: 0, timeout: 20_000 } as const; + +export class BoundedChatOpenAI extends ChatOpenAI { + readonly #guard: () => void; + + constructor(options: ConstructorParameters[0] = {}) { + const apiKey = options.apiKey || process.env['OPENAI_API_KEY']; + const guard = () => { + assertFixtureMode(); + if (!apiKey) throw new Error('OPENAI_API_KEY is required for synthetic model invocation'); + }; + super({ + ...options, + // Schema extraction constructs a model. The placeholder cannot reach the + // provider: both invocation methods and the actual fetch call check guard. + apiKey: apiKey || 'growth-research-schema-only', + ...providerLimits, + configuration: { + ...options.configuration, + maxRetries: providerLimits.maxRetries, + timeout: providerLimits.timeout, + fetch: (input, init) => { guard(); return fetch(input, init); }, + }, + }); + this.#guard = guard; + } + + override async _generate(...args: Parameters) { + this.#guard(); + return super._generate(...args); + } + + override async *_streamResponseChunks(...args: Parameters) { + this.#guard(); + yield* super._streamResponseChunks(...args); + } +} + +// Public Dawn bootstrap hook; this app owns its process and permits one provider. +seedModelImporter(async specifier => { + if (specifier !== '@langchain/openai') throw new Error('Synthetic research supports only the bounded OpenAI provider'); + return { ChatOpenAI: BoundedChatOpenAI }; +}); diff --git a/apps/growth-research/src/tools/coordinatorSummary.ts b/apps/growth-research/src/tools/coordinatorSummary.ts new file mode 100644 index 000000000..eb6821572 --- /dev/null +++ b/apps/growth-research/src/tools/coordinatorSummary.ts @@ -0,0 +1,6 @@ +import { readSyntheticFixture, type FixtureId } from '../runtime/fixture-contract.js'; + +/** Produce a coordinator-only synthetic summary of the fixed corpus. */ +export default function coordinatorSummary(input: { fixtureId: FixtureId }) { + return { label: 'coordinator-only synthetic summary', evidence: readSyntheticFixture(input.fixtureId) }; +} diff --git a/apps/growth-research/src/tools/readFixture.ts b/apps/growth-research/src/tools/readFixture.ts new file mode 100644 index 000000000..4e4b3854f --- /dev/null +++ b/apps/growth-research/src/tools/readFixture.ts @@ -0,0 +1,15 @@ +import { setTimeout } from 'node:timers/promises'; +import { assertFixtureMode, readSyntheticFixture, type FixtureId } from '../runtime/fixture-contract.js'; + +/** Read one compiled synthetic fixture. No network or filesystem access. */ +export default async function readFixture(input: { fixtureId: FixtureId }, context: { signal: AbortSignal }) { + assertFixtureMode(); + const configured = process.env['GROWTH_RESEARCH_FIXTURE_DELAY_MS'] ?? '0'; + if (!/^\d+$/.test(configured) || Number(configured) > 5000) throw new Error('Invalid fixture delay; expected an integer from 0 to 5000 milliseconds'); + const delay = Number(configured); + context.signal.throwIfAborted(); + if (delay > 0) await setTimeout(delay, undefined, { signal: context.signal }); + context.signal.throwIfAborted(); + assertFixtureMode(); + return readSyntheticFixture(input.fixtureId); +} diff --git a/apps/growth-research/test/capabilities.spec.ts b/apps/growth-research/test/capabilities.spec.ts new file mode 100644 index 000000000..eed3f5ac3 --- /dev/null +++ b/apps/growth-research/test/capabilities.spec.ts @@ -0,0 +1,121 @@ +import { cp, mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { resolve, join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createAgentHarness, createAimock, script, type AgentHarness, type Aimock } from '@dawn-ai/testing'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +let harness: AgentHarness | undefined; +let mock: Aimock; +let temporaryRoot: string | undefined; + +beforeEach(async () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', 'synthetic-only'); + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_DELAY_MS', '0'); + vi.stubEnv('OPENAI_API_KEY', 'synthetic-test-key'); + mock = await createAimock({ fixtures: [] }); +}); +afterEach(async () => { + await harness?.close(); harness = undefined; + await mock.close(); + if (temporaryRoot) await rm(temporaryRoot, { recursive: true, force: true }); + temporaryRoot = undefined; + vi.unstubAllEnvs(); +}); + +async function start(root = appRoot) { + harness = await createAgentHarness({ appRoot: root, route: '/enrichment/research#agent', record: true, recordUpstream: mock.baseUrl.replace(/\/v1$/, '') }); + return harness; +} + +describe('synthetic capability boundary', () => { + it('executes direct fixture research with authored planning and loaded skill instructions', async () => { + mock.addFixtures(script().user('direct fixture atlas') + .callsTool('readSkill', { name: 'company-evidence' }) + .callsTool('readFixture', { fixtureId: 'atlas' }) + .callsTool('writeTodos', { todos: [{ content: 'Verify synthetic fixture evidence', status: 'completed' }] }) + .replies('Atlas synthetic evidence reviewed.').build()); + const run = await (await start()).run({ input: 'direct fixture atlas' }); + expect(run.toolResults.find(tool => tool.name === 'readFixture')?.content).toContain('Atlas Synthetic'); + expect(run.toolResults.find(tool => tool.name === 'readSkill')?.content).toContain('Never treat candidate memory as an accepted account fact'); + expect(run.systemPrompt).toContain('Identify the synthetic fixture'); + expect(run.planUpdates.at(-1)?.todos).toEqual([{ content: 'Verify synthetic fixture evidence', status: 'completed' }]); + expect(run.finalMessage).toBe('Atlas synthetic evidence reviewed.'); + const requests = mock.getRequests(); + expect(requests).toHaveLength(4); + const names = requests[0]?.body?.tools?.map(tool => tool.function?.name); + expect(names?.sort()).toEqual(['coordinatorSummary', 'readFixture', 'readSkill', 'recall', 'remember', 'task', 'writeTodos']); + for (const request of requests) expect(request.body).toMatchObject({ model: 'gpt-4.1-mini', max_tokens: 1024 }); + }, 60_000); + + it('rejects arbitrary fixture identifiers through the generated tool schema', async () => { + mock.addFixtures(script().user('invalid fixture').callsTool('readFixture', { fixtureId: 'https://external.example/real-subject' }).replies('Invalid fixture denied.').build()); + const run = await (await start()).run({ input: 'invalid fixture' }); + expect(JSON.stringify(run.toolResults)).toMatch(/invalid|schema|expected/i); + expect(JSON.stringify(run.toolResults)).not.toContain('Atlas Synthetic'); + expect(mock.getRequests()).toHaveLength(2); + }, 60_000); + + it('delegates only to the registered specialist with scoped fixture tools', async () => { + mock.addFixtures([ + ...script().user('delegate atlas') + .callsTool('task', { subagent: 'researcher', input: 'specialist atlas' }) + .replies('Delegation complete.').build(), + ...script().user('specialist atlas') + .callsTool('readFixture', { fixtureId: 'atlas' }) + .replies('Atlas specialist evidence.').build(), + ]); + const run = await (await start()).run({ input: 'delegate atlas' }); + expect(run.subagents).toHaveLength(1); + expect(run.subagents[0]).toMatchObject({ name: 'researcher', finalMessage: 'Atlas specialist evidence.' }); + expect(run.subagents[0]?.toolCalls).toContainEqual({ name: 'readFixture', args: { fixtureId: 'atlas' } }); + const child = mock.getRequests().find(request => JSON.stringify(request.body?.messages).includes('You are the synthetic evidence specialist')); + expect(child).toBeDefined(); + expect(child?.body?.tools?.map(tool => tool.function?.name)).toEqual(['readFixture']); + }, 60_000); + + it('rejects a specialist attempt to call its coordinator-only tool', async () => { + mock.addFixtures([ + ...script().user('attempt parent tool').callsTool('task', { subagent: 'researcher', input: 'specialist attack' }).replies('Denied.').build(), + ...script().user('specialist attack').callsTool('coordinatorSummary', { fixtureId: 'atlas' }).replies('No coordinator access.').build(), + ]); + const run = await (await start()).run({ input: 'attempt parent tool' }); + const childRequests = mock.getRequests().filter(request => JSON.stringify(request.body?.messages).includes('You are the synthetic evidence specialist')); + expect(childRequests).toHaveLength(2); + expect(JSON.stringify(childRequests[1]?.body?.messages)).toMatch(/not found|not available|unknown tool/i); + expect(JSON.stringify(childRequests[1]?.body?.messages)).not.toContain('coordinator-only synthetic summary'); + expect(run.subagents[0]?.finalMessage).toBe('No coordinator access.'); + }, 60_000); + + it('denies an undeclared convention sibling at the dispatch boundary', async () => { + temporaryRoot = await mkdtemp(join(tmpdir(), 'growth-capabilities-')); + await cp(join(appRoot, 'src'), join(temporaryRoot, 'src'), { recursive: true }); + await cp(join(appRoot, 'dawn.config.ts'), join(temporaryRoot, 'dawn.config.ts')); + await cp(join(appRoot, 'package.json'), join(temporaryRoot, 'package.json')); + await symlink(join(appRoot, 'node_modules'), join(temporaryRoot, 'node_modules')); + const sibling = join(temporaryRoot, 'src/app/enrichment/research/subagents/undeclared'); + await mkdir(sibling, { recursive: true }); + await writeFile(join(sibling, 'index.ts'), 'import {agent} from "@dawn-ai/sdk"; export default agent({model:"gpt-4.1-mini",systemPrompt:"UNDECLARED_SIBLING_EXECUTED",description:"Forbidden sibling"});'); + mock.addFixtures(script().user('try sibling').callsTool('task', { subagent: 'undeclared', input: 'forbidden child' }).replies('Sibling denied.').build()); + const run = await (await start(temporaryRoot)).run({ input: 'try sibling' }); + expect(run.subagents).toHaveLength(0); + expect(JSON.stringify(run.toolResults)).toMatch(/DAWN_E3002|DAWN_E5003|invalid|expected/i); + expect(run.systemPrompt).not.toContain('Forbidden sibling'); + expect(mock.getRequests()).toHaveLength(2); + }, 60_000); + + it('blocks model invocation when fixture mode is absent, including a cached model', async () => { + mock.addFixtures(script().user('gate warmup').replies('Warm.').build()); + const h = await start(); + await h.run({ input: 'gate warmup' }); + const count = mock.getRequests().length; + delete process.env['GROWTH_RESEARCH_FIXTURE_MODE']; + h.reset(); + const error = await h.run({ input: 'gate blocked' }).then(() => undefined, (failure: Error) => failure); + expect(error).toBeInstanceOf(Error); + // LangChain's bound completion client wraps fetch failures; retain the gate cause. + expect(error?.cause instanceof Error ? error.cause.message : error?.message).toMatch(/fixture mode/i); + expect(mock.getRequests()).toHaveLength(count); + }, 60_000); +}); diff --git a/apps/growth-research/test/fixture-tool.spec.ts b/apps/growth-research/test/fixture-tool.spec.ts new file mode 100644 index 000000000..d1af1e841 --- /dev/null +++ b/apps/growth-research/test/fixture-tool.spec.ts @@ -0,0 +1,21 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import readFixture from '../src/tools/readFixture.js'; + +afterEach(() => vi.unstubAllEnvs()); + +it('aborts a paused fixture tool without producing later fixture output', async () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', 'synthetic-only'); + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_DELAY_MS', '5000'); + const controller = new AbortController(); + let produced = false; + const result = Promise.resolve(readFixture({ fixtureId: 'atlas' }, { signal: controller.signal })).then(value => { produced = true; return value; }); + controller.abort(); + await expect(result).rejects.toThrow(/abort/i); + expect(produced).toBe(false); +}); + +it.each(['-1', '5001', 'NaN', '1.5'])('rejects invalid server-owned fixture delays: %s', async delay => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', 'synthetic-only'); + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_DELAY_MS', delay); + await expect(Promise.resolve().then(() => readFixture({ fixtureId: 'atlas' }, { signal: new AbortController().signal }))).rejects.toThrow(/fixture delay/i); +}); diff --git a/apps/growth-research/test/memory-store.spec.ts b/apps/growth-research/test/memory-store.spec.ts new file mode 100644 index 000000000..cb2ad5555 --- /dev/null +++ b/apps/growth-research/test/memory-store.spec.ts @@ -0,0 +1,34 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { createDurableMemoryStore, syntheticEmbedder, trustedFixtureScope } from '../src/runtime/memory-store.js'; + +afterEach(() => vi.unstubAllEnvs()); + +it('constructs without credentials but refuses runtime storage with no database', async () => { + vi.stubEnv('DAWN_DATABASE_URL', ''); + const store = createDurableMemoryStore(); + await expect(store.search({ namespace: 'synthetic' })).rejects.toThrow(/DAWN_DATABASE_URL is required/); + await store.close(); +}); + +it('returns the mathematically empty zero-limit index without opening a database', async () => { + vi.stubEnv('DAWN_DATABASE_URL', ''); + const store = createDurableMemoryStore(); + await expect(store.search({ namespace: 'synthetic', limit: 0 })).resolves.toEqual([]); + await expect(store.search({ namespace: 'synthetic', limit: 1 })).rejects.toThrow(/DAWN_DATABASE_URL is required/); + await store.close(); +}); + +it('uses deterministic finite vectors with the declared dimensions', async () => { + const [first, repeated, other] = await syntheticEmbedder.embed(['atlas', 'atlas', 'beacon']); + expect(Array.from(first)).toHaveLength(8); + expect(Array.from(first)).toEqual(Array.from(repeated)); + expect(Array.from(first)).not.toEqual(Array.from(other)); + expect(Array.from(first).every(Number.isFinite)).toBe(true); +}); + +it('accepts only a trusted closed fixture slot for memory addressing', () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_SLOT', 'beacon'); + expect(trustedFixtureScope()).toEqual({ workspace: 'growth-research', agent: 'beacon' }); + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_SLOT', 'external-tenant'); + expect(() => trustedFixtureScope()).toThrow(/fixture slot/); +}); diff --git a/apps/growth-research/test/memory.integration.spec.ts b/apps/growth-research/test/memory.integration.spec.ts new file mode 100644 index 000000000..d5f5e14b2 --- /dev/null +++ b/apps/growth-research/test/memory.integration.spec.ts @@ -0,0 +1,68 @@ +import { execFile } from 'node:child_process'; +import { cp, mkdir, mkdtemp, rm, symlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { promisify } from 'node:util'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { expect, it } from 'vitest'; + +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const execute = promisify(execFile); +const database = process.env['GROWTH_RESEARCH_TEST_DATABASE_URL']; +if (!database) throw new Error('GROWTH_RESEARCH_TEST_DATABASE_URL is required; no production fallback is permitted'); + +async function probe(action: string, fixture: 'atlas' | 'beacon', id?: string, root = appRoot) { + const result = await execute(process.execPath, ['scripts/memory-probe.mts', action, ...(id ? [id] : [])], { + cwd: root, + timeout: 30_000, + env: { ...process.env, DAWN_DATABASE_URL: database, GROWTH_RESEARCH_FIXTURE_MODE: 'synthetic-only', GROWTH_RESEARCH_FIXTURE_SLOT: fixture, OPENAI_API_KEY: 'synthetic-test-key' }, + }); + return JSON.parse(result.stdout.trim()) as { id: string; content: string; candidateIds: string[]; activeIds: string[]; recalled: string; pid: number }; +} + +it('persists candidates across fresh processes, excludes them from active recall, isolates fixture slots and preserves deletion', async () => { + const written = await probe('write', 'atlas'); + const relocated = await mkdtemp(join(tmpdir(), 'growth-memory-relocated-')); + let controlId: string | undefined; + try { + await cp(join(appRoot, 'src'), join(relocated, 'src'), { recursive: true }); + for (const name of ['dawn.config.ts', 'package.json']) await cp(join(appRoot, name), join(relocated, name)); + await mkdir(join(relocated, 'scripts')); + await cp(join(appRoot, 'scripts/memory-probe.mts'), join(relocated, 'scripts/memory-probe.mts')); + await symlink(join(appRoot, 'node_modules'), join(relocated, 'node_modules')); + const read = await probe('read', 'atlas'); + expect(read.pid).not.toBe(written.pid); + expect(read.candidateIds).toContain(written.id); + expect(read.activeIds).not.toContain(written.id); + expect(read.recalled).not.toContain(written.id); + const moved = await probe('read', 'atlas', undefined, relocated); + expect(moved.candidateIds).toContain(written.id); + const other = await probe('read', 'beacon'); + expect(other.candidateIds).not.toContain(written.id); + expect(other.activeIds).not.toContain(written.id); + const control = await probe('seed-active-control', 'atlas'); + controlId = control.id; + expect(control.id).not.toBe(written.id); + const positive = await probe('read', 'atlas', control.id); + expect(positive.candidateIds).toContain(written.id); + expect(positive.activeIds).not.toContain(written.id); + expect(positive.activeIds).toContain(control.id); + expect(positive.recalled).toContain(control.content); + expect(positive.recalled).not.toContain(written.id); + const positiveMoved = await probe('read', 'atlas', control.id, relocated); + expect(positiveMoved.recalled).toContain(control.content); + const negativeOther = await probe('read', 'beacon', control.id); + expect(negativeOther.activeIds).not.toContain(control.id); + expect(negativeOther.recalled).not.toContain(control.content); + await probe('delete', 'atlas', written.id); + const deleted = await probe('read', 'atlas'); + expect(deleted.candidateIds).not.toContain(written.id); + } finally { + try { + await probe('delete', 'atlas', written.id); + if (controlId) await probe('delete', 'atlas', controlId); + } finally { + await rm(relocated, { recursive: true, force: true }); + } + } +}, 90_000); diff --git a/apps/growth-research/test/model-boundary.spec.ts b/apps/growth-research/test/model-boundary.spec.ts new file mode 100644 index 000000000..f8c6dd410 --- /dev/null +++ b/apps/growth-research/test/model-boundary.spec.ts @@ -0,0 +1,65 @@ +import { createServer, type RequestListener, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { afterEach, expect, it, vi } from 'vitest'; +import { BoundedChatOpenAI } from '../src/runtime/model-boundary.js'; + +let server: Server | undefined; +afterEach(async () => { + const current = server; + current?.closeAllConnections(); + if (current) await new Promise(resolve => current.close(() => resolve())); + server = undefined; + vi.unstubAllEnvs(); +}); + +async function endpoint(handler: RequestListener) { + const current = createServer(handler); + server = current; + await new Promise(resolve => current.listen(0, '127.0.0.1', resolve)); + return `http://127.0.0.1:${(current.address() as AddressInfo).port}/v1`; +} + +it('allows schema-only construction but requires the operator fixture gate before invocation', async () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', ''); + const model = new BoundedChatOpenAI({ apiKey: 'synthetic-key' }); + await expect(model.invoke('blocked fixture')).rejects.toThrow(/fixture mode/i); +}); + +it('allows credential-free construction but refuses invocation without an actual credential', async () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', 'synthetic-only'); + vi.stubEnv('OPENAI_API_KEY', ''); + const model = new BoundedChatOpenAI(); + await expect(model.invoke('missing credential')).rejects.toThrow(/OPENAI_API_KEY is required/); +}); + +it('sends one bounded provider request and never retries a retriable server failure', async () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', 'synthetic-only'); + let requests = 0; + let requestBody: unknown; + const baseURL = await endpoint((request, response) => { + requests++; + let body = ''; + request.on('data', chunk => { body += String(chunk); }); + request.on('end', () => { + requestBody = JSON.parse(body); + response.writeHead(503, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ error: { message: 'Synthetic retryable failure' } })); + }); + }); + const model = new BoundedChatOpenAI({ apiKey: 'synthetic-key', model: 'gpt-4.1-mini', maxTokens: 9999, maxRetries: 4, configuration: { baseURL, maxRetries: 4 } }); + await expect(model.invoke('synthetic failure')).rejects.toThrow(/503/); + expect(requests).toBe(1); + expect(requestBody).toMatchObject({ model: 'gpt-4.1-mini', max_tokens: 1024 }); +}); + +it('aborts an unresponsive provider after the configured 20 second request deadline', async () => { + vi.stubEnv('GROWTH_RESEARCH_FIXTURE_MODE', 'synthetic-only'); + let requests = 0; + const baseURL = await endpoint(() => { requests++; }); + const model = new BoundedChatOpenAI({ apiKey: 'synthetic-key', model: 'gpt-4.1-mini', timeout: 90_000, configuration: { baseURL, timeout: 90_000 } }); + const started = Date.now(); + await expect(model.invoke('synthetic timeout')).rejects.toThrow(/timed out|timeout/i); + expect(Date.now() - started).toBeGreaterThanOrEqual(19_000); + expect(Date.now() - started).toBeLessThan(27_000); + expect(requests).toBe(1); +}, 30_000); diff --git a/apps/growth-research/test/packaging.spec.ts b/apps/growth-research/test/packaging.spec.ts new file mode 100644 index 000000000..b44e15870 --- /dev/null +++ b/apps/growth-research/test/packaging.spec.ts @@ -0,0 +1,152 @@ +import { mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { stageLangSmith } from '../scripts/package-langsmith.mts'; + +const roots: string[] = []; +const graphId = '/enrichment/research#agent'; +const publicGraphId = 'growth_research'; +const graphEntry = './.dawn/build/enrichment-research.ts:graph'; + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'growth-packaging-test-')); + roots.push(root); + const files: Record = { + 'package.json': JSON.stringify({ name: 'fixture', version: '0.0.0', private: true, type: 'module', engines: { node: '24' }, dependencies: { '@dawn-ai/core': '0.8.24' } }), + 'deployment-package-lock.json': JSON.stringify({ name: 'fixture', version: '0.0.0', lockfileVersion: 3, packages: { '': { name: 'fixture', version: '0.0.0', engines: { node: '24' }, dependencies: { '@dawn-ai/core': '0.8.24' } }, 'node_modules/@dawn-ai/core': { version: '0.8.24', resolved: 'https://registry.npmjs.org/@dawn-ai/core/-/core-0.8.24.tgz' } } }), + 'dawn.config.ts': 'export default { build: { targets: ["langsmith"] } };', + 'src/app/enrichment/research/index.ts': 'export default {};', + 'src/app/enrichment/research/plan.md': '# Synthetic plan\nVerify fixture evidence.', + 'src/app/enrichment/research/skills/company-evidence/SKILL.md': '# Company evidence\nSynthetic fixtures only.', + '.dawn/build/enrichment-research.ts': 'export const graph = {};', + '.dawn/build/langgraph.json': JSON.stringify({ graphs: { [graphId]: graphEntry }, env: '.env.example', node_version: '22', dependencies: ['.'] }), + '.env': 'SECRET=do-not-copy', + '.env.example': 'SECRET=', + 'README.md': 'Do not copy arbitrary root files', + '.dawn/build/debug.log': 'Do not copy arbitrary build files', + '.dawn/routes/enrichment/research/tools.json': '{"readFixture":{"input":{}}}', + }; + for (const [path, content] of Object.entries(files)) { + await mkdir(dirname(join(root, path)), { recursive: true }); + await writeFile(join(root, path), content); + } + return root; +} + +afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); }); + +describe('standalone LangSmith packaging', () => { + it('normalizes Node 22 to 24 and clears environment file configuration', async () => { + const output = await stageLangSmith(await fixture()); + const config = JSON.parse(await readFile(join(output, 'langgraph.json'), 'utf8')); + expect(config).toEqual({ graphs: { [publicGraphId]: graphEntry }, env: {}, node_version: '24', api_version: '0.13.4', dependencies: ['.'] }); + }); + + it('accepts the explicit pinned Agent Server API version', async () => { + const root = await fixture(); + const path = join(root, '.dawn/build/langgraph.json'); + const config = JSON.parse(await readFile(path, 'utf8')); + config.api_version = '0.13.4'; + await writeFile(path, JSON.stringify(config)); + const output = await stageLangSmith(root); + expect(JSON.parse(await readFile(join(output, 'langgraph.json'), 'utf8')).api_version).toBe('0.13.4'); + }); + + it.each(['0.13', '0.13.5', 0.134, null])('rejects unexpected explicit Agent Server API versions: %j', async version => { + const root = await fixture(); + const path = join(root, '.dawn/build/langgraph.json'); + const config = JSON.parse(await readFile(path, 'utf8')); + config.api_version = version; + await writeFile(path, JSON.stringify(config)); + await expect(stageLangSmith(root)).rejects.toThrow(/API version/i); + }); + + it('copies only approved sources and preserves authored skills and plans unchanged', async () => { + const root = await fixture(); + await writeFile(join(root, 'src/.env.production'), 'SECRET=inside-source'); + const output = await stageLangSmith(root); + expect(await readdir(output)).toEqual(['.dawn', 'dawn.config.ts', 'langgraph.json', 'package-lock.json', 'package.json', 'src', 'tsconfig.json']); + expect(await readdir(join(output, '.dawn/build'))).toEqual(['enrichment-research.ts']); + expect(await readdir(join(output, 'src'))).toEqual(['app']); + for (const path of ['src/app/enrichment/research/plan.md', 'src/app/enrichment/research/skills/company-evidence/SKILL.md']) { + expect(await readFile(join(output, path), 'utf8')).toBe(await readFile(join(root, path), 'utf8')); + } + }); + + it('emits standalone NodeNext compiler settings without workspace inheritance for server schema extraction', async () => { + const root = await fixture(); + await writeFile(join(root, 'tsconfig.json'), JSON.stringify({ extends: '../../tsconfig.base.json', compilerOptions: { paths: { '@internal/*': ['../../libs/*'] } } })); + const output = await stageLangSmith(root); + expect(JSON.parse(await readFile(join(output, 'tsconfig.json'), 'utf8'))).toEqual({ + compilerOptions: { target: 'ES2024', module: 'NodeNext', moduleResolution: 'NodeNext', types: ['node'], skipLibCheck: true, noEmit: true }, + include: ['src/**/*.ts', 'dawn.config.ts', '.dawn/build/**/*.ts'], + }); + }); + + it('preserves generated runtime tool schemas at their original route paths', async () => { + const root = await fixture(); + const output = await stageLangSmith(root); + expect(await readFile(join(output, '.dawn/routes/enrichment/research/tools.json'), 'utf8')).toBe('{"readFixture":{"input":{}}}'); + }); + + it('keeps the known specialist entry private while preserving its generated sources', async () => { + const root = await fixture(); + const configPath = join(root, '.dawn/build/langgraph.json'); + const config = JSON.parse(await readFile(configPath, 'utf8')); + config.graphs['/enrichment/research/subagents/researcher#agent'] = './.dawn/build/enrichment-research-subagents-researcher.ts:graph'; + await writeFile(configPath, JSON.stringify(config)); + await writeFile(join(root, '.dawn/build/enrichment-research-subagents-researcher.ts'), 'export const graph = {};'); + const output = await stageLangSmith(root); + expect(JSON.parse(await readFile(join(output, 'langgraph.json'), 'utf8')).graphs).toEqual({ [publicGraphId]: graphEntry }); + expect(await readFile(join(output, '.dawn/build/enrichment-research-subagents-researcher.ts'), 'utf8')).toContain('export const graph'); + }); + + it.each([{}, { '/unexpected#agent': graphEntry }, { [graphId]: graphEntry, '/extra#agent': graphEntry }])('requires exactly the expected graph discovery: %j', async graphs => { + const root = await fixture(); + await writeFile(join(root, '.dawn/build/langgraph.json'), JSON.stringify({ graphs, env: '.env.example', node_version: '22', dependencies: ['.'] })); + await expect(stageLangSmith(root)).rejects.toThrow(/graph/i); + }); + + it.each(['../../outside.ts:graph', '/tmp/outside.ts:graph', './.dawn/build/missing.ts:graph', './.dawn/build/enrichment-research.ts:nope'])('rejects graph references outside the expected staged layout: %s', async entry => { + const root = await fixture(); + await writeFile(join(root, '.dawn/build/langgraph.json'), JSON.stringify({ graphs: { [graphId]: entry }, env: '.env.example', node_version: '22', dependencies: ['.'] })); + await expect(stageLangSmith(root)).rejects.toThrow(/graph/i); + }); + + it('rejects outside-root symlinks in allowed source files', async () => { + const root = await fixture(); + const outside = await fixture(); + await symlink(join(outside, 'dawn.config.ts'), join(root, 'src/leak.ts')); + await expect(stageLangSmith(root)).rejects.toThrow(/symlink|outside/i); + }); + + it.each(['workspace:*', 'file:../../libs/growth', '^0.8.24'])('rejects non-exact or local dependencies: %s', async version => { + const root = await fixture(); + const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); + manifest.dependencies['@dawn-ai/core'] = version; + await writeFile(join(root, 'package.json'), JSON.stringify(manifest)); + await expect(stageLangSmith(root)).rejects.toThrow(/dependenc/i); + }); + + it('preserves an intentional auth configuration and its staged source', async () => { + const root = await fixture(); + await writeFile(join(root, 'src/auth.ts'), 'export const auth = {};'); + const configPath = join(root, '.dawn/build/langgraph.json'); + const config = JSON.parse(await readFile(configPath, 'utf8')); + config.auth = { path: './src/auth.ts:auth', disable_studio_auth: false }; + await writeFile(configPath, JSON.stringify(config)); + const output = await stageLangSmith(root); + expect(JSON.parse(await readFile(join(output, 'langgraph.json'), 'utf8')).auth).toEqual(config.auth); + expect(await readFile(join(output, 'src/auth.ts'), 'utf8')).toContain('export const auth'); + }); + + it('fails closed on unexpected generated configuration fields', async () => { + const root = await fixture(); + const path = join(root, '.dawn/build/langgraph.json'); + const config = JSON.parse(await readFile(path, 'utf8')); + config.http = { app: '../../outside.ts:app' }; + await writeFile(path, JSON.stringify(config)); + await expect(stageLangSmith(root)).rejects.toThrow(/unexpected/i); + }); +}); diff --git a/apps/growth-research/test/platform-client.spec.ts b/apps/growth-research/test/platform-client.spec.ts new file mode 100644 index 000000000..b15cb85c6 --- /dev/null +++ b/apps/growth-research/test/platform-client.spec.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from 'vitest'; +import { spawnSync } from 'node:child_process'; +import { setTimeout as delay } from 'node:timers/promises'; +import { createPlatformClient } from '../scripts/platform-client.mts'; + +const threadId = '10000000-0000-4000-8000-000000000001'; +const runId = '20000000-0000-4000-8000-000000000001'; +const correlationId = 'synthetic-direct-1'; +const run = (status = 'success') => ({ run_id: runId, thread_id: threadId, status, metadata: { growth_research_correlation: correlationId } }); +const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json' } }); + +function server(handler: (path: string, init: RequestInit) => Response | Promise, options: Partial[0]> = {}) { + const calls: { path: string; init: RequestInit }[] = []; + const fetcher: typeof fetch = async (input, init = {}) => { + const url = new URL(String(input)); + const path = url.pathname + url.search; + calls.push({ path, init }); + return handler(path, init); + }; + return { calls, client: createPlatformClient({ url: 'https://fixture.us.langgraph.app', apiKey: 'secret-fixture-key', pollMs: 1, ...options, fetch: fetcher }) }; +} + +describe('LangSmith synthetic smoke transport', () => { + it('discovers all public graphs so an exposed specialist cannot be hidden by a filter', async () => { + const { client, calls } = server(() => json([])); + await client.discover(); + expect(JSON.parse(String(calls[0]?.init.body))).toEqual({ limit: 100 }); + }); + it('loads with the native Node 24 script runtime', () => { + const moduleUrl = new URL('../scripts/platform-client.mts', import.meta.url).href; + const result = spawnSync(process.execPath, ['--input-type=module', '-e', `import { createPlatformClient } from ${JSON.stringify(moduleUrl)}; createPlatformClient({url: 'http://localhost:8128'});`], { encoding: 'utf8' }); + expect(result.status, result.stderr).toBe(0); + }); + it('submits the graph assistant ID and credential without following redirects', async () => { + const { client, calls } = server((_path, init) => init.method === 'POST' ? json(run('pending')) : json([])); + await client.submitRun(threadId, correlationId, { messages: [{ role: 'user', content: 'synthetic-direct' }] }); + const post = calls.find(c => c.init.method === 'POST'); + expect(post?.path).toBe(`/threads/${threadId}/runs`); + expect(JSON.parse(String(post?.init.body))).toMatchObject({ assistant_id: 'growth_research', metadata: { growth_research_correlation: correlationId }, multitask_strategy: 'reject', config: { recursion_limit: 12 } }); + expect(JSON.parse(String(post?.init.body))).not.toHaveProperty('route'); + expect(new Headers(post?.init.headers).get('x-api-key')).toBe('secret-fixture-key'); + expect(post?.init.redirect).toBe('error'); + }); + + it.each([401, 403])('reports authentication status %s without reflecting response secrets or retrying', async status => { + const { client, calls } = server(() => json({ detail: 'secret-fixture-key' }, status)); + await expect(client.submitRun(threadId, correlationId, {})).rejects.toMatchObject({ code: 'http_error', status }); + expect(calls).toHaveLength(1); + }); + + it('reconciles a lost accepted response without a second POST', async () => { + let accepted = false; + const { client, calls } = server((_path, init) => { + if (init.method === 'POST') { accepted = true; throw new Error('socket closed with secret-fixture-key'); } + return json(accepted ? [run()] : []); + }); + await expect(client.submitRun(threadId, correlationId, {})).resolves.toMatchObject({ run_id: runId }); + expect(calls.filter(c => c.init.method === 'POST')).toHaveLength(1); + }); + + it('retains an ambiguous outcome and refuses automatic resubmission in this client', async () => { + const { client, calls } = server((_path, init) => { + if (init.method === 'POST') throw new Error('secret-fixture-key'); + return json([]); + }); + for (let n = 0; n < 2; n++) { + await expect(client.submitRun(threadId, correlationId, {})).rejects.toMatchObject({ code: 'ambiguous_submission', message: 'Run submission outcome is unknown; reconcile before another attempt.' }); + } + expect(calls.filter(c => c.init.method === 'POST')).toHaveLength(1); + }); + + it('reuses an existing correlated run from a later page', async () => { + const unrelated = { ...run(), metadata: {} }; + const { client, calls } = server(path => json(path.includes('offset=100') ? [run()] : Array.from({ length: 100 }, () => unrelated))); + await expect(client.submitRun(threadId, correlationId, {})).resolves.toMatchObject({ run_id: runId }); + expect(calls).toHaveLength(2); + expect(calls.some(c => c.init.method === 'POST')).toBe(false); + }); + + it('coalesces concurrent submissions made by the same smoke client', async () => { + const { client, calls } = server((_path, init) => init.method === 'POST' ? json(run()) : json([])); + await Promise.all([client.submitRun(threadId, correlationId, {}), client.submitRun(threadId, correlationId, {})]); + expect(calls.filter(c => c.init.method === 'POST')).toHaveLength(1); + }); + + it('does not report a failed run as a successful fixture', async () => { + const { client } = server(() => json(run('error'))); + await expect(client.waitForSuccess(threadId, runId)).rejects.toMatchObject({ code: 'run_failed' }); + }); + + it('rejects a run response for a different run ID', async () => { + const { client } = server(() => json({ ...run(), run_id: '20000000-0000-4000-8000-000000000002' })); + await expect(client.getRun(threadId, runId)).rejects.toMatchObject({ code: 'invalid_response' }); + }); + + it('rejects late success and caps polling request time to the remaining run deadline', async () => { + const { client, calls } = server(async () => { await delay(30); return json(run()); }, { runTimeoutMs: 5, requestTimeoutMs: 1000 }); + await expect(client.waitForSuccess(threadId, runId)).rejects.toMatchObject({ code: 'run_wait_timeout' }); + expect(calls[0]?.init.signal?.aborted).toBe(true); + }); + + it('caps polling sleeps to the remaining deadline', async () => { + const { client } = server(() => json(run('running')), { runTimeoutMs: 10, pollMs: 1000 }); + const outcome = client.waitForTerminal(threadId, runId).then(() => 'unexpected_success', error => error.code); + expect(await Promise.race([outcome, delay(100).then(() => 'deadline_ignored')])).toBe('run_wait_timeout'); + }); + + it.each([200, 202, 204])('uses platform cancellation with HTTP %s and confirms the terminal state', async status => { + const { client, calls } = server(path => path.includes('/cancel?') ? new Response(null, { status }) : json(run('interrupted'))); + await expect(client.cancelRun(threadId, runId)).resolves.toMatchObject({ status: 'interrupted' }); + expect(calls[0]?.path).toBe(`/threads/${threadId}/runs/${runId}/cancel?wait=true&action=interrupt`); + expect(calls[0]?.init.method).toBe('POST'); + }); + + it('refuses to delete an unrelated thread', async () => { + const { client, calls } = server(() => json({ thread_id: threadId, metadata: { growth_research_smoke: 'someone-else' } })); + await expect(client.deleteFixtureThread(threadId, 'our-smoke')).rejects.toMatchObject({ code: 'foreign_thread' }); + expect(calls.some(c => c.init.method === 'DELETE')).toBe(false); + }); + it('does not mistake interrupted status for JavaScript worker quiescence', async () => { + const { client, calls } = server(path => path.endsWith('/runs?limit=100&offset=0') ? json([run('interrupted')]) : json({ thread_id: threadId, metadata: { growth_research_smoke: 'our-smoke' } })); + await expect(client.deleteFixtureThread(threadId, 'our-smoke')).rejects.toMatchObject({ code: 'quiescence_unverified' }); + expect(calls.some(call => call.init.method === 'DELETE')).toBe(false); + }); + + it('refuses cleanup while a run is still active', async () => { + const { client, calls } = server(path => json(path.includes('/runs?') ? [run('running')] : { thread_id: threadId, metadata: { growth_research_smoke: 'our-smoke' } })); + await expect(client.deleteFixtureThread(threadId, 'our-smoke')).rejects.toMatchObject({ code: 'active_run' }); + expect(calls.some(c => c.init.method === 'DELETE')).toBe(false); + }); + + it.each([200, 204])('deletes its quiescent fixture with HTTP %s and verifies absence', async status => { + let deleted = false; + const { client } = server((path, init) => { + if (init.method === 'DELETE') { deleted = true; return new Response(null, { status }); } + if (path.includes('/runs?')) return json([run()]); + return deleted ? json({}, 404) : json({ thread_id: threadId, metadata: { growth_research_smoke: 'our-smoke' } }); + }); + await expect(client.deleteFixtureThread(threadId, 'our-smoke')).resolves.toBeUndefined(); + expect(deleted).toBe(true); + }); + + it('verifies ownership when reusing a deterministic thread ID', async () => { + const { client } = server(() => json({ thread_id: threadId, metadata: { growth_research_smoke: 'someone-else' } })); + await expect(client.ensureFixtureThread(threadId, 'our-smoke')).rejects.toMatchObject({ code: 'foreign_thread' }); + }); + + it.each(['http://public.example', 'https://user:password@example.com', 'https://example.com?key=secret', 'https://example.com/path'])('rejects unsafe or ambiguous server URL %s', url => { + expect(() => createPlatformClient({ url, apiKey: 'key' })).toThrow('Use a bare HTTPS server origin'); + }); +}); diff --git a/apps/growth-research/test/smoke.spec.ts b/apps/growth-research/test/smoke.spec.ts new file mode 100644 index 000000000..9393d6263 --- /dev/null +++ b/apps/growth-research/test/smoke.spec.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { isSmokeFixture, verifyContinuationBase, verifyFixtureState } from '../scripts/langsmith-smoke.mts'; + +const tool = (name: string, content: string) => ({ type: 'tool', name, content }); +const direct = { values: { messages: [ + { type: 'human', content: 'direct fixture atlas' }, + tool('readSkill', 'Never treat candidate memory as an accepted account fact'), + tool('readFixture', '{"name":"Atlas Synthetic","source":"fixture:atlas:v1"}'), + tool('writeTodos', '{}'), { type: 'ai', content: 'Done.' }, +], todos: [{ content: 'Verify evidence', status: 'completed' }] } }; + +describe('deployed fixture evidence', () => { + it('requires executed tools and persisted plan state for a direct result', () => { + expect(verifyFixtureState('direct', direct)).toMatchObject({ fixture: 'direct', tools: ['readSkill', 'readFixture', 'writeTodos'], planComplete: true }); + }); + it('rejects persuasive final prose without evidence', () => { + expect(() => verifyFixtureState('direct', { values: { messages: [{ type: 'ai', content: 'I loaded the skill and verified fixture:atlas:v1' }] } })).toThrow(); + }); + it('rejects a failed tool result despite the final answer', () => { + const state = structuredClone(direct); + Object.assign(state.values.messages[2] ?? {}, { status: 'error' }); + expect(() => verifyFixtureState('direct', state)).toThrow(); + }); + it('requires completed persisted todos', () => { + const state = structuredClone(direct); state.values.todos = [{ content: 'Verify evidence', status: 'pending' }]; + expect(() => verifyFixtureState('direct', state)).toThrow(); + }); + it('requires a real task call to the registered specialist and its returned citation', () => { + const taskArgs = { subagent: 'researcher' }; + const state = { values: { messages: [ + { type: 'human', content: 'delegate atlas' }, + { type: 'ai', tool_calls: [{ name: 'task', args: taskArgs }] }, + tool('task', 'Atlas specialist evidence [fixture:atlas:v1]'), { type: 'ai', content: 'Done.' }, + ] } }; + expect(verifyFixtureState('delegated', state)).toMatchObject({ fixture: 'delegated', tools: ['task'] }); + taskArgs.subagent = 'undeclared'; + expect(() => verifyFixtureState('delegated', state)).toThrow(); + }); + it('recognizes a pending memory candidate without treating it as accepted recall', () => { + const state = { values: { messages: [{ type: 'human', content: 'memory fixture atlas' }, tool('remember', 'Stored memory candidate memory_0123456789abcdef (pending approval).'), { type: 'ai', tool_calls: [{ name: 'recall', args: { query: 'Synthetic Angular evaluation' } }] }, tool('recall', '(no memories found)'), { type: 'ai', content: 'Candidate proposed.' }] } }; + expect(verifyFixtureState('memory', state)).toMatchObject({ candidateId: 'memory_0123456789abcdef' }); + }); + it('rejects recall in the same parallel model tool round as remember', () => { + const state = { values: { messages: [{ type: 'human', content: 'memory fixture atlas' }, { type: 'ai', tool_calls: [{ name: 'remember' }, { name: 'recall' }] }, tool('remember', 'Stored memory candidate memory_0123456789abcdef (pending approval).'), tool('recall', '(no memories found)'), { type: 'ai', content: 'Done.' }] } }; + expect(() => verifyFixtureState('memory', state)).toThrow(); + }); + it('rejects stale evidence and contradictory recall within the current memory turn', () => { + const prior = [{ type: 'human', content: 'memory fixture atlas' }, tool('remember', 'Stored memory candidate memory_0123456789abcdef (pending approval).'), tool('recall', '(no memories found)'), { type: 'ai', content: 'Done.' }]; + const current = [{ type: 'human', content: 'memory fixture atlas' }, tool('remember', 'Stored memory candidate memory_1111111111111111 (pending approval).'), tool('recall', 'Candidate leaked as accepted fact'), { type: 'ai', content: 'Done.' }]; + expect(() => verifyFixtureState('memory', { values: { messages: [...prior, ...current] } })).toThrow(); + current.splice(2, 0, tool('recall', '(no memories found)')); + expect(() => verifyFixtureState('memory', { values: { messages: current } })).toThrow(); + }); + it('accepts only own fixture names', () => { + expect(isSmokeFixture('direct')).toBe(true); + for (const value of ['constructor', 'toString', '__proto__', 'unknown']) expect(isSmokeFixture(value)).toBe(false); + }); + it('requires prior evidence and a new continuation message on the same thread', () => { + expect(() => verifyFixtureState('continuation', direct)).toThrow(); + const state = structuredClone(direct); + state.values.messages.push({ type: 'human', content: 'continuation fixture atlas' }, { type: 'ai', content: 'Prior fixture evidence retained.' }); + expect(verifyFixtureState('continuation', state)).toMatchObject({ fixture: 'continuation', planComplete: true }); + expect(() => verifyContinuationBase(state)).not.toThrow(); + }); +}); diff --git a/apps/growth-research/tsconfig.json b/apps/growth-research/tsconfig.json new file mode 100644 index 000000000..1be754732 --- /dev/null +++ b/apps/growth-research/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": {}, + "composite": false, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false, + "lib": ["es2024", "dom", "dom.iterable"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.ts", "scripts/**/*.mts", "test/**/*.ts", "*.ts"] +} diff --git a/apps/growth-research/vitest.config.ts b/apps/growth-research/vitest.config.ts new file mode 100644 index 000000000..92b8d3e7c --- /dev/null +++ b/apps/growth-research/vitest.config.ts @@ -0,0 +1,12 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: resolve(dirname(fileURLToPath(import.meta.url)), '../..'), + test: { + environment: 'node', + include: ['apps/growth-research/test/**/*.spec.ts'], + exclude: ['apps/growth-research/test/**/*.integration.spec.ts'], + }, +}); diff --git a/apps/growth-research/vitest.memory-integration.config.ts b/apps/growth-research/vitest.memory-integration.config.ts new file mode 100644 index 000000000..8e4252693 --- /dev/null +++ b/apps/growth-research/vitest.memory-integration.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ test: { environment: 'node', include: ['apps/growth-research/test/memory.integration.spec.ts'] } }); diff --git a/package-lock.json b/package-lock.json index 2f9f59a55..85d6faca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,6 +123,1403 @@ "react-dom": "^19.0.0" } }, + "apps/growth-research": { + "name": "@threadplane-internal/growth-research", + "version": "0.0.0", + "dependencies": { + "@dawn-ai/cli": "0.8.24", + "@dawn-ai/core": "0.8.24", + "@dawn-ai/langchain": "0.8.24", + "@dawn-ai/memory": "0.8.24", + "@dawn-ai/memory-pgvector": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@langchain/core": "1.2.9", + "@langchain/langgraph-checkpoint": "1.1.5", + "@langchain/openai": "1.5.11", + "@types/node": "25.6.0", + "pg": "8.23.0", + "zod": "4.5.4" + }, + "devDependencies": { + "@dawn-ai/evals": "0.8.24", + "@dawn-ai/testing": "0.8.24", + "@dawn-ai/workspace": "0.8.24" + }, + "engines": { + "node": "24" + } + }, + "apps/growth-research/node_modules/@ag-ui/core": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ag-ui/core/-/core-0.0.59.tgz", + "integrity": "sha512-hDgy4ipTqXieT8YG8Mr917Y+FD/f11VK1GefZ5CwTDCuNqS/oTwjJ5l/DZkicThgS8hQW/Y7wPylPBMBJ8BkUg==", + "license": "MIT", + "dependencies": { + "zod": "^3.22.4" + } + }, + "apps/growth-research/node_modules/@ag-ui/core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "apps/growth-research/node_modules/@ag-ui/encoder": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ag-ui/encoder/-/encoder-0.0.59.tgz", + "integrity": "sha512-wQCzBsStyZMm8nzhTdTx1G0B1C8yv5rbzZLnz1ka/l5LcJEsK3KTounU8CR9l+7QtcpOUUDJKd0xAbyI6gNcSw==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@ag-ui/proto": "0.0.59" + } + }, + "apps/growth-research/node_modules/@ag-ui/proto": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ag-ui/proto/-/proto-0.0.59.tgz", + "integrity": "sha512-X+uvDaegLEHw5kJu8tv2eSqHH8ouat+JCfFokGV1uuMquY8qCEQSlGsM7xzexwX9fujuxgHOWumg5kJDqa0+RA==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@bufbuild/protobuf": "^2.2.5", + "@protobuf-ts/protoc": "^2.11.1" + } + }, + "apps/growth-research/node_modules/@bufbuild/protobuf": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.14.1.tgz", + "integrity": "sha512-agRJn3+EJDUe8AvxTx/LnHA/GErvLE62pSaSk7+MwFOtOv8eWBu/qCq2qoZjBjVZ3C2aiFJCveuSs17KMkYGOw==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "apps/growth-research/node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, + "apps/growth-research/node_modules/@copilotkit/aimock": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@copilotkit/aimock/-/aimock-1.39.0.tgz", + "integrity": "sha512-AWw4vmW2hBchHoggh0G4McWGmGZD6wtXAehL6K5ncWF5lVIjlv++bPmxmRwrpQCi/K4/xK10N9Zp9srJYipEJw==", + "dev": true, + "license": "MIT", + "bin": { + "aimock": "dist/aimock-cli.js", + "llmock": "dist/cli.js" + }, + "engines": { + "node": ">=20.15.0" + }, + "peerDependencies": { + "jest": ">=29", + "vitest": ">=3" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/@dawn-ai/ag-ui": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/ag-ui/-/ag-ui-0.8.24.tgz", + "integrity": "sha512-7lce3QKiT4ZosMFIju+k1EebeSqxTqvPux+EuSTi/l0YblA1IMxtF+oMIrA0slDxJ3dv5IfRzYNbOcznVnOT/Q==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@ag-ui/encoder": "0.0.59", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@copilotkit/react-core": ">=1.66.0", + "react": ">=19.0.0" + }, + "peerDependenciesMeta": { + "@copilotkit/react-core": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/@dawn-ai/cli": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/cli/-/cli-0.8.24.tgz", + "integrity": "sha512-18+jxTh9vjXHNwX4Y+TrM5bYBSK94W1qevuU50BM54NA2ETw9dfUSPYxY2Se2Gffln/u6ubO6UCxChTLgTj+jQ==", + "license": "MIT", + "dependencies": { + "@ag-ui/core": "0.0.59", + "@dawn-ai/ag-ui": "0.8.24", + "@dawn-ai/core": "0.8.24", + "@dawn-ai/langchain": "0.8.24", + "@dawn-ai/langgraph": "0.8.24", + "@dawn-ai/memory": "0.8.24", + "@dawn-ai/permissions": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/sqlite-storage": "0.8.24", + "commander": "15.0.0", + "esbuild": "^0.28.1", + "tsx": "^4.23.5" + }, + "bin": { + "dawn": "dist/index.js" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/growth-research/node_modules/@dawn-ai/core": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/core/-/core-0.8.24.tgz", + "integrity": "sha512-zrx6H1vhFpfvbO9BQIkpKTUymTJPumyMZj/vkd4gWv/3L5iEAS+QOHkdmn8v1nUNPlXzag7W6NovmBIQCC5E0w==", + "license": "MIT", + "dependencies": { + "@dawn-ai/permissions": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/sqlite-storage": "0.8.24", + "@dawn-ai/workspace": "0.8.24", + "@langchain/langgraph": "^1.4.9", + "@typescript/old": "npm:typescript@6.0.2", + "tsx": "^4.23.5", + "typescript": "npm:@typescript/typescript6@6.0.2", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@langchain/langgraph-checkpoint": "^1.1.3" + } + }, + "apps/growth-research/node_modules/@dawn-ai/evals": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/evals/-/evals-0.8.24.tgz", + "integrity": "sha512-0VfCXZlMittXPQZgfrXjhd/gkBE/ahLB8QM7gItIqbPlcSmc9Ag/RX1H3ToC9ugqG+daaqfz3cR0f/6AzEl9wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@dawn-ai/testing": "0.8.24" + } + }, + "apps/growth-research/node_modules/@dawn-ai/langchain": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/langchain/-/langchain-0.8.24.tgz", + "integrity": "sha512-smwRLyflWG4fkvbv8bTXoILlEZX7kYB0NJWCFC4oX1tWf4rjO+cgeohECQgdkhktpVxuj0g34ASe2zOxonQHJQ==", + "license": "MIT", + "dependencies": { + "@dawn-ai/core": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/workspace": "0.8.24", + "@langchain/langgraph": "^1.4.9", + "@langchain/openai": "^1.5.5", + "gpt-tokenizer": "^3.4.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@langchain/anthropic": "^1.5.2", + "@langchain/core": "^1.1.47", + "@langchain/google-genai": "^2.2.0", + "@langchain/groq": "^1.3.1", + "@langchain/langgraph-checkpoint": "^1.1.3", + "@langchain/mistralai": "^1.2.0", + "@langchain/ollama": "^1.3.0", + "@langchain/openrouter": "^0.4.5", + "@langchain/xai": "^1.4.5" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/langgraph-checkpoint": { + "optional": false + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "@langchain/openrouter": { + "optional": true + }, + "@langchain/xai": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/@dawn-ai/langgraph": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/langgraph/-/langgraph-0.8.24.tgz", + "integrity": "sha512-Tb/gLuuDQOYZJbEf4e6ZqBasvH38OJL9UdaM0jsXRanCFrC5u8mA1hRF9JXLqqfuA1Eyr1zJzDQHqv2/ZDv1HA==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sdk": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/growth-research/node_modules/@dawn-ai/memory": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/memory/-/memory-0.8.24.tgz", + "integrity": "sha512-yVptc9AeDEGm73j1SysyVDZLJmYriAp5mRBbhdVFzb5cULEI5X3Ysk3sxqlhZoM1z/7VZONhNA8g6zEL8ppRuA==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sqlite-storage": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/growth-research/node_modules/@dawn-ai/memory-pgvector": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/memory-pgvector/-/memory-pgvector-0.8.24.tgz", + "integrity": "sha512-i6WWyts+Uga/xwRK7nFOEtlKt6fAPL8OgDo5U6m5VzGg0J35Gmv02gHww9PQ70RiyyVn5x8vp+RFCju/FRaQtw==", + "license": "MIT", + "dependencies": { + "@dawn-ai/memory": "0.8.24", + "pg": "^8.22.0", + "pgvector": "^0.3.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/growth-research/node_modules/@dawn-ai/permissions": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/permissions/-/permissions-0.8.24.tgz", + "integrity": "sha512-PfFQ9rm08TGmTi4twAeaUN+7cZLOB3s+Anfa5isVI/uM0mRKuRFr4hg/WEBQPaZVyDuTrCqiDgOcXTJ1mfySeA==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sdk": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/growth-research/node_modules/@dawn-ai/sdk": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/sdk/-/sdk-0.8.24.tgz", + "integrity": "sha512-YzBVD53dzPUNTkFwbYYdh/XMCX92wmSwmOmIsxkBgnxc3gX11b5z18F6NO7IeWJdmJbxRaLVBAAcUJvO/9E0qg==", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/@dawn-ai/sqlite-storage": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/sqlite-storage/-/sqlite-storage-0.8.24.tgz", + "integrity": "sha512-2fGt9K7PabDpN9KdguYrdzMC6mI+lMud/8chvRriCdIqJYeWGUXfZB+GWihXbTU+dR6o3NE1hyAabl+Sj6upkQ==", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.2.4", + "@langchain/langgraph-checkpoint": "^1.1.3" + } + }, + "apps/growth-research/node_modules/@dawn-ai/testing": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/testing/-/testing-0.8.24.tgz", + "integrity": "sha512-AJIsMp3jWWGz4NrnfbLDBfNTvndDg0NlOXxaEqvPKu/5BLMY7DSYYv+VeHvrKTyIeS2rby55mErAVY7gL5C7fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@copilotkit/aimock": "^1.37.4", + "@dawn-ai/memory": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@dawn-ai/cli": "0.8.24", + "@dawn-ai/core": "0.8.24", + "@dawn-ai/sdk": "0.8.24", + "@dawn-ai/workspace": "0.8.24" + } + }, + "apps/growth-research/node_modules/@dawn-ai/workspace": { + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/@dawn-ai/workspace/-/workspace-0.8.24.tgz", + "integrity": "sha512-bW4c1Xj3lLqnbiPSHXkTDxcdJ4py/SGe4aKuPOWMKdxo64qso/2cuRcVh2kCOFP3sUv9KJXuE2RqdeJIV2Rf4Q==", + "license": "MIT", + "dependencies": { + "@dawn-ai/sdk": "0.8.24" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/growth-research/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/growth-research/node_modules/@langchain/core": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.9.tgz", + "integrity": "sha512-conzSEj9Zu1AyXJLXsSbgrtxtxinmI1yGqQ5CIJZSoV5rvv+yvQE/vgBnoySpBQ/bl3YPgj2FL/gbDjWykLSfg==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "apps/growth-research/node_modules/@langchain/langgraph": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.4.14.tgz", + "integrity": "sha512-uWAdRYTllfKCnTrlyovExPJCHJwcf3Wl2LzUlnaqsT7Rmoo3aCeYtq/7MV/Pw4q11motG8pR8bjr6T6V8Pe1gQ==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.1.5", + "@langchain/langgraph-sdk": "~1.10.2", + "@langchain/protocol": "^0.0.19", + "@standard-schema/spec": "1.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48", + "zod": "^3.25.32 || ^4.2.0" + } + }, + "apps/growth-research/node_modules/@langchain/langgraph-checkpoint": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.5.tgz", + "integrity": "sha512-BwDwl5VeTOh6CVuiIPgsUgfK51vTJDMSbFcSCUfjJWsl8/DPdK/mbv+ejxJstkSk/BlSPMP4JfXWcN6jD2ea2Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48" + } + }, + "apps/growth-research/node_modules/@langchain/langgraph-sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.10.2.tgz", + "integrity": "sha512-86qsfdBZWu1ZgywLN8AThU/jXi9rjPDZPWcTJp4SA1A/L62ypTNoSXbvtiwZt1odokXccYTxK1XWS8tmVdvEmw==", + "license": "MIT", + "dependencies": { + "@langchain/protocol": "^0.0.19", + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "apps/growth-research/node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/growth-research/node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/growth-research/node_modules/@langchain/openai": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.5.11.tgz", + "integrity": "sha512-BvGp5lQk5//0WVwTIepscazFpneT9I9+mc+kp+cLuhGHFb7mc9zGNrusZOXoa3p73SN0i3XqTo8lyIndpVx3Hw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^7.5.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@langchain/core": "^1.2.9" + } + }, + "apps/growth-research/node_modules/@langchain/protocol": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.19.tgz", + "integrity": "sha512-9hKcRrH7cBX6gfutdfXPoft1OCchHe4FEpALoDJMl5Qu+n/YG5ynZmyu8+8cxORlPwHBoKTxggvXz+76M1yX1Q==", + "license": "MIT" + }, + "apps/growth-research/node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "apps/growth-research/node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "apps/growth-research/node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "apps/growth-research/node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "apps/growth-research/node_modules/@typescript/old": { + "name": "typescript", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "apps/growth-research/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "apps/growth-research/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "apps/growth-research/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "apps/growth-research/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "apps/growth-research/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "apps/growth-research/node_modules/gpt-tokenizer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", + "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", + "license": "MIT" + }, + "apps/growth-research/node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/growth-research/node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "apps/growth-research/node_modules/langsmith": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.10.1.tgz", + "integrity": "sha512-zRDCnLznGdzx1VottX4CWr8v9ZZLRoSql2pbjEXYA1Jeg+NMDdq87x/v0Dk4GNkNZYhE+ZxFX3vTxwWq6W6gVA==", + "license": "MIT", + "dependencies": { + "p-queue": "6.6.2" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*", + "ws": ">=7" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "apps/growth-research/node_modules/openai": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.10.0.tgz", + "integrity": "sha512-sn9t2Kls7O52PwuF9BUTYNu4Gk/r0lXJyrgaNht4TNRlZFb3dJIGO0RciSgjARGCBRtWjySubAQFJttlzUvGQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "undici": ">=5 <9", + "ws": "^8.21.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "undici": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "apps/growth-research/node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/growth-research/node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/growth-research/node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "apps/growth-research/node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "apps/growth-research/node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "apps/growth-research/node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "apps/growth-research/node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "apps/growth-research/node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "apps/growth-research/node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "apps/growth-research/node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "apps/growth-research/node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "apps/growth-research/node_modules/pgvector": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/pgvector/-/pgvector-0.3.0.tgz", + "integrity": "sha512-+t7qcQD2us8fO8YIq/3lA0gUrD+bVO70MG1MhcDcxJz/OlRGGIIHzFq/4x57Vn/LpzX5wFdfOTLQp9QMPd4ljQ==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "apps/growth-research/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "apps/growth-research/node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "apps/growth-research/node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "apps/growth-research/node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "apps/growth-research/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "apps/growth-research/node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "apps/growth-research/node_modules/typescript": { + "name": "@typescript/typescript6", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript6/-/typescript6-6.0.2.tgz", + "integrity": "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==", + "license": "Apache-2.0", + "dependencies": { + "@typescript/old": "npm:typescript@^6" + }, + "bin": { + "tsc6": "bin/tsc6" + } + }, + "apps/growth-research/node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "apps/growth-research/node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "apps/growth-research/node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "apps/lifecycle": { "name": "@threadplane-internal/lifecycle", "version": "0.0.0", @@ -21081,6 +22478,10 @@ "resolved": "libs/growth", "link": true }, + "node_modules/@threadplane-internal/growth-research": { + "resolved": "apps/growth-research", + "link": true + }, "node_modules/@threadplane-internal/lifecycle": { "resolved": "apps/lifecycle", "link": true