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.
- package/dist/feedback-factory/dryrun/HttpParitySource.d.ts +82 -0
- package/dist/feedback-factory/dryrun/HttpParitySource.d.ts.map +1 -0
- package/dist/feedback-factory/dryrun/HttpParitySource.js +138 -0
- package/dist/feedback-factory/dryrun/HttpParitySource.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.49.md +31 -0
- package/upgrades/side-effects/feedback-factory-http-source.md +99 -0
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-28T03:
|
|
5
|
-
"instarVersion": "1.3.
|
|
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 -->
|