instar 1.3.48 → 1.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * HttpParitySource.ts — the live Phase-1/3 ParitySource adapter.
3
+ *
4
+ * Portal exposes `GET /api/instar/read` (Dawn, committed at d65136b3b6) as the
5
+ * read-only seam — the architectural pivot from raw Postgres after Prisma Data
6
+ * Platform refused to mint a database role (it forbids CREATE ROLE / GRANT). API
7
+ * level is actually a cleaner seam than direct DB access: the dry-run tests
8
+ * against the same contract Phase 2 (write/update APIs) will use, with no
9
+ * Prisma-admin dependency, and the architectural levels line up (Portal's HTTP
10
+ * surface ↔ Instar's {@link ParitySource} interface).
11
+ *
12
+ * This adapter implements ParitySource via that endpoint. The runner's seam,
13
+ * comparator, invariants, and JSONL audit trail are unchanged — only the read
14
+ * adapter swaps. Reads are PRE-FETCHED in {@link prepare}: the dry-run captures
15
+ * one snapshot of Portal's clusters at a moment in time, then compares against
16
+ * that frozen view. This makes the sync `readPortalClusters()` contract trivially
17
+ * satisfiable and stabilises the comparison against in-flight Portal writes
18
+ * (Portal remains the sole writer; we just look at one consistent slice).
19
+ *
20
+ * The endpoint is paginated (server-side cap: 1000 rows per request). The
21
+ * pagination is keyed off the feedback table — clusters/dispatches accompany each
22
+ * page and may repeat — so cluster collection deduplicates by clusterId across
23
+ * pages and stops when a page returns fewer feedback rows than the requested
24
+ * limit (the documented "no more pages" signal) OR after a hard safety cap.
25
+ *
26
+ * Field naming is intentionally tolerant: cluster rows may arrive as camelCase
27
+ * (Prisma default) or snake_case (raw SQL projection). The mapper accepts both.
28
+ */
29
+ import type { ParitySource } from './dryRunCompare.js';
30
+ import type { PortalCluster } from '../processor/parity.js';
31
+ /** Minimal fetch shape the adapter needs (lets tests pass a stub). */
32
+ export type FetchLike = (input: string, init?: {
33
+ headers?: Record<string, string>;
34
+ signal?: AbortSignal;
35
+ }) => Promise<{
36
+ ok: boolean;
37
+ status: number;
38
+ statusText: string;
39
+ json(): Promise<unknown>;
40
+ text(): Promise<string>;
41
+ }>;
42
+ export interface HttpParitySourceConfig {
43
+ /** Portal base URL, e.g. `https://portal.bot-me.ai`. */
44
+ baseUrl: string;
45
+ /** Bearer token with `instar:read` scope. */
46
+ token: string;
47
+ /** Page size sent as `?limit=`. Default 1000 (server max per Dawn's contract). */
48
+ pageSize?: number;
49
+ /** Safety cap on pagination loops. Default 200 pages (= 200k feedback rows). */
50
+ maxPages?: number;
51
+ /** Optional `?status=` filter passed through to the endpoint. */
52
+ status?: string;
53
+ /** Injectable fetch (for tests). Defaults to global `fetch`. */
54
+ fetchImpl?: FetchLike;
55
+ /** Optional path override (defaults to `/api/instar/read`). */
56
+ readPath?: string;
57
+ }
58
+ /** Error thrown when Portal returns non-OK. The status code is preserved for callers. */
59
+ export declare class HttpParitySourceError extends Error {
60
+ readonly status: number;
61
+ constructor(status: number, message: string);
62
+ }
63
+ /**
64
+ * Live ParitySource backed by Portal's `/api/instar/read` endpoint. Construct
65
+ * with the base URL + read-scope token, then `await source.prepare()` once
66
+ * before passing to {@link runDryRunCompare}. The pre-fetch captures a single
67
+ * consistent snapshot of Portal's clusters; the runner reads it synchronously.
68
+ */
69
+ export declare class HttpParitySource implements ParitySource {
70
+ private readonly config;
71
+ private snapshot;
72
+ private readonly pageSize;
73
+ private readonly maxPages;
74
+ private readonly fetch;
75
+ private readonly readPath;
76
+ constructor(config: HttpParitySourceConfig);
77
+ /** Pre-fetch and cache the cluster snapshot. Idempotent: re-calling re-fetches. */
78
+ prepare(): Promise<void>;
79
+ /** Sync ParitySource read — returns the snapshot captured by {@link prepare}. */
80
+ readPortalClusters(): PortalCluster[];
81
+ }
82
+ //# sourceMappingURL=HttpParitySource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpParitySource.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/dryrun/HttpParitySource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,sEAAsE;AACtE,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAAK,OAAO,CAAC;IACpH,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB,CAAC,CAAC;AAEH,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAiBD,yFAAyF;AACzF,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK5C;AAgCD;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IAOvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEL,MAAM,EAAE,sBAAsB;IAU3D,mFAAmF;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAqC9B,iFAAiF;IACjF,kBAAkB,IAAI,aAAa,EAAE;CAMtC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * HttpParitySource.ts — the live Phase-1/3 ParitySource adapter.
3
+ *
4
+ * Portal exposes `GET /api/instar/read` (Dawn, committed at d65136b3b6) as the
5
+ * read-only seam — the architectural pivot from raw Postgres after Prisma Data
6
+ * Platform refused to mint a database role (it forbids CREATE ROLE / GRANT). API
7
+ * level is actually a cleaner seam than direct DB access: the dry-run tests
8
+ * against the same contract Phase 2 (write/update APIs) will use, with no
9
+ * Prisma-admin dependency, and the architectural levels line up (Portal's HTTP
10
+ * surface ↔ Instar's {@link ParitySource} interface).
11
+ *
12
+ * This adapter implements ParitySource via that endpoint. The runner's seam,
13
+ * comparator, invariants, and JSONL audit trail are unchanged — only the read
14
+ * adapter swaps. Reads are PRE-FETCHED in {@link prepare}: the dry-run captures
15
+ * one snapshot of Portal's clusters at a moment in time, then compares against
16
+ * that frozen view. This makes the sync `readPortalClusters()` contract trivially
17
+ * satisfiable and stabilises the comparison against in-flight Portal writes
18
+ * (Portal remains the sole writer; we just look at one consistent slice).
19
+ *
20
+ * The endpoint is paginated (server-side cap: 1000 rows per request). The
21
+ * pagination is keyed off the feedback table — clusters/dispatches accompany each
22
+ * page and may repeat — so cluster collection deduplicates by clusterId across
23
+ * pages and stops when a page returns fewer feedback rows than the requested
24
+ * limit (the documented "no more pages" signal) OR after a hard safety cap.
25
+ *
26
+ * Field naming is intentionally tolerant: cluster rows may arrive as camelCase
27
+ * (Prisma default) or snake_case (raw SQL projection). The mapper accepts both.
28
+ */
29
+ /** Error thrown when Portal returns non-OK. The status code is preserved for callers. */
30
+ export class HttpParitySourceError extends Error {
31
+ status;
32
+ constructor(status, message) {
33
+ super(message);
34
+ this.name = 'HttpParitySourceError';
35
+ this.status = status;
36
+ }
37
+ }
38
+ /**
39
+ * Coerce one raw cluster row into the {@link PortalCluster} shape. Accepts both
40
+ * camelCase and snake_case keys; missing optional fields stay undefined. Throws
41
+ * only when the required keys (clusterId, type, title, fingerprint) cannot be
42
+ * resolved — a row that opaque is a contract violation we want to surface.
43
+ */
44
+ function coerceCluster(row) {
45
+ if (!row || typeof row !== 'object') {
46
+ throw new HttpParitySourceError(502, 'cluster row is not an object');
47
+ }
48
+ const r = row;
49
+ const pick = (...keys) => {
50
+ for (const k of keys)
51
+ if (r[k] !== undefined && r[k] !== null)
52
+ return r[k];
53
+ return undefined;
54
+ };
55
+ const clusterId = pick('clusterId', 'cluster_id', 'id');
56
+ const type = pick('type');
57
+ const title = pick('title');
58
+ const fingerprint = pick('fingerprint') ?? '';
59
+ if (typeof clusterId !== 'string' || typeof type !== 'string' || typeof title !== 'string') {
60
+ throw new HttpParitySourceError(502, `cluster row missing required fields (clusterId/type/title)`);
61
+ }
62
+ const out = { clusterId, type, title, fingerprint: String(fingerprint) };
63
+ const status = pick('status');
64
+ if (typeof status === 'string')
65
+ out.status = status;
66
+ const rc = pick('recurrenceCount', 'recurrence_count');
67
+ if (typeof rc === 'number')
68
+ out.recurrenceCount = rc;
69
+ return out;
70
+ }
71
+ /**
72
+ * Live ParitySource backed by Portal's `/api/instar/read` endpoint. Construct
73
+ * with the base URL + read-scope token, then `await source.prepare()` once
74
+ * before passing to {@link runDryRunCompare}. The pre-fetch captures a single
75
+ * consistent snapshot of Portal's clusters; the runner reads it synchronously.
76
+ */
77
+ export class HttpParitySource {
78
+ config;
79
+ snapshot = null;
80
+ pageSize;
81
+ maxPages;
82
+ fetch;
83
+ readPath;
84
+ constructor(config) {
85
+ this.config = config;
86
+ this.pageSize = config.pageSize ?? 1000;
87
+ this.maxPages = config.maxPages ?? 200;
88
+ this.fetch = config.fetchImpl ?? globalThis.fetch;
89
+ this.readPath = config.readPath ?? '/api/instar/read';
90
+ if (!this.fetch) {
91
+ throw new HttpParitySourceError(500, 'no fetch available — pass fetchImpl or run on a runtime with global fetch');
92
+ }
93
+ }
94
+ /** Pre-fetch and cache the cluster snapshot. Idempotent: re-calling re-fetches. */
95
+ async prepare() {
96
+ const byId = new Map();
97
+ const base = this.config.baseUrl.replace(/\/+$/, '');
98
+ const authHeader = `Bearer ${this.config.token}`;
99
+ for (let page = 0; page < this.maxPages; page++) {
100
+ const offset = page * this.pageSize;
101
+ const qs = new URLSearchParams({ limit: String(this.pageSize), offset: String(offset) });
102
+ if (this.config.status)
103
+ qs.set('status', this.config.status);
104
+ const url = `${base}${this.readPath}?${qs.toString()}`;
105
+ const res = await this.fetch(url, { headers: { Authorization: authHeader, Accept: 'application/json' } });
106
+ if (!res.ok) {
107
+ let detail = '';
108
+ try {
109
+ detail = (await res.text()).slice(0, 200);
110
+ }
111
+ catch { /* ignore */ }
112
+ throw new HttpParitySourceError(res.status, `Portal /api/instar/read failed (page ${page}, status ${res.status} ${res.statusText}): ${detail}`);
113
+ }
114
+ const envelope = (await res.json());
115
+ const rawClusters = envelope?.data?.clusters ?? [];
116
+ for (const raw of rawClusters) {
117
+ const c = coerceCluster(raw);
118
+ if (!byId.has(c.clusterId))
119
+ byId.set(c.clusterId, c);
120
+ }
121
+ // Pagination stop signal: returned_count < pageSize means the feedback table
122
+ // is exhausted (clusters/dispatches per Dawn's contract accompany the feedback
123
+ // pages and stabilise quickly via the byId dedup above).
124
+ const returned = envelope?.meta?.returned_count ?? rawClusters.length;
125
+ if (returned < this.pageSize)
126
+ break;
127
+ }
128
+ this.snapshot = [...byId.values()];
129
+ }
130
+ /** Sync ParitySource read — returns the snapshot captured by {@link prepare}. */
131
+ readPortalClusters() {
132
+ if (this.snapshot === null) {
133
+ throw new HttpParitySourceError(500, 'HttpParitySource.prepare() must be awaited before readPortalClusters()');
134
+ }
135
+ return this.snapshot.map((c) => ({ ...c }));
136
+ }
137
+ }
138
+ //# sourceMappingURL=HttpParitySource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpParitySource.js","sourceRoot":"","sources":["../../../src/feedback-factory/dryrun/HttpParitySource.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.48",
3
+ "version": "1.3.49",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-28T03:46:09.134Z",
5
- "instarVersion": "1.3.48",
4
+ "generatedAt": "2026-05-28T03:52:42.963Z",
5
+ "instarVersion": "1.3.49",
6
6
  "entryCount": 193,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,31 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ Next increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ships the **live Portal adapter** behind the dry-run/compare seam after a clean architectural pivot from Dawn (committed Portal-side at `d65136b3b6`): Prisma Data Platform forbids `CREATE ROLE`/`GRANT` for any account, so a read-only Postgres role can't be minted. Dawn replaced direct DB access with a Portal-internal endpoint `GET /api/instar/read` — Bearer-token-authed (`instar:read` scope), max 1000 rows per request, returns the same three tables (feedback, clusters, dispatches) with paging metadata.
8
+
9
+ New module:
10
+
11
+ - **`src/feedback-factory/dryrun/HttpParitySource.ts`** — implements the existing `ParitySource` interface via that endpoint. Async `prepare()` pre-fetches a consistent snapshot (walking pages, deduping clusters by `clusterId`, stopping when `returned_count < limit`). Sync `readPortalClusters()` returns the snapshot. The runner's seam, the parity comparator, the three order-independent invariants, and the JSONL audit trail are all unchanged — only the read adapter swaps.
12
+
13
+ Still internal — not yet wired into any route or job. Live verification waits on the `instar:read` token landing via Secret Drop.
14
+
15
+ ## What to Tell Your User
16
+
17
+ - Dawn hit a database-platform limitation (Prisma Cloud refuses to mint a read-only user) and pivoted to an internal HTTP endpoint instead. It's actually a cleaner seam — my dry-run tests against the same contract her Phase-2 work will use, with no Prisma-admin dependency.
18
+
19
+ ## Summary of New Capabilities
20
+
21
+ | Capability | How to Use |
22
+ |-----------|-----------|
23
+ | HTTP-backed live ParitySource for Phase-1/3 dry-run | Internal `src/feedback-factory/dryrun/HttpParitySource.ts` — `await new HttpParitySource({baseUrl,token}).prepare()`, then pass to `runDryRunCompare` |
24
+
25
+ ## Evidence
26
+
27
+ - **10 new unit tests**, all green; full feedback-factory unit dir green; `tsc --noEmit` clean.
28
+ - **Both-sides-of-boundary**: Bearer auth + field mapping (camelCase AND snake_case tolerance), single-page snapshot + multi-page pagination with `clusterId`-dedup + `returned_count < pageSize` stop signal, `maxPages` safety cap, error-status preservation, malformed-row rejection (no silent skip of contract violations), prepare-before-read invariant, defensive-copy snapshot.
29
+ - **Faithful to the existing seam**: `HttpParitySource` is purely additive — implements the same `ParitySource` interface as `InMemoryParitySource`, so the runner / comparator / invariants / cutover gate are byte-identical for in-memory tests and the live Portal path.
30
+
31
+ Side-effects: `upgrades/side-effects/feedback-factory-http-source.md`.
@@ -0,0 +1,99 @@
1
+ # Side-effects review — feedback-factory HttpParitySource adapter
2
+
3
+ Spec: `docs/specs/feedback-factory-migration.md` (converged v2, approved 2026-05-26).
4
+ Increment: the live Portal `/api/instar/read` adapter implementing the existing
5
+ `ParitySource` seam. Single new src module + its unit test; zero edits to existing
6
+ code.
7
+
8
+ ## What this ships
9
+
10
+ The HTTP adapter behind the dry-run/compare seam after Dawn's architectural pivot
11
+ (committed Portal-side at `d65136b3b6`): Prisma Data Platform forbids `CREATE ROLE`/
12
+ `GRANT`/`REVOKE` for any account including superusers, so a read-only Postgres role
13
+ couldn't be minted. Dawn replaced direct DB access with a Portal-internal endpoint
14
+ `GET /api/instar/read` (max 1000 rows/request, Bearer-token-authed,
15
+ `instar:read` scope, JSON envelope with `data.{feedback,clusters,dispatches}` +
16
+ `meta.{total_feedback_rows,returned_count,query_time_ms,timestamp}`).
17
+
18
+ `HttpParitySource` implements `ParitySource` by pre-fetching that snapshot via an
19
+ async `prepare()` and serving it through the existing sync `readPortalClusters()`
20
+ read. The runner's seam, comparator (parity.ts), invariants, audit trail
21
+ (JSONL), and `divergent === true` cutover gate are unchanged — only the read
22
+ adapter swaps. This is exactly what the dependency-injection seam was for.
23
+
24
+ ## Over-block / under-block
25
+
26
+ - **Errors**: any non-OK Portal response throws `HttpParitySourceError` with the
27
+ preserved status code. No silent partial snapshots — a 401/403/5xx surfaces
28
+ immediately and halts the prepare(). This is the right direction (false
29
+ block is recoverable; a silent partial snapshot would produce a phantom
30
+ "divergent" verdict that blocks Phase 4 cutover for the wrong reason).
31
+ - **Malformed cluster rows**: a row missing required fields (`clusterId` / `type`
32
+ / `title`) throws, NOT silently skipped. A contract violation from Portal is
33
+ exactly the kind of thing we need to see, not hide.
34
+ - **Field naming tolerance**: the mapper accepts both camelCase (Prisma default)
35
+ and snake_case (raw SQL projection) — chosen because Dawn's endpoint contract
36
+ doesn't pin the projection style and over-strictness here would cause
37
+ needless live-rollout friction.
38
+ - **Snapshot freshness**: `prepare()` is idempotent — re-calling re-fetches.
39
+ Callers that want a stable snapshot for one dry-run pass simply call it once;
40
+ callers that want a fresh snapshot per pass call it per pass. Neither path is
41
+ blocked.
42
+ - **`readPortalClusters()` before `prepare()`**: throws (no silent empty array).
43
+ An empty snapshot would also produce a phantom "divergent: false" verdict that
44
+ would mis-gate cutover.
45
+
46
+ ## Level-of-abstraction fit
47
+
48
+ - The adapter lives in `dryrun/` next to `dryRunCompare.ts` because it is the
49
+ composition's read-source, not a pure piece of the processor logic
50
+ (`processor/` stays pure / decision-only).
51
+ - It implements the existing `ParitySource` interface verbatim — the seam was
52
+ designed for exactly this swap. The runner doesn't know whether it's reading
53
+ from `InMemoryParitySource` or `HttpParitySource`; that's the test/live
54
+ symmetry the migration spec called for.
55
+ - `fetch` is injected via `fetchImpl` so unit tests can stub it. The default
56
+ uses the runtime's global `fetch` (Node ≥18 / modern fetch).
57
+
58
+ ## Signal-vs-authority compliance
59
+
60
+ Still signal-only. The adapter produces data; the runner produces a verdict; no
61
+ authority to act flows from this code. Portal stays sole writer (the
62
+ read-only API surface does not expose mutation). The cutover authority remains
63
+ where the spec puts it: Dawn's line-by-line review + Justin's go/no-go.
64
+
65
+ ## Interactions
66
+
67
+ - **Portal `/api/instar/read`**: the only external surface this adapter touches.
68
+ Read-only by Dawn's design (no PUT/POST/DELETE in the endpoint contract). The
69
+ Bearer token (`instar:read` scope) sits in the adapter's config — never logged,
70
+ never persisted by the adapter, owned by the caller. Token delivery happens
71
+ out of band via Secret Drop (in-memory only, never on disk, never in chat
72
+ history).
73
+ - **`ParitySource` interface, `runDryRunCompare`, `parity.ts`**: unchanged. No
74
+ edits to existing feedback-factory code — additive only.
75
+ - **No config, hook, route, template, or migration surface is touched.** No
76
+ Migration Parity / Agent Awareness obligation triggered.
77
+ - **Pagination contract**: keyed off the feedback table (`returned_count <
78
+ pageSize` = exhausted). Clusters/dispatches per Dawn's contract accompany
79
+ each page; the adapter dedupes clusters by `clusterId` across pages, so
80
+ repeated clusters across pages cost only the network round-trip, not
81
+ correctness. Safety cap at `maxPages` (default 200 = 200k feedback rows)
82
+ prevents infinite-loop pathology if Portal returned the wrong meta.
83
+
84
+ ## Rollback cost
85
+
86
+ Trivial. One new `src/feedback-factory/dryrun/HttpParitySource.ts` plus its test.
87
+ Nothing imports it from production paths yet. Reverting removes the adapter
88
+ without any downstream impact.
89
+
90
+ ## Deferrals
91
+
92
+ - **Live verification** waits on the `instar:read` token landing via the open
93
+ Secret Drop. The adapter + 10 unit tests fully prove the read shape, mapping,
94
+ pagination, error path, and prepare-invariant against a stubbed `fetch`; the
95
+ live token only exercises the actual Portal endpoint. <!-- tracked: topic-12476 -->
96
+ - **Cursor-based pagination** (a Last-Modified-style cursor instead of offset)
97
+ is not used: Dawn's documented contract is offset/limit. If she adds a cursor
98
+ later, the adapter takes one config field and a small page-loop tweak. The
99
+ current offset-with-dedup is correct under offset semantics. <!-- tracked: topic-12476 -->