+ These operator-curated collections freeze a finite set of sealed public
+ non-calibration Run IDs. Dynamic filters may find newer Runs later,
+ but an existing collection never silently changes membership.
+
{collection?.title ?? "Research collection not found"}
+
+ {collection?.description ??
+ "The collection manifest records exact sealed Run identities, not an automatically refreshing query."}
+
+
+ {collection ? (
+ <>
+
+
+
REPRODUCIBILITY
Manifest & exports
+ {collection.runIds.length} captured Runs
+
+
+ Captured {collection.createdAt} · scope: sealed public non-calibration
+ · selection limit {collection.selectionLimit}. The manifest stays unchanged
+ when newer Runs arrive. Comparability and calibration eligibility have
+ not been evaluated for this collection.
+
+ Research filters above are live and can find new Runs. Saving creates a
+ public immutable snapshot of the current matching Run IDs (1–50 only).
+ Capture requires operator access. Empty or oversized slices are rejected
+ rather than silently truncated. Collections never include calibration Runs.
+
+
+ Operator · capture this filtered slice
+
+
+
+
+
+
+ Saved manifests store no Provider credentials or account identifiers.
+
: null}
From 1bc26dbcc190cb6a1085bee970597506a3cf8dd9 Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:38:50 +0800
Subject: [PATCH 18/24] Run v0.27 research digest and export tests in CI
---
packages/persistence/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/persistence/package.json b/packages/persistence/package.json
index 932f3ce..10a3091 100644
--- a/packages/persistence/package.json
+++ b/packages/persistence/package.json
@@ -10,7 +10,7 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
- "test": "vitest run test/record-provider-run.test.ts test/archive-context-transitions.test.ts test/archive-research.test.ts",
+ "test": "vitest run test/record-provider-run.test.ts test/archive-context-transitions.test.ts test/archive-research.test.ts test/research-collections.test.ts",
"test:integration": "vitest run test/integration.test.ts test/comparability.integration.test.ts"
},
"dependencies": {
From b3e8c7ea88287fed509aa1d8adfa547f5952de9f Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:39:18 +0800
Subject: [PATCH 19/24] Document Archive v0.27 immutable snapshots and sourced
exports
---
docs/research-collections-exports.md | 92 ++++++++++++++++++++++++++++
1 file changed, 92 insertions(+)
create mode 100644 docs/research-collections-exports.md
diff --git a/docs/research-collections-exports.md b/docs/research-collections-exports.md
new file mode 100644
index 0000000..6db7733
--- /dev/null
+++ b/docs/research-collections-exports.md
@@ -0,0 +1,92 @@
+# Research Collections & Evidence Exports — Archive v0.27
+
+Archive v0.27 introduces **operator-curated, publicly shareable immutable Run selections** and sourced JSON/CSV exports.
+
+## What is saved?
+
+A dynamic Research Explorer URL (`/research?…`) is a query. As the Archive collects new Runs, repeating that query may return different records.
+
+A Research Collection (`/research/collections/:id`) freezes **a specific ordered list of up to 50 sealed Run IDs**, the original validated filters, title/note, creator label and capture time. Existing membership never changes as more Runs arrive.
+
+A collection is a *selection snapshot*, not a byte-for-byte copy of the entire public Run view. It references sealed, immutable Run facts, qualification envelopes and cost evidence. Derived policy interpretations may evolve separately. Export includes a content digest for the selection manifest, not a hash of the exported JSON/CSV bytes.
+
+Creation fails closed when:
+
+- no matching Run exists;
+- more than 50 matching Runs exist (no silent first-page truncation);
+- a caller supplies a pagination cursor (a partial page is not a complete slice);
+- filter, title or creator metadata is invalid;
+- one of the selected IDs is not a completed, sealed, public, non-calibration Run.
+
+## Control and public endpoints
+
+**Operator-only, token authenticated:**
+
+`POST /v1/control/research/collections`
+
+Body example:
+
+```json
+{
+ "title": "US paid-account snapshot",
+ "description": "Bounded first-party Run selection",
+ "actor": "archive-operator",
+ "filters": {
+ "providerSlug": "deepseek",
+ "evidence": "E4+",
+ "region": "US",
+ "cost": "unknown"
+ }
+}
+```
+
+The API uses the same validated facets as `GET /v1/archive/research`, but caps the capture at 50 and refuses a partial result.
+
+**Public, read-only:**
+
+```text
+GET /v1/archive/research/collections?limit=30
+GET /v1/archive/research/collections/:collectionId
+GET /v1/archive/research/collections/:collectionId/export?format=json
+GET /v1/archive/research/collections/:collectionId/export?format=csv
+```
+
+The export response uses the appropriate MIME type and attachment filename.
+
+## Manifest identity
+
+`research_collections` is append-only. PostgreSQL validates every selected Run's sealed/public/non-calibration status on insert and rejects duplicate Run IDs.
+
+The SHA-256 `contentSha256` uses canonical JSON of:
+
+- manifest schema version;
+- normalized Provider, Model, Test, evidence, region, account, service tier and cost facets;
+- ordered Run UUIDs.
+
+It excludes mutable presentation details such as title and capture timestamp. The repository verifies the digest again when the collection is read. This hash proves the identity of **the selected filters and Run IDs**, not a cryptographic attestation by the Provider or a guarantee that the entire externally downloaded file has never been modified.
+
+## Exports and evidence language
+
+Each exported row retains immutable identifiers, execution path, completion time, E-level, deterministic evaluator outcome, region/account/effective service tier/service assurance, referenced first-party access/policy evidence source IDs, pricing source ID, native currency/cost estimate and qualification/cost caveats.
+
+JSON includes the full manifest, explicit interpretation scope and structured rows.
+
+CSV includes one flat sourced row per Run; all cells are quoted, embedded quotes escaped, and values beginning with spreadsheet formula triggers are prefixed to prevent formula execution in spreadsheet software.
+
+Cost unavailable remains empty or null, **never zero or free**. CSV/JSON is not a ranking, not a matched comparability set, and not proof of model degradation. Calibration Tests are excluded from snapshot membership; calibration eligibility itself is not silently inferred.
+
+## UI
+
+- `/research`: live filters plus a collapsed Operator-only capture form;
+- `/research/collections`: public recently published collections;
+- `/research/collections/:id`: fixed Run membership, manifest digest, filter replay link and JSON/CSV download.
+
+Replaying a collection's filters launches a **new live search**, not an update to the frozen collection. Public viewing does not require operator credentials. Provider secrets and personal account identifiers never enter the collection.
+
+## Tests and deployment
+
+Migration `0025_research_collections.sql` adds the append-only manifest table and Run status/visibility validation trigger. It is idempotently applied by the existing migration runner.
+
+Tests cover digest determinism, CSV formula escaping, public GET/export vs operator POST boundaries, re-open of a captured collection in PostgreSQL, cursor rejection and append-only/forged Run constraints.
+
+If a selection grows beyond 50, operators must narrow it (for example by Provider, exact Test, or historical context). Future phases may introduce explicit multi-page snapshot transactions, but may not silently save only a partial result.
From abf617d611f04584012cdd966c2e2b8317932b92 Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:39:20 +0800
Subject: [PATCH 20/24] ADR 0023: freeze research selection IDs, not live query
results
---
docs/adr/0023-immutable-research-selection.md | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 docs/adr/0023-immutable-research-selection.md
diff --git a/docs/adr/0023-immutable-research-selection.md b/docs/adr/0023-immutable-research-selection.md
new file mode 100644
index 0000000..0f63a6d
--- /dev/null
+++ b/docs/adr/0023-immutable-research-selection.md
@@ -0,0 +1,28 @@
+# ADR 0023 — Saved research collections freeze Run identity, not live query results
+
+## Status
+
+Accepted for Archive v0.27.
+
+## Context
+
+Archive v0.26 offers shareable filter URLs, but rerunning a query after new collection may return different Runs. A published research citation must not silently expand or drift.
+
+Anonymous persistence endpoints would also permit unauthenticated public content creation and abuse.
+
+## Decision
+
+1. Define research collections as append-only manifests of validated filters and **ordered sealed public non-calibration Run IDs**.
+2. Allow collection capture only through the operator-authenticated control API.
+3. Make saved collection reading and structured JSON/CSV export public.
+4. Cap captures at 50 Runs and reject empty, oversized, or cursor-restricted selections.
+5. Compute a canonical SHA-256 selection-manifest digest; verify it when reading.
+6. Validate snapshot membership with PostgreSQL trigger rules and prevent updates/deletes.
+7. Keep CSV exports safe from spreadsheet formula injection, preserve evidence source references and unknown cost as unknown.
+8. Distinguish query replay from fixed Run membership and from derived policy eligibility.
+9. Treat the manifest hash as selection identity, not as a provider-signed result or a hash of all response blobs.
+10. Never change the immutable Run, Evaluation, qualification or cost ledger as part of research snapshot creation.
+
+## Consequences
+
+Public citations can reference stable evidence selections while allowing live faceted discovery to evolve. Longer research collections need a future explicit bounded-batch protocol instead of truncating queries without disclosure.
From c648b169d4dbf8e36cb0089cf63ce4fff0275f53 Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:39:32 +0800
Subject: [PATCH 21/24] Mark v0.27 immutable collections and sourced exports
implemented; outline v0.28 research facets
---
docs/archive-roadmap.md | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/docs/archive-roadmap.md b/docs/archive-roadmap.md
index 6a1e370..7abbaa7 100644
--- a/docs/archive-roadmap.md
+++ b/docs/archive-roadmap.md
@@ -309,9 +309,17 @@ The new `/research` public surface supports server-side bounded browsing of seal
Every research row shows qualification and cost caveats and links into immutable Run, Model, Test, History and same-Test Compare. Reproducible URL filters, deterministic keyset pagination and dedicated indexes support broader discovery without ranking Models or equating collection volume with quality.
-### Archive v0.27+ — Extended research workflows
+### Archive v0.27 — Immutable Research Collections & Sourced Exports
-Consider saved/shareable research collections, sourced CSV/JSON exports, typed facet discovery, and richer policy-aware comparisons while preserving the evidence-first, no-aggregate-ranking boundary. Continue to make registration, account, region and calibration restrictions explicit.
+**Implemented:** see `docs/research-collections-exports.md` and ADR 0023.
+
+Operators can publish bounded, immutable collections of up to 50 sealed public non-calibration Run IDs. Every public collection retains the normalized query filters, an ordered Run manifest and a SHA-256 selection digest; new Runs never silently alter its membership.
+
+Public collection pages support sharing, live filter replay and source-backed JSON/CSV exports with qualification, access, pricing, region/tier and evidence caveats. Empty, oversized and cursor-partial captures fail closed. The export is not a comparability or quality ranking.
+
+### Archive v0.28+ — Typed Research Facets & Policy-Aware Workflows
+
+Next, consider facet discovery with observed counts, policy-version anchored research annotations and reproducible multi-page collection protocols. Explicitly distinguish current Provider policy from historical Run state; never aggregate heterogeneous Test results into a leaderboard.
## Scheduling principle
From ba3c119d63144352be7dcc704cb62dc17aaadf80 Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:39:57 +0800
Subject: [PATCH 22/24] Allow independent append-only captures of an identical
research selection
---
packages/database/migrations/0025_research_collections.sql | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/database/migrations/0025_research_collections.sql b/packages/database/migrations/0025_research_collections.sql
index 05e097c..be0f9ca 100644
--- a/packages/database/migrations/0025_research_collections.sql
+++ b/packages/database/migrations/0025_research_collections.sql
@@ -11,8 +11,7 @@ CREATE TABLE research_collections (
content_sha256 text NOT NULL CHECK (content_sha256 ~ '^[a-f0-9]{64}$'),
created_by text NOT NULL CHECK (char_length(btrim(created_by)) > 0),
created_at timestamptz NOT NULL DEFAULT now(),
- selection_limit integer NOT NULL DEFAULT 50 CHECK (selection_limit = 50),
- UNIQUE (content_sha256, title)
+ selection_limit integer NOT NULL DEFAULT 50 CHECK (selection_limit = 50)
);
CREATE INDEX research_collections_created_idx
From 1edb7a52c2861d5133dccf80813a922df3e4f9be Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:41:41 +0800
Subject: [PATCH 23/24] Complete strongly typed Run fixture for research export
tests
---
.../test/research-collections.test.ts | 45 +++++++++++++++----
1 file changed, 37 insertions(+), 8 deletions(-)
diff --git a/packages/persistence/test/research-collections.test.ts b/packages/persistence/test/research-collections.test.ts
index 0037f4d..43d41d6 100644
--- a/packages/persistence/test/research-collections.test.ts
+++ b/packages/persistence/test/research-collections.test.ts
@@ -8,9 +8,10 @@ import type { ArchiveRunView } from "../src/archive-repository.js";
const runId = "00000000-0000-4000-8000-000000000401";
const collectionId = "00000000-0000-4000-8000-000000000402";
-const mockRun = {
+const mockRun: ArchiveRunView = {
id: runId,
- provider: { slug: "=HYPERLINK(\"evil\")", name: "Fixture" },
+ status: "completed",
+ provider: { id: "00000000-0000-4000-8000-000000000405", slug: "=HYPERLINK(\"evil\")", name: "Fixture" },
model: { id: null, canonicalSlug: "test", marketingName: "Test" },
test: {
testCaseId: "00000000-0000-4000-8000-000000000403",
@@ -18,24 +19,52 @@ const mockRun = {
caseSlug: "exact",
},
requestedModel: "test",
+ returnedModel: "test",
+ runnerBuild: "research-export-test",
+ createdAt: "2026-10-08T00:00:00.000Z",
+ sealedAt: "2026-10-08T00:00:03.000Z",
completedAt: "2026-10-08T00:00:01.000Z",
evidenceLevel: "E4",
- evaluation: { status: "completed", exactMatch: true },
+ evaluation: {
+ id: "00000000-0000-4000-8000-000000000406",
+ status: "completed", exactMatch: true,
+ evaluatorSlug: "exact-text", evaluatorVersion: "1.0.0",
+ evaluatorKind: "deterministic", definitionSha256: "a".repeat(64),
+ rawResultSha256: null,
+ },
executionPath: "first_party_direct",
executionQualification: {
+ selectedAt: "2026-10-08T00:00:00.000Z",
+ executionEnvironment: null,
+ requestedServiceTier: "default",
+ contextKey: null,
executionRegion: "US", accountTier: "paid", serviceTier: "default",
returnedServiceTier: "default", serviceAssurance: "documented_default",
- providerPolicyObservation: { sourceId: "source-policy" },
- runnerAccessObservation: { sourceId: "source-runner" },
+ providerPolicyObservation: {
+ id: "00000000-0000-4000-8000-000000000407",
+ sourceId: "source-policy", accessState: "available",
+ },
+ runnerAccessObservation: {
+ id: "00000000-0000-4000-8000-000000000408",
+ sourceId: "source-runner", accessState: "available",
+ },
caveats: ["operator_uncertain"],
},
cost: {
- pricingObservation: { sourceId: "source-price" },
- pricing: { currency: "USD" },
+ selectedAt: "2026-10-08T00:00:00.000Z",
+ pricingObservation: {
+ id: "00000000-0000-4000-8000-000000000409",
+ sourceId: "source-price",
+ },
+ pricing: {
+ currency: "USD",
+ inputPerMillion: "1.0", outputPerMillion: "2.0", perRequest: null,
+ },
+ usage: null,
estimatedNativeCost: "0.001",
caveats: ["pricing_tier_generic"],
},
-} as ArchiveRunView;
+};
function collection(): ResearchCollectionDetail {
const filters = { providerSlug: "deepseek", evidence: "E4+" as const };
From f9d4d091a8d86a05a4793012232eaf4271f493a7 Mon Sep 17 00:00:00 2001
From: WangEn
Date: Thu, 8 Oct 2026 11:43:10 +0800
Subject: [PATCH 24/24] Complete Test identity in strongly typed research
export fixture
---
packages/persistence/test/research-collections.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/persistence/test/research-collections.test.ts b/packages/persistence/test/research-collections.test.ts
index 43d41d6..e70db1f 100644
--- a/packages/persistence/test/research-collections.test.ts
+++ b/packages/persistence/test/research-collections.test.ts
@@ -15,7 +15,7 @@ const mockRun: ArchiveRunView = {
model: { id: null, canonicalSlug: "test", marketingName: "Test" },
test: {
testCaseId: "00000000-0000-4000-8000-000000000403",
- familySlug: "smoke", variantSlug: "text", version: "1.0.0",
+ familySlug: "smoke", familyName: "Smoke", variantSlug: "text", variantName: "Text", version: "1.0.0",
caseSlug: "exact",
},
requestedModel: "test",