From d488e9134b2e0a056c7199ba94130badc2dd4be9 Mon Sep 17 00:00:00 2001 From: fabbrik <22822543+fabbrik@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:07:32 -0500 Subject: [PATCH] test: contract 1.6, vector search calls (story 23.1) Add-only. Publishes the embedding write and nearest-neighbour search statements, the privileges for experience_embeddings, the summary/hash and relevance rules, and the hybrid merge, with .NET-generated fixtures using fixed vectors (families embedding-summary, embedding-write, vector-search, hybrid-merge). The .NET store and the contract SQL as written agree on every case. Python and Go still claim 1.0-1.5 and skip the vector families by name until 23.2 and 23.3. No library behaviour changed. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 18 +- README.md | 5 +- _sdlc/planning-artifacts/epics.md | 2 + contract/README-1.6.md | 188 ++++++ .../fixtures/embedding-summary/cases.json | 92 +++ .../expected.json | 18 + .../seed.json | 160 +++++ .../steps.json | 115 ++++ .../expected.json | 40 ++ .../seed.json | 81 +++ .../steps.json | 132 ++++ .../expected.json | 44 ++ .../written-with-the-record-scope/seed.json | 160 +++++ .../written-with-the-record-scope/steps.json | 53 ++ contract/fixtures/hybrid-merge/cases.json | 345 ++++++++++ .../expected.json | 83 +++ .../an-exact-tie-is-ordered-by-id/seed.json | 239 +++++++ .../an-exact-tie-is-ordered-by-id/steps.json | 103 +++ .../another-model-or-dimension/expected.json | 67 ++ .../another-model-or-dimension/seed.json | 160 +++++ .../another-model-or-dimension/steps.json | 134 ++++ .../model-authored-excluded/expected.json | 69 ++ .../model-authored-excluded/seed.json | 161 +++++ .../model-authored-excluded/steps.json | 105 +++ .../vector-search/nearest-first/expected.json | 98 +++ .../vector-search/nearest-first/seed.json | 239 +++++++ .../vector-search/nearest-first/steps.json | 129 ++++ .../expected.json | 33 + .../seed.json | 141 ++++ .../steps.json | 81 +++ .../expected.json | 158 +++++ .../seed.json | 615 ++++++++++++++++++ .../steps.json | 237 +++++++ .../zero-magnitude-query/expected.json | 59 ++ .../zero-magnitude-query/seed.json | 160 +++++ .../zero-magnitude-query/steps.json | 79 +++ .../expected.json | 83 +++ .../zero-magnitude-stored-vector/seed.json | 239 +++++++ .../zero-magnitude-stored-vector/steps.json | 103 +++ contract/lock.json | 43 +- .../sql/application-role-privileges-1.6.sql | 14 + contract/sql/search-experience-vector.sql | 40 ++ contract/sql/write-experience-embedding.sql | 31 + go/contract_test.go | 21 +- go/internal/contracttest/contracttest.go | 13 +- python/tests/_contract.py | 13 +- python/tests/test_contract.py | 19 +- .../AgentExperience.Contract.Tests.csproj | 2 + .../ContractFiles.cs | 3 +- .../ContractPrivilegesOrderPostgresTests.cs | 2 +- .../SearchContractPostgresTests.cs | 29 +- .../VectorContract.cs | 584 +++++++++++++++++ .../VectorContractFixtureTests.cs | 172 +++++ .../VectorContractPostgresTests.cs | 390 +++++++++++ .../packages.lock.json | 18 + 55 files changed, 6404 insertions(+), 18 deletions(-) create mode 100644 contract/README-1.6.md create mode 100644 contract/fixtures/embedding-summary/cases.json create mode 100644 contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/expected.json create mode 100644 contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/seed.json create mode 100644 contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/steps.json create mode 100644 contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/expected.json create mode 100644 contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/seed.json create mode 100644 contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/steps.json create mode 100644 contract/fixtures/embedding-write/written-with-the-record-scope/expected.json create mode 100644 contract/fixtures/embedding-write/written-with-the-record-scope/seed.json create mode 100644 contract/fixtures/embedding-write/written-with-the-record-scope/steps.json create mode 100644 contract/fixtures/hybrid-merge/cases.json create mode 100644 contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/expected.json create mode 100644 contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/seed.json create mode 100644 contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/steps.json create mode 100644 contract/fixtures/vector-search/another-model-or-dimension/expected.json create mode 100644 contract/fixtures/vector-search/another-model-or-dimension/seed.json create mode 100644 contract/fixtures/vector-search/another-model-or-dimension/steps.json create mode 100644 contract/fixtures/vector-search/model-authored-excluded/expected.json create mode 100644 contract/fixtures/vector-search/model-authored-excluded/seed.json create mode 100644 contract/fixtures/vector-search/model-authored-excluded/steps.json create mode 100644 contract/fixtures/vector-search/nearest-first/expected.json create mode 100644 contract/fixtures/vector-search/nearest-first/seed.json create mode 100644 contract/fixtures/vector-search/nearest-first/steps.json create mode 100644 contract/fixtures/vector-search/nothing-comparable-is-found-empty/expected.json create mode 100644 contract/fixtures/vector-search/nothing-comparable-is-found-empty/seed.json create mode 100644 contract/fixtures/vector-search/nothing-comparable-is-found-empty/steps.json create mode 100644 contract/fixtures/vector-search/scope-status-confidence-and-erasure/expected.json create mode 100644 contract/fixtures/vector-search/scope-status-confidence-and-erasure/seed.json create mode 100644 contract/fixtures/vector-search/scope-status-confidence-and-erasure/steps.json create mode 100644 contract/fixtures/vector-search/zero-magnitude-query/expected.json create mode 100644 contract/fixtures/vector-search/zero-magnitude-query/seed.json create mode 100644 contract/fixtures/vector-search/zero-magnitude-query/steps.json create mode 100644 contract/fixtures/vector-search/zero-magnitude-stored-vector/expected.json create mode 100644 contract/fixtures/vector-search/zero-magnitude-stored-vector/seed.json create mode 100644 contract/fixtures/vector-search/zero-magnitude-stored-vector/steps.json create mode 100644 contract/sql/application-role-privileges-1.6.sql create mode 100644 contract/sql/search-experience-vector.sql create mode 100644 contract/sql/write-experience-embedding.sql create mode 100644 tests/AgentExperience.Contract.Tests/VectorContract.cs create mode 100644 tests/AgentExperience.Contract.Tests/VectorContractFixtureTests.cs create mode 100644 tests/AgentExperience.Contract.Tests/VectorContractPostgresTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ba057..d6a3e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,23 @@ AgentExperience.NET is a **preview**. It is not production ready, and public API ## Unreleased -Nothing yet. +### Contract 1.6: vector search calls (story 23.1) + +- **Contract 1.6** (add-only: every earlier file is unchanged; [what 1.6 adds](contract/README-1.6.md)) brings + retrieval by meaning into the contract, so the Python and Go ports can match .NET's recall on reworded queries. It + publishes the .NET embedding index's statements line for line: the conditional embedding write and its revision probe + (`contract/sql/write-experience-embedding.sql`), and the nearest-neighbour search, with and without the model-authored + exclusion, and its two compatibility probes (`contract/sql/search-experience-vector.sql`, the dimension written in as + `vector({dim})`). It also publishes the application role's grants on `agent_experience.experience_embeddings` + (`contract/sql/application-role-privileges-1.6.sql`, applied after the 1.0 and 1.4 files), and states the text that + is embedded and its content hash, the validation rules, the relevance formula, the text-only fallbacks, and how the two + channels merge before the existing ranking. Four fixture families, generated from .NET with fixed vectors and no + embedding model: `embedding-summary` and `hybrid-merge` (no database), and `embedding-write` and `vector-search` + (PostgreSQL with pgvector). The .NET index and the contract statements run as written must give the same results. A + contract test keeps the .NET privileges call equal to the 1.0, 1.4 and 1.6 files together. No library behaviour or + public API changed. +- **The Python and Go ports still pass contract 1.5**, not 1.6. Their suites accept a 1.6 lock and skip the four vector + families by name until stories 23.2 and 23.3 implement them. ## 0.1.0-preview.14 diff --git a/README.md b/README.md index 56c0005..036172e 100644 --- a/README.md +++ b/README.md @@ -238,10 +238,11 @@ previews fixed is in [Limits history](docs/limits-history.md). **Beyond .NET.** Microsoft Agent Framework also ships in Python and Go. What a port must match to share lessons with this library (the stored record format, the injected block, the task text, the text search, and recording when a -lesson was injected) is published as [a language-neutral contract](contract/README.md), version `1.5` +lesson was injected) is published as [a language-neutral contract](contract/README.md), version `1.6` ([what 1.1 adds](contract/README-1.1.md), [what 1.2 adds](contract/README-1.2.md), [what 1.3 adds](contract/README-1.3.md), [what 1.4 adds](contract/README-1.4.md), -[what 1.5 adds](contract/README-1.5.md)): a JSON Schema, the SQL to run, and +[what 1.5 adds](contract/README-1.5.md), [what 1.6 adds](contract/README-1.6.md): retrieval by meaning, with +embeddings): a JSON Schema, the SQL to run, and fixtures generated from and checked against this implementation on every build. A pure-Python core that passes contract 1.5, with a one-call Python Agent Framework integration, is in [`python/`](python/README.md). A lesson a .NET agent learned is injected into a Python agent, and the other way round. A Go core and PostgreSQL store that pass contract 1.5, with a one-call Go Agent diff --git a/_sdlc/planning-artifacts/epics.md b/_sdlc/planning-artifacts/epics.md index c59b995..b27155a 100644 --- a/_sdlc/planning-artifacts/epics.md +++ b/_sdlc/planning-artifacts/epics.md @@ -2220,6 +2220,8 @@ Add-only: the hybrid text+vector search call, embedding dimension/normalization rules, and fixtures generated by .NET with fixed vectors (no model). Python and Go pass unchanged on everything else. +**Acceptance Criteria:** **Given** the .NET embedding index and retrieval service **When** contract 1.6 is published (add-only) **Then** the conditional embedding write and its revision probe, the nearest-neighbour search (dimension written in as `vector({dim})`, with and without the model-authored exclusion) and its compatibility probes, and the embeddings table's application-role grants (applied after the 1.0 and 1.4 files) are contract SQL files equal to the .NET statements line for line; README-1.6 states the embedded summary and its content hash, validation, relevance `clamp(1 − d/2, 0, 1)`, ties by experience ID, the text-only fallbacks and the channel merge before the existing ranking; .NET-generated `embedding-summary`, `hybrid-merge`, `embedding-write` and `vector-search` fixtures pin them, the PostgreSQL ones agreeing between the .NET index and the contract SQL run as written; and the Python and Go suites accept a 1.6 lock, keep claiming 1.0–1.5, and skip the vector families by name. + ### Story 23.2: Python Semantic Retrieval **Depends on:** 23.1 · **Added:** 2026-10-16 diff --git a/contract/README-1.6.md b/contract/README-1.6.md new file mode 100644 index 0000000..06e8af6 --- /dev/null +++ b/contract/README-1.6.md @@ -0,0 +1,188 @@ +# Contract 1.6: additions + +This page lists what contract 1.6 adds to [contract 1.0](README.md), [contract 1.1](README-1.1.md), +[contract 1.2](README-1.2.md), [contract 1.3](README-1.3.md), [contract 1.4](README-1.4.md) and +[contract 1.5](README-1.5.md). Those pages are still normative and unchanged: a minor version only adds files, this page +among them. The contract's version is the one `lock.json` records (`contractVersion`), which the .NET suite holds in +`ContractFiles.ContractVersion`. + +A port that passes contract 1.6 passes every 1.0 to 1.5 fixture and the fixtures below. + +## Vectors are now in scope + +Contract 1.0 says "a port uses text retrieval only", and the 1.0 and 1.1 privileges pages leave the +`agent_experience.experience_embeddings` table out. That stays true of contract 1.0 to 1.5. From 1.6, a port may also +retrieve by meaning: it stores an embedding of each record's summary, searches the stored embeddings for the +nearest neighbours of the task text's embedding, and merges those candidates with the text search's before the +existing ranking. Everything here is fixed vectors and SQL: no embedding model is part of the contract, and a port +brings its own. + +A port that does not implement 1.6 keeps passing 1.5. A port that implements it and has no embedder registered answers +from text alone, and says so (`NotConfigured`, below). + +## What 1.6 adds + +- **SQL:** [`sql/write-experience-embedding.sql`](sql/write-experience-embedding.sql) (the conditional write and the + revision probe), [`sql/search-experience-vector.sql`](sql/search-experience-vector.sql) (the nearest-neighbour search, + with and without the model-authored exclusion, and the two compatibility probes) and + [`sql/application-role-privileges-1.6.sql`](sql/application-role-privileges-1.6.sql) (the application role's grants + on the embeddings table). +- **Fixtures:** `embedding-summary` and `hybrid-merge` (no database), `embedding-write` and `vector-search` (PostgreSQL + with pgvector). + +Nothing else changes: no schema object or search function is added, and every existing file is byte-identical. The +statements are the .NET embedding index's (`PostgresExperienceEmbeddingIndex`), line for line: a contract test joins +each statement's lines with single spaces and compares it with the .NET text. + +## The database + +The vector channel needs PostgreSQL with the pgvector extension (the `pgvector/pgvector` images) and the vectors +package's migrations, `0004_add_experience_embeddings.sql` and `0020_embeddings_row_level_security.sql`. They are not +copied into the contract: the .NET migrator applies them, as it applies the base schema. + +1. `ExperienceSchemaMigrator.MigrateAsync`, as for 1.0. +2. `CREATE EXTENSION IF NOT EXISTS vector`, as a superuser (pgvector is not a trusted extension), unless an operator + already installed it. +3. `ExperienceVectorSchemaMigrator.MigrateAsync`, as the schema's owner. +4. The application role's privileges, with row-level security on: the 1.0 file, then the 1.4 file, then the 1.6 file, + in that order (the 1.0 file's `REVOKE ALL` removes the others' grants). Together they are exactly what + `ExperienceSchemaMigrator.ApplyApplicationRolePrivilegesAsync` grants on such a schema, and a contract test keeps them + so. Running the .NET call instead is equivalent. + +`experience_embeddings` holds one row per indexed record: its scope (copied from the record), `model_id`, `dimension`, +`content_hash`, `source_revision`, `embedding` (an unconstrained pgvector `vector`) and `created_at`/`updated_at`. A row +is derived data: it never takes part in a lifecycle decision, and an erasure removes it. + +## What is embedded + +**The summary** is the only text of a record that is embedded: its task ID, task summary and lesson (the reflection's +`lesson`; nothing when there is no reflection), in that order. Reference: +[`ExperienceRetrievalSummary`](../src/AgentExperience.Abstractions/ExperienceIndex.cs). + +- A missing field, and one made only of separators, contributes nothing. +- Inside a field, every run of separators becomes one space; fields are joined by one space; the result has no + leading or trailing space. A separator is a UTF-16 code unit of Unicode general category `Cc`, `Zs`, `Zl` or `Zp` + (control characters, among them U+001F and U+0085, and every space and line or paragraph separator). Format + characters (`Cf`, such as U+200B and U+FEFF) are kept. +- The result is capped at 8,192 UTF-16 code units: cut at 8,192; when the last kept unit is a high surrogate, drop it + too; then drop trailing spaces. + +**The content hash** is the lowercase hex SHA-256 of the UTF-8 bytes of the model ID, U+001F, and the summary. A port +stores it with the vector and may skip re-embedding a record whose stored row has the same model ID and content hash. + +**The task text** a port embeds for a search is the one it searches text with (contract 1.0), unchanged. + +## Validation + +Before any statement runs, and without one when it fails (an invalid write or search is refused, not run). These rules +are normative, not fixtured in 1.6: + +- the model ID is non-blank and at most 256 UTF-16 code units; +- a vector has 1 to 16,000 components, each finite (no NaN or infinity); a write's vector has exactly `dimension` + components, and `dimension` is 1 to 16,000; +- the content hash is non-blank, and the source revision is not negative. + +Vectors are stored and compared as given: nothing normalizes them (cosine distance does not depend on length). A +zero-magnitude vector is valid; a search with one gets a NaN distance for every row, and a stored one gets a NaN +distance from every query; either way its relevance is 0. + +`@embedding` and `@query_vector` are pgvector literals: `[x,y,...]`, each component a decimal number. Any text pgvector +parses as the same vector is fine; the .NET index writes each component's shortest round-trip text. + +## The write + +In one transaction that declared the bounds for the record's scope, run `write`; when it wrote no row, run +`probe-revision`; then commit. The parameters are in the file's header. + +| Result | Outcome | +| --- | --- | +| `write` wrote 1 row | `Written`: the row now holds this vector and descriptor (an existing row keeps its `created_at`) | +| `write` wrote 0 rows, `probe-revision` returns a revision | `Stale`, with that revision: the record moved on since the summary was read, and nothing changed | +| `write` wrote 0 rows, `probe-revision` returns no row | `Missing`: an unknown ID, a record in another scope, or an erased record; no row is created | + +A row stored from a newer revision is never replaced by an older one. Embeddings are derived: a port writes one after +the record's own commit has landed, never inside it, and a failed or stale write never fails anything else. + +## The search + +In one transaction that declared the bounds for the reader's scope, run `search` (or +`search-excluding-model-authored`, when model-written lessons are excluded) with every `{dim}` replaced by the query +vector's length. When it returns no row, run the matching compatibility probe in the same transaction. + +- **Parameters** are the text search's where they overlap (scope, `{Validated,Reinforced}`, `0.5`, a limit of 51), plus + `@model_id` and `@query_vector`. Only embeddings of the query's model and dimension are compared; the scope is the + reader's own, as with `p_with_grants => false`. +- **Rows** come back nearest first. The statement has no tie-break (one would stop an index serving the order), so a + port orders rows of equal `distance` by `experience_id` ascending (the lower-case hyphenated text, ordinally); NaN + distances sort after every number (PostgreSQL's order) and equal each other. +- **Relevance** is `clamp(1 - distance / 2, 0, 1)` in double precision, and 0 when `distance` is NaN. `distance` is + pgvector's cosine distance (`<=>`), in [0, 2]. +- **An empty search's outcome** is the probe's. The first column false: `Found`, empty (nothing comparable is stored, + or nothing eligible). The first true and the second false: `ModelMismatch`. Both true: `DimensionMismatch`. A + mismatch returns no candidates. + +## Hybrid retrieval + +With an embedder registered, a retrieval runs both channels concurrently under the call's one timeout. Reference: +[`ExperienceRetrievalService.cs`](../src/AgentExperience.Core/Retrieval/ExperienceRetrievalService.cs). + +**The vector channel never fails the call.** Whatever goes wrong there, the call answers from text alone and reports +which fallback applied: + +| Fallback | When | +| --- | --- | +| `NotConfigured` | no embedder or no embedding store is registered | +| `ProviderUnavailable` | the embedder failed or timed out, reported a blank model ID, or returned a vector that is empty, of a length other than its declared dimension, or not finite; no statement runs | +| `ModelMismatch` | the search's outcome was `ModelMismatch` | +| `DimensionMismatch` | the search's outcome was `DimensionMismatch` | +| `VectorSearchFailed` | the search statement failed, or the store refused or did not answer the search | + +**The text channel's failure fails the call**, exactly as in contract 1.0, whatever the vector channel found. + +**From two channels to one list**, in this order (the `hybrid-merge` fixtures pin steps 1 and 3): + +1. **The floor.** When a minimum vector relevance in [0, 1] is configured, drop every vector candidate whose relevance + is below it (a candidate exactly at it is kept; a NaN relevance is below any floor). Text candidates are never + floored. No floor is the default. +2. **Each channel's checks and truncation**, as contract 1.0 states them for the text rows: over each channel's first + 50 candidates, a candidate outside the requested scope or a repeated `experience_id` fails the whole result + (normative, not fixtured in 1.6); keep + the first 50 of each, and report the result truncated when either channel returned 51. +3. **The merge.** The text candidates in their order, then the vector candidates in theirs. A record already in the + list is not added again: it keeps its position and its first channel's record (the text channel's), and takes the + vector relevance only when that is higher, comparing relevances clamped to [0, 1] with NaN as 0. +4. **Ranking**, exactly as contract 1.0 states it, with each merged candidate's relevance as the `Relevance` input. + Being found by both channels earns nothing more. + +## The fixtures + +``` +fixtures/embedding-summary/cases.json taskId + taskSummary + lesson + modelId -> summary + contentHash +fixtures/hybrid-merge/cases.json minimumVectorRelevance + text + vector -> merged +fixtures/embedding-write// seed.json + steps.json -> expected.json (needs PostgreSQL with pgvector) +fixtures/vector-search// seed.json + steps.json -> expected.json (needs PostgreSQL with pgvector) +``` + +**embedding-summary:** for each case, the summary of `taskId`, `taskSummary` and `lesson` (each may be `null`) is +`summary`, exactly, and the content hash under `modelId` is `contentHash`. + +**hybrid-merge:** for each case, flooring `vector` at `minimumVectorRelevance` (`null`: no floor) and merging it with +`text` gives `merged`, in order: each entry's `experienceId`, the `relevance` kept, and `from`, the channel whose +record was kept (`text` or `vector`). Relevances compare exactly; a NaN relevance is written as the string `"NaN"`. + +**embedding-write and vector-search**, against a database set up as above: + +1. Insert `seed.json`'s records exactly as a search seed is inserted (the eighteen record columns, as the application + role, under bounds declared for each record's scope). +2. Run `steps.json`'s `steps` in order: + - `write`: the port's write, with `experienceId`, `scope`, `modelId`, `dimension`, `contentHash`, `sourceRevision`, + `vector` (32-bit floats) and `now` (the write's clock, for `@now`). Its entry in `expected.json` is + `{"outcome": "Written"}`, `{"outcome": "Missing"}` or `{"outcome": "Stale", "currentRevision": n}`. + - `search`: the port's search, with `scope`, `modelId`, `vector`, `statuses`, `minConfidence`, `limit` and + `excludeModelAuthored`. Its entry is the `outcome` and the `rows` in order, each with `experienceId`, `distance` + (`null` for NaN) and `relevance`. Compare distances and relevances to 1e-9. + - `setRevision`: as the schema's owner, `UPDATE agent_experience.experience_records SET revision = WHERE + experience_id = `, which stands for a lifecycle commit moving the record on. Entry `null`. + - `erase`: exactly as in the record-injection family ([README-1.4](README-1.4.md)). Entry `null`. +3. Compare with `expected.json`: `steps`, one entry per step, and `experience_embeddings`, every row of the table + afterwards, read as the owner, ordered by `experience_id`, each column cast to `text` in the time zone UTC. diff --git a/contract/fixtures/embedding-summary/cases.json b/contract/fixtures/embedding-summary/cases.json new file mode 100644 index 0000000..48436e0 --- /dev/null +++ b/contract/fixtures/embedding-summary/cases.json @@ -0,0 +1,92 @@ +[ + { + "description": "The task ID, the task summary and the lesson, joined by single spaces.", + "taskId": "refund-stuck-pending", + "taskSummary": "Refund stuck in pending", + "lesson": "Release the lock before retrying.", + "modelId": "contract-embedding", + "summary": "refund-stuck-pending Refund stuck in pending Release the lock before retrying.", + "contentHash": "afa9c556dfd618e56e01f639b81c2e2fb1908861aa1e2e1d1ab9d80aa6605aee" + }, + { + "description": "Every run of whitespace or control characters inside a field (tab, CR, LF, NBSP, U+2028, NUL, U+0085, U+001F, DEL, U+009F) is one space; the result is trimmed.", + "taskId": "deploy\tservice", + "taskSummary": " Line one\r\n\n line two\u00A0end\u2028x ", + "lesson": "a\u0000b\u0085c\u001Fd\u007Fe\u009Ff\n", + "modelId": "contract-embedding", + "summary": "deploy service Line one line two end x a b c d e f", + "contentHash": "3f206928686c00a49459a761f82b64cd74c4dae5368bd15c67e5fe5aa00c031a" + }, + { + "description": "A missing or blank field contributes nothing, and no separator.", + "taskId": "rotate-keys", + "taskSummary": null, + "lesson": " \t\r\n ", + "modelId": "contract-embedding", + "summary": "rotate-keys", + "contentHash": "baf6bd00cdb8910d92632bf1053056b981f12bc182a2e2d0385b5e527f464d24" + }, + { + "description": "A missing summary and lesson and an empty task ID: the summary is empty, and is still hashed.", + "taskId": "", + "taskSummary": null, + "lesson": null, + "modelId": "contract-embedding", + "summary": "", + "contentHash": "01bf08e4d2d040e718447d84842dad1e6d28e37ca2d66aac406cfc2ed7654dc9" + }, + { + "description": "Format characters (U+200B, U+FEFF) are not whitespace and are kept; a character outside the BMP is kept whole.", + "taskId": "zero\u200Bwidth", + "taskSummary": "bom\uFEFFinside", + "lesson": "emoji \uD83D\uDE00 kept", + "modelId": "contract-embedding", + "summary": "zero\u200Bwidth bom\uFEFFinside emoji \uD83D\uDE00 kept", + "contentHash": "72278dc9ae06fbd0b0a3a078a892318b219175b7f52558c9d539edf21abd6cfa" + }, + { + "description": "The model ID is inside the hash, as UTF-8: the same summary under another model hashes differently.", + "taskId": "refund-stuck-pending", + "taskSummary": "Refund stuck in pending", + "lesson": "Release the lock before retrying.", + "modelId": "modèle-ü/2", + "summary": "refund-stuck-pending Refund stuck in pending Release the lock before retrying.", + "contentHash": "20ecb6cc96571715342ff243ab282f140d1fac5d7728a70af939f7795ae31258" + }, + { + "description": "Longer than 8,192 UTF-16 code units: cut at 8,192.", + "taskId": "t", + "taskSummary": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "lesson": "never reached", + "modelId": "contract-embedding", + "summary": "t aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "contentHash": "cb32aea8653803b32b110bb341402496291eaba61a98d4fa26fbd0b9be68598c" + }, + { + "description": "Cut where the last kept unit is a high surrogate: that unit goes too, so no character is split (8,191 units).", + "taskId": "t", + "taskSummary": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\uD83D\uDE00tail", + "lesson": null, + "modelId": "contract-embedding", + "summary": "t aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "contentHash": "a68fef1be239b3a04a34977532b55eb9d71abe2caa6f6be3b1159c5d6abac3bf" + }, + { + "description": "Cut right after a space: the trailing space is dropped (8,191 units).", + "taskId": "t", + "taskSummary": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbb", + "lesson": null, + "modelId": "contract-embedding", + "summary": "t aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "contentHash": "a68fef1be239b3a04a34977532b55eb9d71abe2caa6f6be3b1159c5d6abac3bf" + }, + { + "description": "Cut inside a surrogate pair that follows a space: the high surrogate, then the space, are dropped (8,190 units).", + "taskId": "t", + "taskSummary": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \uD83D\uDE00", + "lesson": null, + "modelId": "contract-embedding", + "summary": "t aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "contentHash": "c10f33b29778933a4109f0d2188fcc16787f1640effac8e4b35ef3c6f2e1d001" + } +] diff --git a/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/expected.json b/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/expected.json new file mode 100644 index 0000000..eaecf39 --- /dev/null +++ b/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/expected.json @@ -0,0 +1,18 @@ +{ + "steps": [ + { + "outcome": "Missing" + }, + { + "outcome": "Missing" + }, + { + "outcome": "Written" + }, + null, + { + "outcome": "Missing" + } + ], + "experience_embeddings": [] +} diff --git a/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/seed.json b/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/seed.json new file mode 100644 index 0000000..4c91f38 --- /dev/null +++ b/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/seed.json @@ -0,0 +1,160 @@ +[ + { + "columns": { + "experience_id": "00000072-0000-4000-8000-000000000001", + "source_run_id": "00000072-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000072-0000-4000-8000-000000000e01", + "verificationRoundId": "00000072-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000072-0000-4000-8000-000000000201", + "experienceRunId": "00000072-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000072-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000072-0000-4000-8000-000000000002", + "source_run_id": "00000072-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000072-0000-4000-8000-000000000e02", + "verificationRoundId": "00000072-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000072-0000-4000-8000-000000000202", + "experienceRunId": "00000072-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000072-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/steps.json b/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/steps.json new file mode 100644 index 0000000..b30f86f --- /dev/null +++ b/contract/fixtures/embedding-write/missing-unknown-other-scope-or-erased/steps.json @@ -0,0 +1,115 @@ +{ + "description": "Missing, and no row: a write for an unknown ID, for a record named in another scope (its own scope, one agent away), and for an erased record (whose row the erasure removed).", + "steps": [ + { + "write": { + "experienceId": "00000072-0000-4000-8000-000000000009", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000072-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "other-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000072-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "erase": { + "experienceId": "00000072-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "deletedAt": "2026-03-02T10:01:00+00:00" + } + }, + { + "write": { + "experienceId": "00000072-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:02:00+00:00" + } + } + ] +} diff --git a/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/expected.json b/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/expected.json new file mode 100644 index 0000000..3ca14f1 --- /dev/null +++ b/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/expected.json @@ -0,0 +1,40 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + null, + { + "outcome": "Stale", + "currentRevision": 2 + }, + { + "outcome": "Written" + }, + { + "outcome": "Stale", + "currentRevision": 2 + }, + { + "outcome": "Written" + } + ], + "experience_embeddings": [ + { + "experience_id": "00000071-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding-v2", + "dimension": "5", + "content_hash": "315a04c8783712bdba16399da796a251d5ea5fdc8db781d6c62cc151122d2395", + "source_revision": "2", + "embedding": "[1,2,3,4,5]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:04:00+00" + } + ] +} diff --git a/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/seed.json b/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/seed.json new file mode 100644 index 0000000..9c4444e --- /dev/null +++ b/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/seed.json @@ -0,0 +1,81 @@ +[ + { + "columns": { + "experience_id": "00000071-0000-4000-8000-000000000001", + "source_run_id": "00000071-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000071-0000-4000-8000-000000000e01", + "verificationRoundId": "00000071-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000071-0000-4000-8000-000000000201", + "experienceRunId": "00000071-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000071-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/steps.json b/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/steps.json new file mode 100644 index 0000000..98b53c1 --- /dev/null +++ b/contract/fixtures/embedding-write/revisions-newer-replaces-older-is-stale/steps.json @@ -0,0 +1,132 @@ +{ + "description": "A record moves from revision 1 to 2. A write computed at revision 1 is then Stale, with the current revision, and leaves the stored row as it was; a write at revision 2 replaces it (created_at kept, updated_at the new clock), including with another model and dimension.", + "steps": [ + { + "write": { + "experienceId": "00000071-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "setRevision": { + "experienceId": "00000071-0000-4000-8000-000000000001", + "revision": 2 + } + }, + { + "write": { + "experienceId": "00000071-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:01:00+00:00" + } + }, + { + "write": { + "experienceId": "00000071-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 2, + "vector": [ + 0, + 0, + 1, + 0 + ], + "now": "2026-03-02T10:02:00+00:00" + } + }, + { + "write": { + "experienceId": "00000071-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 0, + 0, + 1 + ], + "now": "2026-03-02T10:03:00+00:00" + } + }, + { + "write": { + "experienceId": "00000071-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding-v2", + "dimension": 5, + "contentHash": "315a04c8783712bdba16399da796a251d5ea5fdc8db781d6c62cc151122d2395", + "sourceRevision": 2, + "vector": [ + 1, + 2, + 3, + 4, + 5 + ], + "now": "2026-03-02T10:04:00+00:00" + } + } + ] +} diff --git a/contract/fixtures/embedding-write/written-with-the-record-scope/expected.json b/contract/fixtures/embedding-write/written-with-the-record-scope/expected.json new file mode 100644 index 0000000..6b59b75 --- /dev/null +++ b/contract/fixtures/embedding-write/written-with-the-record-scope/expected.json @@ -0,0 +1,44 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + } + ], + "experience_embeddings": [ + { + "experience_id": "00000070-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000070-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": "payments", + "agent_id": null, + "user_id": "user-7", + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[0,1,0,0]", + "created_at": "2026-03-02 10:01:00+00", + "updated_at": "2026-03-02 10:01:00+00" + } + ] +} diff --git a/contract/fixtures/embedding-write/written-with-the-record-scope/seed.json b/contract/fixtures/embedding-write/written-with-the-record-scope/seed.json new file mode 100644 index 0000000..41873bd --- /dev/null +++ b/contract/fixtures/embedding-write/written-with-the-record-scope/seed.json @@ -0,0 +1,160 @@ +[ + { + "columns": { + "experience_id": "00000070-0000-4000-8000-000000000001", + "source_run_id": "00000070-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000070-0000-4000-8000-000000000e01", + "verificationRoundId": "00000070-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000070-0000-4000-8000-000000000201", + "experienceRunId": "00000070-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000070-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000070-0000-4000-8000-000000000002", + "source_run_id": "00000070-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": "payments", + "agent_id": null, + "user_id": "user-7", + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000070-0000-4000-8000-000000000e02", + "verificationRoundId": "00000070-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000070-0000-4000-8000-000000000202", + "experienceRunId": "00000070-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000070-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/embedding-write/written-with-the-record-scope/steps.json b/contract/fixtures/embedding-write/written-with-the-record-scope/steps.json new file mode 100644 index 0000000..e0c6424 --- /dev/null +++ b/contract/fixtures/embedding-write/written-with-the-record-scope/steps.json @@ -0,0 +1,53 @@ +{ + "description": "A write for a live record at its revision stores one row: the scope columns are the record's (a scope with a team and a user, and one with an agent only), the descriptor and the vector are the write's, and created_at and updated_at are the write's clock.", + "steps": [ + { + "write": { + "experienceId": "00000070-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000070-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": "payments", + "agentId": null, + "userId": "user-7" + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:01:00+00:00" + } + } + ] +} diff --git a/contract/fixtures/hybrid-merge/cases.json b/contract/fixtures/hybrid-merge/cases.json new file mode 100644 index 0000000..c81d5b2 --- /dev/null +++ b/contract/fixtures/hybrid-merge/cases.json @@ -0,0 +1,345 @@ +[ + { + "description": "Disjoint channels: the text candidates in their order, then the vector candidates in theirs.", + "minimumVectorRelevance": null, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.6 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.4 + } + ], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000004", + "relevance": 0.9 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.7 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.6, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.4, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000004", + "relevance": 0.9, + "from": "vector" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.7, + "from": "vector" + } + ] + }, + { + "description": "A record in both channels is one entry, at its text position, with the higher relevance (here the vector's) and the text channel's snapshot.", + "minimumVectorRelevance": null, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.3 + } + ], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.8 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.6 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.8, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.6, + "from": "vector" + } + ] + }, + { + "description": "The text relevance is the higher: it is kept, with the text channel's snapshot.", + "minimumVectorRelevance": null, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.9 + } + ], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.9, + "from": "text" + } + ] + }, + { + "description": "Equal relevances: the text channel's entry is kept unchanged.", + "minimumVectorRelevance": null, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5 + } + ], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5, + "from": "text" + } + ] + }, + { + "description": "A floor drops vector candidates below it before the merge (a candidate exactly at it is kept); text candidates are never floored, so a record whose vector relevance is below the floor keeps its text relevance.", + "minimumVectorRelevance": 0.75, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.2 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.6 + } + ], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.9 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.8 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000004", + "relevance": 0.7 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.74 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000005", + "relevance": 0.75 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.2, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.9, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.8, + "from": "vector" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000005", + "relevance": 0.75, + "from": "vector" + } + ] + }, + { + "description": "No text candidates: the vector candidates, in their order.", + "minimumVectorRelevance": null, + "text": [], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.9 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.4 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.9, + "from": "vector" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.4, + "from": "vector" + } + ] + }, + { + "description": "No vector candidates (the vector channel fell back, or matched nothing): the text candidates.", + "minimumVectorRelevance": null, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.4 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.3 + } + ], + "vector": [], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.4, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.3, + "from": "text" + } + ] + }, + { + "description": "Relevances are compared clamped to [0, 1], NaN as 0, but the raw value is kept: a NaN text relevance takes the vector's 0.5; a text 1.2 beats a vector 0.9 and stays 1.2; a vector 1.5 beats a text 0.9 and is kept as 1.5; with no floor a NaN vector candidate is kept.", + "minimumVectorRelevance": null, + "text": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": "NaN" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 1.2 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.4 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000005", + "relevance": 0.9 + } + ], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0.9 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000004", + "relevance": "NaN" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000005", + "relevance": 1.5 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": 0.5, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 1.2, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.4, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000005", + "relevance": 1.5, + "from": "text" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000004", + "relevance": "NaN", + "from": "vector" + } + ] + }, + { + "description": "A floor of 0 keeps every vector candidate with a number for a relevance, 0 included, and drops a NaN one.", + "minimumVectorRelevance": 0, + "text": [], + "vector": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000001", + "relevance": "NaN" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0 + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.3 + } + ], + "merged": [ + { + "experienceId": "0000006a-0000-4000-8000-000000000002", + "relevance": 0, + "from": "vector" + }, + { + "experienceId": "0000006a-0000-4000-8000-000000000003", + "relevance": 0.3, + "from": "vector" + } + ] + } +] diff --git a/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/expected.json b/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/expected.json new file mode 100644 index 0000000..253f2df --- /dev/null +++ b/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/expected.json @@ -0,0 +1,83 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000074-0000-4000-8000-000000000001", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "00000074-0000-4000-8000-000000000002", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "00000074-0000-4000-8000-000000000003", + "distance": 1, + "relevance": 0.5 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000074-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000074-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[2,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000074-0000-4000-8000-000000000003", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[0,1,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/seed.json b/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/seed.json new file mode 100644 index 0000000..29e747f --- /dev/null +++ b/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/seed.json @@ -0,0 +1,239 @@ +[ + { + "columns": { + "experience_id": "00000074-0000-4000-8000-000000000001", + "source_run_id": "00000074-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000074-0000-4000-8000-000000000e01", + "verificationRoundId": "00000074-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000074-0000-4000-8000-000000000201", + "experienceRunId": "00000074-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000074-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000074-0000-4000-8000-000000000002", + "source_run_id": "00000074-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000074-0000-4000-8000-000000000e02", + "verificationRoundId": "00000074-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000074-0000-4000-8000-000000000202", + "experienceRunId": "00000074-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000074-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000074-0000-4000-8000-000000000003", + "source_run_id": "00000074-0000-4000-8000-000000000103", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000074-0000-4000-8000-000000000e03", + "verificationRoundId": "00000074-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000074-0000-4000-8000-000000000203", + "experienceRunId": "00000074-0000-4000-8000-000000000103", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000074-0000-4000-8000-000000000e03" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/steps.json b/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/steps.json new file mode 100644 index 0000000..329afe5 --- /dev/null +++ b/contract/fixtures/vector-search/an-exact-tie-is-ordered-by-id/steps.json @@ -0,0 +1,103 @@ +{ + "description": "Two records at exactly the same distance ([2,0,0,0] and [1,0,0,0] point the same way), written in descending ID order: they are ordered by experience ID ascending.", + "steps": [ + { + "write": { + "experienceId": "00000074-0000-4000-8000-000000000003", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000074-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 2, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000074-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/fixtures/vector-search/another-model-or-dimension/expected.json b/contract/fixtures/vector-search/another-model-or-dimension/expected.json new file mode 100644 index 0000000..c64b5ab --- /dev/null +++ b/contract/fixtures/vector-search/another-model-or-dimension/expected.json @@ -0,0 +1,67 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "ModelMismatch", + "rows": [] + }, + { + "outcome": "DimensionMismatch", + "rows": [] + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000078-0000-4000-8000-000000000001", + "distance": 0.29289321881345254, + "relevance": 0.8535533905932737 + }, + { + "experienceId": "00000078-0000-4000-8000-000000000002", + "distance": 0.5, + "relevance": 0.75 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000078-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "5", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000078-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "5", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[0,1,0,0,1]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/another-model-or-dimension/seed.json b/contract/fixtures/vector-search/another-model-or-dimension/seed.json new file mode 100644 index 0000000..5319719 --- /dev/null +++ b/contract/fixtures/vector-search/another-model-or-dimension/seed.json @@ -0,0 +1,160 @@ +[ + { + "columns": { + "experience_id": "00000078-0000-4000-8000-000000000001", + "source_run_id": "00000078-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000078-0000-4000-8000-000000000e01", + "verificationRoundId": "00000078-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000078-0000-4000-8000-000000000201", + "experienceRunId": "00000078-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000078-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000078-0000-4000-8000-000000000002", + "source_run_id": "00000078-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000078-0000-4000-8000-000000000e02", + "verificationRoundId": "00000078-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000078-0000-4000-8000-000000000202", + "experienceRunId": "00000078-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000078-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/another-model-or-dimension/steps.json b/contract/fixtures/vector-search/another-model-or-dimension/steps.json new file mode 100644 index 0000000..c55e407 --- /dev/null +++ b/contract/fixtures/vector-search/another-model-or-dimension/steps.json @@ -0,0 +1,134 @@ +{ + "description": "Embeddings stored under the model at dimension 5 only (the one dimension-5 case). A dimension-4 query under another model is ModelMismatch; under this model it is DimensionMismatch; a dimension-5 query under this model finds them. No rows come back with a mismatch.", + "steps": [ + { + "write": { + "experienceId": "00000078-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 5, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000078-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 5, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0, + 1 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "another-model", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0, + 1 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/fixtures/vector-search/model-authored-excluded/expected.json b/contract/fixtures/vector-search/model-authored-excluded/expected.json new file mode 100644 index 0000000..a4ff38d --- /dev/null +++ b/contract/fixtures/vector-search/model-authored-excluded/expected.json @@ -0,0 +1,69 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000076-0000-4000-8000-000000000002", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "00000076-0000-4000-8000-000000000001", + "distance": 0.10557280900008414, + "relevance": 0.9472135954999579 + } + ] + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000076-0000-4000-8000-000000000001", + "distance": 0.10557280900008414, + "relevance": 0.9472135954999579 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000076-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0.5,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000076-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "6aa867511ff2a5cdb06b6f30489ca811d35636dda1e16e55961afa5d649fe898", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/model-authored-excluded/seed.json b/contract/fixtures/vector-search/model-authored-excluded/seed.json new file mode 100644 index 0000000..dc61024 --- /dev/null +++ b/contract/fixtures/vector-search/model-authored-excluded/seed.json @@ -0,0 +1,161 @@ +[ + { + "columns": { + "experience_id": "00000076-0000-4000-8000-000000000001", + "source_run_id": "00000076-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000076-0000-4000-8000-000000000e01", + "verificationRoundId": "00000076-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000076-0000-4000-8000-000000000201", + "experienceRunId": "00000076-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000076-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000076-0000-4000-8000-000000000002", + "source_run_id": "00000076-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000076-0000-4000-8000-000000000e02", + "verificationRoundId": "00000076-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000076-0000-4000-8000-000000000202", + "experienceRunId": "00000076-0000-4000-8000-000000000102", + "lesson": "Use a staged rollout.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000076-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-model-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00", + "authorship": "Model" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/model-authored-excluded/steps.json b/contract/fixtures/vector-search/model-authored-excluded/steps.json new file mode 100644 index 0000000..8fb1781 --- /dev/null +++ b/contract/fixtures/vector-search/model-authored-excluded/steps.json @@ -0,0 +1,105 @@ +{ + "description": "A model-authored record is returned by the search, and left out by the search that excludes model-written lessons (before the limit), as the text search leaves it out.", + "steps": [ + { + "write": { + "experienceId": "00000076-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0.5, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000076-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "6aa867511ff2a5cdb06b6f30489ca811d35636dda1e16e55961afa5d649fe898", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 1, + "excludeModelAuthored": true + } + } + ] +} diff --git a/contract/fixtures/vector-search/nearest-first/expected.json b/contract/fixtures/vector-search/nearest-first/expected.json new file mode 100644 index 0000000..8b42ef2 --- /dev/null +++ b/contract/fixtures/vector-search/nearest-first/expected.json @@ -0,0 +1,98 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000073-0000-4000-8000-000000000003", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "00000073-0000-4000-8000-000000000002", + "distance": 0.004962805092280975, + "relevance": 0.9975185974538595 + }, + { + "experienceId": "00000073-0000-4000-8000-000000000001", + "distance": 0.29289321881345254, + "relevance": 0.8535533905932737 + } + ] + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000073-0000-4000-8000-000000000003", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "00000073-0000-4000-8000-000000000002", + "distance": 0.004962805092280975, + "relevance": 0.9975185974538595 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000073-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,1,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000073-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0.1,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000073-0000-4000-8000-000000000003", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/nearest-first/seed.json b/contract/fixtures/vector-search/nearest-first/seed.json new file mode 100644 index 0000000..7469e9f --- /dev/null +++ b/contract/fixtures/vector-search/nearest-first/seed.json @@ -0,0 +1,239 @@ +[ + { + "columns": { + "experience_id": "00000073-0000-4000-8000-000000000001", + "source_run_id": "00000073-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000073-0000-4000-8000-000000000e01", + "verificationRoundId": "00000073-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000073-0000-4000-8000-000000000201", + "experienceRunId": "00000073-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000073-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000073-0000-4000-8000-000000000002", + "source_run_id": "00000073-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000073-0000-4000-8000-000000000e02", + "verificationRoundId": "00000073-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000073-0000-4000-8000-000000000202", + "experienceRunId": "00000073-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000073-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000073-0000-4000-8000-000000000003", + "source_run_id": "00000073-0000-4000-8000-000000000103", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000073-0000-4000-8000-000000000e03", + "verificationRoundId": "00000073-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000073-0000-4000-8000-000000000203", + "experienceRunId": "00000073-0000-4000-8000-000000000103", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000073-0000-4000-8000-000000000e03" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/nearest-first/steps.json b/contract/fixtures/vector-search/nearest-first/steps.json new file mode 100644 index 0000000..7beb720 --- /dev/null +++ b/contract/fixtures/vector-search/nearest-first/steps.json @@ -0,0 +1,129 @@ +{ + "description": "Query [1,0,0,0] over [1,1,0,0], [1,0.1,0,0] and [1,0,0,0]: nearest first, whatever the ID order, each with its cosine distance and relevance = 1 - distance / 2. A limit of 2 keeps the two nearest.", + "steps": [ + { + "write": { + "experienceId": "00000073-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000073-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0.1, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000073-0000-4000-8000-000000000003", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 2, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/fixtures/vector-search/nothing-comparable-is-found-empty/expected.json b/contract/fixtures/vector-search/nothing-comparable-is-found-empty/expected.json new file mode 100644 index 0000000..057deb6 --- /dev/null +++ b/contract/fixtures/vector-search/nothing-comparable-is-found-empty/expected.json @@ -0,0 +1,33 @@ +{ + "steps": [ + { + "outcome": "Found", + "rows": [] + }, + { + "outcome": "Written" + }, + { + "outcome": "Found", + "rows": [] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000077-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "97d21f6f3272b82d750452dde9ed88913dad12bff056879ae3f334d25a11d6bc", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/nothing-comparable-is-found-empty/seed.json b/contract/fixtures/vector-search/nothing-comparable-is-found-empty/seed.json new file mode 100644 index 0000000..f17321f --- /dev/null +++ b/contract/fixtures/vector-search/nothing-comparable-is-found-empty/seed.json @@ -0,0 +1,141 @@ +[ + { + "columns": { + "experience_id": "00000077-0000-4000-8000-000000000001", + "source_run_id": "00000077-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000077-0000-4000-8000-000000000e01", + "verificationRoundId": "00000077-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000077-0000-4000-8000-000000000201", + "experienceRunId": "00000077-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000077-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000077-0000-4000-8000-000000000002", + "source_run_id": "00000077-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Quarantined", + "reuse_confidence": 0, + "supporting_validations": 0, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Failed", + "evidence": [ + { + "evidenceId": "00000077-0000-4000-8000-000000000e02", + "verificationRoundId": "00000077-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 0, + "reflection": null, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/nothing-comparable-is-found-empty/steps.json b/contract/fixtures/vector-search/nothing-comparable-is-found-empty/steps.json new file mode 100644 index 0000000..37187f8 --- /dev/null +++ b/contract/fixtures/vector-search/nothing-comparable-is-found-empty/steps.json @@ -0,0 +1,81 @@ +{ + "description": "Found and empty: no embedding is stored in the scope; and later the only one stored belongs to a record the search leaves out (Quarantined), which the compatibility probe leaves out too.", + "steps": [ + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + }, + { + "write": { + "experienceId": "00000077-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "97d21f6f3272b82d750452dde9ed88913dad12bff056879ae3f334d25a11d6bc", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/fixtures/vector-search/scope-status-confidence-and-erasure/expected.json b/contract/fixtures/vector-search/scope-status-confidence-and-erasure/expected.json new file mode 100644 index 0000000..1ed14db --- /dev/null +++ b/contract/fixtures/vector-search/scope-status-confidence-and-erasure/expected.json @@ -0,0 +1,158 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + null, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000075-0000-4000-8000-000000000001", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "00000075-0000-4000-8000-000000000008", + "distance": 0, + "relevance": 1 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000075-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000075-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "other-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000075-0000-4000-8000-000000000003", + "tenant_id": "other-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000075-0000-4000-8000-000000000004", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "97d21f6f3272b82d750452dde9ed88913dad12bff056879ae3f334d25a11d6bc", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000075-0000-4000-8000-000000000005", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000075-0000-4000-8000-000000000006", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000075-0000-4000-8000-000000000008", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "7b78c09d7fd8c00365dfc0e42a997811ed3a42bd8d5fbae3737c1ebd9a4b0311", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/scope-status-confidence-and-erasure/seed.json b/contract/fixtures/vector-search/scope-status-confidence-and-erasure/seed.json new file mode 100644 index 0000000..c83c632 --- /dev/null +++ b/contract/fixtures/vector-search/scope-status-confidence-and-erasure/seed.json @@ -0,0 +1,615 @@ +[ + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000001", + "source_run_id": "00000075-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e01", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000201", + "experienceRunId": "00000075-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000002", + "source_run_id": "00000075-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "other-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e02", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000202", + "experienceRunId": "00000075-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000003", + "source_run_id": "00000075-0000-4000-8000-000000000103", + "tenant_id": "other-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e03", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000203", + "experienceRunId": "00000075-0000-4000-8000-000000000103", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e03" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000004", + "source_run_id": "00000075-0000-4000-8000-000000000104", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Quarantined", + "reuse_confidence": 0, + "supporting_validations": 0, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Failed", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e04", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 0, + "reflection": null, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000005", + "source_run_id": "00000075-0000-4000-8000-000000000105", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Candidate", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e05", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000205", + "experienceRunId": "00000075-0000-4000-8000-000000000105", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e05" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000006", + "source_run_id": "00000075-0000-4000-8000-000000000106", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.4, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e06", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000206", + "experienceRunId": "00000075-0000-4000-8000-000000000106", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e06" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000007", + "source_run_id": "00000075-0000-4000-8000-000000000107", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e07", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000207", + "experienceRunId": "00000075-0000-4000-8000-000000000107", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e07" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000075-0000-4000-8000-000000000008", + "source_run_id": "00000075-0000-4000-8000-000000000108", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Reinforced", + "reuse_confidence": 0.9, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000075-0000-4000-8000-000000000e08", + "verificationRoundId": "00000075-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000075-0000-4000-8000-000000000208", + "experienceRunId": "00000075-0000-4000-8000-000000000108", + "lesson": "Verified after 2 attempts.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000075-0000-4000-8000-000000000e08" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/scope-status-confidence-and-erasure/steps.json b/contract/fixtures/vector-search/scope-status-confidence-and-erasure/steps.json new file mode 100644 index 0000000..92ab4da --- /dev/null +++ b/contract/fixtures/vector-search/scope-status-confidence-and-erasure/steps.json @@ -0,0 +1,237 @@ +{ + "description": "Every record has the query's own vector. Only the reader's live, eligible records at or above the confidence floor come back: not another agent's or tenant's (each written in its own scope), not a Quarantined or Candidate record, not one under 0.5 confidence, and not an erased one.", + "steps": [ + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "other-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000003", + "scope": { + "tenantId": "other-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000004", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "97d21f6f3272b82d750452dde9ed88913dad12bff056879ae3f334d25a11d6bc", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000005", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000006", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000007", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000075-0000-4000-8000-000000000008", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "7b78c09d7fd8c00365dfc0e42a997811ed3a42bd8d5fbae3737c1ebd9a4b0311", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "erase": { + "experienceId": "00000075-0000-4000-8000-000000000007", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "deletedAt": "2026-03-02T10:01:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/fixtures/vector-search/zero-magnitude-query/expected.json b/contract/fixtures/vector-search/zero-magnitude-query/expected.json new file mode 100644 index 0000000..9149ee6 --- /dev/null +++ b/contract/fixtures/vector-search/zero-magnitude-query/expected.json @@ -0,0 +1,59 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "00000079-0000-4000-8000-000000000001", + "distance": null, + "relevance": 0 + }, + { + "experienceId": "00000079-0000-4000-8000-000000000002", + "distance": null, + "relevance": 0 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "00000079-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[0,1,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "00000079-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/zero-magnitude-query/seed.json b/contract/fixtures/vector-search/zero-magnitude-query/seed.json new file mode 100644 index 0000000..a48f1de --- /dev/null +++ b/contract/fixtures/vector-search/zero-magnitude-query/seed.json @@ -0,0 +1,160 @@ +[ + { + "columns": { + "experience_id": "00000079-0000-4000-8000-000000000001", + "source_run_id": "00000079-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000079-0000-4000-8000-000000000e01", + "verificationRoundId": "00000079-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000079-0000-4000-8000-000000000201", + "experienceRunId": "00000079-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000079-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "00000079-0000-4000-8000-000000000002", + "source_run_id": "00000079-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "00000079-0000-4000-8000-000000000e02", + "verificationRoundId": "00000079-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "00000079-0000-4000-8000-000000000202", + "experienceRunId": "00000079-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "00000079-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/zero-magnitude-query/steps.json b/contract/fixtures/vector-search/zero-magnitude-query/steps.json new file mode 100644 index 0000000..77ac9f2 --- /dev/null +++ b/contract/fixtures/vector-search/zero-magnitude-query/steps.json @@ -0,0 +1,79 @@ +{ + "description": "A zero-magnitude query vector is valid, but has no direction: every distance is NaN (written null) and every relevance 0, and the rows are in experience ID order.", + "steps": [ + { + "write": { + "experienceId": "00000079-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "00000079-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 0, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/fixtures/vector-search/zero-magnitude-stored-vector/expected.json b/contract/fixtures/vector-search/zero-magnitude-stored-vector/expected.json new file mode 100644 index 0000000..f9ac65c --- /dev/null +++ b/contract/fixtures/vector-search/zero-magnitude-stored-vector/expected.json @@ -0,0 +1,83 @@ +{ + "steps": [ + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Written" + }, + { + "outcome": "Found", + "rows": [ + { + "experienceId": "0000007b-0000-4000-8000-000000000002", + "distance": 0, + "relevance": 1 + }, + { + "experienceId": "0000007b-0000-4000-8000-000000000003", + "distance": 1, + "relevance": 0.5 + }, + { + "experienceId": "0000007b-0000-4000-8000-000000000001", + "distance": null, + "relevance": 0 + } + ] + } + ], + "experience_embeddings": [ + { + "experience_id": "0000007b-0000-4000-8000-000000000001", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[0,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "0000007b-0000-4000-8000-000000000002", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[1,0,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + }, + { + "experience_id": "0000007b-0000-4000-8000-000000000003", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "model_id": "contract-embedding", + "dimension": "4", + "content_hash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "source_revision": "1", + "embedding": "[0,1,0,0]", + "created_at": "2026-03-02 10:00:00+00", + "updated_at": "2026-03-02 10:00:00+00" + } + ] +} diff --git a/contract/fixtures/vector-search/zero-magnitude-stored-vector/seed.json b/contract/fixtures/vector-search/zero-magnitude-stored-vector/seed.json new file mode 100644 index 0000000..185c400 --- /dev/null +++ b/contract/fixtures/vector-search/zero-magnitude-stored-vector/seed.json @@ -0,0 +1,239 @@ +[ + { + "columns": { + "experience_id": "0000007b-0000-4000-8000-000000000001", + "source_run_id": "0000007b-0000-4000-8000-000000000101", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "0000007b-0000-4000-8000-000000000e01", + "verificationRoundId": "0000007b-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "0000007b-0000-4000-8000-000000000201", + "experienceRunId": "0000007b-0000-4000-8000-000000000101", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "0000007b-0000-4000-8000-000000000e01" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "0000007b-0000-4000-8000-000000000002", + "source_run_id": "0000007b-0000-4000-8000-000000000102", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "0000007b-0000-4000-8000-000000000e02", + "verificationRoundId": "0000007b-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "0000007b-0000-4000-8000-000000000202", + "experienceRunId": "0000007b-0000-4000-8000-000000000102", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "0000007b-0000-4000-8000-000000000e02" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + }, + { + "columns": { + "experience_id": "0000007b-0000-4000-8000-000000000003", + "source_run_id": "0000007b-0000-4000-8000-000000000103", + "tenant_id": "contract-tenant", + "application_id": "contract-app", + "project_id": "contract-project", + "team_id": null, + "agent_id": "refund-agent", + "user_id": null, + "task_id": "rotate-keys", + "status": "Validated", + "reuse_confidence": 0.6666666666666666, + "supporting_validations": 1, + "contradictions": 0, + "revision": 1, + "created_at": "2026-02-20T09:00:00+00:00", + "updated_at": "2026-02-20T09:00:00+00:00", + "payload_version": 1 + }, + "payload": { + "taskSummary": "Rotate the signing keys", + "attempts": [], + "outcome": { + "status": "Verified", + "evidence": [ + { + "evidenceId": "0000007b-0000-4000-8000-000000000e03", + "verificationRoundId": "0000007b-0000-4000-8000-000000000d00", + "artifactRevision": "rev-1", + "checkId": "check", + "kind": "integration-test", + "result": "Pass", + "producer": "contract-ci", + "detail": null, + "capturedAt": "2026-02-20T09:00:00+00:00" + } + ], + "reason": null, + "evaluatedAt": "2026-02-20T09:00:00+00:00" + }, + "completionScore": 1, + "reflection": { + "reflectionId": "0000007b-0000-4000-8000-000000000203", + "experienceRunId": "0000007b-0000-4000-8000-000000000103", + "lesson": "Verified after 1 attempt.", + "successfulApproaches": [], + "failedApproaches": [], + "preconditions": [ + "Runtime version: runtime-1" + ], + "warnings": [], + "reuseGuidance": "Reuse only where the listed preconditions match.", + "evidenceIds": [ + "0000007b-0000-4000-8000-000000000e03" + ], + "verificationStatus": "Verified", + "completionScore": 1, + "verificationRuleVersion": "1.0.0", + "producer": "host-reflector/1.0", + "createdAt": "2026-02-20T09:00:00+00:00" + }, + "environment": { + "hostName": "contract-host", + "runtimeVersion": "runtime-1", + "operatingSystem": "linux", + "applicationVersion": "1.0.0", + "metadata": { + "region": "eu-west" + } + }, + "provenance": { + "source": "contract-fixture", + "sourceVersion": "1", + "recordedAt": "2026-02-20T09:00:00+00:00", + "correlationId": null + } + } + } +] diff --git a/contract/fixtures/vector-search/zero-magnitude-stored-vector/steps.json b/contract/fixtures/vector-search/zero-magnitude-stored-vector/steps.json new file mode 100644 index 0000000..8192a50 --- /dev/null +++ b/contract/fixtures/vector-search/zero-magnitude-stored-vector/steps.json @@ -0,0 +1,103 @@ +{ + "description": "A stored zero-magnitude vector is valid, but has no direction: against a normal query its distance is NaN (written null) and its relevance 0, and it comes after every row with a number for a distance (PostgreSQL orders NaN after every number), whatever its ID.", + "steps": [ + { + "write": { + "experienceId": "0000007b-0000-4000-8000-000000000001", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "0000007b-0000-4000-8000-000000000002", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 1, + 0, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "write": { + "experienceId": "0000007b-0000-4000-8000-000000000003", + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "dimension": 4, + "contentHash": "e0e1482d74952dab40a932bb4e82e0e5f6fca002cad91d51086954c155b38f02", + "sourceRevision": 1, + "vector": [ + 0, + 1, + 0, + 0 + ], + "now": "2026-03-02T10:00:00+00:00" + } + }, + { + "search": { + "scope": { + "tenantId": "contract-tenant", + "applicationId": "contract-app", + "projectId": "contract-project", + "teamId": null, + "agentId": "refund-agent", + "userId": null + }, + "modelId": "contract-embedding", + "vector": [ + 1, + 0, + 0, + 0 + ], + "statuses": [ + "Validated", + "Reinforced" + ], + "minConfidence": 0.5, + "limit": 51, + "excludeModelAuthored": false + } + } + ] +} diff --git a/contract/lock.json b/contract/lock.json index 0457778..f91a073 100644 --- a/contract/lock.json +++ b/contract/lock.json @@ -1,12 +1,23 @@ { - "contractVersion": "1.5", + "contractVersion": "1.6", "files": { "README-1.1.md": "eb7c3a0ec370ec5370aae110a57701d1818d70348f9eef1d88e73442b05398aa", "README-1.2.md": "71948ff812426e85ebe3cc849025db6b8e6a959eb34fd8e10bd4757ea4065d94", "README-1.3.md": "b63d0342726eca79d2c321b1dda1115c3f8d7de5bf94228f459dbc8c3372b8e3", "README-1.4.md": "6e0281d621ea903718126c6d6f6b00c565a459063e7f70d902fb054375200a75", "README-1.5.md": "0276ac3ec614a74585e3e12410a72fa7d95289e315f67a40fe02a73c88740b33", + "README-1.6.md": "e36a54a5a3b05a372e7c35d4a491c259f51203b8810febc9d0116c1f40dfdfaa", "README.md": "0a2a234943ff9647cbd1d42260d31cccbe7063f5536864c71fd5bf77ef5eb99f", + "fixtures/embedding-summary/cases.json": "b135ef060361185d7009f4613302c6acf66b28f4d7de8704c28d501b35ed72c2", + "fixtures/embedding-write/missing-unknown-other-scope-or-erased/expected.json": "8e083d7368e95d75edc62e07c308df3373eae86c4b6130ceb104a7d496591159", + "fixtures/embedding-write/missing-unknown-other-scope-or-erased/seed.json": "3ead3c186a886b69c4b68e7ae6f9679231f1591d85962ae2156e1e97ae15d3ad", + "fixtures/embedding-write/missing-unknown-other-scope-or-erased/steps.json": "9ac52dd49f50f342c43630eeef6bd895954197343955dca469fb378ade369aab", + "fixtures/embedding-write/revisions-newer-replaces-older-is-stale/expected.json": "2b4f1cb6b46c20cfb08336c38dc56ce9be3cc41006f95255af51f80d9e022406", + "fixtures/embedding-write/revisions-newer-replaces-older-is-stale/seed.json": "58653722e2e91241909e810952cc20d7c42da961cf1124841b9218573dbd427a", + "fixtures/embedding-write/revisions-newer-replaces-older-is-stale/steps.json": "6cae4d08c6a5aa7ceb459d937e85987faef39fc919eaab1922e4c9db89cb9075", + "fixtures/embedding-write/written-with-the-record-scope/expected.json": "86aced4cc137d9d5a47afb518df0288b4a22f553fadc18339ee2843ea63fbaec", + "fixtures/embedding-write/written-with-the-record-scope/seed.json": "80724b801cc24ccc2d39271364aeed34fc48da3240cc4fbd7eea9832e2354906", + "fixtures/embedding-write/written-with-the-record-scope/steps.json": "b2ce4ebddb26f8e47202d4eae07ca7e671cb9518a148b409ce1636b9745c8997", "fixtures/encoding/cases.json": "d3ac5a968bc8912a19bacedebf9fe40aa20236573a758aeaec64dfc730c0868d", "fixtures/erased/get-of-an-erased-record/expected.json": "da07c11c15e7e5796c5028dad5c170fb2a02334a330fdd11c58e9522eb79be73", "fixtures/erased/get-of-an-erased-record/seed.json": "91ab18e22198d8c1bb7f043ed3943484d559f5dac5e2e7bc935d08d2d2251cd0", @@ -95,6 +106,7 @@ "fixtures/finalize/verified-after-one-failed-attempt/expected-event.json": "eaa07838d586fda4423659bf7a3a8fc80d8983a3589cef4888e45f0a13b92305", "fixtures/finalize/verified-after-one-failed-attempt/expected-record.json": "e890aff6890ca6b493ce027da19b75343269f2f5ee961ffd61d8aee4cb5554aa", "fixtures/finalize/verified-after-one-failed-attempt/input.json": "4070df84cd79f4c81599c77c36f74cc4f8fc88b3bc82bea4380b0d55c3cae029", + "fixtures/hybrid-merge/cases.json": "984a382cb47f0ca9702232c70ee09c4f619a834a0a902b549f174d715c29a6a7", "fixtures/inject/argument-numbers/expected-block.txt": "254601b2e4884e4ec528f1bff85485b0f8f5d51a1f460fc6423d7f67ea9f7f8f", "fixtures/inject/argument-numbers/options.json": "f3d45d07537d3d42865f0ac5c25f8b1939b59f22098d570a2910ee52152b90ea", "fixtures/inject/argument-numbers/records.json": "6f9f22f4f3f551a5df921bb7985b681910a13e4bf16a553e1ec44acae5ef7899", @@ -195,11 +207,38 @@ "fixtures/task-text/single-prompt/messages.json": "e6dc0010dea4afaa7b894e3a658c4667155f248bc67c68e2bae439ac26f75a2f", "fixtures/task-text/threshold-off/expected.json": "507e2c22107565657c27d9c2fca88013ba6c6794cdb31f32e74c9ef0b28c490e", "fixtures/task-text/threshold-off/messages.json": "7991333018ea93cf711a634dabd9ecfc34216e022b4109ffc3ca923676110afb", + "fixtures/vector-search/an-exact-tie-is-ordered-by-id/expected.json": "f5a9d70c44222d87e066dce6b4509c6cdcba1cf9ec064e276f2cb0c269a43cf1", + "fixtures/vector-search/an-exact-tie-is-ordered-by-id/seed.json": "7fecddcc3a077622403be5651d14c02eb05739698164544eb172a6043641d5f8", + "fixtures/vector-search/an-exact-tie-is-ordered-by-id/steps.json": "82633d3bce0badd1d770b34f9ad90be7c5a51a044b48dd2a749a00afbaeb063a", + "fixtures/vector-search/another-model-or-dimension/expected.json": "54c2f20aecda6cede2973d52485bc88404bcebaff8878bf551649a0bb20f3755", + "fixtures/vector-search/another-model-or-dimension/seed.json": "f4ad6cd7f3ecce32d6c2b9e2ec06a204fdbbae15873f574cd24f6de027c84d9d", + "fixtures/vector-search/another-model-or-dimension/steps.json": "ad624ce16a0c6152cd783f76208340f5a76ea3b9e1083c492391ad5c1a755dac", + "fixtures/vector-search/model-authored-excluded/expected.json": "2dfe137121dcfe71097ff86ca3ac104ebe382f630b360a10ac86f11a7ab94aa5", + "fixtures/vector-search/model-authored-excluded/seed.json": "c04dd22499dfacbf74a0a5e59704b7d1769f0a24a51d111d6079569676de0f76", + "fixtures/vector-search/model-authored-excluded/steps.json": "cf54300c1dfa22a8ba89e006d7b5dbe01fb7edcf426774edda8f0c6fde9660e1", + "fixtures/vector-search/nearest-first/expected.json": "0d1935e9e7aad162e4ad3bcc8503c494b1c62c99a9f58fb822e20db923ffb6e4", + "fixtures/vector-search/nearest-first/seed.json": "38788469230850c9ea0ca0340a28cedd19e7df6bb2c4c40944c9b064938a2b0a", + "fixtures/vector-search/nearest-first/steps.json": "e2e78fb44fea9b83d3813685d57ce345ecef0b4dbf217edcd5144547d9e48fbb", + "fixtures/vector-search/nothing-comparable-is-found-empty/expected.json": "d0253087582f102c8cbfccc44a0596704b8dd9e64072a6180125062c32bbcf40", + "fixtures/vector-search/nothing-comparable-is-found-empty/seed.json": "7008f5055c9c1952f4174bb2c428d7d0921af5fb1f545f70acb5cc6436d1598a", + "fixtures/vector-search/nothing-comparable-is-found-empty/steps.json": "7e031ccdc61b2272c340d04323a3cd64ba092641492d66ebe005b9ff1e353e1f", + "fixtures/vector-search/scope-status-confidence-and-erasure/expected.json": "55b95f5b1a74d0146451a24e42a943f90c4acde4d33ca2831b6f5fd0f0911462", + "fixtures/vector-search/scope-status-confidence-and-erasure/seed.json": "833ad9e0cb51ecf020ce50504a8981792836c39e208d53f9566155a3af6cd924", + "fixtures/vector-search/scope-status-confidence-and-erasure/steps.json": "21d18b54e30297746202307e4eba6a0855e1470740dd68b216b4ec5874b62a2e", + "fixtures/vector-search/zero-magnitude-query/expected.json": "a0d02b393565471c6ac29434a228bb1462a355482e31040115515374e29f066b", + "fixtures/vector-search/zero-magnitude-query/seed.json": "26c0b4ffab1fda4a49289c1da89f0a41616627c431bbc5e8694578099e3c0a87", + "fixtures/vector-search/zero-magnitude-query/steps.json": "4830a39b0f45495b33b5d384e8abc8ecc35dc1d9dbf9cd96db811de87be74d5e", + "fixtures/vector-search/zero-magnitude-stored-vector/expected.json": "99ad26e54f7384d5a004738cb8cecca45586d3f994673148ac7a6a31ec740f1c", + "fixtures/vector-search/zero-magnitude-stored-vector/seed.json": "47df273acec29713219434c9c97cc3c463d7ef5df07712a54de7a32f786af8a5", + "fixtures/vector-search/zero-magnitude-stored-vector/steps.json": "2053b63b601725cb374fe1832413b199e93cdd4b0bc23b93fccfb6f7c6b1e8e6", "schema/experience-record.v1.json": "e897462707143149e93a0a22faeff62e338f99aa2cec368a5975e4b4976c5670", "sql/application-role-privileges-1.4.sql": "4d6127fa205b1e174d11ec3b7f08b5dff0f518d0980ef1d483c4a34f060abf0b", + "sql/application-role-privileges-1.6.sql": "55590df58f9543d3dd53851f0d9b1903b0f1497e0bcbe133227a4801c8a17304", "sql/application-role-privileges.sql": "d6ba9aa62c4eae99f07be06afb1909002ba961796c53bfc94906352745eae789", "sql/declare-bounds.sql": "dfcbfa701cdbf18e2da9ab04bf8cd91cb143116bafd8d4f9c3f534ef90e1a2b7", "sql/record-injection.sql": "17bc8aefd38e2da5e453258082d944f37e2eab47212e63fd2f833373cc48d7aa", - "sql/search-experience-text.sql": "3b0e1c503cd513dfdfa3bc8782589783f0a925e3c3451d75aa1a327b6c7d6b5d" + "sql/search-experience-text.sql": "3b0e1c503cd513dfdfa3bc8782589783f0a925e3c3451d75aa1a327b6c7d6b5d", + "sql/search-experience-vector.sql": "dfba645646591fffbfbb672d8aec1d4e375f41f593c8cbc60c342436df82c50e", + "sql/write-experience-embedding.sql": "804502dc36cf3432a54715bcbd4251aa6920cf229067b8e56272fb9d46e7d66c" } } diff --git a/contract/sql/application-role-privileges-1.6.sql b/contract/sql/application-role-privileges-1.6.sql new file mode 100644 index 0000000..a770766 --- /dev/null +++ b/contract/sql/application-role-privileges-1.6.sql @@ -0,0 +1,14 @@ +-- contract 1.6: the application role's privileges on agent_experience.experience_embeddings (the vectors migrations, +-- 0004 and 0020), which sql/write-experience-embedding.sql writes and sql/search-experience-vector.sql reads. Run them +-- after application-role-privileges.sql (whose REVOKE ALL removes them) and application-role-privileges-1.4.sql, as the +-- schema's owner, in the same transaction or a later one, with the same :"application_role" substitution and the same +-- modes. Together the three files are exactly what ExperienceSchemaMigrator.ApplyApplicationRolePrivilegesAsync grants on +-- a schema migrated to 0026 or later with the vectors migrations applied, with the default options. Every statement is +-- one line ending in ";". UPDATE is column-level, the six columns the write's ON CONFLICT DO UPDATE sets: never +-- experience_id, created_at or the scope columns. DELETE is the .NET index's removal of a vector; a contract 1.6 port +-- never deletes one (an erasure removes it, as the owner). +-- mode: always +GRANT SELECT, INSERT, DELETE ON agent_experience.experience_embeddings TO :"application_role"; +GRANT UPDATE (content_hash, dimension, embedding, model_id, source_revision, updated_at) ON agent_experience.experience_embeddings TO :"application_role"; +-- mode: row-level-security +ALTER TABLE agent_experience.experience_embeddings ENABLE ROW LEVEL SECURITY; diff --git a/contract/sql/search-experience-vector.sql b/contract/sql/search-experience-vector.sql new file mode 100644 index 0000000..702302d --- /dev/null +++ b/contract/sql/search-experience-vector.sql @@ -0,0 +1,40 @@ +-- contract 1.6: the nearest-neighbour search over stored embeddings, in a transaction that ran declare-bounds.sql for +-- the reader's scope. Four statements, each starting at its "-- statement:" line and running to the next one (or the +-- end): run "search", or "search-excluding-model-authored" when model-written lessons are excluded; only when it +-- returns no row, run the matching compatibility probe ("compatibility" or "compatibility-excluding-model-authored") +-- in the same transaction. Parameters: +-- @tenant_id, @application_id, @project_id text, the reader's scope +-- @team_id, @agent_id, @user_id text, NULL when the scope leaves the field unset +-- @statuses text[], {Validated,Reinforced} +-- @min_confidence double precision, 0.5 +-- @model_id text, the query vector's model ID: non-blank, at most 256 UTF-16 +-- code units +-- @query_vector text, the query vector as a pgvector literal, [x,y,...] +-- @limit integer, 51 (the candidate limit, 50, plus one); search only +-- {dim} is not a parameter: replace every {dim} with the query vector's length (1 to 16000) in decimal digits before +-- running the statement. A pgvector type modifier is part of the type, so it cannot be bound. The probes have no {dim}. +-- Rows come back nearest first; the statement has no tie-break, so order rows of equal distance by experience_id +-- ascending (the lower-case hyphenated text, ordinally). distance is double precision, NaN for a zero-magnitude vector; +-- relevance = clamp(1 - distance / 2, 0, 1), and 0 for NaN. Shared rows are out of contract 1.x scope: the statement +-- matches the reader's own scope only, as the text search does with @with_grants false. +-- An empty search's outcome is the probe's: both false, or the first false, is Found (nothing comparable is stored); +-- the first true and the second false is ModelMismatch; both true is DimensionMismatch. +-- The application role needs the grants in application-role-privileges-1.6.sql. +-- statement: search +SELECT r.experience_id, r.source_run_id, r.tenant_id, r.application_id, r.project_id, r.team_id, r.agent_id, r.user_id, r.task_id, r.status, r.reuse_confidence, r.supporting_validations, r.contradictions, r.revision, r.created_at, r.updated_at, r.payload_version, r.payload, false AS shared_by_grant, NULL::uuid AS permitting_grant_id, NULL::text AS permitting_grant_disclosure, (e.embedding::vector({dim}) <=> CAST(@query_vector AS vector({dim}))) AS distance +FROM agent_experience.experience_embeddings e JOIN agent_experience.experience_records r ON r.experience_id = e.experience_id +WHERE (e.tenant_id = @tenant_id AND e.application_id = @application_id AND e.project_id = @project_id AND e.team_id IS NOT DISTINCT FROM @team_id AND e.agent_id IS NOT DISTINCT FROM @agent_id AND e.user_id IS NOT DISTINCT FROM @user_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id) AND r.deleted_at IS NULL AND r.status = ANY(@statuses) AND r.reuse_confidence >= @min_confidence +AND e.model_id = @model_id AND e.dimension = {dim} +ORDER BY (e.embedding::vector({dim}) <=> CAST(@query_vector AS vector({dim}))) LIMIT @limit +-- statement: search-excluding-model-authored +SELECT r.experience_id, r.source_run_id, r.tenant_id, r.application_id, r.project_id, r.team_id, r.agent_id, r.user_id, r.task_id, r.status, r.reuse_confidence, r.supporting_validations, r.contradictions, r.revision, r.created_at, r.updated_at, r.payload_version, r.payload, false AS shared_by_grant, NULL::uuid AS permitting_grant_id, NULL::text AS permitting_grant_disclosure, (e.embedding::vector({dim}) <=> CAST(@query_vector AS vector({dim}))) AS distance +FROM agent_experience.experience_embeddings e JOIN agent_experience.experience_records r ON r.experience_id = e.experience_id +WHERE (e.tenant_id = @tenant_id AND e.application_id = @application_id AND e.project_id = @project_id AND e.team_id IS NOT DISTINCT FROM @team_id AND e.agent_id IS NOT DISTINCT FROM @agent_id AND e.user_id IS NOT DISTINCT FROM @user_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id) AND r.deleted_at IS NULL AND r.status = ANY(@statuses) AND r.reuse_confidence >= @min_confidence +AND r.reflection_model_authored IS FALSE AND e.model_id = @model_id AND e.dimension = {dim} +ORDER BY (e.embedding::vector({dim}) <=> CAST(@query_vector AS vector({dim}))) LIMIT @limit +-- statement: compatibility +SELECT EXISTS (SELECT 1 FROM agent_experience.experience_embeddings e JOIN agent_experience.experience_records r ON r.experience_id = e.experience_id WHERE (e.tenant_id = @tenant_id AND e.application_id = @application_id AND e.project_id = @project_id AND e.team_id IS NOT DISTINCT FROM @team_id AND e.agent_id IS NOT DISTINCT FROM @agent_id AND e.user_id IS NOT DISTINCT FROM @user_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id) AND r.deleted_at IS NULL AND r.status = ANY(@statuses) AND r.reuse_confidence >= @min_confidence), +EXISTS (SELECT 1 FROM agent_experience.experience_embeddings e JOIN agent_experience.experience_records r ON r.experience_id = e.experience_id WHERE (e.tenant_id = @tenant_id AND e.application_id = @application_id AND e.project_id = @project_id AND e.team_id IS NOT DISTINCT FROM @team_id AND e.agent_id IS NOT DISTINCT FROM @agent_id AND e.user_id IS NOT DISTINCT FROM @user_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id) AND r.deleted_at IS NULL AND r.status = ANY(@statuses) AND r.reuse_confidence >= @min_confidence AND e.model_id = @model_id) +-- statement: compatibility-excluding-model-authored +SELECT EXISTS (SELECT 1 FROM agent_experience.experience_embeddings e JOIN agent_experience.experience_records r ON r.experience_id = e.experience_id WHERE (e.tenant_id = @tenant_id AND e.application_id = @application_id AND e.project_id = @project_id AND e.team_id IS NOT DISTINCT FROM @team_id AND e.agent_id IS NOT DISTINCT FROM @agent_id AND e.user_id IS NOT DISTINCT FROM @user_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id) AND r.deleted_at IS NULL AND r.status = ANY(@statuses) AND r.reuse_confidence >= @min_confidence AND r.reflection_model_authored IS FALSE), +EXISTS (SELECT 1 FROM agent_experience.experience_embeddings e JOIN agent_experience.experience_records r ON r.experience_id = e.experience_id WHERE (e.tenant_id = @tenant_id AND e.application_id = @application_id AND e.project_id = @project_id AND e.team_id IS NOT DISTINCT FROM @team_id AND e.agent_id IS NOT DISTINCT FROM @agent_id AND e.user_id IS NOT DISTINCT FROM @user_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id) AND r.deleted_at IS NULL AND r.status = ANY(@statuses) AND r.reuse_confidence >= @min_confidence AND r.reflection_model_authored IS FALSE AND e.model_id = @model_id) diff --git a/contract/sql/write-experience-embedding.sql b/contract/sql/write-experience-embedding.sql new file mode 100644 index 0000000..2ebb5e0 --- /dev/null +++ b/contract/sql/write-experience-embedding.sql @@ -0,0 +1,31 @@ +-- contract 1.6: stores one record's embedding, conditionally, in a transaction that ran declare-bounds.sql for the same +-- scope. Two statements, each starting at its "-- statement:" line and running to the next one (or the end); run +-- "write", and only when it reports 0 rows written, "probe-revision" in the same transaction, then commit. Parameters: +-- @experience_id uuid, the record whose summary was embedded +-- @tenant_id, @application_id, @project_id text, the record's scope (exactly; a write never crosses a scope) +-- @team_id, @agent_id, @user_id text, NULL when the scope leaves the field unset +-- @model_id text, the embedding model's ID: non-blank, at most 256 UTF-16 code units +-- @dimension integer, the vector's length, 1 to 16000 +-- @content_hash text, the content hash of the summary that was embedded (README-1.6.md) +-- @source_revision bigint, the record revision the summary was read at +-- @embedding text, the vector as a pgvector literal, [x,y,...]; any text pgvector +-- parses as a vector of @dimension finite components +-- @now timestamptz, the writer's clock, truncated to whole microseconds in UTC +-- "write" inserts or replaces the row only for a record that is live (not erased) in exactly that scope and still at +-- exactly @source_revision, its scope columns copied from the record, never from the parameters; the record row is +-- locked FOR KEY SHARE, so an erasure racing it either waits and then removes the vector, or runs first and leaves a +-- tombstone this statement does not select. A row already stored from a newer revision is never replaced by an older +-- one. Outcomes: 1 row written is Written. 0 rows: "probe-revision" returns the record's current revision (Stale, with +-- that revision) or no row at all (Missing: an unknown ID, a record in another scope, or an erased record). +-- The application role needs the grants in application-role-privileges-1.6.sql. +-- statement: write +INSERT INTO agent_experience.experience_embeddings AS t (experience_id, tenant_id, application_id, project_id, team_id, agent_id, user_id, model_id, dimension, content_hash, source_revision, embedding, created_at, updated_at) +SELECT r.experience_id, r.tenant_id, r.application_id, r.project_id, r.team_id, r.agent_id, r.user_id, @model_id, @dimension, @content_hash, @source_revision, CAST(@embedding AS vector), @now, @now +FROM agent_experience.experience_records r +WHERE r.experience_id = @experience_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id AND r.deleted_at IS NULL AND r.revision = @source_revision +FOR KEY SHARE OF r +ON CONFLICT (experience_id) DO UPDATE SET model_id = EXCLUDED.model_id, dimension = EXCLUDED.dimension, content_hash = EXCLUDED.content_hash, source_revision = EXCLUDED.source_revision, embedding = EXCLUDED.embedding, updated_at = EXCLUDED.updated_at +WHERE t.source_revision <= EXCLUDED.source_revision +-- statement: probe-revision +SELECT r.revision FROM agent_experience.experience_records r +WHERE r.experience_id = @experience_id AND r.tenant_id = @tenant_id AND r.application_id = @application_id AND r.project_id = @project_id AND r.team_id IS NOT DISTINCT FROM @team_id AND r.agent_id IS NOT DISTINCT FROM @agent_id AND r.user_id IS NOT DISTINCT FROM @user_id AND r.deleted_at IS NULL diff --git a/go/contract_test.go b/go/contract_test.go index b86b0d1..c09c5c1 100644 --- a/go/contract_test.go +++ b/go/contract_test.go @@ -15,6 +15,7 @@ import ( "io/fs" "os" "path/filepath" + "slices" "sort" "strings" "testing" @@ -42,7 +43,7 @@ func TestTheContractVersionIsOneThisPackageSupports(t *testing.T) { if err != nil { t.Fatal(err) } - if version != ae.ContractVersion { + if version != ae.ContractVersion && !slices.Contains(ct.PendingContractVersions, version) { t.Fatalf("the lock is contract %s, this package implements %s", version, ae.ContractVersion) } if strings.Join(ae.SupportedContractVersions, ", ") != "1.0, 1.1, 1.2, 1.3, 1.4, 1.5" { @@ -51,7 +52,7 @@ func TestTheContractVersionIsOneThisPackageSupports(t *testing.T) { } func TestAnUnsupportedContractVersionFailsWithAClearMessage(t *testing.T) { - for _, version := range []any{"2.0", "1.6", nil} { + for _, version := range []any{"2.0", "1.7", nil} { _, err := ct.CheckContractVersion(ae.NewObject(ae.Member{Key: "contractVersion", Value: version})) if !errors.Is(err, ct.ErrUnsupportedContractVersion) || !strings.Contains(err.Error(), "supports only 1.0, 1.1, 1.2, 1.3, 1.4, 1.5") { t.Fatalf("version %v: %v", version, err) @@ -112,7 +113,8 @@ func TestEveryFixtureFamilyHasCases(t *testing.T) { families = append(families, entry.Name()) } } - if strings.Join(families, ",") != "encoding,erased,error-class,finalize,inject,record-injection,search,task-text" { + if strings.Join(families, ",") != "embedding-summary,embedding-write,encoding,erased,error-class,finalize,hybrid-merge,inject,"+ + "record-injection,search,task-text,vector-search" { t.Fatalf("families: %v", families) } for _, family := range []string{"erased", "finalize", "inject", "record-injection", "search", "task-text"} { @@ -122,6 +124,19 @@ func TestEveryFixtureFamilyHasCases(t *testing.T) { } } +// TestPendingVectorFamilies skips contract 1.6's vector families by name: this package does not implement them yet +// (stories 23.2 and 23.3 do, and remove ct.PendingVectorFamilies). Each must exist, so a renamed family fails here. +func TestPendingVectorFamilies(t *testing.T) { + for _, family := range ct.PendingVectorFamilies { + t.Run(family, func(t *testing.T) { + if _, err := os.Stat(fixture(family)); err != nil { + t.Fatal(err) + } + t.Skipf("%s: a contract 1.6 vector family, pending in this package", family) + }) + } +} + // ---- finalize -------------------------------------------------------------------------------------------------- func TestFinalize(t *testing.T) { diff --git a/go/internal/contracttest/contracttest.go b/go/internal/contracttest/contracttest.go index f96ca68..31fe5f2 100644 --- a/go/internal/contracttest/contracttest.go +++ b/go/internal/contracttest/contracttest.go @@ -72,12 +72,21 @@ func Cases(family string) []string { // ErrUnsupportedContractVersion is a lock naming a contract version this package does not support. var ErrUnsupportedContractVersion = errors.New("unsupported contract version") -// CheckContractVersion is the lock's contract version, when this package supports it; a clear failure otherwise. +// PendingContractVersions are the contract versions the lock may name although this package does not pass them yet: +// contract 1.6 only adds PendingVectorFamilies, so every 1.0 to 1.5 fixture still runs under its lock. +var PendingContractVersions = []string{"1.6"} + +// PendingVectorFamilies are contract 1.6's vector fixture families, which this package does not implement yet. They +// are skipped by name (TestPendingVectorFamilies), never ignored. Stories 23.2 and 23.3 implement them and remove this. +var PendingVectorFamilies = []string{"embedding-summary", "embedding-write", "hybrid-merge", "vector-search"} + +// CheckContractVersion is the lock's contract version, when this package supports it or it is pending; a clear +// failure otherwise. func CheckContractVersion(lock any) (string, error) { object, _ := lock.(*ae.Object) value, _ := object.Get("contractVersion") version, _ := value.(string) - for _, supported := range ae.SupportedContractVersions { + for _, supported := range append(append([]string{}, ae.SupportedContractVersions...), PendingContractVersions...) { if version == supported { return version, nil } diff --git a/python/tests/_contract.py b/python/tests/_contract.py index 2a08634..cb9952c 100644 --- a/python/tests/_contract.py +++ b/python/tests/_contract.py @@ -43,14 +43,23 @@ def cases(family: str) -> list[str]: return sorted(entry.name for entry in (FIXTURES / family).iterdir() if entry.is_dir()) +PENDING_CONTRACT_VERSIONS: tuple[str, ...] = ("1.6",) +"""Contract versions the lock may name although this package does not pass them yet: contract 1.6 only adds +``PENDING_VECTOR_FAMILIES``, so every 1.0 to 1.5 fixture still runs under its lock.""" + +PENDING_VECTOR_FAMILIES: tuple[str, ...] = ("embedding-summary", "embedding-write", "hybrid-merge", "vector-search") +"""Contract 1.6's vector fixture families, which this package does not implement yet. They are skipped by name +(``test_pending_vector_family``), never ignored. Stories 23.2 and 23.3 implement them and remove this.""" + + class UnsupportedContractVersion(Exception): pass def check_contract_version(lock: dict[str, Any]) -> str: - """The lock's contract version, when this package supports it; a clear failure otherwise.""" + """The lock's contract version, when this package supports it or it is pending; a clear failure otherwise.""" version = lock.get("contractVersion") - if version not in SUPPORTED_CONTRACT_VERSIONS: + if version not in SUPPORTED_CONTRACT_VERSIONS + PENDING_CONTRACT_VERSIONS: raise UnsupportedContractVersion( f"contract/lock.json is contract version {version!r}, but this package supports only " f"{', '.join(SUPPORTED_CONTRACT_VERSIONS)}. Implement the new version before running its fixtures." diff --git a/python/tests/test_contract.py b/python/tests/test_contract.py index 9dd17d3..2598671 100644 --- a/python/tests/test_contract.py +++ b/python/tests/test_contract.py @@ -20,6 +20,8 @@ from _contract import ( CONTRACT, FIXTURES, + PENDING_CONTRACT_VERSIONS, + PENDING_VECTOR_FAMILIES, SCHEMA, UnsupportedContractVersion, cases, @@ -105,10 +107,11 @@ def assert_json_equal(expected: Any, actual: Any, what: str) -> None: def test_the_contract_version_is_one_this_package_supports() -> None: - assert check_contract_version(read_json(CONTRACT / "lock.json")) == CONTRACT_VERSION + version = check_contract_version(read_json(CONTRACT / "lock.json")) + assert version == CONTRACT_VERSION or version in PENDING_CONTRACT_VERSIONS -@pytest.mark.parametrize("version", ["2.0", "1.6", None]) +@pytest.mark.parametrize("version", ["2.0", "1.7", None]) def test_an_unsupported_contract_version_fails_with_a_clear_message(version: str | None) -> None: with pytest.raises(UnsupportedContractVersion, match=r"supports only 1\.0, 1\.1, 1\.2, 1\.3, 1\.4, 1\.5"): check_contract_version({"contractVersion": version}) @@ -136,19 +139,31 @@ def test_the_packaged_sql_is_the_contract_sql(name: str) -> None: def test_every_fixture_family_has_cases() -> None: assert {entry.name for entry in FIXTURES.iterdir() if entry.is_dir()} == { + "embedding-summary", + "embedding-write", "encoding", "erased", "error-class", "finalize", + "hybrid-merge", "inject", "record-injection", "search", "task-text", + "vector-search", } for family in ("erased", "finalize", "inject", "record-injection", "search", "task-text"): assert cases(family), family +@pytest.mark.parametrize("family", PENDING_VECTOR_FAMILIES) +def test_pending_vector_family(family: str) -> None: + # Contract 1.6's vector families, skipped by name: this package does not implement them yet (stories 23.2 and 23.3 + # do, and remove PENDING_VECTOR_FAMILIES). Each must exist, so a renamed family fails here rather than going unseen. + assert (FIXTURES / family).is_dir(), family + pytest.skip(f"{family}: a contract 1.6 vector family, pending in this package") + + # ---- finalize --------------------------------------------------------------------------------------------------- diff --git a/tests/AgentExperience.Contract.Tests/AgentExperience.Contract.Tests.csproj b/tests/AgentExperience.Contract.Tests/AgentExperience.Contract.Tests.csproj index 7ce0e95..18a03cf 100644 --- a/tests/AgentExperience.Contract.Tests/AgentExperience.Contract.Tests.csproj +++ b/tests/AgentExperience.Contract.Tests/AgentExperience.Contract.Tests.csproj @@ -37,6 +37,8 @@ + + diff --git a/tests/AgentExperience.Contract.Tests/ContractFiles.cs b/tests/AgentExperience.Contract.Tests/ContractFiles.cs index 6b85ea7..789a2b7 100644 --- a/tests/AgentExperience.Contract.Tests/ContractFiles.cs +++ b/tests/AgentExperience.Contract.Tests/ContractFiles.cs @@ -19,7 +19,7 @@ internal static class ContractFiles /// what minor version N adds) bumps the minor version; a change to anything already published (a fixture, the /// schema, the SQL, a README) bumps the major version. /// - public const string ContractVersion = "1.5"; + public const string ContractVersion = "1.6"; /// /// The SHA-256 of contract/lock.json for every published version. This, not lock.json, is the authority: @@ -34,6 +34,7 @@ internal static class ContractFiles ["1.3"] = "8de87d33707e1d7c7c170d17dd6a9992a358301a2e0df82bed6e5ff63edcbc4e", ["1.4"] = "e8f058379b3f1ddf545ec76c376af64f30de734b8a60fb801ce9ac975a4287d1", ["1.5"] = "bb0b1eb2529d9b48bea47da548bdd9e19aa9a08b0c5a31b735374e47060d5560", + ["1.6"] = "c16f2803a667fca0f621847d90ae50340bb6304eed1e97295599f98049e547ff", }; /// The environment variable that rewrites the fixtures instead of only comparing against them. diff --git a/tests/AgentExperience.Contract.Tests/ContractPrivilegesOrderPostgresTests.cs b/tests/AgentExperience.Contract.Tests/ContractPrivilegesOrderPostgresTests.cs index a2a1b84..4038df9 100644 --- a/tests/AgentExperience.Contract.Tests/ContractPrivilegesOrderPostgresTests.cs +++ b/tests/AgentExperience.Contract.Tests/ContractPrivilegesOrderPostgresTests.cs @@ -59,7 +59,7 @@ await ExperienceSchemaMigrator.ApplyApplicationRolePrivilegesAsync( } /// A contract privileges file, both modes, in one transaction, as a port's setup runs it. - private static async Task ApplyContractFileAsync(NpgsqlDataSource owner, string file) + internal static async Task ApplyContractFileAsync(NpgsqlDataSource owner, string file) { var role = "\"" + ContractPostgresDatabase.ApplicationRole + "\""; await using var connection = await owner.OpenConnectionAsync(); diff --git a/tests/AgentExperience.Contract.Tests/SearchContractPostgresTests.cs b/tests/AgentExperience.Contract.Tests/SearchContractPostgresTests.cs index d579185..7fa0da7 100644 --- a/tests/AgentExperience.Contract.Tests/SearchContractPostgresTests.cs +++ b/tests/AgentExperience.Contract.Tests/SearchContractPostgresTests.cs @@ -205,18 +205,36 @@ public sealed class ContractPostgresCollection : ICollectionFixture -public sealed class ContractPostgresDatabase : IAsyncLifetime +public class ContractPostgresDatabase : IAsyncLifetime { private const string Owner = "contract_owner"; private const string Application = "contract_app"; private const string Password = "contract-role-password"; private readonly List _sources = []; + private readonly string _image; + private readonly bool _vectors; private PostgreSqlContainer? _container; + public ContractPostgresDatabase() + : this(AgentExperience.Tests.Shared.PostgresTestImage.Stock, vectors: false) + { + } + + /// + /// A container of ; with , every case's database also gets the + /// vector extension (created by the superuser, as pgvector requires) and the vectors package's migrations, before + /// the application role's privileges are applied. + /// + protected ContractPostgresDatabase(string image, bool vectors) + { + _image = image; + _vectors = vectors; + } + public async Task InitializeAsync() { - _container = new PostgreSqlBuilder(AgentExperience.Tests.Shared.PostgresTestImage.Stock).Build(); + _container = new PostgreSqlBuilder(_image).Build(); await _container.StartAsync(); await ExecuteAsync(_container.GetConnectionString(), $"CREATE ROLE {Owner} LOGIN PASSWORD '{Password}'"); await ExecuteAsync(_container.GetConnectionString(), $"CREATE ROLE {Application} LOGIN PASSWORD '{Password}'"); @@ -238,6 +256,13 @@ public async Task CreateAsync(string caseName) await using (var owner = NpgsqlDataSource.Create(ConnectionString(name, Owner))) { await ExperienceSchemaMigrator.MigrateAsync(owner, CancellationToken.None); + if (_vectors) + { + var superuser = new NpgsqlConnectionStringBuilder(_container.GetConnectionString()) { Database = name }.ConnectionString; + await ExecuteAsync(superuser, "CREATE EXTENSION IF NOT EXISTS vector"); + await AgentExperience.Storage.Postgres.Vectors.ExperienceVectorSchemaMigrator.MigrateAsync(owner, CancellationToken.None); + } + await ExperienceSchemaMigrator.ApplyApplicationRolePrivilegesAsync( owner, new ExperienceApplicationRoleOptions(Application) { EnableRowLevelSecurity = true }, diff --git a/tests/AgentExperience.Contract.Tests/VectorContract.cs b/tests/AgentExperience.Contract.Tests/VectorContract.cs new file mode 100644 index 0000000..ffe6acc --- /dev/null +++ b/tests/AgentExperience.Contract.Tests/VectorContract.cs @@ -0,0 +1,584 @@ +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using AgentExperience.Abstractions; +using AgentExperience.Core.Retrieval; +using AgentExperience.Storage.Postgres.Vectors; + +namespace AgentExperience.Contract.Tests; + +/// +/// Contract 1.6's SQL: the statements of sql/write-experience-embedding.sql and +/// sql/search-experience-vector.sql, and the .NET statements they must equal. +/// +/// +/// A contract file holds several statements, each starting at a -- statement: name line and running to the next one +/// (or the end), its other -- lines skipped. A statement may span lines; its lines joined by one space are the .NET +/// statement exactly, so the file breaks lines only where the .NET text has a space. +/// +internal static class VectorStatements +{ + public const string WriteFile = "sql/write-experience-embedding.sql"; + + public const string SearchFile = "sql/search-experience-vector.sql"; + + public const string PrivilegesFile = "sql/application-role-privileges-1.6.sql"; + + /// The placeholder the search statements carry for the query vector's length. + public const string DimensionPlaceholder = "{dim}"; + + private const string Marker = "-- statement: "; + + private const BindingFlags Private = BindingFlags.NonPublic | BindingFlags.Static; + + /// A contract file's statements by name, in file order, each as written (its lines joined by \n). + public static IReadOnlyDictionary Read(string file) + { + var statements = new Dictionary>(StringComparer.Ordinal); + List? current = null; + foreach (var line in ContractFiles.Read(file)!.Split('\n').Where(line => line.Length > 0)) + { + if (line.StartsWith(Marker, StringComparison.Ordinal)) + { + current = []; + statements.Add(line[Marker.Length..], current); + continue; + } + + if (line.StartsWith("--", StringComparison.Ordinal)) + { + continue; + } + + Assert.True(current is not null, $"{file}: a statement line before any '{Marker}' line: {line}"); + current.Add(line); + } + + return statements.ToDictionary(entry => entry.Key, entry => string.Join('\n', entry.Value), StringComparer.Ordinal); + } + + /// A search statement as a port runs it: every {dim} replaced with the query vector's length. + public static string WithDimension(string statement, int dimension) => + statement.Replace(DimensionPlaceholder, dimension.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); + + /// The .NET index's conditional write. + public static string DotnetWrite => Constant("WriteSql"); + + /// The .NET index's revision probe, run when the write wrote nothing. + public static string DotnetProbeRevision => Constant("ProbeRevisionSql"); + + /// + /// The .NET index's nearest-neighbour statement for , exact scope only (the one it runs + /// without grants, as the text search runs in contract 1.x). + /// + public static string DotnetSearch(int dimension, bool excludeModelAuthored) => + (string)typeof(PostgresExperienceEmbeddingIndex).GetMethod("SearchSql", Private)!.Invoke(null, [dimension, false, excludeModelAuthored])!; + + /// The .NET index's exact-scope compatibility probe. + public static string DotnetCompatibility(bool excludeModelAuthored) => + (string)typeof(PostgresExperienceEmbeddingIndex) + .GetField(excludeModelAuthored ? "ExcludingCompatibilityProbeExactSql" : "CompatibilityProbeExactSql", Private)! + .GetValue(null)!; + + private static string Constant(string name) => + (string)typeof(PostgresExperienceEmbeddingIndex).GetField(name, Private)!.GetRawConstantValue()!; +} + +/// One case of the embedding-summary family: the three fields, the model, and what they give. +internal sealed record EmbeddingSummaryCase( + string Description, + string? TaskId, + string? TaskSummary, + string? Lesson, + string ModelId, + string Summary, + string ContentHash); + +/// +/// Contract 1.6's embedding-summary family (no database): the text a record contributes to an embedding, and its +/// content hash. Inputs and answers in one file, as the encoding family has them. +/// +internal static class EmbeddingSummaryContract +{ + public const string Family = "embedding-summary"; + + public const string FileName = "cases.json"; + + private const string Model = "contract-embedding"; + + /// Every case, with its summary and hash as the library computes them now. + public static IReadOnlyList Run() => + [ + .. Inputs().Select(input => Rerun(new EmbeddingSummaryCase(input.Description, input.TaskId, input.TaskSummary, input.Lesson, input.ModelId, string.Empty, string.Empty))), + ]; + + /// A case's summary and hash computed again from its inputs. + public static EmbeddingSummaryCase Rerun(EmbeddingSummaryCase input) + { + var summary = ExperienceRetrievalSummary.For(input.TaskId, input.TaskSummary, input.Lesson); + return input with { Summary = summary, ContentHash = ExperienceEmbeddingDescriptor.ComputeContentHash(input.ModelId, summary) }; + } + + private static IEnumerable<(string Description, string? TaskId, string? TaskSummary, string? Lesson, string ModelId)> Inputs() + { + yield return ("The task ID, the task summary and the lesson, joined by single spaces.", + "refund-stuck-pending", "Refund stuck in pending", "Release the lock before retrying.", Model); + yield return ("Every run of whitespace or control characters inside a field (tab, CR, LF, NBSP, U+2028, NUL, U+0085, " + + "U+001F, DEL, U+009F) is one space; the result is trimmed.", + "deploy\tservice", " Line one\r\n\n line two\u00A0end\u2028x ", "a\u0000b\u0085c\u001Fd\u007Fe\u009Ff\n", Model); + yield return ("A missing or blank field contributes nothing, and no separator.", + "rotate-keys", null, " \t\r\n ", Model); + yield return ("A missing summary and lesson and an empty task ID: the summary is empty, and is still hashed.", + string.Empty, null, null, Model); + yield return ("Format characters (U+200B, U+FEFF) are not whitespace and are kept; a character outside the BMP is kept " + + "whole.", "zero\u200Bwidth", "bom\uFEFFinside", "emoji \U0001F600 kept", Model); + yield return ("The model ID is inside the hash, as UTF-8: the same summary under another model hashes differently.", + "refund-stuck-pending", "Refund stuck in pending", "Release the lock before retrying.", "mod\u00E8le-\u00FC/2"); + yield return ("Longer than 8,192 UTF-16 code units: cut at 8,192.", + "t", new string('a', 9000), "never reached", Model); + yield return ("Cut where the last kept unit is a high surrogate: that unit goes too, so no character is split (8,191 " + + "units).", "t", new string('a', 8189) + "\U0001F600" + "tail", null, Model); + yield return ("Cut right after a space: the trailing space is dropped (8,191 units).", + "t", new string('a', 8189) + " " + new string('b', 20), null, Model); + yield return ("Cut inside a surrogate pair that follows a space: the high surrogate, then the space, are dropped (8,190 " + + "units).", "t", new string('a', 8188) + " \U0001F600", null, Model); + } +} + +/// One candidate of a hybrid-merge case: the record and its normalized relevance from one channel. +/// A NaN relevance is written as the string "NaN". +internal sealed record MergeCandidate( + Guid ExperienceId, + [property: System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals)] double Relevance); + +/// One entry of a merged list: the record, the relevance kept, and the channel whose snapshot was kept. +/// A NaN relevance is written as the string "NaN". +internal sealed record MergedCandidate( + Guid ExperienceId, + [property: System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals)] double Relevance, + string From); + +/// One case of the hybrid-merge family. +internal sealed record HybridMergeCase( + string Description, + double? MinimumVectorRelevance, + IReadOnlyList Text, + IReadOnlyList Vector, + IReadOnlyList Merged); + +/// +/// Contract 1.6's hybrid-merge family (no database): how the two channels' candidates become one list before the +/// existing ranking, from the retrieval service's own floor and merge. Inputs and answers in one file. +/// +internal static class HybridMergeContract +{ + public const string Family = "hybrid-merge"; + + public const string FileName = "cases.json"; + + private const int F = 0x6a; + + /// Every case, with its merged list as the library computes it now. + public static IReadOnlyList Run() => [.. Inputs().Select(Rerun)]; + + /// A case's merged list computed again from its inputs, through the retrieval service's floor and merge. + public static HybridMergeCase Rerun(HybridMergeCase input) + { + var records = input.Text.Concat(input.Vector).Select(candidate => candidate.ExperienceId).Distinct() + .ToDictionary(id => id, id => SearchContract.Seed(F, Convert.ToInt32(id.ToString("N")[20..], 16), "task", "Summary", "Lesson.")); + + // Each channel reads its own snapshot of the record: distinct instances, so the one kept tells which channel it came from. + List text = [.. input.Text.Select(candidate => new ExperienceCandidate(records[candidate.ExperienceId] with { }, candidate.Relevance))]; + List vector = [.. input.Vector.Select(candidate => new ExperienceCandidate(records[candidate.ExperienceId] with { }, candidate.Relevance))]; + var textRecords = text.Select(candidate => candidate.Record).ToHashSet(ReferenceEqualityComparer.Instance); + + var merged = ExperienceRetrievalService.MergeChannels(text, AboveFloor(input.MinimumVectorRelevance, vector)); + return input with + { + Merged = [.. merged.Select(candidate => new MergedCandidate( + candidate.Record.ExperienceId, + candidate.Relevance, + textRecords.Contains(candidate.Record) ? "text" : "vector"))], + }; + } + + /// The retrieval service's vector floor, under a policy with . + private static IReadOnlyList AboveFloor(double? floor, IReadOnlyList candidates) + { + var service = new ExperienceRetrievalService( + new NoCandidates(), + RetrievalPolicy.Default with { MinimumVectorRelevance = floor }, + RankingWeights.Default, + new FixedTimeProvider(Fixed.T0)); + return (IReadOnlyList)typeof(ExperienceRetrievalService) + .GetMethod("AboveFloor", BindingFlags.NonPublic | BindingFlags.Instance)! + .Invoke(service, [candidates])!; + } + + private static IEnumerable Inputs() + { + Guid Id(int n) => Fixed.Id(F, n); + MergeCandidate C(int n, double relevance) => new(Id(n), relevance); + HybridMergeCase Case(string description, double? floor, MergeCandidate[] text, MergeCandidate[] vector) => + new(description, floor, text, vector, []); + + yield return Case("Disjoint channels: the text candidates in their order, then the vector candidates in theirs.", + null, [C(2, 0.6), C(1, 0.4)], [C(4, 0.9), C(3, 0.7)]); + yield return Case("A record in both channels is one entry, at its text position, with the higher relevance (here the " + + "vector's) and the text channel's snapshot.", null, [C(1, 0.5), C(2, 0.3)], [C(2, 0.8), C(3, 0.6)]); + yield return Case("The text relevance is the higher: it is kept, with the text channel's snapshot.", + null, [C(1, 0.9)], [C(1, 0.5)]); + yield return Case("Equal relevances: the text channel's entry is kept unchanged.", + null, [C(1, 0.5)], [C(1, 0.5)]); + yield return Case("A floor drops vector candidates below it before the merge (a candidate exactly at it is kept); text " + + "candidates are never floored, so a record whose vector relevance is below the floor keeps its text relevance.", + 0.75, [C(1, 0.2), C(2, 0.6)], [C(2, 0.9), C(3, 0.8), C(4, 0.7), C(1, 0.74), C(5, 0.75)]); + yield return Case("No text candidates: the vector candidates, in their order.", + null, [], [C(2, 0.9), C(1, 0.4)]); + yield return Case("No vector candidates (the vector channel fell back, or matched nothing): the text candidates.", + null, [C(1, 0.4), C(2, 0.3)], []); + yield return Case("Relevances are compared clamped to [0, 1], NaN as 0, but the raw value is kept: a NaN text relevance " + + "takes the vector's 0.5; a text 1.2 beats a vector 0.9 and stays 1.2; a vector 1.5 beats a text 0.9 and is kept as " + + "1.5; with no floor a NaN vector candidate is kept.", + null, [C(1, double.NaN), C(2, 1.2), C(3, 0.4), C(5, 0.9)], [C(1, 0.5), C(2, 0.9), C(4, double.NaN), C(5, 1.5)]); + yield return Case("A floor of 0 keeps every vector candidate with a number for a relevance, 0 included, and drops a NaN one.", + 0, [], [C(1, double.NaN), C(2, 0), C(3, 0.3)]); + } + + /// A candidate source the floor's service never calls. + private sealed class NoCandidates : IExperienceCandidateSource + { + public Task SearchAsync(AuthorizationContext authorization, ExperienceCandidateQuery query, CancellationToken cancellationToken) => + throw new InvalidOperationException("Not called."); + } +} + +/// One step of an embedding-write or vector-search case. +internal abstract record VectorStep; + +/// Stores a record's embedding through the conditional write (the port's write call). +internal sealed record WriteEmbeddingStep( + Guid ExperienceId, + Scope Scope, + string ModelId, + int Dimension, + string ContentHash, + long SourceRevision, + float[] Vector, + DateTimeOffset Now) : VectorStep; + +/// Runs the nearest-neighbour search (the port's search call). +internal sealed record VectorSearchStep( + Scope Scope, + string ModelId, + float[] Vector, + IReadOnlyList Statuses, + double MinConfidence, + int Limit, + bool ExcludeModelAuthored) : VectorStep; + +/// +/// Moves a record to a later revision, as the schema's owner: what a lifecycle commit does to it, so a write computed at +/// the earlier revision is stale. +/// +internal sealed record SetRevisionStep(Guid ExperienceId, long Revision) : VectorStep; + +/// Erases a record through agent_experience.purge_experience_record, as the schema's owner. +internal sealed record EraseEmbeddingStep(EraseStep Erase) : VectorStep; + +/// An embedding-write or vector-search case: the records seeded (as stored), the description and the steps. +internal sealed record VectorCase(string Description, IReadOnlyList Records, IReadOnlyList Steps); + +/// +/// Contract 1.6's two PostgreSQL families, embedding-write and vector-search, with fixed vectors (no model). +/// Running them needs PostgreSQL with pgvector, so the vector PostgreSQL contract test runs them. Each case writes +/// seed.json (stored records, exactly as a search seed holds them), steps.json (the description and the +/// steps) and expected.json (one entry per step, and every row of agent_experience.experience_embeddings +/// afterwards). +/// +internal static class VectorContract +{ + public const string WriteFamily = "embedding-write"; + + public const string SearchFamily = "vector-search"; + + /// The model every case's embeddings come from, unless a case names another. + public const string Model = "contract-embedding"; + + /// Every embedding-write case, by directory name. + public static IReadOnlyList<(string Name, VectorCase Case)> WriteCases { get; } = + [ + ("written-with-the-record-scope", WrittenWithTheRecordScope()), + ("revisions-newer-replaces-older-is-stale", Revisions()), + ("missing-unknown-other-scope-or-erased", Missing()), + ]; + + /// Every vector-search case, by directory name. + public static IReadOnlyList<(string Name, VectorCase Case)> SearchCases { get; } = + [ + ("nearest-first", NearestFirst()), + ("an-exact-tie-is-ordered-by-id", ExactTie()), + ("scope-status-confidence-and-erasure", ScopeStatusConfidenceAndErasure()), + ("model-authored-excluded", ModelAuthoredExcluded()), + ("nothing-comparable-is-found-empty", NothingComparable()), + ("another-model-or-dimension", AnotherModelOrDimension()), + ("zero-magnitude-query", ZeroMagnitudeQuery()), + ("zero-magnitude-stored-vector", ZeroMagnitudeStoredVector()), + ]; + + private static DateTimeOffset T1 => Fixed.T0.AddHours(1); + + private static VectorCase WrittenWithTheRecordScope() + { + const int f = 0x70; + var a = Record(f, 1); + var b = SearchContract.Seed(f, 2, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt.", + scope: new Scope("contract-tenant", "contract-app", "contract-project", TeamId: "payments", AgentId: null, UserId: "user-7")); + return new( + "A write for a live record at its revision stores one row: the scope columns are the record's (a scope with a team " + + "and a user, and one with an agent only), the descriptor and the vector are the write's, and created_at and " + + "updated_at are the write's clock.", + [a, b], + [Write(a, [1f, 0f, 0f, 0f], T1), Write(b, [0f, 1f, 0f, 0f], T1.AddMinutes(1))]); + } + + private static VectorCase Revisions() + { + const int f = 0x71; + var a = Record(f, 1); + return new( + "A record moves from revision 1 to 2. A write computed at revision 1 is then Stale, with the current revision, and " + + "leaves the stored row as it was; a write at revision 2 replaces it (created_at kept, updated_at the new clock), " + + "including with another model and dimension.", + [a], + [ + Write(a, [1f, 0f, 0f, 0f], T1), + new SetRevisionStep(a.ExperienceId, 2), + Write(a, [0f, 1f, 0f, 0f], T1.AddMinutes(1)), + Write(a, [0f, 0f, 1f, 0f], T1.AddMinutes(2), revision: 2), + Write(a, [0f, 0f, 0f, 1f], T1.AddMinutes(3)), + Write(a, [1f, 2f, 3f, 4f, 5f], T1.AddMinutes(4), revision: 2, model: "contract-embedding-v2"), + ]); + } + + private static VectorCase Missing() + { + const int f = 0x72; + var a = Record(f, 1); + var b = Record(f, 2); + return new( + "Missing, and no row: a write for an unknown ID, for a record named in another scope (its own scope, one agent " + + "away), and for an erased record (whose row the erasure removed).", + [a, b], + [ + Write(a, [1f, 0f, 0f, 0f], T1) with { ExperienceId = Fixed.Id(f, 9) }, + Write(a, [1f, 0f, 0f, 0f], T1) with { Scope = Fixed.Scope with { AgentId = "other-agent" } }, + Write(b, [0f, 1f, 0f, 0f], T1), + new EraseEmbeddingStep(new EraseStep(b.ExperienceId, b.Scope, T1.AddMinutes(1))), + Write(b, [0f, 1f, 0f, 0f], T1.AddMinutes(2)), + ]); + } + + private static VectorCase NearestFirst() + { + const int f = 0x73; + var (far, near, nearest) = (Record(f, 1), Record(f, 2), Record(f, 3)); + return new( + "Query [1,0,0,0] over [1,1,0,0], [1,0.1,0,0] and [1,0,0,0]: nearest first, whatever the ID order, each with its " + + "cosine distance and relevance = 1 - distance / 2. A limit of 2 keeps the two nearest.", + [far, near, nearest], + [ + Write(far, [1f, 1f, 0f, 0f], T1), + Write(near, [1f, 0.1f, 0f, 0f], T1), + Write(nearest, [1f, 0f, 0f, 0f], T1), + Search([1f, 0f, 0f, 0f]), + Search([1f, 0f, 0f, 0f]) with { Limit = 2 }, + ]); + } + + private static VectorCase ExactTie() + { + const int f = 0x74; + var (a, b, c) = (Record(f, 1), Record(f, 2), Record(f, 3)); + return new( + "Two records at exactly the same distance ([2,0,0,0] and [1,0,0,0] point the same way), written in descending ID " + + "order: they are ordered by experience ID ascending.", + [a, b, c], + [ + Write(c, [0f, 1f, 0f, 0f], T1), + Write(b, [2f, 0f, 0f, 0f], T1), + Write(a, [1f, 0f, 0f, 0f], T1), + Search([1f, 0f, 0f, 0f]), + ]); + } + + private static VectorCase ScopeStatusConfidenceAndErasure() + { + const int f = 0x75; + var mine = Record(f, 1); + var otherAgent = SearchContract.Seed(f, 2, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt.", scope: Fixed.Scope with { AgentId = "other-agent" }); + var otherTenant = SearchContract.Seed(f, 3, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt.", scope: Fixed.Scope with { TenantId = "other-tenant" }); + var quarantined = SearchContract.Seed(f, 4, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt.", status: ExperienceStatus.Quarantined, confidence: 0); + var candidate = SearchContract.Seed(f, 5, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt.", status: ExperienceStatus.Candidate); + var unsure = SearchContract.Seed(f, 6, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt.", confidence: 0.4); + var erased = Record(f, 7); + var reinforced = SearchContract.Seed(f, 8, "rotate-keys", "Rotate the signing keys", "Verified after 2 attempts.", status: ExperienceStatus.Reinforced, confidence: 0.9); + float[] same = [1f, 0f, 0f, 0f]; + return new( + "Every record has the query's own vector. Only the reader's live, eligible records at or above the confidence floor " + + "come back: not another agent's or tenant's (each written in its own scope), not a Quarantined or Candidate record, " + + "not one under 0.5 confidence, and not an erased one.", + [mine, otherAgent, otherTenant, quarantined, candidate, unsure, erased, reinforced], + [ + .. new[] { mine, otherAgent, otherTenant, quarantined, candidate, unsure, erased, reinforced }.Select(record => (VectorStep)Write(record, same, T1)), + new EraseEmbeddingStep(new EraseStep(erased.ExperienceId, erased.Scope, T1.AddMinutes(1))), + Search(same), + ]); + } + + private static VectorCase ModelAuthoredExcluded() + { + const int f = 0x76; + var deterministic = Record(f, 1); + var modelAuthored = SearchContract.Seed(f, 2, "rotate-keys", "Rotate the signing keys", "Use a staged rollout.", modelAuthored: true); + return new( + "A model-authored record is returned by the search, and left out by the search that excludes model-written lessons " + + "(before the limit), as the text search leaves it out.", + [deterministic, modelAuthored], + [ + Write(deterministic, [1f, 0.5f, 0f, 0f], T1), + Write(modelAuthored, [1f, 0f, 0f, 0f], T1), + Search([1f, 0f, 0f, 0f]), + Search([1f, 0f, 0f, 0f]) with { ExcludeModelAuthored = true, Limit = 1 }, + ]); + } + + private static VectorCase NothingComparable() + { + const int f = 0x77; + var (a, quarantined) = (Record(f, 1), SearchContract.Seed(f, 2, "rotate-keys", "Rotate the signing keys", "Lock timeout.", status: ExperienceStatus.Quarantined, confidence: 0)); + return new( + "Found and empty: no embedding is stored in the scope; and later the only one stored belongs to a record the search " + + "leaves out (Quarantined), which the compatibility probe leaves out too.", + [a, quarantined], + [ + Search([1f, 0f, 0f, 0f]), + Write(quarantined, [1f, 0f, 0f, 0f], T1), + Search([1f, 0f, 0f, 0f]), + ]); + } + + private static VectorCase AnotherModelOrDimension() + { + const int f = 0x78; + var (a, b) = (Record(f, 1), Record(f, 2)); + return new( + "Embeddings stored under the model at dimension 5 only (the one dimension-5 case). A dimension-4 query under another model is ModelMismatch; " + + "under this model it is DimensionMismatch; a dimension-5 query under this model finds them. No rows come back with " + + "a mismatch.", + [a, b], + [ + Write(a, [1f, 0f, 0f, 0f, 0f], T1), + Write(b, [0f, 1f, 0f, 0f, 1f], T1), + Search([1f, 0f, 0f, 0f]) with { ModelId = "another-model" }, + Search([1f, 0f, 0f, 0f]), + Search([1f, 0f, 0f, 0f, 1f]), + ]); + } + + private static VectorCase ZeroMagnitudeQuery() + { + const int f = 0x79; + var (a, b) = (Record(f, 1), Record(f, 2)); + return new( + "A zero-magnitude query vector is valid, but has no direction: every distance is NaN (written null) and every " + + "relevance 0, and the rows are in experience ID order.", + [a, b], + [ + Write(b, [1f, 0f, 0f, 0f], T1), + Write(a, [0f, 1f, 0f, 0f], T1), + Search([0f, 0f, 0f, 0f]), + ]); + } + + private static VectorCase ZeroMagnitudeStoredVector() + { + const int f = 0x7b; + var (zero, same, orthogonal) = (Record(f, 1), Record(f, 2), Record(f, 3)); + return new( + "A stored zero-magnitude vector is valid, but has no direction: against a normal query its distance is NaN (written " + + "null) and its relevance 0, and it comes after every row with a number for a distance (PostgreSQL orders NaN after " + + "every number), whatever its ID.", + [zero, same, orthogonal], + [ + Write(zero, [0f, 0f, 0f, 0f], T1), + Write(same, [1f, 0f, 0f, 0f], T1), + Write(orthogonal, [0f, 1f, 0f, 0f], T1), + Search([1f, 0f, 0f, 0f]), + ]); + } + + private static ExperienceRecord Record(int family, int n) => + SearchContract.Seed(family, n, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt."); + + /// A write of for , its content hash the record's summary's. + private static WriteEmbeddingStep Write(ExperienceRecord record, float[] vector, DateTimeOffset now, long? revision = null, string? model = null) + { + var modelId = model ?? Model; + return new( + record.ExperienceId, + record.Scope, + modelId, + vector.Length, + ExperienceEmbeddingDescriptor.ComputeContentHash(modelId, ExperienceRetrievalSummary.For(record)), + revision ?? record.Revision, + vector, + now); + } + + private static VectorSearchStep Search(float[] vector) => new( + Fixed.Scope, + Model, + vector, + ExperienceRetrievalService.EligibleStatuses, + RetrievalPolicy.DefaultMinimumConfidence, + RetrievalPolicy.DefaultCandidateLimit + 1, + ExcludeModelAuthored: false); + + /// seed.json: the records as stored. + public static JsonArray ToSeedJson(VectorCase definition) => SearchContract.ToSeedJson(definition.Records); + + /// steps.json: the description and the steps, in order. + public static JsonObject ToStepsJson(VectorCase definition) => new() + { + ["description"] = definition.Description, + ["steps"] = new JsonArray([.. definition.Steps.Select(step => (JsonNode)(step switch + { + WriteEmbeddingStep write => new JsonObject { ["write"] = JsonSerializer.SerializeToNode(write, ContractJson.Options) }, + VectorSearchStep search => new JsonObject { ["search"] = JsonSerializer.SerializeToNode(search, ContractJson.Options) }, + SetRevisionStep set => new JsonObject { ["setRevision"] = JsonSerializer.SerializeToNode(set, ContractJson.Options) }, + EraseEmbeddingStep erase => InjectionRecordContract.StepJson(erase.Erase), + _ => throw new InvalidOperationException($"Unknown step {step}."), + }))]), + }; + + /// Reads steps.json back: the steps a runner runs. + public static IReadOnlyList ReadSteps(JsonNode steps) => + [ + .. steps["steps"]!.AsArray().Select(step => step!["write"] is { } write + ? ContractJson.Read(write.ToJsonString()) + : step["search"] is { } search + ? ContractJson.Read(search.ToJsonString()) + : step["setRevision"] is { } set + ? ContractJson.Read(set.ToJsonString()) + : (VectorStep)new EraseEmbeddingStep((EraseStep)InjectionRecordContract.ReadStep(step))), + ]; + + /// A vector as a pgvector literal, [x,y,...], each component's shortest round-trip text. + public static string Literal(float[] vector) => + "[" + string.Join(",", vector.Select(component => component.ToString("R", CultureInfo.InvariantCulture))) + "]"; + + /// The contract's relevance for a cosine distance: clamp(1 - d / 2, 0, 1), and 0 for NaN. + public static double Relevance(double distance) => double.IsNaN(distance) ? 0d : Math.Clamp(1d - (distance / 2d), 0d, 1d); +} diff --git a/tests/AgentExperience.Contract.Tests/VectorContractFixtureTests.cs b/tests/AgentExperience.Contract.Tests/VectorContractFixtureTests.cs new file mode 100644 index 0000000..54f6ac6 --- /dev/null +++ b/tests/AgentExperience.Contract.Tests/VectorContractFixtureTests.cs @@ -0,0 +1,172 @@ +using System.Globalization; +using System.Text.Json.Nodes; +using AgentExperience.Abstractions; +using AgentExperience.Storage.Postgres; + +namespace AgentExperience.Contract.Tests; + +/// +/// Contract 1.6's checks that need no database: the published statements are the .NET index's, line for line; the 1.0, +/// 1.4 and 1.6 privileges files together are the .NET privileges call's grants; and the embedding-summary and +/// hybrid-merge fixtures are what the library computes from their checked-in inputs. +/// +public sealed partial class ContractFixtureTests +{ + [Fact] + public void The_embedding_write_statements_are_the_ones_the_index_runs() + { + var statements = VectorStatements.Read(VectorStatements.WriteFile); + Assert.Equal(new[] { "write", "probe-revision" }, statements.Keys.ToArray()); + Assert.Equal(VectorStatements.DotnetWrite, OneLine(statements["write"])); + Assert.Equal(VectorStatements.DotnetProbeRevision, OneLine(statements["probe-revision"])); + } + + [Fact] + public void The_vector_search_statements_are_the_ones_the_index_runs_for_every_dimension() + { + var statements = VectorStatements.Read(VectorStatements.SearchFile); + Assert.Equal(new[] { "search", "search-excluding-model-authored", "compatibility", "compatibility-excluding-model-authored" }, statements.Keys.ToArray()); + foreach (var dimension in new[] { 1, 4, 5, 1536, ExperienceEmbeddingDescriptor.MaxDimension }) + { + Assert.Equal(VectorStatements.DotnetSearch(dimension, excludeModelAuthored: false), OneLine(VectorStatements.WithDimension(statements["search"], dimension))); + Assert.Equal(VectorStatements.DotnetSearch(dimension, excludeModelAuthored: true), OneLine(VectorStatements.WithDimension(statements["search-excluding-model-authored"], dimension))); + } + + Assert.Equal(VectorStatements.DotnetCompatibility(excludeModelAuthored: false), OneLine(statements["compatibility"])); + Assert.Equal(VectorStatements.DotnetCompatibility(excludeModelAuthored: true), OneLine(statements["compatibility-excluding-model-authored"])); + Assert.DoesNotContain(VectorStatements.DimensionPlaceholder, statements["compatibility"], StringComparison.Ordinal); + } + + [Fact] + public void The_1_0_1_4_and_1_6_privileges_files_together_are_what_the_migrator_grants_on_a_schema_with_the_embeddings_table() + { + // On a schema migrated to 0026 or later with the vectors migrations: every non-optional table, the injection table and + // the embeddings table exist, and none has row-level security yet. The 1.0, 1.4 and 1.6 files are the .NET call's + // grants, each mode in turn; grants commute, so the set is compared, and neither side may repeat a statement. + const string role = ":\"application_role\""; + var tables = ApplicationRolePrivileges.Tables + .Where(table => !table.Optional || table.Name is ApplicationRolePrivileges.InjectionsTable or ApplicationRolePrivileges.EmbeddingsTable) + .Select(table => table.Name) + .ToHashSet(StringComparer.Ordinal); + Assert.Contains(ApplicationRolePrivileges.EmbeddingsTable, tables); + var fresh = tables.ToDictionary( + table => table, + _ => new ApplicationRolePrivileges.RowSecurityState(Enabled: false, Forced: false, Policies: []), + StringComparer.Ordinal); + var (always, rowLevelSecurity) = PrivilegeStatements(ContractFiles.Read("sql/application-role-privileges.sql")!); + var (always14, rowLevelSecurity14) = PrivilegeStatements(ContractFiles.Read("sql/application-role-privileges-1.4.sql")!); + var (always16, rowLevelSecurity16) = PrivilegeStatements(ContractFiles.Read(VectorStatements.PrivilegesFile)!); + Assert.NotEmpty(always16); + Assert.NotEmpty(rowLevelSecurity16); + Assert.All( + [.. always16, .. rowLevelSecurity16], + statement => Assert.Contains("agent_experience.experience_embeddings", statement, StringComparison.Ordinal)); + + foreach (var enable in new[] { false, true }) + { + var options = new ExperienceApplicationRoleOptions("contract_app") { EnableRowLevelSecurity = enable }; + List expected = + [ + .. ApplicationRolePrivileges.Statements(role, tables, options), + .. ApplicationRolePrivileges.RowLevelSecurityStatements(fresh, tables, enable), + ]; + List contract = enable + ? [.. always, .. rowLevelSecurity, .. always14, .. rowLevelSecurity14, .. always16, .. rowLevelSecurity16] + : [.. always, .. always14, .. always16]; + Assert.Equal(expected.Count, expected.Distinct(StringComparer.Ordinal).Count()); + Assert.Equal(contract.Count, contract.Distinct(StringComparer.Ordinal).Count()); + Assert.Equal(expected.Order(StringComparer.Ordinal), contract.Order(StringComparer.Ordinal)); + } + } + + [Fact] + public void The_embedding_summary_fixture_is_what_the_library_derives_and_hashes() + { + var drift = new List(); + var path = $"fixtures/{EmbeddingSummaryContract.Family}/{EmbeddingSummaryContract.FileName}"; + ContractFiles.Verify(path, ContractJson.Write(EmbeddingSummaryContract.Run()), drift); + + if (ContractFiles.Read(path) is { } text) + { + foreach (var (entry, index) in ContractJson.Read>(text).Select((entry, index) => (entry, index))) + { + var again = EmbeddingSummaryContract.Rerun(entry); + if (again != entry) + { + drift.Add($"{path}[{index}]: the library now derives '{again.Summary}' ({again.ContentHash})."); + } + } + } + + ContractFiles.VerifyNoStray(EmbeddingSummaryContract.Family, [], [], drift, EmbeddingSummaryContract.FileName); + Relock(drift); + ContractFiles.AssertNoDrift(drift); + } + + [Fact] + public void A_summary_separator_is_exactly_a_character_of_category_Cc_Zs_Zl_or_Zp() + { + // README-1.6 states the summary's separator rule by Unicode general category, so a port need not know .NET's + // char.IsWhiteSpace: this pins that the two are the same set, over every UTF-16 code unit. + for (var unit = 0; unit <= char.MaxValue; unit++) + { + var c = (char)unit; + var category = char.GetUnicodeCategory(c); + var byCategory = category is System.Globalization.UnicodeCategory.Control or System.Globalization.UnicodeCategory.SpaceSeparator + or System.Globalization.UnicodeCategory.LineSeparator or System.Globalization.UnicodeCategory.ParagraphSeparator; + Assert.True(byCategory == (char.IsWhiteSpace(c) || char.IsControl(c)), $"U+{unit:X4}"); + } + } + + [Fact] + public void The_hybrid_merge_fixture_is_what_the_retrieval_service_floors_and_merges() + { + var drift = new List(); + var path = $"fixtures/{HybridMergeContract.Family}/{HybridMergeContract.FileName}"; + ContractFiles.Verify(path, ContractJson.Write(HybridMergeContract.Run()), drift); + + if (ContractFiles.Read(path) is { } text) + { + foreach (var (entry, index) in JsonNode.Parse(text)!.AsArray().Select((entry, index) => (entry, index))) + { + var again = JsonNode.Parse(ContractJson.Write(HybridMergeContract.Rerun(ContractJson.Read(entry!.ToJsonString())))); + if (!JsonNode.DeepEquals(again!["merged"], entry["merged"])) + { + drift.Add($"{path}[{index.ToString(CultureInfo.InvariantCulture)}]: the retrieval service now merges {again["merged"]!.ToJsonString()}."); + } + } + } + + ContractFiles.VerifyNoStray(HybridMergeContract.Family, [], [], drift, HybridMergeContract.FileName); + Relock(drift); + ContractFiles.AssertNoDrift(drift); + } + + [Fact] + public void Every_vector_seed_record_validates_against_the_schema() + { + // The embedding-write and vector-search families' results need PostgreSQL with pgvector; their seeds do not. + var problems = new List(); + var directories = VectorContract.WriteCases.Select(entry => $"fixtures/{VectorContract.WriteFamily}/{entry.Name}") + .Concat(VectorContract.SearchCases.Select(entry => $"fixtures/{VectorContract.SearchFamily}/{entry.Name}")); + foreach (var directory in directories) + { + var seed = ContractFiles.Read($"{directory}/seed.json"); + Assert.True(seed is not null, $"{directory}/seed.json is missing; run the PostgreSQL contract tests with {ContractFiles.UpdateVariable}=1."); + foreach (var (entry, index) in JsonNode.Parse(seed)!.AsArray().Select((entry, index) => (entry, index))) + { + RecordSchema.Check($"{directory}/seed.json[{index}]", entry!, problems); + } + } + + Assert.True(problems.Count == 0, string.Join("\n", problems)); + } + + /// A contract statement as one line: its lines joined by one space, which is the .NET statement exactly. + private static string OneLine(string statement) + { + Assert.DoesNotContain(" ", statement, StringComparison.Ordinal); + Assert.All(statement.Split('\n'), line => Assert.True(line.Length > 0 && line.Trim() == line, $"A blank or padded statement line: '{line}'")); + return statement.Replace('\n', ' '); + } +} diff --git a/tests/AgentExperience.Contract.Tests/VectorContractPostgresTests.cs b/tests/AgentExperience.Contract.Tests/VectorContractPostgresTests.cs new file mode 100644 index 0000000..090679e --- /dev/null +++ b/tests/AgentExperience.Contract.Tests/VectorContractPostgresTests.cs @@ -0,0 +1,390 @@ +using System.Globalization; +using System.Text.Json.Nodes; +using AgentExperience.Abstractions; +using AgentExperience.Storage.Postgres; +using AgentExperience.Storage.Postgres.Vectors; +using Npgsql; +using NpgsqlTypes; + +namespace AgentExperience.Contract.Tests; + +/// +/// The conformance runner for contract 1.6's PostgreSQL families, embedding-write and vector-search, against +/// a schema the .NET migrator and the vectors migrator applied, with row-level security on. Each case runs twice, each time +/// in a database of its own: once with its writes through the .NET embedding index (which generates +/// expected.json), and once with the contract's write statements run exactly as written, as a port runs them. Both +/// must give the checked-in results. Every search step runs the contract's search statements as written, and the .NET +/// index's search must agree with them. +/// +[Collection(ContractVectorPostgresCollection.Name)] +public sealed class VectorContractPostgresTests(ContractVectorPostgresDatabase database) +{ + [Fact] + public async Task Every_embedding_write_fixture_is_what_the_dotnet_index_and_the_contract_statements_give() => + await RunFamilyAsync(VectorContract.WriteFamily, "ew", VectorContract.WriteCases); + + [Fact] + public async Task Every_vector_search_fixture_is_what_the_dotnet_index_and_the_contract_statements_give() => + await RunFamilyAsync(VectorContract.SearchFamily, "vs", VectorContract.SearchCases); + + [Fact] + public async Task The_1_6_contract_script_after_the_1_0_and_1_4_scripts_grants_the_embeddings_table() + { + const string CaseName = "privileges-order-1-6"; + var application = await database.CreateAsync(CaseName); + var owner = database.OwnerOf(CaseName); + const string CanRead = "SELECT has_table_privilege(@role, 'agent_experience.experience_embeddings', 'SELECT')"; + + await ContractPrivilegesOrderPostgresTests.ApplyContractFileAsync(owner, "sql/application-role-privileges.sql"); + Assert.False(await ScalarAsync(owner, CanRead)); + await ContractPrivilegesOrderPostgresTests.ApplyContractFileAsync(owner, "sql/application-role-privileges-1.4.sql"); + Assert.False(await ScalarAsync(owner, CanRead)); + await ContractPrivilegesOrderPostgresTests.ApplyContractFileAsync(owner, VectorStatements.PrivilegesFile); + Assert.True(await ScalarAsync(owner, CanRead)); + + // The contract write and the .NET index both work for the application role, and the .NET call after the three files + // changes nothing it would refuse: the three files are exactly its grants. + var record = SearchContract.Seed(0x7a, 1, "rotate-keys", "Rotate the signing keys", "Verified after 1 attempt."); + await SearchContractPostgresTests.InsertAsync(application, ContractJson.ToStored(record)); + var write = new WriteEmbeddingStep(record.ExperienceId, record.Scope, VectorContract.Model, 4, "hash", record.Revision, [1f, 0f, 0f, 0f], Fixed.T0); + Assert.Equal("Written", (await WriteAsWrittenAsync(application, write))["outcome"]!.GetValue()); + await ExperienceSchemaMigrator.ApplyApplicationRolePrivilegesAsync( + owner, + new ExperienceApplicationRoleOptions(ContractPostgresDatabase.ApplicationRole) { EnableRowLevelSecurity = true }, + CancellationToken.None); + Assert.Equal("Written", (await WriteThroughIndexAsync(application, write with { Vector = [0f, 1f, 0f, 0f] }))["outcome"]!.GetValue()); + } + + private async Task RunFamilyAsync(string family, string prefix, IReadOnlyList<(string Name, VectorCase Case)> cases) + { + var drift = new List(); + foreach (var (name, definition) in cases) + { + var directory = $"fixtures/{family}/{name}"; + var seedJson = ContractJson.Write(VectorContract.ToSeedJson(definition)); + var stepsJson = ContractJson.Write(VectorContract.ToStepsJson(definition)); + ContractFiles.Verify($"{directory}/seed.json", seedJson, drift); + ContractFiles.Verify($"{directory}/steps.json", stepsJson, drift); + + var seed = JsonNode.Parse(ContractFiles.Read($"{directory}/seed.json") ?? seedJson)!.AsArray(); + var steps = VectorContract.ReadSteps(JsonNode.Parse(ContractFiles.Read($"{directory}/steps.json") ?? stepsJson)!); + + var byIndex = await RunAsync($"{prefix}_{name}_net", seed, steps, throughIndex: true); + ContractFiles.Verify($"{directory}/expected.json", ContractJson.Write(byIndex), drift); + + var byStatement = await RunAsync($"{prefix}_{name}_sql", seed, steps, throughIndex: false); + if (!JsonNode.DeepEquals(byIndex, byStatement)) + { + drift.Add($"{directory}: the contract statements give {byStatement.ToJsonString()}, the .NET index {byIndex.ToJsonString()}."); + } + } + + ContractFiles.VerifyNoStray(family, cases.Select(entry => entry.Name), ["seed.json", "steps.json", "expected.json"], drift); + if (ContractFiles.Updating) + { + ContractLock.Relock(drift); + } + + ContractFiles.AssertNoDrift(drift); + } + + /// Seeds a fresh database, runs the steps, and returns what expected.json holds. + private async Task RunAsync(string caseName, JsonArray seed, IReadOnlyList steps, bool throughIndex) + { + var name = caseName.Replace('-', '_'); + var application = await database.CreateAsync(name); + var owner = database.OwnerOf(name); + foreach (var record in seed) + { + await SearchContractPostgresTests.InsertAsync(application, record!); + } + + var results = new JsonArray(); + foreach (var step in steps) + { + switch (step) + { + case WriteEmbeddingStep write: + results.Add(throughIndex ? await WriteThroughIndexAsync(application, write) : await WriteAsWrittenAsync(application, write)); + break; + case VectorSearchStep search: + results.Add(await SearchAsync(application, search)); + break; + case SetRevisionStep set: + await using (var command = owner.CreateCommand("UPDATE agent_experience.experience_records SET revision = @revision WHERE experience_id = @id")) + { + command.Parameters.Add(new("revision", NpgsqlDbType.Bigint) { Value = set.Revision }); + command.Parameters.Add(new("id", NpgsqlDbType.Uuid) { Value = set.ExperienceId }); + Assert.Equal(1, await command.ExecuteNonQueryAsync()); + } + + results.Add(null); + break; + case EraseEmbeddingStep erase: + await InjectionRecordContractPostgresTests.EraseAsync(owner, erase.Erase); + results.Add(null); + break; + default: + throw new InvalidOperationException($"Unknown step {step}."); + } + } + + return new JsonObject + { + ["steps"] = results, + ["experience_embeddings"] = await RowsAsync(owner), + }; + } + + /// The .NET index's write, its clock fixed at the step's instant. + private static async Task WriteThroughIndexAsync(NpgsqlDataSource application, WriteEmbeddingStep write) + { + var index = new PostgresExperienceEmbeddingIndex(application, timeProvider: new FixedTimeProvider(write.Now)); + var result = await index.WriteAsync( + SearchContractPostgresTests.Authorization(write.Scope), + new ExperienceIndexWrite( + write.Scope, + write.ExperienceId, + new ExperienceEmbeddingDescriptor(write.ModelId, write.Dimension, write.ContentHash, write.SourceRevision), + write.Vector), + CancellationToken.None); + return result.Outcome switch + { + ExperienceIndexOutcome.Written => WriteResult("Written", null), + ExperienceIndexOutcome.Stale => WriteResult("Stale", result.CurrentRevision), + ExperienceIndexOutcome.Missing => WriteResult("Missing", null), + _ => throw new Xunit.Sdk.XunitException($"The .NET index answered {result.Outcome} for a contract write."), + }; + } + + /// + /// declare-bounds.sql for the write's scope, then write-experience-embedding.sql's write as written, + /// and when it wrote nothing its probe-revision, in the same transaction. + /// + private static async Task WriteAsWrittenAsync(NpgsqlDataSource application, WriteEmbeddingStep write) + { + var statements = VectorStatements.Read(VectorStatements.WriteFile); + await using var connection = await application.OpenConnectionAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + await DeclareAsync(connection, transaction, write.Scope); + + int written; + await using (var command = new NpgsqlCommand(statements["write"], connection, transaction)) + { + AddScope(command.Parameters, write.Scope); + var ticks = write.Now.UtcTicks; + command.Parameters.Add(new("experience_id", NpgsqlDbType.Uuid) { Value = write.ExperienceId }); + command.Parameters.Add(new("model_id", NpgsqlDbType.Text) { Value = write.ModelId }); + command.Parameters.Add(new("dimension", NpgsqlDbType.Integer) { Value = write.Dimension }); + command.Parameters.Add(new("content_hash", NpgsqlDbType.Text) { Value = write.ContentHash }); + command.Parameters.Add(new("source_revision", NpgsqlDbType.Bigint) { Value = write.SourceRevision }); + command.Parameters.Add(new("embedding", NpgsqlDbType.Text) { Value = VectorContract.Literal(write.Vector) }); + command.Parameters.Add(new("now", NpgsqlDbType.TimestampTz) { Value = new DateTimeOffset(ticks - (ticks % 10), TimeSpan.Zero) }); + written = await command.ExecuteNonQueryAsync(); + } + + JsonObject result; + if (written > 0) + { + result = WriteResult("Written", null); + } + else + { + await using var probe = new NpgsqlCommand(statements["probe-revision"], connection, transaction); + AddScope(probe.Parameters, write.Scope); + probe.Parameters.Add(new("experience_id", NpgsqlDbType.Uuid) { Value = write.ExperienceId }); + result = await probe.ExecuteScalarAsync() is long revision ? WriteResult("Stale", revision) : WriteResult("Missing", null); + } + + await transaction.CommitAsync(); + return result; + } + + private static JsonObject WriteResult(string outcome, long? currentRevision) + { + var result = new JsonObject { ["outcome"] = outcome }; + if (currentRevision is { } revision) + { + result["currentRevision"] = revision; + } + + return result; + } + + /// + /// declare-bounds.sql, then the contract's search statement as written (its {dim} replaced), and when it + /// returned nothing the matching compatibility probe; the rows ordered by distance, then by ID. The .NET index's search + /// must answer the same outcome and the same records with the same relevances. + /// + private static async Task SearchAsync(NpgsqlDataSource application, VectorSearchStep search) + { + var statements = VectorStatements.Read(VectorStatements.SearchFile); + var statuses = search.Statuses.Select(status => status.ToString()).ToArray(); + var rows = new List<(Guid Id, double Distance)>(); + string outcome; + await using (var connection = await application.OpenConnectionAsync()) + await using (var transaction = await connection.BeginTransactionAsync()) + { + await DeclareAsync(connection, transaction, search.Scope); + var sql = VectorStatements.WithDimension( + statements[search.ExcludeModelAuthored ? "search-excluding-model-authored" : "search"], + search.Vector.Length); + await using (var command = new NpgsqlCommand(sql, connection, transaction)) + { + AddSearch(command.Parameters, search, statuses); + command.Parameters.Add(new("query_vector", NpgsqlDbType.Text) { Value = VectorContract.Literal(search.Vector) }); + command.Parameters.Add(new("limit", NpgsqlDbType.Integer) { Value = search.Limit }); + await using var reader = await command.ExecuteReaderAsync(); + Assert.Equal("experience_id", reader.GetName(0)); + var distance = reader.GetOrdinal("distance"); + while (await reader.ReadAsync()) + { + rows.Add((reader.GetGuid(0), reader.GetDouble(distance))); + } + } + + if (rows.Count > 0) + { + outcome = nameof(ExperienceVectorSearchOutcome.Found); + } + else + { + var probeSql = statements[search.ExcludeModelAuthored ? "compatibility-excluding-model-authored" : "compatibility"]; + Assert.DoesNotContain(VectorStatements.DimensionPlaceholder, probeSql, StringComparison.Ordinal); + await using var probe = new NpgsqlCommand(probeSql, connection, transaction); + AddSearch(probe.Parameters, search, statuses); + await using var reader = await probe.ExecuteReaderAsync(); + Assert.True(await reader.ReadAsync()); + var (anything, thisModel) = (reader.GetBoolean(0), reader.GetBoolean(1)); + outcome = !anything + ? nameof(ExperienceVectorSearchOutcome.Found) + : thisModel ? nameof(ExperienceVectorSearchOutcome.DimensionMismatch) : nameof(ExperienceVectorSearchOutcome.ModelMismatch); + } + + await transaction.CommitAsync(); + } + + // No tie-break in SQL: equal distances (NaN equal to NaN, after every number) are ordered by ID. + var ordered = rows + .OrderBy(row => double.IsNaN(row.Distance) ? 1 : 0) + .ThenBy(row => double.IsNaN(row.Distance) ? 0d : row.Distance) + .ThenBy(row => row.Id.ToString("D", CultureInfo.InvariantCulture), StringComparer.Ordinal) + .ToList(); + + var dotnet = await new PostgresExperienceEmbeddingIndex(application).SearchAsync( + SearchContractPostgresTests.Authorization(search.Scope), + new ExperienceVectorQuery(search.Scope, search.ModelId, search.Vector, search.Statuses, search.MinConfidence, search.Limit) + { + ExcludeModelAuthored = search.ExcludeModelAuthored, + }, + CancellationToken.None); + Assert.Equal(outcome, dotnet.Outcome.ToString()); + Assert.Equal( + ByRelevance(ordered.Select(row => (row.Id, VectorContract.Relevance(row.Distance)))), + ByRelevance(dotnet.Candidates.Select(candidate => (candidate.Record.ExperienceId, candidate.Relevance)))); + + return new JsonObject + { + ["outcome"] = outcome, + ["rows"] = new JsonArray([.. ordered.Select(row => (JsonNode)new JsonObject + { + ["experienceId"] = row.Id.ToString("D", CultureInfo.InvariantCulture), + ["distance"] = double.IsNaN(row.Distance) ? null : JsonValue.Create(row.Distance), + ["relevance"] = VectorContract.Relevance(row.Distance), + })]), + }; + + static List<(Guid, double)> ByRelevance(IEnumerable<(Guid Id, double Relevance)> entries) => + [ + .. entries.OrderByDescending(entry => entry.Relevance) + .ThenBy(entry => entry.Id.ToString("D", CultureInfo.InvariantCulture), StringComparer.Ordinal) + .Select(entry => (entry.Id, entry.Relevance)), + ]; + } + + /// Every row of agent_experience.experience_embeddings, read as the owner, ordered by experience_id, each column in PostgreSQL's text form in UTC. + private static async Task RowsAsync(NpgsqlDataSource owner) + { + string[] columns = + [ + "experience_id", "tenant_id", "application_id", "project_id", "team_id", "agent_id", "user_id", "model_id", "dimension", + "content_hash", "source_revision", "embedding", "created_at", "updated_at", + ]; + await using var connection = await owner.OpenConnectionAsync(); + await using (var zone = new NpgsqlCommand("SET TIME ZONE 'UTC'", connection)) + { + await zone.ExecuteNonQueryAsync(); + } + + await using var select = new NpgsqlCommand( + "SELECT " + string.Join(", ", columns.Select(column => column + "::text")) + + " FROM agent_experience.experience_embeddings ORDER BY experience_id", + connection); + var rows = new JsonArray(); + await using var reader = await select.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + var row = new JsonObject(); + for (var i = 0; i < columns.Length; i++) + { + row[columns[i]] = reader.IsDBNull(i) ? null : reader.GetString(i); + } + + rows.Add(row); + } + + return rows; + } + + private static void AddSearch(NpgsqlParameterCollection parameters, VectorSearchStep search, string[] statuses) + { + AddScope(parameters, search.Scope); + parameters.Add(new("statuses", NpgsqlDbType.Array | NpgsqlDbType.Text) { Value = statuses }); + parameters.Add(new("min_confidence", NpgsqlDbType.Double) { Value = search.MinConfidence }); + parameters.Add(new("model_id", NpgsqlDbType.Text) { Value = search.ModelId }); + } + + private static void AddScope(NpgsqlParameterCollection parameters, Scope scope) + { + parameters.Add(new("tenant_id", NpgsqlDbType.Text) { Value = scope.TenantId }); + parameters.Add(new("application_id", NpgsqlDbType.Text) { Value = scope.ApplicationId }); + parameters.Add(new("project_id", NpgsqlDbType.Text) { Value = scope.ProjectId }); + parameters.Add(new("team_id", NpgsqlDbType.Text) { Value = (object?)scope.TeamId ?? DBNull.Value }); + parameters.Add(new("agent_id", NpgsqlDbType.Text) { Value = (object?)scope.AgentId ?? DBNull.Value }); + parameters.Add(new("user_id", NpgsqlDbType.Text) { Value = (object?)scope.UserId ?? DBNull.Value }); + } + + private static async Task DeclareAsync(NpgsqlConnection connection, NpgsqlTransaction transaction, Scope bounds) + { + await using var declare = new NpgsqlCommand(ContractFiles.Read("sql/declare-bounds.sql"), connection, transaction); + declare.Parameters.Add(new("auth_tenant", NpgsqlDbType.Text) { Value = "=" + bounds.TenantId }); + declare.Parameters.Add(new("auth_application", NpgsqlDbType.Text) { Value = Bound(bounds.ApplicationId) }); + declare.Parameters.Add(new("auth_project", NpgsqlDbType.Text) { Value = Bound(bounds.ProjectId) }); + declare.Parameters.Add(new("auth_team", NpgsqlDbType.Text) { Value = Bound(bounds.TeamId) }); + declare.Parameters.Add(new("auth_agent", NpgsqlDbType.Text) { Value = Bound(bounds.AgentId) }); + declare.Parameters.Add(new("auth_user", NpgsqlDbType.Text) { Value = Bound(bounds.UserId) }); + await declare.ExecuteNonQueryAsync(); + } + + private static string Bound(string? value) => value is null ? string.Empty : "=" + value; + + private static async Task ScalarAsync(NpgsqlDataSource source, string sql) + { + await using var command = source.CreateCommand(sql); + command.Parameters.Add(new NpgsqlParameter("role", ContractPostgresDatabase.ApplicationRole)); + return (T)(await command.ExecuteScalarAsync())!; + } +} + +/// The one pgvector container every contract 1.6 PostgreSQL test shares. +[CollectionDefinition(Name)] +public sealed class ContractVectorPostgresCollection : ICollectionFixture +{ + public const string Name = "Contract PostgreSQL with pgvector"; +} + +/// +/// A pgvector PostgreSQL container for contract 1.6: every case's database migrated by the .NET migrator and the vectors +/// migrator, with the application role's privileges applied, row-level security on. +/// +public sealed class ContractVectorPostgresDatabase() : ContractPostgresDatabase(AgentExperience.Tests.Shared.PostgresTestImage.Pgvector, vectors: true); diff --git a/tests/AgentExperience.Contract.Tests/packages.lock.json b/tests/AgentExperience.Contract.Tests/packages.lock.json index 5670be0..0557e42 100644 --- a/tests/AgentExperience.Contract.Tests/packages.lock.json +++ b/tests/AgentExperience.Contract.Tests/packages.lock.json @@ -391,6 +391,14 @@ "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, + "Pgvector": { + "type": "Transitive", + "resolved": "0.3.2", + "contentHash": "n7M5LuNejHUmtWky3zCbNO+tP1Gnjiuv9Qtu4LyvB1602dD8RiBxxCQp9jEjM0ZFDxAZF1oOWkNIkXw46KT00Q==", + "dependencies": { + "Npgsql": "8.0.5" + } + }, "SharpZipLib": { "type": "Transitive", "resolved": "1.4.2", @@ -498,6 +506,16 @@ "dbup-core": "[6.1.1, )", "dbup-postgresql": "[7.0.1, )" } + }, + "agentexperience.storage.postgres.vectors": { + "type": "Project", + "dependencies": { + "AgentExperience.Storage.Postgres": "[0.1.0-preview.14, )", + "Microsoft.Extensions.AI.Abstractions": "[10.10.0, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.12, )", + "Npgsql": "[10.0.3, )", + "Pgvector": "[0.3.2, )" + } } } }