{"_id":"@astragenie/astramem-contracts","_rev":"5-b62fea2dcba58af97f3aa8ac56327864","name":"@astragenie/astramem-contracts","dist-tags":{"latest":"2.4.0"},"versions":{"2.1.0":{"name":"@astragenie/astramem-contracts","version":"2.1.0","license":"MIT","_id":"@astragenie/astramem-contracts@2.1.0","maintainers":[{"name":"heroboec","email":"shishkosv@gmail.com"}],"homepage":"https://github.com/astragenie/astramem-local#readme","bugs":{"url":"https://github.com/astragenie/astramem-local/issues"},"dist":{"shasum":"e92729531964e321ca86fd2626ca01b4e82acf81","tarball":"https://registry.npmjs.org/@astragenie/astramem-contracts/-/astramem-contracts-2.1.0.tgz","fileCount":84,"integrity":"sha512-rS1y1K4OZXPYqSk2wDgi9bb0nJKFvoxyXEwXoz0ZYm/2o9CgZBhC5KkOlOmvYxRh68VpXgpUaO3gzQSczkbSIg==","signatures":[{"sig":"MEUCIQCyCzW503tRKxkbnMQcsHkxrMupTEsfVax4VW663oCGnAIgKjHo/PclQYksyQ/pJeu8uL2pYi05aPwNb/hCN7n2fmM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":183496},"type":"module","types":"./types/index.d.ts","exports":{".":{"types":"./types/index.d.ts"},"./zod":"./zod/index.ts","./wire":"./wire.ts","./types":"./types/index.d.ts","./zod/*":"./zod/*","./types/*":"./types/*","./schemas/*":"./schemas/*","./fixtures/*":"./fixtures/*","./manifests/*":"./manifests/*"},"gitHead":"6300e946043f49fe4012154335f6f9e617261716","scripts":{"build":"node generate-types.mjs && node generate-zod.mjs && node validate.mjs","generate":"node generate-types.mjs && node generate-zod.mjs","validate":"node validate.mjs"},"_npmUser":{"name":"heroboec","email":"shishkosv@gmail.com"},"repository":{"url":"git+https://github.com/astragenie/astramem-local.git","type":"git","directory":"contracts"},"_npmVersion":"10.9.8","description":"Canonical JSON Schemas + golden fixtures + generated TypeScript types + Zod validators for the AstraMem cross-repo wire contracts (atom@1, atom@2, retrieval@1, sync@1, capture@1). Source of truth consumed by astramem-local, astramem cloud, and astramem-pl","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"peerDependencies":{"zod":"^4.4.3"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/astramem-contracts_2.1.0_1783701243459_0.3083970894123538","host":"s3://npm-registry-packages-npm-production"}},"2.1.1":{"name":"@astragenie/astramem-contracts","version":"2.1.1","license":"MIT","_id":"@astragenie/astramem-contracts@2.1.1","maintainers":[{"name":"heroboec","email":"shishkosv@gmail.com"}],"homepage":"https://github.com/astragenie/astramem-local#readme","bugs":{"url":"https://github.com/astragenie/astramem-local/issues"},"dist":{"shasum":"3cf7e802d57b3de869e4018341838c415535c9e2","tarball":"https://registry.npmjs.org/@astragenie/astramem-contracts/-/astramem-contracts-2.1.1.tgz","fileCount":84,"integrity":"sha512-UH/iO4RIUGiD04h0v87geO/tbbtulOYDOb0dYpDX5QqJiJ93wqCucau1Rx+U6MaCZLHN0OVndO9UNQyWhfTIoA==","signatures":[{"sig":"MEQCIDLn/e/rlzwxvnQDCnyHDlAO+NTajVwqHdXTGlG4v+NXAiBkZV4WZ3tp36zRINB7/IYuQqIsPc9FazarOSAp5BhYiQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":183496},"type":"module","types":"./types/index.d.ts","exports":{".":{"types":"./types/index.d.ts"},"./zod":"./zod/index.ts","./wire":"./wire.ts","./types":"./types/index.d.ts","./zod/*":"./zod/*","./types/*":"./types/*","./schemas/*":"./schemas/*","./fixtures/*":"./fixtures/*","./manifests/*":"./manifests/*"},"gitHead":"f272ecb134332a844761d35214d275ffd132a69b","scripts":{"build":"node generate-types.mjs && node generate-zod.mjs && node validate.mjs","generate":"node generate-types.mjs && node generate-zod.mjs","validate":"node validate.mjs"},"_npmUser":{"name":"heroboec","email":"shishkosv@gmail.com"},"repository":{"url":"git+https://github.com/astragenie/astramem-local.git","type":"git","directory":"contracts"},"_npmVersion":"10.9.8","description":"Canonical JSON Schemas + golden fixtures + generated TypeScript types + Zod validators for the AstraMem cross-repo wire contracts (atom@1, atom@2, retrieval@1, sync@1, capture@1). Source of truth consumed by astramem-local, astramem cloud, and astramem-pl","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"peerDependencies":{"zod":"^4.4.3"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/astramem-contracts_2.1.1_1783706648263_0.7196618512015613","host":"s3://npm-registry-packages-npm-production"}},"2.2.0":{"name":"@astragenie/astramem-contracts","version":"2.2.0","license":"MIT","_id":"@astragenie/astramem-contracts@2.2.0","maintainers":[{"name":"heroboec","email":"shishkosv@gmail.com"}],"homepage":"https://github.com/astragenie/astramem-local#readme","bugs":{"url":"https://github.com/astragenie/astramem-local/issues"},"dist":{"shasum":"9d17709a6c7befbba0ba313d704983a99eb114aa","tarball":"https://registry.npmjs.org/@astragenie/astramem-contracts/-/astramem-contracts-2.2.0.tgz","fileCount":85,"integrity":"sha512-MsgBp+33sAvuGbm6yVqNHTxydJhEwTxUXhPyeGHbPKPe1sF1hn8tOAcntU9Cx6IBHMxbcx949vP+IfGN/dos2Q==","signatures":[{"sig":"MEYCIQCuC92WvMi9JI6tfNahcyk9AIgBfy70yODpIHQ46KpU6QIhAO6t3pgL08XTUWggVhlTuA/43k+gY50gSgEKs22zIPRH","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":196586},"type":"module","types":"./types/index.d.ts","exports":{".":{"types":"./types/index.d.ts"},"./zod":"./zod/index.ts","./wire":"./wire.ts","./types":"./types/index.d.ts","./zod/*":"./zod/*","./types/*":"./types/*","./schemas/*":"./schemas/*","./fixtures/*":"./fixtures/*","./manifests/*":"./manifests/*"},"gitHead":"5c7af6ef9d603b66e72a58922e6ba0e58d6ee6bc","scripts":{"build":"node generate-types.mjs && node generate-zod.mjs && node validate.mjs","generate":"node generate-types.mjs && node generate-zod.mjs","validate":"node validate.mjs"},"_npmUser":{"name":"heroboec","email":"shishkosv@gmail.com"},"repository":{"url":"git+https://github.com/astragenie/astramem-local.git","type":"git","directory":"contracts"},"_npmVersion":"10.9.8","description":"Canonical JSON Schemas + golden fixtures + generated TypeScript types + Zod validators for the AstraMem cross-repo wire contracts (atom@1, atom@2, retrieval@1, sync@1, capture@1). Source of truth consumed by astramem-local, astramem cloud, and astramem-pl","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"peerDependencies":{"zod":"^4.4.3"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/astramem-contracts_2.2.0_1783930356265_0.8076662189050248","host":"s3://npm-registry-packages-npm-production"}},"2.3.0":{"name":"@astragenie/astramem-contracts","version":"2.3.0","license":"MIT","_id":"@astragenie/astramem-contracts@2.3.0","maintainers":[{"name":"heroboec","email":"shishkosv@gmail.com"}],"homepage":"https://github.com/astragenie/astramem-local#readme","bugs":{"url":"https://github.com/astragenie/astramem-local/issues"},"dist":{"shasum":"5126a1562e5bb3296f606ccd5399fc1d43147853","tarball":"https://registry.npmjs.org/@astragenie/astramem-contracts/-/astramem-contracts-2.3.0.tgz","fileCount":87,"integrity":"sha512-Mt+WNOJA+T0Qs9H/cFJ9WtFHJI0JnPUw1FuuI3YcabTjvwysLDSuGcnOv7UJwLoHkIu+PBth/2G2/xN8zr7grw==","signatures":[{"sig":"MEUCICPz/ZqiUYmH4ilJoeFy8iQx+LtyTJe1lE31azJN5aAzAiEAl10VaxGTp4LnCW6T8NkcUo3FMyo2e7idqij89seehHg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":215407},"type":"module","types":"./types/index.d.ts","exports":{".":{"types":"./types/index.d.ts"},"./zod":"./zod/index.ts","./wire":"./wire.ts","./types":"./types/index.d.ts","./zod/*":"./zod/*","./types/*":"./types/*","./schemas/*":"./schemas/*","./fixtures/*":"./fixtures/*","./manifests/*":"./manifests/*"},"gitHead":"53f4bcd3eb8c087749412d944b591be1d60cdaa3","scripts":{"build":"node generate-types.mjs && node generate-zod.mjs && node validate.mjs","generate":"node generate-types.mjs && node generate-zod.mjs","validate":"node validate.mjs"},"_npmUser":{"name":"heroboec","email":"shishkosv@gmail.com"},"repository":{"url":"git+https://github.com/astragenie/astramem-local.git","type":"git","directory":"contracts"},"_npmVersion":"10.9.8","description":"Canonical JSON Schemas + golden fixtures + generated TypeScript types + Zod validators for the AstraMem cross-repo wire contracts (atom@1, atom@2, retrieval@1, sync@1, capture@1). Source of truth consumed by astramem-local, astramem cloud, and astramem-pl","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"peerDependencies":{"zod":"^4.4.3"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/astramem-contracts_2.3.0_1784047718948_0.6928540132513663","host":"s3://npm-registry-packages-npm-production"}},"2.4.0":{"name":"@astragenie/astramem-contracts","version":"2.4.0","description":"Canonical JSON Schemas + golden fixtures + generated TypeScript types + Zod validators for the AstraMem cross-repo wire contracts (atom@1, atom@2, retrieval@1, sync@1, capture@1). Source of truth consumed by astramem-local, astramem cloud, and astramem-pl","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/astragenie/astramem-local.git","directory":"contracts"},"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"peerDependencies":{"zod":"^4.4.3"},"peerDependenciesMeta":{"zod":{"optional":true}},"types":"./types/index.d.ts","exports":{".":{"types":"./types/index.d.ts"},"./types":"./types/index.d.ts","./types/*":"./types/*","./zod":"./zod/index.ts","./zod/*":"./zod/*","./wire":"./wire.ts","./schemas/*":"./schemas/*","./fixtures/*":"./fixtures/*","./manifests/*":"./manifests/*"},"scripts":{"generate":"node generate-types.mjs && node generate-zod.mjs","validate":"node validate.mjs","build":"node generate-types.mjs && node generate-zod.mjs && node validate.mjs"},"_id":"@astragenie/astramem-contracts@2.4.0","gitHead":"a70cad7b4a710c94b5199a92b9841c1050c49f05","bugs":{"url":"https://github.com/astragenie/astramem-local/issues"},"homepage":"https://github.com/astragenie/astramem-local#readme","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-xM2pYHRGUFPyi2RjkE45QR5dlhz95JS97PV0EvcBYMDDv2fhZl1f0mldp3vxAj6uBXwqV7GSGNsAaP1FMCINgw==","shasum":"dab944d5b2eef45d62e5d6f3493f13406fe7ecd7","tarball":"https://registry.npmjs.org/@astragenie/astramem-contracts/-/astramem-contracts-2.4.0.tgz","fileCount":90,"unpackedSize":232344,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCtegSFabSpwd0Z6zHUrVXNy3asL9h172pkiRsR9KTuOgIgJ6jRtple+NzgZ5NP50/1w+9tnoPSyzHWLuU/h6/i99Y="}]},"_npmUser":{"name":"heroboec","email":"shishkosv@gmail.com"},"directories":{},"maintainers":[{"name":"heroboec","email":"shishkosv@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/astramem-contracts_2.4.0_1784068017632_0.38322768382875116"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-10T16:34:03.280Z","modified":"2026-07-14T22:26:57.964Z","2.1.0":"2026-07-10T16:34:03.611Z","2.1.1":"2026-07-10T18:04:08.412Z","2.2.0":"2026-07-13T08:12:36.412Z","2.3.0":"2026-07-14T16:48:39.093Z","2.4.0":"2026-07-14T22:26:57.776Z"},"bugs":{"url":"https://github.com/astragenie/astramem-local/issues"},"license":"MIT","homepage":"https://github.com/astragenie/astramem-local#readme","repository":{"type":"git","url":"git+https://github.com/astragenie/astramem-local.git","directory":"contracts"},"description":"Canonical JSON Schemas + golden fixtures + generated TypeScript types + Zod validators for the AstraMem cross-repo wire contracts (atom@1, atom@2, retrieval@1, sync@1, capture@1). Source of truth consumed by astramem-local, astramem cloud, and astramem-pl","maintainers":[{"name":"heroboec","email":"shishkosv@gmail.com"}],"readme":"# @astragenie/astramem-contracts\n\nThe cross-repo technical constitution for AstraMem. `astramem-local` (Bun/TS/SQLite)\nand AstraMem cloud (.NET/Postgres) are two different implementations of the\nsame product; this directory is the single source of truth both must\nconform to, enforced in CI on both sides. See:\n\n- [ADR-001: Canonical Memory Atom](../docs/adr/ADR-001-canonical-memory-atom.md)\n- [ADR-003: Sync Protocol](../docs/adr/ADR-003-sync-protocol.md)\n- [ADR-005: Retrieval — contract + shared eval harness](../docs/adr/ADR-005-retrieval-contract.md)\n- [ADR-008: Capture Protocol](../docs/adr/ADR-008-capture-protocol.md) / [docs/capture-protocol.md](../docs/capture-protocol.md)\n\nv1 ships **in-repo**, consumed directly by this repo's CI (`.github/workflows/lint.yml`)\nand vitest suite (`tests/contracts/conformance.test.ts`). The `package.json`\nin this directory is publishable (`@astragenie/astramem-contracts`, GitHub\nPackages) via `.github/workflows/contracts-publish.yml`, tag-triggered on\n`contracts-v*` — separate from the root package's `v*` tag scheme. See\n\"Publishing\" and \"Cloud + plugin consumption\" below.\n\n## What's here\n\n```\ncontracts/\n  package.json              — publishable package metadata (GitHub Packages,\n                                see \"Publishing\" below)\n  validate.mjs               — plain Node script; compiles every schema, asserts\n                                every fixtures/valid/* passes and every\n                                fixtures/invalid/* fails. No deps beyond ajv +\n                                ajv-formats (installed at the repo root).\n  wire.ts                    — hand-written wire-protocol constants (WIRE_VERSION,\n                                WIRE_VERSION_PATTERN, WIRE_VERSIONS_SUPPORTED,\n                                SYNC_PROTOCOL), see \"Wire-protocol constants\" below.\n  manifests/\n    mcp-tools.v1.json         — canonical MCP verb_noun tool-name manifest (U5),\n                                see \"MCP tool-name manifest\" below.\n  schemas/\n    atom.v1.schema.json               — astramem/atom@1 (ADR-001)\n    atom.v2.schema.json               — astramem/atom@2 (ADR-001 amendment, 2026-07-09) — see\n                                          \"Memory atom v2 (atom@2 breaking cut)\" below\n    retrieval-query.v1.schema.json    — astramem-retrieval@1 query envelope (ADR-005)\n    retrieval-result.v1.schema.json   — astramem-retrieval@1 result envelope + ScoreExplanation (ADR-005)\n    sync-envelope.v1.schema.json      — astramem-sync@1 envelope (ADR-003)\n    capture-envelope.v1.schema.json   — astramem-capture@1 envelope (ADR-008)\n  fixtures/\n    valid/<schema-prefix>-*.json      — >=3 per schema, each a distinct valid shape\n    invalid/<schema-prefix>-*.json    — >=3 per schema, each violating a DIFFERENT constraint\n    eval/\n      corpus.json    — ADR-005 seed retrieval-eval corpus: 12 atoms, all 7 types,\n                        one superseded pair, one entity-heavy fact\n      queries.json   — 8 graded queries incl. one bitemporal as_of case\n```\n\nEvery JSON Schema is **draft 2020-12**, plain JSON with no TypeScript-only\nconstructs — this is deliberate so the .NET cloud repo's CI can consume the\nexact same files with a JVM/.NET JSON Schema validator (NJsonSchema,\nJsonSchema.Net, etc.) without any transpilation step.\n\n## Generated TypeScript types (`types/`)\n\nFor TypeScript consumers (astramem-local itself, astramem-plugin) that want\ncompile-time types on the hot path rather than a runtime ajv validator,\n`generate-types.mjs` compiles each schema to a `.d.ts` under `types/` plus an\n`index.d.ts` barrel. **These are GENERATED — never hand-edit.** The JSON Schema\nstays the single source of truth; the types are a build artifact.\n\n```bash\nnode contracts/generate-types.mjs   # regenerate types/*.d.ts\nnpm run contracts:generate          # same, via root script\nnpm run contracts:build             # generate + validate in one step\n```\n\nRoot interface names are deterministic `<Name>V<N>` (`AtomV1`,\n`RetrievalQueryV1`, `RetrievalResultV1`, `CaptureEnvelopeV1`, `SyncEnvelopeV1`),\nre-exported from `@astragenie/astramem-contracts/types`. CI should run\n`contracts:generate` and fail on a non-empty git diff, so a schema change that\nwasn't accompanied by a regenerated type is caught as drift.\n\nCloud (.NET) does NOT use these — it consumes the JSON Schema directly (above).\nThe generated `.d.ts` are a TypeScript-consumer convenience only.\n\n## Generated Zod validators (`zod/`)\n\nTypeScript consumers that validate untrusted wire input at **runtime** (the\nplugin's providers call `.parse()` on backend responses; local's routes validate\nrequest bodies) want a Zod schema, not just compile-time types. `generate-zod.mjs`\ncompiles each JSON Schema to a runtime Zod validator under `zod/` +\nan `index.ts` barrel. **GENERATED — never hand-edit.**\n\n```ts\nimport { RetrievalQueryV1Schema, RetrievalResultV1Schema } from '@astragenie/astramem-contracts/zod';\nconst result = RetrievalResultV1Schema.parse(await res.json()); // runtime-validated + typed\n```\n\nConst names are `<Name>V<N>Schema` (`AtomV1Schema`, `RetrievalQueryV1Schema`, …),\neach with a co-exported `z.infer` type. This is what lets the plugin delete its\nhand-rolled `RecallResponseSchema`/`IngestPayloadSchema` (program slice U3) and\nvalidate against the canonical contract instead. Round-tripped against the same\n`fixtures/{valid,invalid}/*` in CI. Cloud (.NET) still uses JSON Schema + a .NET\nvalidator — Zod is a TypeScript-consumer convenience only.\n\n> **⚠ Caveat — Zod is LOSSY on conditional schemas.** `json-schema-to-zod` cannot\n> translate JSON-Schema `if/then`/`allOf` conditionals; it emits a no-op\n> (`z.intersection(z.any(), z.any())`). This affects **`capture-envelope.v1`**,\n> whose `kind:\"events\" → require events` / else `require turns` rule is therefore\n> **NOT enforced by `CaptureEnvelopeV1Schema`**. The `validate.mjs` ajv gate (and\n> any .NET JSON-Schema validator) DOES enforce it — so the JSON Schema stays the\n> authoritative validator. Consumers needing the conditional at runtime should\n> `.superRefine()` on top of the generated Zod, or validate with ajv against\n> `schemas/capture-envelope.v1.schema.json` directly. `atom.v1`, `retrieval-query.v1`,\n> `retrieval-result.v1`, `sync-envelope.v1` have no conditionals and generate\n> faithful Zod.\n\n## Wire-protocol constants (`wire.ts`)\n\nSome cross-repo wire agreements aren't a JSON Schema shape — they're small,\nstable literals (a version string, a compiled regex, a protocol name) that\nstill need to be bit-for-bit identical across astramem-local, astramem cloud\n(.NET), and astramem-plugin. `wire.ts` is a **hand-written** module for\nexactly these (never touched by `generate-types.mjs` / `generate-zod.mjs`),\nimportable as `@astragenie/astramem-contracts/wire`:\n\n```ts\nimport { WIRE_VERSION, WIRE_VERSION_PATTERN, WIRE_VERSIONS_SUPPORTED, SYNC_PROTOCOL } from '@astragenie/astramem-contracts/wire';\n```\n\n- **`WIRE_VERSION`** (`'v1.0'`) — the capture-protocol `wire_version` value\n  this package's `capture-envelope.v1` schema targets for new writers.\n- **`WIRE_VERSION_PATTERN`** — the M-R7 **tightened** `wire_version` regex,\n  `^v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)$`: ASCII digits only, leading\n  zeros disallowed in *both* the major and minor component (so `\"v01.0\"`\n  and `\"v1.01\"` are both rejected). Byte-for-byte the same pattern as\n  `capture-envelope.v1.schema.json`'s `wire_version.pattern` (this change\n  tightened that schema to match — see changelog below) and cloud's\n  `IngestTranscriptRequest.cs:65` `RegularExpression` attribute.\n  **Not yet adopted** by astramem-local's own\n  `src/server/routes/ingest.ts` (`wire_version` Zod `.regex()`), which still\n  uses the older, looser pattern (`^v(?:0|[1-9][0-9]*)\\.[0-9]+$`, permits a\n  leading-zero minor) — adopting this constant there is a follow-up.\n- **`WIRE_VERSIONS_SUPPORTED`** — the domain@gen vocabulary\n  (`['atom@1', 'atom@2', 'retrieval@1', 'sync@1', 'capture@1']`) this package's\n  schemas correspond to. `atom@2`'s addition here is package-level bookkeeping\n  only — `src/server/lib/wire-meta.ts`'s hand-mirrored `WIRE_VERSIONS_SUPPORTED`\n  (derived from `SCHEMA_FILE_TO_DOMAIN`, for the same `rootDir` reason\n  described under [MCP tool-name manifest](#mcp-tool-name-manifest-u5) below)\n  is scoped to `*.v1.schema.json` files only and is therefore untouched by\n  this addition; wiring the daemon/cloud reader to actually accept `atom@2` on\n  the wire is separate follow-up work (see \"Memory atom v2\" below). This is a\n  separate axis from the capture protocol's per-request `wire_version` field\n  above — don't conflate the two.\n- **`SYNC_PROTOCOL`** (`'astramem-sync@1'`) — mirrors `SYNC_PROTOCOL` in\n  `src/sync/shipper.ts`.\n- **`ENTITY_KINDS`** (12 values: `actor, team, org, project, product,\n  workitem, decision, tech, tool, concept, event, other`) and **`ACTOR_KINDS`**\n  (`['actor', 'team', 'org']`, the subset for which `entities[].subtype` is a\n  meaningful refinement) — the canonical entity-kind registry `atom.v2.schema.json`\n  `entities[].kind` enforces. Kept in sync with the schema by\n  `tests/contracts/entity-kind-parity.test.ts`. See \"Memory atom v2\" below.\n\nLike the Zod barrel, this is a TypeScript-consumer convenience — cloud\n(.NET) has its own literal constants and doesn't consume this file.\n\n## Memory atom `type` registry (U4, ADR D4)\n\n`atom.v1.schema.json`'s `type` enum is the canonical, cross-repo vocabulary\nfor memory atoms — 10 values, ratified in the U4 contract-unification wave\n(ADR D4) as the union of astramem-local's original 7 and cloud's 3:\n\n| Type          | Ships from                         | Notes                                                                 |\n|---------------|-------------------------------------|------------------------------------------------------------------------|\n| `decision`    | local (+ cloud consumes via sync)   | Architectural/design choice.                                          |\n| `fact`        | local (+ cloud consumes via sync)   | Objective project fact.                                               |\n| `lesson`      | local (+ cloud consumes via sync)   | Learning from failure/surprise.                                       |\n| `command`     | local (+ cloud consumes via sync)   | Useful shell incantation.                                             |\n| `todo`        | local (+ cloud consumes via sync)   | Pending work item.                                                    |\n| `note`        | local (+ cloud consumes via sync)   | Freeform observation not fitting other types.                        |\n| `event`       | local (+ cloud consumes via sync)   | Something that happened at a point in time.                          |\n| `preference`  | cloud only (today)                  | User/agent preference atom. Local has no writer yet.                 |\n| `task_result` | cloud only (today)                  | Outcome of a completed task. Local has no writer yet.                 |\n| `summary`     | cloud only (today)                  | Session/thread summary atom. Local has no writer yet.                |\n\nLocal's own writer-facing surfaces (the extraction LLM prompt\n`src/distill/prompts/extract.ts`, the ADR-008 `events`-kind request schemas\nin `src/server/routes/ingest.ts` / `src/pipeline/handlers/distill-events.ts`,\nand the `memories.type` SQLite `CHECK` constraint) are **deliberately left at\nthe original 7** by this change — astramem-local does not yet produce\n`preference`/`task_result`/`summary` atoms anywhere, so widening those\nwriter-side surfaces (and the DB `CHECK`) is a separate follow-up, not part\nof authoring the shared contract vocabulary. `src/contracts/memory.ts`'s\n`MEMORY_TYPES`/`MemoryType` (the daemon's in-process type union, used by\nread paths like search/sync/eval) IS reconciled to the full 10 here, since\nthose paths must be able to represent an atom of any canonical type (e.g. one\npulled from cloud via `src/sync/puller.ts`) even before local gains its own\nwriter for it.\n\n## Memory atom v2 (atom@2 breaking cut, 2026-07-09)\n\n`atom.v2.schema.json` is the first `@2` bump under the \"Versioning rules\"\nbelow — a deliberate, zero-customers-only breaking cut across three warts\nidentified in `atom.v1.schema.json`, done in one window rather than three\nseparate additive-then-deprecate cycles. Full rationale: ADR-001 amendment\n(`docs/adr/ADR-001-canonical-memory-atom.md`).\n\n`atom.v1.schema.json` is **not deleted or modified** by this change — it\nstill validates `'personal'`-scoped, `repo`-carrying, flat-`entities: string[]`\natoms exactly as before, so existing atom@1 producers/consumers are\nunaffected until they explicitly migrate. The two schemas are shipped\nside-by-side for a **cloud-reader dual-accept window**: cloud accepts both\n`atom@1` and `atom@2` on ingest during rollout (reusing the `content ?? text`\nfallback idiom from #618), astramem-local's writer + offline sync queue\nmigrate to `atom@2`, then a follow-up chore commit removes the `atom@1`\nfallback and flips `/sync/capabilities` to `atom@2`-only. None of that\ndaemon/cloud wiring is part of this package-only change — see the plan doc\nreferenced in the ADR-001 amendment for the cutover task breakdown.\n\nThe three breaking changes, all scoped to `atom.v2.schema.json` only (except\nwhere noted):\n\n1. **`entities` is now a typed object array**, not `string[]`:\n   `[{ name: string(1..256), kind: <12-value enum>, subtype?: \"human\"|\"ai\" }]`.\n   The flat string shape was lossy in practice — astramem-local's own\n   `harness.ts:137` already re-synthesizes a fake kind for every entity\n   string it reads back, which is the concrete evidence that downstream\n   consumers need `kind`, not just a name. The 12-value `kind` registry\n   (`actor, team, org, project, product, workitem, decision, tech, tool,\n   concept, event, other`) and the `actor, team, org` subset for which\n   `subtype` (`human`/`ai`) is meaningful are exported from `wire.ts` as\n   `ENTITY_KINDS`/`ACTOR_KINDS` (see \"Wire-protocol constants\" above).\n   `actor` covers both humans and AI agents — there is no separate\n   person/agent kind. `repo` was deliberately cut as an entity *kind*: a\n   repo/project identity is owned by the structural `provenance.project`\n   field (point 2 below), and cross-repo mentions land in `product` instead.\n2. **`provenance.repo` is deleted.** `provenance.project` is the single\n   canonical field going forward — cloud already collapses `project_id ??\n   repo` onto one `WorkspaceProjectId` column, so a still-populated `repo`\n   value under atom@1 could silently diverge from what cloud actually keys\n   on. Writers that only ever had a repo value should send it as `project`.\n   `retrieval-query.v1.schema.json`'s `filters.repo` is dropped in the same\n   cut (see its own changelog entry below) — that file stays `v1` (no\n   `retrieval@2`) because, unlike the sync-envelope wire, retrieval is a\n   synchronous request/response surface with no durable offline queue\n   forcing a dual-accept window; the four named `repo`→`project` consumer\n   call sites are daemon-side follow-up work, not part of this package.\n3. **The `'personal'` scope alias is dropped.** `atom.v1.schema.json` kept\n   `'personal'` as a one-release dual-read accommodation for the\n   `personal`→`private` rename (U7-local astramem-local#110, DEC-048),\n   tracked as `TODO(remove-personal-alias)` (astramem-local#115) — this is\n   the release that removes it. `atom.v2.schema.json`'s `scope` enum is\n   `private | team | org` only. `retrieval-query.v1.schema.json`'s\n   `filters.scope` and `retrieval-result.v1.schema.json`'s `hits[].scope`\n   drop `'personal'` in the same cut (both stay `v1` — same reasoning as\n   point 2: synchronous surfaces, no offline-queue dual-accept need).\n\n**Also bundled in this release, additive (no version bump needed on their\nown schema)**: `retrieval-result.v1.schema.json`'s `hits[].type` enum widens\nfrom the original 7 values to the full canonical 10 (adds `preference`,\n`task_result`, `summary`) — cloud already returns these types, so a\nconformant hit from a real cloud response used to fail this schema. Per the\n\"Memory atom `type` registry\" section above, widening an already-open\nregistry enum is additive, not breaking.\n\n### atom@2 additive extension — transcript speaker/segment provenance (FEAT-496, 2026-07-14)\n\nSame `atom.v2.schema.json` file, no version bump — new optional fields only,\nper the \"Additive change\" rule below. Motivated by FEAT-491 (meeting-caption\ncapture) and cloud's WS-B/FEAT-516, both of which need per-evidence speaker\nidentity and timing to reach stored atom provenance, not just the wire.\n\n- **`evidence[].speaker_label`** (optional string) and\n  **`evidence[].segment_start_ms`** (optional non-negative integer) — added\n  to the structured-receipt branch of `evidence`'s `anyOf` (the\n  `{ transcript_id, span }` object shape). `speaker_label` is a display\n  name/label as surfaced by a diarized or caption-labeled source (e.g.\n  \"Alice\", \"Speaker 2\"). `segment_start_ms` is a millisecond offset into the\n  source recording/transcript, complementing `span` (a character offset into\n  the transcript *text*, not a time offset) — the two are independent axes\n  and a source may populate either, both, or neither.\n- **`provenance.worker` / `provenance.model` / `provenance.attestation`**\n  (all optional, nullable strings) — the Screenpipe-inspired provenance\n  triple, reserved now because `provenance` has `additionalProperties: false`\n  (line 68) and every field needs an explicit schema edit; reserving the slot\n  in this pass is nearly free and avoids a second breaking-adjacent PR later.\n  **None of these three are written by any producer yet** — this is a\n  forward-reserve, not a shipped capability. `worker` names the compute tier\n  that ran extraction (e.g. `local-cpu`/`local-gpu`/`cloud-worker`); `model`\n  is `name@version` of the underlying LLM/ASR model (distinct from\n  `provenance.extractor`, which names the *pipeline*, not the model);\n  `attestation` is an opaque signature/hash for future tamper-evidence use.\n  No enum on any of the three — freeform strings, since the value sets don't\n  exist yet.\n- **`provenance.consent_disclosed`** (optional, nullable boolean; ADR-017\n  decision 6) — whether the operator disclosure/opt-in required for capturing\n  a non-user speaker (audio FEAT-489, meeting captions FEAT-491) was\n  satisfied at capture time. Tri-state semantics: `null`/absent = not\n  applicable or not recorded (e.g. atoms with no non-user speaker involved);\n  `true`/`false` = an explicit disclosure determination was made. Scoped onto\n  atom@2 provenance deliberately (ADR-017) so consent provenance has ONE\n  schema-change surface instead of being duplicated per capture source.\n  Server-side enforcement policy (hard-reject on missing/false at ingest) is\n  FEAT-489/491 daemon-side work, not part of this contract.\n\n**Deliberately NOT added here: `platform` / `meeting_id`.** FEAT-491's\nmeeting-caption capture source needs these, but they describe the *capture\nsession* (which meeting, which platform), not a single evidence span or a\nsingle atom's provenance — many atoms can be distilled from one captured\nsession. They belong on `capture-envelope.v1.schema.json` (FEAT-491's own\nschema widen: `capture_source` enum + envelope fields), and flow into stored\natoms via the existing `provenance.session_id` correlation, the same way\n`session_id` already bridges capture envelope and atom today. Coordinated\nwith FEAT-491 so its speaker labels land in `evidence[].speaker_label`\nabove rather than a competing shape.\n\nFixtures: `atom-v2-speaker-segment-evidence.json` (valid, exercises both new\nevidence fields) and `atom-v2-provenance-triple.json` (valid, exercises the\nreserved provenance triple plus `consent_disclosed: true`) added under\n`fixtures/valid/`.\n\n### capture-envelope@1 additive extension — meeting-caption capture (FEAT-491, 2026-07-14)\n\nSame `capture-envelope.v1.schema.json` file, no version bump — new optional\nfields plus one conditional requirement, per the \"Additive change\" rule\nbelow. This is the schema-widen the FEAT-496 section above deliberately left\nfor FEAT-491 to own (`platform`/`meeting_id` describe the *capture session*,\nnot a single atom or evidence span).\n\n- **`capture_source`** (optional string enum: `auto_capture | hook_close |\n  proxy | meeting_captions`) — first appearance of this field in the\n  *published* contract. astramem-local's server-side envelope parser\n  (`CanonicalIngestSchema`, `src/server/routes/ingest.ts`) already accepted\n  the first three values; this is the widen to match plus the new\n  `meeting_captions` value for FEAT-491's browser-extension caption-scraping\n  source (astramem-plugin owns the scraping; this daemon owns acceptance).\n  Closed enum deliberately, same rationale the daemon's own code comment\n  gives: an unrecognized value is a client bug worth a 400, not a silent\n  coercion to null.\n- **`platform`** (optional string, open — not an enum) and **`meeting_id`**\n  (optional string) — describe the meeting/call the transcript was captured\n  from (e.g. `platform: \"google_meet\"`). Deliberately kept off\n  `atom.v2.schema.json`'s `provenance` (see the FEAT-496 section above);\n  atoms distilled from a captured session correlate back to it via the\n  existing `provenance.session_id` bridge, the same way `session_id` already\n  connects a capture envelope to every atom it produces.\n- **`consent_disclosed`** (optional boolean) — ADR-017 decision 6's\n  server-side consent marker, set by the capturing adapter once the operator\n  completes the required third-party-speaker disclosure/opt-in. Optional in\n  general (every capture source with no third-party speaker omits it,\n  byte-identical to today), but the new `allOf` conditional below makes it\n  **required and `true`** whenever `capture_source: \"meeting_captions\"` is\n  present — a missing or `false` value on a meeting-captions envelope fails\n  schema validation, the contract-level backstop for ADR-017's client-side\n  opt-in gate.\n- **`turns[].speaker`** (optional string, `minLength: 1`) — display\n  name/label for who actually said this turn, additive alongside (not a\n  replacement for) `role`. `role` stays the closed `user|assistant`\n  discriminator astramem-local's turn-flattening already depends on — a\n  multi-participant caption transcript can't be losslessly collapsed into\n  that binary, so `speaker` carries the real per-turn identity. Flows to the\n  stored atom's `evidence[].speaker_label` (FEAT-496, above) via\n  astramem-local's distill pipeline.\n\nNew `allOf` branch: `capture_source == \"meeting_captions\"` (when present)\nrequires `consent_disclosed` to be present and `true`. This mirrors the\nexisting `kind`-based conditional requirements already in this schema\n(`kind: \"events\"` requires `events`; otherwise requires `turns`) — same\npattern, new predicate.\n\nFixtures added: `capture-envelope-v1-meeting-captions.json` (valid, a\nrealistic 3-speaker meeting transcript exercising every new field) under\n`fixtures/valid/`; `capture-envelope-v1-meeting-captions-missing-consent.json`\n(invalid — `capture_source: \"meeting_captions\"` present, `consent_disclosed`\nomitted) and `capture-envelope-v1-unknown-top-level-field.json` (invalid —\nconfirms `additionalProperties: false` still rejects an unmodeled top-level\nfield after this widen) under `fixtures/invalid/`.\n\n## Fixture naming convention (load-bearing)\n\n`validate.mjs` and `tests/contracts/conformance.test.ts` both route fixtures\nto schemas by filename prefix, derived mechanically from the schema file\nname: `<name>.v<N>.schema.json` -> fixture files must start with\n`<name>-v<N>-`. Example: `atom.v1.schema.json` matches\n`fixtures/valid/atom-v1-decision-string-evidence.json`. A fixture whose name\ndoesn't match any schema's prefix fails the run (silently-skipped fixtures\nwould defeat the gate) — both runners assert every fixture file was matched\nexactly once.\n\n## Versioning rules (ADR-001)\n\n- **Additive change** (new optional field, new enum value in an\n  already-open registry) = **minor** version bump on the schema `$id` /\n  `title` (e.g. `astramem/atom@1` stays `@1`; a genuinely breaking shape\n  change would be `@2`).\n- **Breaking change** (removing/renaming a required field, narrowing a type,\n  removing an enum value) = **new major** (`@2`), shipped alongside the old\n  schema for a **dual-read window**: both repos accept both versions until\n  every writer has migrated, then the old schema is deleted. This mirrors\n  the wire-version negotiation already used by the capture protocol\n  (`wire_version` field) and sync protocol (`GET /sync/capabilities`).\n- Fixtures for a deprecated major stay in `fixtures/` (under `deprecated/`\n  once that's needed) until the dual-read window closes, so regression\n  coverage doesn't silently disappear mid-migration.\n\n## Evidence reconciliation (atom.v1.schema.json)\n\nADR-001's decision text specifies `evidence` as\n`[{ transcript_id, span: [start, end] }]` — structured receipts pointing at\nsource transcript spans. **Reality**: `astramem-local` v1 stores evidence as\na single free-text excerpt string (`memories.evidence` column, migration\n`004-provenance.sql`; see `src/contracts/memory.ts` `Memory.evidence:\nstring | null`).\n\nRather than let the schema describe an aspiration astramem-local doesn't\nactually produce, `atom.v1.schema.json` models `evidence` as\n`anyOf [string, array-of-refs]`:\n\n- the **string** form is documented in the schema as \"local v1 form\" and is\n  what `src/contracts/atom-wire.ts` (`toAtomWireV1`) emits today;\n- the **array-of-refs** form is the ADR-001 canonical shape, which cloud can\n  emit once/if it tracks per-span provenance.\n\nThis is a pragmatic-contract-truth-beats-aspiration call: a schema that\nrejects every atom astramem-local actually produces is not a contract, it's\na wishlist. When local gains span-level evidence tracking, the string arm\ncan be deprecated behind the versioning rule above rather than breaking the\nschema retroactively.\n\n## Retrieval result — ScoreExplanation signal map\n\nADR-005 requires every retrieval hit to carry a `ScoreExplanation` with\nper-signal raw score, weight, and final contribution, and states this is\nnever optional. `retrieval-result.v1.schema.json` makes `explanation`\n**required** on every hit (not conditional on the query's `explain` flag).\n\n`explanation.signals` is an **open map** keyed by signal name\n(`additionalProperties`, not a closed enum) so both engines validate without\neither one dictating the other's fusion formula:\n\n- **local** (`src/search/fuse.ts`): `bm25`, `cosine`, `importance`,\n  `freshness` — 4-signal fusion (α=β=0.4, γ=δ=0.1).\n- **cloud**: 6-signal fusion + RRF fallback + cross-encoder rerank seam\n  (ADR-005 engine-specific rulings) — different signal names entirely\n  (e.g. `rrf`, `cross_encoder`).\n\nEach signal's own shape is fixed: `{ raw: number, weight: number, final:\nnumber }`. See `contracts/fixtures/valid/retrieval-result-v1-cloud-six-signals.json`\nvs. `retrieval-result-v1-single-hit-local-signals.json` for both engines\nvalidating against the same schema.\n\n## Retrieval query — project/agent/entity filters (FEAT-424)\n\n`retrieval-query.v1.schema.json`'s `filters` block is the **canonical**\nproject/agent/entity recall-filter contract — the single source of truth\nFEAT-424 unifies `astramem-local`, `astramem-plugin`, and the memory SaaS\nbackend around, replacing three independently-drifted filter shapes:\n\n- `filters.project` and `filters.agent` are `anyOf [string, string[]]` — a\n  single value is exact match, a list is OR/IN-semantics. An empty array is\n  \"no constraint\" (never match-nothing).\n- `filters.entity` is a plain `string`, resolved by each engine's own\n  normalize seam (case/whitespace-insensitive, exact match then substring\n  fallback per ADR-005 / FEAT-402).\n- `project` + `agent` AND-compose when both are present.\n\n`astramem-local`'s own `tsconfig.json` scopes `rootDir` to `src/`, so its\ndaemon code cannot `import` `contracts/zod/retrieval-query.v1.ts` directly\n(TS6059 — the file lives outside `rootDir`). Its wiring\n(`src/contracts/recall-filters.ts`) is therefore a hand-mirrored copy of this\nschema's filter shape, held honest by\n`tests/contracts/recall-filter-parity.test.ts` (ajv-compiles this schema at\ntest time and asserts the Zod mirror agrees). `astramem-plugin` and the\nmemory SaaS backend do not have that constraint — they should import\n`@astragenie/astramem-contracts/zod`'s `RetrievalQueryV1Schema` (or the\n`.d.ts` types) directly rather than hand-rolling a third copy. See\n`docs/recall-filters.md` \"Canonical contract (FEAT-424)\" for full daemon-side\nwiring detail and the cross-repo adoption follow-up.\n\n## Sync envelope timestamps\n\n`sync-envelope.v1.schema.json` keeps `created_at` as an **epoch-ms\ninteger**, not an ISO string, deliberately breaking from `atom.v1`'s ISO\n8601 convention: the event shape is a 1:1 mirror of the `memory_events`\ntable (migration `007-memory-events.sql`, an `INTEGER` column), because\nADR-003 sync is log-shipping — the wire form of a log row should be the log\nrow, not a reformatted view of it.\n\n## Running the gate locally\n\n```bash\nnode contracts/validate.mjs      # standalone — schemas + fixtures only\nnpm run contracts:validate       # same thing, via the package.json script\nbun run test                     # full vitest suite, includes\n                                  # tests/contracts/conformance.test.ts\n                                  # (schema compile + fixtures + LIVE\n                                  # conformance against the real pipeline)\n```\n\nCI: `.github/workflows/lint.yml` runs `node contracts/validate.mjs` as a\nstep immediately after `tsc --noEmit` (cheap, fast, no build required).\n`.github/workflows/test.yml` gates on the vitest conformance suite as part\nof the normal `bun run test` run.\n\n## Publishing\n\n`.github/workflows/contracts-publish.yml` publishes `@astragenie/astramem-contracts`\nto GitHub Packages (`npm.pkg.github.com`) on push of a `contracts-v*` tag\n(NOT the root package's `v*` tag — the two packages release independently;\nsee \"Versioning\" below for why). The workflow:\n\n1. Asserts the tag suffix matches `contracts/package.json` version (same\n   parity gate pattern as `version-tag-parity.yml`, scoped to this package).\n2. Regenerates `types/` + `zod/` from `schemas/` and fails the job if that\n   produces a diff — the checked-in generated output must already match\n   what's published (prevents publishing stale generated code).\n3. Publishes with the ambient Actions `GITHUB_TOKEN` (`packages: write`\n   permission) — this is CI-to-same-org publish, not the classic-PAT path.\n\nCutting a release: bump `contracts/package.json` version, land that on\n`main`, then push tag `contracts-vX.Y.Z` pointing at that commit.\n\n**Consuming the published package** (plugin repo CI, or any local install)\nneeds read access to GitHub Packages, which — unlike consuming a public\nnpmjs.org package — always requires an authenticated request. Configure:\n\n```\n# ~/.npmrc or repo .npmrc\n@astragenie:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}\n```\n\nwith `NODE_AUTH_TOKEN` set to a classic PAT (`ghp_...`, `read:packages` scope\n— `gho_` OAuth tokens from `gh auth login` are rejected, see\n`github-packages-auth.md`). Whether the consuming repo's own CI can instead\nrely on its ambient `GITHUB_TOKEN` for a same-org cross-repo package read\nwas **not verified live** in this change — treat that as open until checked\nagainst this org's GitHub Packages visibility settings.\n\n## Cloud + plugin consumption (what task 3b/3c needs)\n\nThe cloud (.NET) repo's CI needs:\n\n1. **The files**: either (a) a git submodule / subtree pointing at this\n   repo's `contracts/` directory, or (b) an npm-side artifact download of\n   `@astragenie/astramem-contracts` via GitHub Packages once a\n   `contracts-v*` tag has been published (see \"Publishing\" above). Cloud\n   only needs the JSON Schemas + fixtures (not the TS types/Zod), so a thin\n   fetch-and-unpack step (`npm pack` + extract, or a raw GH Packages tarball\n   download) is enough — cloud CI does not need a Node toolchain otherwise.\n2. **A runner**: cloud's CI does not need Node — the schemas are pure JSON\n   Schema draft 2020-12 with `additionalProperties`, `anyOf`, `allOf`/`if`/\n   `then`, `format` (uuid, date-time), and `pattern` keywords only. Any\n   conformant .NET validator (e.g. `JsonSchema.Net`, `NJsonSchema`) can load\n   `contracts/schemas/*.schema.json` directly and run the same\n   valid-passes/invalid-fails assertion against `contracts/fixtures/{valid,invalid}/*.json`\n   using the same filename-prefix routing rule described above.\n3. **The eval harness**: `contracts/fixtures/eval/corpus.json` +\n   `queries.json` are the shared golden retrieval-eval fixture set (ADR-005\n   part 3). Cloud's retrieval CI job loads both, runs its own engine against\n   `queries.json`, and computes recall@10 / NDCG@10 against the graded\n   `graded_relevant` lists — thresholds are not gated yet (v1 = seed size\n   only), but the harness format is fixed now so both sides measure the same\n   thing when thresholds land.\n4. **Versioning discipline**: cloud CI should fail (not warn) if it detects\n   a schema `$id`/`title` version it doesn't recognize, per the dual-read\n   window rule above — that failure is the signal a contract bump needs a\n   coordinated two-repo rollout, not a silent skip.\n\n## MCP tool-name manifest (U5)\n\n`manifests/mcp-tools.v1.json` is the canonical, cross-repo source of truth\nfor the MCP `verb_noun` tool-name vocabulary — astragenie/memory#671 (U5)\nneeds both MCP servers (this daemon + the cloud .NET server) to expose the\nidentical tool-name surface so `astramem-plugin`'s providers can call one\ncontract regardless of which backend answers.\n\nShape:\n\n```json\n{\n  \"version\": \"1\",\n  \"tools\": [\n    { \"name\": \"search_memory\", \"owner\": \"local\", \"description\": \"...\", \"status\": \"stable\" }\n  ]\n}\n```\n\nFields:\n\n- `name` — the wire-level MCP tool name (`verb_noun`).\n- `owner` — `local` | `cloud` | `both`, mirroring the \"ships from\" convention\n  in the [memory atom `type` registry](#memory-atom-type-registry-u4-adr-d4)\n  above. astramem-local's own SLICE-A only lists tools it registers today\n  (`owner: \"local\"`) — cloud-owned rows are added once the cloud-side\n  manifest draft exists, so this file never drifts ahead of what's actually\n  registered on either side.\n- `description` — short, human-facing summary. **Not** required to\n  byte-match the MCP server's own `description` string passed to\n  `registerTool()` — this manifest pins names + ownership, not full\n  per-tool schema parity (see \"Scope\" below).\n- `status` — `stable` | `planned` | `deprecated`, reserved for a future\n  dual-name migration window (mirrors the wire-version dual-read convention\n  above).\n\n**Scope**: this manifest intentionally does NOT carry a JSON-Schema-per-tool\ninput contract. Each server's own `inputSchema` (zod locally, its .NET\nequivalent in cloud) remains the authoritative validator for tool arguments —\nwidening this manifest to full input/output schemas was considered and\nrejected as out of scope for #671, whose actual ask is name parity (client\nrename-risk), not input-shape parity (already covered per-tool elsewhere).\n\n**Naming decision (SLICE-A)**: all 15 tools astramem-local registers today\n(`src/mcp/server.ts`) are already `verb_noun` or verb-only where the noun is\nimplicit/global, with two names that read noun-noun (`session_digest`,\n`memory_history`) and one bare verb (`remember`). SLICE-A ships these\n**as-is** — `remember` stays as-is (a `remember_memory`/`create_memory`\nrename was considered and rejected: `remember` is the daemon's most-used\ntool and a rename carries real client-config blast radius for no vocabulary\ngain). Whether `session_digest`/`memory_history` need a `get_`-prefixed\nrename to strictly match `verb_noun` (mirroring `get_health`'s shape) is\ndeferred to a coordinated SLICE-B, gated on the cross-repo #671 naming call\n— this manifest is additive-only and renames nothing.\n\n**Naming decision (SLICE-B, U5, astragenie/astramem-local#120)**: the\ncross-repo call landed as follows:\n\n- **`memory_history`** — grandfathered exact-match noun_verb name on both\n  backends (cloud adopted local's name rather than renaming to\n  `get_memory_history` — see the shipped `src/AstraMemory.Mcp/canonical-tool-manifest.json`\n  in the cloud repo). Not renamed here either.\n- **`session_digest`** — also grandfathered, **not** renamed to\n  `get_session_digest`. Unlike `memory_history`, cloud has no MCP tool with\n  this name at all, so there is no cross-repo naming collision forcing a\n  decision — renaming a local-only tool for `verb_noun` purity alone, with\n  no unification benefit, was judged not worth the alias-churn cost. Follows\n  the same grandfather precedent set for `memory_history`.\n- **`mark_memory_used` → `submit_feedback`** — this *is* a real cross-repo\n  collision (two different signal types under two different names: local's\n  ADR-010 implicit \"recall was used\" vs. cloud's explicit +1/-1 score) and\n  is collapsed onto cloud's `submit_feedback` wire name. `mark_memory_used`\n  is retained as a one-release **deprecated** alias (same handler) rather\n  than a clean break, since D2's \"no alias needed\" reasoning was written\n  for tools nobody calls yet — `mark_memory_used` predates U5 and may\n  already have callers.\n- This adds the 18 cloud-only tools (`owner: \"cloud\"`) from the actual\n  shipped cloud manifest, giving 34 total entries (16 `local`/`both` +\n  18 `cloud`). `get_health` stays `owner: \"local\"` — cloud exposes an\n  equivalent only as a REST `/health` endpoint, not an MCP tool, per the\n  cloud manifest's own notes.\n\nParity is enforced locally by\n`tests/contracts/mcp-tool-manifest-parity.test.ts`, which builds the real\n`McpServer` (mock deps, no live Ollama/network) and asserts its registered\ntool names equal this manifest's `owner: \"local\"`/`\"both\"` entries exactly\n— same \"readdirSync/JSON.parse at test time, no `src/` import\" shape as\n[`tests/contracts/wire-version-map-parity.test.ts`](../tests/contracts/wire-version-map-parity.test.ts),\nrequired because `tsconfig.json` scopes `rootDir` to `src/` (TS6059 blocks\n`src/` from importing `contracts/`, but nothing stops `tests/` reading it).\nCloud's half (asserting its own registered names against this same file)\nand the plugin's half (asserting provider tool-call names against it) are\nout of this repo's build.\n\n## Versioning: package semver vs. wire_version (two separate axes)\n\nThese are deliberately decoupled and must not be conflated:\n\n- **`contracts/package.json` version** (npm semver, e.g. `1.3.0`) tracks\n  changes to the *package artifact*: new fixtures, regenerated types/Zod,\n  README updates, tooling changes, additive schema fields. It bumps on every\n  `contracts-v*` release regardless of whether any wire-visible shape\n  changed.\n- **`wire_version`** (the field embedded inside envelopes, e.g.\n  `capture-envelope.v1.schema.json`'s `wire_version`, and the `v1` suffix in\n  schema `$id`/filenames like `atom.v1.schema.json`) is the *protocol*\n  version — it only changes on a breaking change to the wire shape itself,\n  and a bump requires the dual-read migration window described above,\n  coordinated across all three consuming repos.\n\nConsequence: a package version bump (e.g. `1.0.0` -> `1.1.0`) does **not**\nimply a `wire_version` bump, and is safe for consumers to pick up without a\nprotocol migration. A `wire_version` bump (e.g. introducing `atom.v2`) always\nforces at least a package **major** bump, but the reverse is not true — most\npackage releases will be minor/patch schema-package hygiene, not protocol\nchanges. CI on the consuming side should gate on `wire_version` compatibility\nexplicitly (point 4 above), not infer it from the npm package's semver.\n\n## Changelog\n\n- **2.3.0** — `atom.v2.schema.json` additive extension (FEAT-496, 2026-07-14;\n  see \"atom@2 additive extension\" above). New optional fields, no schema\n  version bump per the additive-minor rule: `evidence[].speaker_label`,\n  `evidence[].segment_start_ms`, the reserved provenance triple\n  `provenance.worker`/`model`/`attestation`, and\n  `provenance.consent_disclosed` (ADR-017 decision 6 consent provenance).\n  New fixtures: `atom-v2-speaker-segment-evidence.json`,\n  `atom-v2-provenance-triple.json` (the latter also exercises\n  `consent_disclosed: true`).\n- **2.1.0** — `atom.v2.schema.json` `entities[]` items gain optional\n  `valid_at`/`invalid_at` (both nullable `date-time`, FEAT-453 fact-level\n  bitemporal tracking) — additive, in place, no schema major bump per the\n  \"Versioning rules\" below (removes/renames/narrows nothing; every document\n  valid under the pre-2.1.0 schema stays valid). New fixtures:\n  `atom-v2-entity-validity-window.json` (populated), `atom-v2-entity-\n  validity-null.json` (both explicitly `null`, proving the nullable-not-\n  just-optional type union), and an `atom-v2-entity-extra-key-rejected.json`\n  regression fixture proving `additionalProperties: false` still rejects an\n  unrecognized key. See `docs/specs/FEAT-453-bitemporal-design.md` §2 for\n  the full rationale (including why this is additive, not a `@3` cut).\n- **2.0.0** — `atom.v2.schema.json` added (breaking; `atom.v1.schema.json`\n  kept, unmodified, for the dual-read window) — see \"Memory atom v2\" above\n  for the full rationale. Package major bump per the \"Versioning rules\"\n  above: a `wire_version` bump always forces at least a package major.\n  Summary of the wire-visible changes in this release:\n  - `atom.v2.schema.json`: `entities` is now `[{name, kind, subtype?}]`\n    instead of `string[]`; `provenance.repo` deleted (`project` is\n    canonical); `scope` enum narrowed to `private|team|org` (`'personal'`\n    dropped).\n  - `retrieval-query.v1.schema.json` (in place, still `v1`): `filters.repo`\n    deleted; `filters.scope` narrowed to `private|team|org`.\n  - `retrieval-result.v1.schema.json` (in place, still `v1`): `hits[].scope`\n    narrowed to `private|team|org`; `hits[].type` widened 7→10 values\n    (additive — `preference`, `task_result`, `summary` added).\n  - `wire.ts`: `WIRE_VERSIONS_SUPPORTED` gains `'atom@2'` (both `atom@1` and\n    `atom@2` listed during the dual-accept window); new `ENTITY_KINDS`\n    (12-value registry) and `ACTOR_KINDS` (`actor|team|org`) exports.\n  - New fixtures for every change above (typed entities, unknown-kind\n    rejection, `repo`/`personal` rejection on the affected schemas, the\n    widened `hits[].type` set) plus `tests/contracts/entity-kind-parity.test.ts`.\n  - ADR-001 amended in place with the atom@2 decision record.\n- **1.3.0** — `wire.ts` hand-written module (`WIRE_VERSION`,\n  `WIRE_VERSION_PATTERN`, `WIRE_VERSIONS_SUPPORTED`, `SYNC_PROTOCOL`),\n  exported as `@astragenie/astramem-contracts/wire`; see \"Wire-protocol\n  constants\" above. `capture-envelope.v1.schema.json`'s `wire_version`\n  pattern **tightened** (M-R7) from `^v(?:0|[1-9][0-9]*)\\.[0-9]+$` to\n  `^v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)$` to match cloud's\n  `IngestTranscriptRequest.cs:65` — a `\"v1.01\"`-style leading-zero minor\n  component is now rejected. Types/Zod regenerated for the schema change;\n  added `fixtures/invalid/capture-envelope-v1-wire-version-leading-zero-minor.json`\n  to cover it. Additive/tightening-only for a field that was already\n  required — no schema major bump. Not yet adopted by\n  `src/server/routes/ingest.ts` (still the looser pattern) — daemon-side\n  and cloud-side adoption of `WIRE_VERSION_PATTERN` are follow-ups, out of\n  scope for this package-only change.\n- **1.2.0** — `manifests/mcp-tools.v1.json` MCP tool-name manifest (U5-local\n  SLICE-A/B); see \"MCP tool-name manifest\" above.\n","readmeFilename":"README.md"}