@gate-forge/pack-playwright 0.1.0
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/LICENSE +202 -0
- package/bin/gateforge-witness.js +40 -0
- package/dist/attestation/proxy.d.ts +40 -0
- package/dist/attestation/proxy.d.ts.map +1 -0
- package/dist/attestation/proxy.js +78 -0
- package/dist/attestation/proxy.js.map +1 -0
- package/dist/constants.d.ts +130 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +138 -0
- package/dist/constants.js.map +1 -0
- package/dist/discovery/adapters.d.ts +167 -0
- package/dist/discovery/adapters.d.ts.map +1 -0
- package/dist/discovery/adapters.js +276 -0
- package/dist/discovery/adapters.js.map +1 -0
- package/dist/discovery/discover.d.ts +57 -0
- package/dist/discovery/discover.d.ts.map +1 -0
- package/dist/discovery/discover.js +544 -0
- package/dist/discovery/discover.js.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +15 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/inference.d.ts +63 -0
- package/dist/discovery/inference.d.ts.map +1 -0
- package/dist/discovery/inference.js +173 -0
- package/dist/discovery/inference.js.map +1 -0
- package/dist/discovery/pytest-adapter.d.ts +205 -0
- package/dist/discovery/pytest-adapter.d.ts.map +1 -0
- package/dist/discovery/pytest-adapter.js +548 -0
- package/dist/discovery/pytest-adapter.js.map +1 -0
- package/dist/discovery/reconcile.d.ts +106 -0
- package/dist/discovery/reconcile.d.ts.map +1 -0
- package/dist/discovery/reconcile.js +268 -0
- package/dist/discovery/reconcile.js.map +1 -0
- package/dist/discovery/runner-env.d.ts +80 -0
- package/dist/discovery/runner-env.d.ts.map +1 -0
- package/dist/discovery/runner-env.js +139 -0
- package/dist/discovery/runner-env.js.map +1 -0
- package/dist/discovery/static-discovery.d.ts +121 -0
- package/dist/discovery/static-discovery.d.ts.map +1 -0
- package/dist/discovery/static-discovery.js +1028 -0
- package/dist/discovery/static-discovery.js.map +1 -0
- package/dist/discovery/supervised-run.d.ts +110 -0
- package/dist/discovery/supervised-run.d.ts.map +1 -0
- package/dist/discovery/supervised-run.js +328 -0
- package/dist/discovery/supervised-run.js.map +1 -0
- package/dist/discovery/trusted-config.d.ts +103 -0
- package/dist/discovery/trusted-config.d.ts.map +1 -0
- package/dist/discovery/trusted-config.js +117 -0
- package/dist/discovery/trusted-config.js.map +1 -0
- package/dist/fixture/evidence.d.ts +191 -0
- package/dist/fixture/evidence.d.ts.map +1 -0
- package/dist/fixture/evidence.js +366 -0
- package/dist/fixture/evidence.js.map +1 -0
- package/dist/fixture/fixture.d.ts +37 -0
- package/dist/fixture/fixture.d.ts.map +1 -0
- package/dist/fixture/fixture.js +38 -0
- package/dist/fixture/fixture.js.map +1 -0
- package/dist/fixture/witness-client.d.ts +164 -0
- package/dist/fixture/witness-client.d.ts.map +1 -0
- package/dist/fixture/witness-client.js +253 -0
- package/dist/fixture/witness-client.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +16 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +21 -0
- package/dist/json.js.map +1 -0
- package/dist/reporter/ledger.d.ts +105 -0
- package/dist/reporter/ledger.d.ts.map +1 -0
- package/dist/reporter/ledger.js +197 -0
- package/dist/reporter/ledger.js.map +1 -0
- package/dist/reporter/reporter.cjs +105 -0
- package/dist/reporter/reporter.cjs.map +1 -0
- package/dist/reporter/reporter.d.cts +35 -0
- package/dist/reporter/reporter.d.cts.map +1 -0
- package/dist/reporter/reporter.d.ts +167 -0
- package/dist/reporter/reporter.d.ts.map +1 -0
- package/dist/reporter/reporter.js +673 -0
- package/dist/reporter/reporter.js.map +1 -0
- package/dist/setup.d.ts +49 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +167 -0
- package/dist/setup.js.map +1 -0
- package/dist/supervisor/client.d.ts +73 -0
- package/dist/supervisor/client.d.ts.map +1 -0
- package/dist/supervisor/client.js +164 -0
- package/dist/supervisor/client.js.map +1 -0
- package/dist/supervisor/drain.d.ts +45 -0
- package/dist/supervisor/drain.d.ts.map +1 -0
- package/dist/supervisor/drain.js +235 -0
- package/dist/supervisor/drain.js.map +1 -0
- package/dist/supervisor/index.d.ts +16 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +13 -0
- package/dist/supervisor/index.js.map +1 -0
- package/dist/supervisor/spool.d.ts +137 -0
- package/dist/supervisor/spool.d.ts.map +1 -0
- package/dist/supervisor/spool.js +263 -0
- package/dist/supervisor/spool.js.map +1 -0
- package/dist/surface.d.ts +134 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +139 -0
- package/dist/surface.js.map +1 -0
- package/dist/witness/adapter-registry.d.ts +38 -0
- package/dist/witness/adapter-registry.d.ts.map +1 -0
- package/dist/witness/adapter-registry.js +158 -0
- package/dist/witness/adapter-registry.js.map +1 -0
- package/dist/witness/bin.d.ts +21 -0
- package/dist/witness/bin.d.ts.map +1 -0
- package/dist/witness/bin.js +172 -0
- package/dist/witness/bin.js.map +1 -0
- package/dist/witness/browser.d.ts +173 -0
- package/dist/witness/browser.d.ts.map +1 -0
- package/dist/witness/browser.js +580 -0
- package/dist/witness/browser.js.map +1 -0
- package/dist/witness/classifications.d.ts +42 -0
- package/dist/witness/classifications.d.ts.map +1 -0
- package/dist/witness/classifications.js +87 -0
- package/dist/witness/classifications.js.map +1 -0
- package/dist/witness/env-attestation.d.ts +98 -0
- package/dist/witness/env-attestation.d.ts.map +1 -0
- package/dist/witness/env-attestation.js +202 -0
- package/dist/witness/env-attestation.js.map +1 -0
- package/dist/witness/loopback-pins.d.ts +79 -0
- package/dist/witness/loopback-pins.d.ts.map +1 -0
- package/dist/witness/loopback-pins.js +244 -0
- package/dist/witness/loopback-pins.js.map +1 -0
- package/dist/witness/server.d.ts +34 -0
- package/dist/witness/server.d.ts.map +1 -0
- package/dist/witness/server.js +2539 -0
- package/dist/witness/server.js.map +1 -0
- package/dist/witness/types.d.ts +643 -0
- package/dist/witness/types.d.ts.map +1 -0
- package/dist/witness/types.js +2 -0
- package/dist/witness/types.js.map +1 -0
- package/package.json +64 -0
- package/python/gateforge_persistence_intents.py +100 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateforge Playwright reporter (plan §4.6/§4.7, GF-23/GF-24;
|
|
3
|
+
* Phase 4 adds claim injection, runner-outcome capture, and the honest
|
|
4
|
+
* aggregate).
|
|
5
|
+
*
|
|
6
|
+
* Per test it extracts claims from `{type: 'gateforge', description:
|
|
7
|
+
* '<obligation id>'}` annotations AND from the CLI-written claim
|
|
8
|
+
* injections (`claim-injections.json` — the resolved sidecar/native
|
|
9
|
+
* mappings, Phase 4): injections are carried on the session-open path so
|
|
10
|
+
* a mapped test's evidence lands on the right claims; native annotations
|
|
11
|
+
* keep working unchanged. At run end it writes the run-state artifacts
|
|
12
|
+
* the CLI's verifier consumes:
|
|
13
|
+
*
|
|
14
|
+
* - `claims.json` — Claim-shaped entries (schemaVersion, obligationId,
|
|
15
|
+
* testId, testFile, location) extracted from annotations + injections.
|
|
16
|
+
* - `records.json` — the WITNESS-ISSUED ledger verbatim: the witness is
|
|
17
|
+
* the only issuer of recordIds, and the reporter copies `GET
|
|
18
|
+
* /records` — it never reconstructs records from test-side data, so a
|
|
19
|
+
* fabricated bundle never enters this file (GF-23).
|
|
20
|
+
* - `ledger.json` — per-claim verdicts for downstream tools.
|
|
21
|
+
* - runner-outcomes doc (`GATEFORGE_OUTCOMES_FILE`) — per-instance
|
|
22
|
+
* outcomes, attempts, expected failures, and the run-level
|
|
23
|
+
* fixture/teardown outcome for trusted runner supervision (ADR 0005
|
|
24
|
+
* D2: input only, never signature authority).
|
|
25
|
+
*
|
|
26
|
+
* It then computes a per-claim ledger with the REAL verdict engine
|
|
27
|
+
* (`evaluateObligation`, G3) — never trusting the test — and prints a
|
|
28
|
+
* per-claim summary. The summary is HONEST BY CONSTRUCTION (plan Phase 4
|
|
29
|
+
* item 7): it NEVER prints an overall gate pass while unclaimed
|
|
30
|
+
* obligations remain — the authoritative CLI result is the only final
|
|
31
|
+
* gate result. Exit-code semantics: the AUTHORITATIVE code comes from
|
|
32
|
+
* `gateforge test-gates` (contract 4); the reporter only writes
|
|
33
|
+
* `process.exitCode = 1` when a blocking verdict exists AND
|
|
34
|
+
* `GATEFORGE_REPORTER_FAIL_RUN=1` is explicitly set (best-effort in
|
|
35
|
+
* standalone runs, where Playwright's own exit handling may clobber it).
|
|
36
|
+
*
|
|
37
|
+
* Claim-registry vs records mismatch (GF-24): obligations in the run
|
|
38
|
+
* document with no claim, and records with no matching claim, are
|
|
39
|
+
* printed as ledger notes — the CLI grades the former `missing`, so
|
|
40
|
+
* bypassing the fixture can never read satisfied.
|
|
41
|
+
*
|
|
42
|
+
* Phase 1 — session identity is supervisor-issued: evidence primitives
|
|
43
|
+
* resolve their session by the exact (workerIndex, testId) pair, and the
|
|
44
|
+
* witness mints sessions ONLY for the supervisor channel. Since the
|
|
45
|
+
* enforcement review (fix 3), the runner child holds NO supervisor
|
|
46
|
+
* rights: `onTestBegin`/`onTestEnd` write lifecycle events (testBegin/
|
|
47
|
+
* testEnd with the observed outcome) to the run-state SPOOL
|
|
48
|
+
* (`<stateDir>/spool/<runId>/events.jsonl`, NUL-safe JSON lines) and the
|
|
49
|
+
* TRUSTED CLI drains the spool, performing the witness's session
|
|
50
|
+
* open/close with supervisor credentials that exist only in the CLI
|
|
51
|
+
* process. Session RPC failures are therefore impossible here by
|
|
52
|
+
* construction; a missing drain leaves no sessions open, and the
|
|
53
|
+
* witness rejects every submission fail-closed.
|
|
54
|
+
*/
|
|
55
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
56
|
+
import { join, relative, sep } from 'node:path';
|
|
57
|
+
import { canonicalOf } from '../json.js';
|
|
58
|
+
import { CLAIM_ANNOTATION_TYPE, CLAIM_INJECTIONS_FILE, ENV_OBLIGATIONS, ENV_OUTCOMES_FILE, ENV_REPORTER_FAIL_RUN, ENV_RUN_ID, ENV_STATE_DIR, ENV_WITNESS_URL, } from '../constants.js';
|
|
59
|
+
import { appendSpoolEvent, spoolPathFor } from '../supervisor/spool.js';
|
|
60
|
+
import { WitnessClient } from '../fixture/witness-client.js';
|
|
61
|
+
import { resolveWitnessUrl } from '../fixture/witness-client.js';
|
|
62
|
+
import { claimOf, isBlocking, ledgerRowFor, parseObligationsDocument, } from './ledger.js';
|
|
63
|
+
/**
|
|
64
|
+
* Computes the honest aggregate line (plan Phase 4 item 7, §2 row): the
|
|
65
|
+
* reporter NEVER prints an overall gate pass based only on claimed rows
|
|
66
|
+
* while unclaimed obligations block. Pure and deterministic — exported
|
|
67
|
+
* so the honesty contract is directly testable.
|
|
68
|
+
*
|
|
69
|
+
* Args:
|
|
70
|
+
* rows: the per-claim ledger rows.
|
|
71
|
+
* unclaimedObligations: count of run obligations with no claim row.
|
|
72
|
+
*
|
|
73
|
+
* Returns:
|
|
74
|
+
* string: the aggregate summary line.
|
|
75
|
+
*/
|
|
76
|
+
export function gateSummaryLine(rows, unclaimedObligations) {
|
|
77
|
+
const blocking = rows.filter((row) => isBlocking(row.verdict));
|
|
78
|
+
const authority = 'final gate result: the gateforge CLI (test-gates/check), never this reporter';
|
|
79
|
+
if (blocking.length > 0) {
|
|
80
|
+
return `GATEFORGE GATE: FAIL (${String(blocking.length)}/${String(rows.length)} claimed obligations not satisfied; ${authority})`;
|
|
81
|
+
}
|
|
82
|
+
if (unclaimedObligations > 0) {
|
|
83
|
+
return (`GATEFORGE GATE: NOT PASSED (${String(rows.length)} claimed obligation(s) satisfied, ` +
|
|
84
|
+
`${String(unclaimedObligations)} unclaimed obligation(s) still block; ${authority})`);
|
|
85
|
+
}
|
|
86
|
+
if (rows.length === 0) {
|
|
87
|
+
return `GATEFORGE GATE: NO CLAIMS (no gateforge claims found in this run; ${authority})`;
|
|
88
|
+
}
|
|
89
|
+
return `GATEFORGE GATE: PASS (${String(rows.length)}/${String(rows.length)} claimed obligations satisfied; ${authority})`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The gateforge reporter. No options today; the constructor signature is
|
|
93
|
+
* the Playwright reporter contract (`(options: object)`).
|
|
94
|
+
*/
|
|
95
|
+
export class GateforgeReporter {
|
|
96
|
+
rows = [];
|
|
97
|
+
/**
|
|
98
|
+
* Phase 4 claim injections (reconciliation key → obligation ids),
|
|
99
|
+
* loaded once from the CLI-written run-state document. Declarations
|
|
100
|
+
* only — they never satisfy anything by themselves.
|
|
101
|
+
*/
|
|
102
|
+
injections;
|
|
103
|
+
/** Phase 4 runner-outcome capture (supervision input, ADR 0005 D2). */
|
|
104
|
+
runnerOutcomes = [];
|
|
105
|
+
runStatus = null;
|
|
106
|
+
runnerErrors = [];
|
|
107
|
+
/** The lifecycle spool file (null when the run has no state dir). */
|
|
108
|
+
spoolFile;
|
|
109
|
+
/** Resolved run-state paths (options win, env is the legacy fallback). */
|
|
110
|
+
resolved;
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
const stateDir = options.stateDir ?? process.env[ENV_STATE_DIR];
|
|
113
|
+
const runId = options.runId ?? process.env[ENV_RUN_ID];
|
|
114
|
+
const outcomesPath = options.outcomesPath ?? process.env[ENV_OUTCOMES_FILE];
|
|
115
|
+
const obligationsPath = options.obligationsPath ?? process.env[ENV_OBLIGATIONS];
|
|
116
|
+
this.resolved = {
|
|
117
|
+
stateDir: stateDir !== undefined && stateDir !== '' ? stateDir : null,
|
|
118
|
+
runId: runId !== undefined && runId !== '' ? runId : null,
|
|
119
|
+
outcomesPath: outcomesPath !== undefined && outcomesPath !== '' ? outcomesPath : null,
|
|
120
|
+
obligationsPath: obligationsPath !== undefined && obligationsPath !== '' ? obligationsPath : null,
|
|
121
|
+
};
|
|
122
|
+
const wired = (process.env[ENV_WITNESS_URL] ?? '') !== '' ||
|
|
123
|
+
(this.resolved.stateDir ?? '') !== '';
|
|
124
|
+
if (!wired) {
|
|
125
|
+
console.warn(`[gateforge] no witness/state wiring (${ENV_WITNESS_URL} or ${ENV_STATE_DIR}); ` +
|
|
126
|
+
'claims/records will not be written and evidence primitives fail closed');
|
|
127
|
+
}
|
|
128
|
+
this.injections = readClaimInjections(this.resolved.stateDir ?? undefined);
|
|
129
|
+
// The lifecycle spool (enforcement-review fix 3): the trusted CLI
|
|
130
|
+
// drains these events and drives the witness supervisor channel.
|
|
131
|
+
// Without a state dir or run id there is nothing to drain — the
|
|
132
|
+
// supervisor then never opens sessions and submissions fail closed.
|
|
133
|
+
this.spoolFile =
|
|
134
|
+
this.resolved.stateDir !== null && this.resolved.runId !== null
|
|
135
|
+
? spoolPathFor(this.resolved.stateDir, this.resolved.runId)
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Lifecycle spool write (enforcement-review fix 3): announces the
|
|
140
|
+
* STARTED test to the trusted CLI's drain — which opens the witness
|
|
141
|
+
* session on the supervisor channel, carrying the mapped obligation
|
|
142
|
+
* claims for sidecar/native-mapped tests (Phase 4 claim injection) —
|
|
143
|
+
* and binds worker → session there. The runner itself performs no
|
|
144
|
+
* privileged call. Synchronous API: the CLI's poll cadence absorbs
|
|
145
|
+
* the dispatch latency (the fixture's session resolve waits up to 5s).
|
|
146
|
+
*/
|
|
147
|
+
onTestBegin(test, result) {
|
|
148
|
+
if (this.spoolFile === null)
|
|
149
|
+
return;
|
|
150
|
+
const workerIndex = typeof result.workerIndex === 'number' ? result.workerIndex : 0;
|
|
151
|
+
const claims = [...new Set([...this.annotationClaimsOf(test), ...this.injectedClaimsFor(test)])].sort();
|
|
152
|
+
appendSpoolEvent(this.spoolFile, {
|
|
153
|
+
kind: 'testBegin',
|
|
154
|
+
testId: test.id,
|
|
155
|
+
workerIndex,
|
|
156
|
+
file: this.repoRelativeOf(test),
|
|
157
|
+
titlePath: this.titlePathOf(test),
|
|
158
|
+
project: this.projectOf(test),
|
|
159
|
+
...(claims.length > 0 ? { claims } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Runner-level error hook: a global setup/teardown/runner error. */
|
|
163
|
+
onError(error) {
|
|
164
|
+
this.runnerErrors.push(typeof error?.message === 'string' ? error.message : String(error));
|
|
165
|
+
}
|
|
166
|
+
/** Collects claims + test identity + the outcome row at test end (synchronous). */
|
|
167
|
+
onTestEnd(test, result) {
|
|
168
|
+
// Phase 4: capture the outcome row for trusted runner supervision
|
|
169
|
+
// (every test, claimed or not — the expected set includes them all).
|
|
170
|
+
const titlePath = this.titlePathOf(test);
|
|
171
|
+
const file = this.repoRelativeOf(test);
|
|
172
|
+
this.runnerOutcomes.push({
|
|
173
|
+
testId: test.id,
|
|
174
|
+
...(file !== null ? { file } : { file: '' }),
|
|
175
|
+
titlePath,
|
|
176
|
+
project: this.projectOf(test),
|
|
177
|
+
status: result.status,
|
|
178
|
+
attempt: (typeof result.retry === 'number' ? result.retry : 0) + 1,
|
|
179
|
+
expectedFailure: (test.annotations ?? []).some((annotation) => annotation.type === 'fail'),
|
|
180
|
+
});
|
|
181
|
+
// Lifecycle spool write (fix 3): the CLI drains this and seals the
|
|
182
|
+
// session with the OBSERVED outcome on the supervisor channel.
|
|
183
|
+
// Sealing is final, so records cannot be injected after the test.
|
|
184
|
+
if (this.spoolFile !== null) {
|
|
185
|
+
appendSpoolEvent(this.spoolFile, {
|
|
186
|
+
kind: 'testEnd',
|
|
187
|
+
testId: test.id,
|
|
188
|
+
workerIndex: typeof result.workerIndex === 'number' ? result.workerIndex : 0,
|
|
189
|
+
file,
|
|
190
|
+
titlePath,
|
|
191
|
+
project: this.projectOf(test),
|
|
192
|
+
outcome: result.status,
|
|
193
|
+
attempt: (typeof result.retry === 'number' ? result.retry : 0) + 1,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Claims: native annotations keep working unchanged; Phase 4 adds
|
|
197
|
+
// the injected claims (reconciled by file + title path) so a mapped
|
|
198
|
+
// test's evidence lands on the right claims. Both merge into one row
|
|
199
|
+
// (deduplicated, sorted) with the runner's own testId.
|
|
200
|
+
const claims = [...new Set([...this.annotationClaimsOf(test), ...this.injectedClaimsFor(test)])].sort();
|
|
201
|
+
if (claims.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
this.rows.push({
|
|
204
|
+
testId: test.id,
|
|
205
|
+
testFile: file ?? '',
|
|
206
|
+
location: test.location === undefined || test.location === null
|
|
207
|
+
? null
|
|
208
|
+
: { file: test.location.file, line: test.location.line, col: test.location.column },
|
|
209
|
+
claims,
|
|
210
|
+
status: result.status,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Playswright `onEnd`: seal the run-level outcome + write artifacts. */
|
|
214
|
+
async onEnd(result) {
|
|
215
|
+
this.runStatus = typeof result?.status === 'string' ? result.status : null;
|
|
216
|
+
// (Session settlement moved to the trusted CLI's spool drain —
|
|
217
|
+
// enforcement-review fix 3; nothing awaits here.)
|
|
218
|
+
this.writeRunnerOutcomes();
|
|
219
|
+
const stateDir = this.resolved.stateDir;
|
|
220
|
+
if (stateDir === null) {
|
|
221
|
+
if (this.rows.length > 0) {
|
|
222
|
+
console.warn('[gateforge] claims collected but no state dir: artifacts not written');
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// claims.json — the claim registry (GF-24's left side). Empty when
|
|
227
|
+
// the suite bypassed the fixture (GF-24) — the CLI still grades the
|
|
228
|
+
// run's obligations `missing`.
|
|
229
|
+
const claims = this.rows.flatMap((row) => row.claims.map((obligationId) => ({
|
|
230
|
+
schemaVersion: 1,
|
|
231
|
+
obligationId,
|
|
232
|
+
testId: row.testId,
|
|
233
|
+
...(row.testFile === '' ? {} : { testFile: row.testFile }),
|
|
234
|
+
...(row.location === null ? {} : { location: row.location }),
|
|
235
|
+
})));
|
|
236
|
+
writeJson(stateDir, 'claims.json', claims);
|
|
237
|
+
// records.json — ONLY the witness-issued ledger (GF-23 enforcement).
|
|
238
|
+
let records = [];
|
|
239
|
+
try {
|
|
240
|
+
const client = new WitnessClient(resolveWitnessUrl());
|
|
241
|
+
const ledger = (await client.listRecords());
|
|
242
|
+
records = Array.isArray(ledger.records) ? ledger.records : [];
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.warn(`[gateforge] cannot fetch the witness ledger: ${error.message}`);
|
|
246
|
+
}
|
|
247
|
+
writeJson(stateDir, 'records.json', records);
|
|
248
|
+
const obligationsRaw = readObligationsRaw(this.resolved.obligationsPath);
|
|
249
|
+
const obligations = obligationsRaw === null ? null : parseObligationsDocument(obligationsRaw);
|
|
250
|
+
const classifications = await this.fetchClassifications();
|
|
251
|
+
const now = this.runInstant(stateDir);
|
|
252
|
+
// Advisory route inventory (plan §9): the CLI-derived
|
|
253
|
+
// `http-routes.json` when present. Absent → null, and the core
|
|
254
|
+
// resolver returns its blocking missing-context result for HTTP
|
|
255
|
+
// rows. Never authoritative: the CLI recomputes from source.
|
|
256
|
+
const httpRoutes = readHttpRoutes(stateDir);
|
|
257
|
+
const ledger = this.ledgerRows(obligations, classifications, records, now, httpRoutes);
|
|
258
|
+
writeJson(stateDir, 'ledger.json', ledger);
|
|
259
|
+
const unclaimed = obligations === null
|
|
260
|
+
? 0
|
|
261
|
+
: new Set(obligations.obligations
|
|
262
|
+
.filter((entry) => !this.rows.some((row) => row.claims.includes(entry.id)))
|
|
263
|
+
.map((entry) => entry.id)).size;
|
|
264
|
+
this.printLedger(ledger, unclaimed);
|
|
265
|
+
this.printRegistryMismatches(obligations, records);
|
|
266
|
+
const blocking = ledger.some((row) => isBlocking(row.verdict));
|
|
267
|
+
if (blocking && process.env[ENV_REPORTER_FAIL_RUN] === '1') {
|
|
268
|
+
// Playwright overrides `process.exitCode` after reporters run, so
|
|
269
|
+
// setting it here does not fail the run (1.58.2 observed; the
|
|
270
|
+
// spike documented the same). GATEFORGE_REPORTER_FAIL_RUN=1 is an
|
|
271
|
+
// explicit opt-in to hard-exit: the gate ledger above is already
|
|
272
|
+
// printed, so exiting here loses nothing but the runner summary.
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Full title path of a test (runner API when present, else [title]). */
|
|
277
|
+
/**
|
|
278
|
+
* The catalog-identity title path: describe titles + the test title —
|
|
279
|
+
* NOT the runner's full path. Playwright's suite hierarchy is
|
|
280
|
+
* root → project → file → describe; identity joins (supervision,
|
|
281
|
+
* claim injections) use the SAME shape the native `--list` enumeration
|
|
282
|
+
* reports (describes + title), so the root/project/file suites are
|
|
283
|
+
* stripped here — by suite `type` when exposed, else by the location
|
|
284
|
+
* heuristic (root/project suites carry no source location; the file
|
|
285
|
+
* suite is the outermost suite that does). A project name or file name
|
|
286
|
+
* must never leak into the identity. Without a parent chain, the bare
|
|
287
|
+
* title keeps the row visible (never misjoined).
|
|
288
|
+
*/
|
|
289
|
+
titlePathOf(test) {
|
|
290
|
+
const title = test.title ?? test.id;
|
|
291
|
+
const describes = [];
|
|
292
|
+
let suite = test.parent ?? null;
|
|
293
|
+
while (suite !== null) {
|
|
294
|
+
if (this.isDescribeBoundary(suite))
|
|
295
|
+
break;
|
|
296
|
+
if (typeof suite.title === 'string' && suite.title.length > 0)
|
|
297
|
+
describes.unshift(suite.title);
|
|
298
|
+
suite = suite.parent ?? null;
|
|
299
|
+
}
|
|
300
|
+
return [...describes, title];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The runner project name for the supervision identity: the
|
|
304
|
+
* `project`-typed ancestor's title (reporter-API `TestCase` carries no
|
|
305
|
+
* `project` member), else the first location-less ancestor, else the
|
|
306
|
+
* runner-provided `project.name` best-effort. Absence stays `null` —
|
|
307
|
+
* never a fabricated project.
|
|
308
|
+
*/
|
|
309
|
+
projectOf(test) {
|
|
310
|
+
let suite = test.parent ?? null;
|
|
311
|
+
while (suite !== null) {
|
|
312
|
+
if (suite.type !== undefined) {
|
|
313
|
+
if (suite.type === 'project') {
|
|
314
|
+
return typeof suite.title === 'string' && suite.title.length > 0 ? suite.title : null;
|
|
315
|
+
}
|
|
316
|
+
if (suite.type === 'root')
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
else if (suite.location === undefined || suite.location === null) {
|
|
320
|
+
return typeof suite.title === 'string' && suite.title.length > 0 ? suite.title : null;
|
|
321
|
+
}
|
|
322
|
+
suite = suite.parent ?? null;
|
|
323
|
+
}
|
|
324
|
+
return typeof test.project?.name === 'string' && test.project.name.length > 0 ? test.project.name : null;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Whether this suite is at or above the describe level (root, project,
|
|
328
|
+
* or the file suite itself) — walking must stop here. By `type` when
|
|
329
|
+
* exposed; otherwise the file suite is the OUTERMOST suite with a
|
|
330
|
+
* source location (its parent is root/project or absent).
|
|
331
|
+
*/
|
|
332
|
+
isDescribeBoundary(suite) {
|
|
333
|
+
if (suite.type !== undefined) {
|
|
334
|
+
return suite.type !== 'describe';
|
|
335
|
+
}
|
|
336
|
+
const locationless = suite.location === undefined || suite.location === null;
|
|
337
|
+
if (locationless)
|
|
338
|
+
return true;
|
|
339
|
+
const parent = suite.parent ?? null;
|
|
340
|
+
if (parent === null)
|
|
341
|
+
return true;
|
|
342
|
+
if (parent.type !== undefined)
|
|
343
|
+
return parent.type !== 'describe';
|
|
344
|
+
return parent.location === undefined || parent.location === null;
|
|
345
|
+
}
|
|
346
|
+
/** Repo-relative posix file of a test (null when unknown). */
|
|
347
|
+
repoRelativeOf(test) {
|
|
348
|
+
if (test.location?.file === undefined || test.location.file === null)
|
|
349
|
+
return null;
|
|
350
|
+
const raw = test.location.file;
|
|
351
|
+
if (raw.length === 0)
|
|
352
|
+
return null;
|
|
353
|
+
return relative(process.cwd(), raw).split(sep).join('/');
|
|
354
|
+
}
|
|
355
|
+
/** Native annotation claims of one test (`{type: 'gateforge'}`). */
|
|
356
|
+
annotationClaimsOf(test) {
|
|
357
|
+
return (test.annotations ?? [])
|
|
358
|
+
.filter((annotation) => annotation.type === CLAIM_ANNOTATION_TYPE)
|
|
359
|
+
.map((annotation) => annotation.description ?? '')
|
|
360
|
+
.filter((description) => description.length > 0);
|
|
361
|
+
}
|
|
362
|
+
/** Injection-key lookup for one test: `<file>#<titlePath.join('>')>`. */
|
|
363
|
+
injectedClaimsFor(test) {
|
|
364
|
+
const file = this.repoRelativeOf(test);
|
|
365
|
+
if (file === null || this.injections.size === 0)
|
|
366
|
+
return [];
|
|
367
|
+
const key = `${file}#${this.titlePathOf(test).join('>')}`;
|
|
368
|
+
return this.injections.get(key) ?? [];
|
|
369
|
+
}
|
|
370
|
+
/** Writes the runner-outcomes document when the supervisor asked for it. */
|
|
371
|
+
writeRunnerOutcomes() {
|
|
372
|
+
const outcomesPath = this.resolved.outcomesPath;
|
|
373
|
+
if (outcomesPath === null)
|
|
374
|
+
return;
|
|
375
|
+
const shardRaw = process.env['TEST_SHARD'] ?? '';
|
|
376
|
+
const shardMatch = /^(\d+)\/(\d+)$/.exec(shardRaw);
|
|
377
|
+
const document = {
|
|
378
|
+
schemaVersion: 1,
|
|
379
|
+
runStatus: this.runStatus,
|
|
380
|
+
runnerErrors: [...this.runnerErrors],
|
|
381
|
+
outcomes: [...this.runnerOutcomes].sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
|
|
382
|
+
(a.titlePath.join('>') < b.titlePath.join('>') ? -1 : 1) ||
|
|
383
|
+
(a.testId < b.testId ? -1 : 1)),
|
|
384
|
+
shard: shardMatch !== null
|
|
385
|
+
? { index: Number(shardMatch[1]), total: Number(shardMatch[2]) }
|
|
386
|
+
: null,
|
|
387
|
+
};
|
|
388
|
+
try {
|
|
389
|
+
mkdirSync(join(outcomesPath, '..'), { recursive: true });
|
|
390
|
+
writeFileSync(outcomesPath, `${canonicalOf(document)}\n`, 'utf8');
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
// The outcomes document is supervision input; failing to write it
|
|
394
|
+
// must not crash the run — supervision then sees NO outcomes
|
|
395
|
+
// document and fails closed on its own.
|
|
396
|
+
console.warn(`[gateforge] cannot write runner outcomes: ${error.message}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** The witness classification projection (real lifecycle + primaryKey). */
|
|
400
|
+
async fetchClassifications() {
|
|
401
|
+
try {
|
|
402
|
+
const client = new WitnessClient(resolveWitnessUrl());
|
|
403
|
+
const response = (await authenticatedFetch(client.url, '/classifications', client.token));
|
|
404
|
+
if (typeof response !== 'object' || response === null)
|
|
405
|
+
return {};
|
|
406
|
+
const resources = response['resources'];
|
|
407
|
+
if (typeof resources !== 'object' || resources === null)
|
|
408
|
+
return {};
|
|
409
|
+
const out = {};
|
|
410
|
+
for (const [resourceId, view] of Object.entries(resources)) {
|
|
411
|
+
const entry = view;
|
|
412
|
+
const lifecycle = (entry['lifecycle'] ?? {});
|
|
413
|
+
out[resourceId] = {
|
|
414
|
+
primaryKey: Array.isArray(entry['primaryKey'])
|
|
415
|
+
? entry['primaryKey'].filter((key) => typeof key === 'string')
|
|
416
|
+
: ['id'],
|
|
417
|
+
exposure: typeof entry['exposure'] === 'string' ? entry['exposure'] : 'user-facing',
|
|
418
|
+
plane: typeof entry['plane'] === 'string' ? entry['plane'] : 'tenant',
|
|
419
|
+
...(typeof entry['evidenceAdapter'] === 'string'
|
|
420
|
+
? { evidenceAdapter: entry['evidenceAdapter'] }
|
|
421
|
+
: {}),
|
|
422
|
+
// The claims lane (http.endpoint resources) MUST reach the
|
|
423
|
+
// engine: without it a user-facing adapter-free entry fails the
|
|
424
|
+
// engine's classification validation and grades unclassified.
|
|
425
|
+
...(entry['evidenceLane'] === 'adapter' || entry['evidenceLane'] === 'claims'
|
|
426
|
+
? { evidenceLane: entry['evidenceLane'] }
|
|
427
|
+
: {}),
|
|
428
|
+
lifecycle: {
|
|
429
|
+
create: lifecycle['create'] === true,
|
|
430
|
+
read: lifecycle['read'] === true,
|
|
431
|
+
update: lifecycle['update'] === true,
|
|
432
|
+
delete: lifecycle['delete'] === true,
|
|
433
|
+
...(lifecycle['deleteSemantics'] === 'hard' || lifecycle['deleteSemantics'] === 'archive'
|
|
434
|
+
? { deleteSemantics: lifecycle['deleteSemantics'] }
|
|
435
|
+
: {}),
|
|
436
|
+
// Owner-owned archived state must reach the engine: it grades
|
|
437
|
+
// archive postconditions against it (audit round 5).
|
|
438
|
+
...(typeof lifecycle['archiveFields'] === 'object' &&
|
|
439
|
+
lifecycle['archiveFields'] !== null &&
|
|
440
|
+
!Array.isArray(lifecycle['archiveFields'])
|
|
441
|
+
? {
|
|
442
|
+
archiveFields: lifecycle['archiveFields'],
|
|
443
|
+
}
|
|
444
|
+
: {}),
|
|
445
|
+
...(Array.isArray(lifecycle['updateableFields'])
|
|
446
|
+
? {
|
|
447
|
+
updateableFields: lifecycle['updateableFields'].filter((field) => typeof field === 'string'),
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return {};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
ledgerRows(obligations, classifications, records, now, httpRoutes) {
|
|
460
|
+
const classMap = {};
|
|
461
|
+
for (const [resourceId, view] of Object.entries(classifications)) {
|
|
462
|
+
const primaryKey = view.primaryKey.length > 0 ? view.primaryKey : ['id'];
|
|
463
|
+
classMap[resourceId] = {
|
|
464
|
+
exposure: view.exposure === 'internal' ? 'internal' : 'user-facing',
|
|
465
|
+
plane: view.plane === 'master' ? 'master' : view.plane === 'global' ? 'global' : 'tenant',
|
|
466
|
+
...(view.evidenceAdapter === undefined ? {} : { evidenceAdapter: view.evidenceAdapter }),
|
|
467
|
+
...(view.evidenceLane === undefined ? {} : { evidenceLane: view.evidenceLane }),
|
|
468
|
+
lifecycle: view.lifecycle,
|
|
469
|
+
primaryKey,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const rows = [];
|
|
473
|
+
for (const row of this.rows) {
|
|
474
|
+
for (const obligationId of row.claims) {
|
|
475
|
+
rows.push(ledgerRowFor(claimOf(obligationId, {
|
|
476
|
+
id: row.testId,
|
|
477
|
+
location: row.location === null
|
|
478
|
+
? null
|
|
479
|
+
: {
|
|
480
|
+
file: row.location.file,
|
|
481
|
+
line: row.location.line,
|
|
482
|
+
column: row.location.col,
|
|
483
|
+
},
|
|
484
|
+
}), obligations, classMap, records, now, httpRoutes));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return rows.sort((a, b) => a.claim === b.claim ? (a.testId < b.testId ? -1 : 1) : a.claim < b.claim ? -1 : 1);
|
|
488
|
+
}
|
|
489
|
+
/** Deterministic instant: the run manifest's injected `startedAt`. */
|
|
490
|
+
runInstant(stateDir) {
|
|
491
|
+
try {
|
|
492
|
+
const manifest = JSON.parse(readFileSync(join(stateDir, 'manifest.json'), 'utf8'));
|
|
493
|
+
if (typeof manifest.startedAt === 'string')
|
|
494
|
+
return manifest.startedAt;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// fall through
|
|
498
|
+
}
|
|
499
|
+
return new Date().toISOString();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Prints the per-claim verdict ledger + the HONEST aggregate line
|
|
503
|
+
* (plan Phase 4 item 7): never an overall pass while unclaimed
|
|
504
|
+
* obligations block; the authoritative CLI remains the only final
|
|
505
|
+
* gate result.
|
|
506
|
+
*/
|
|
507
|
+
printLedger(rows, unclaimedObligations) {
|
|
508
|
+
const width = Math.max('CLAIMED OBLIGATION'.length, ...rows.map((row) => row.claim.length));
|
|
509
|
+
console.log('\n=== GATEFORGE VERDICTS ===');
|
|
510
|
+
if (rows.length === 0) {
|
|
511
|
+
console.log('no gateforge claims found in this run');
|
|
512
|
+
}
|
|
513
|
+
for (const row of rows) {
|
|
514
|
+
console.log(`${String(row.verdict).toUpperCase().padEnd(10)} ${row.claim.padEnd(width)} ${row.testFile}`);
|
|
515
|
+
if (row.reason !== null) {
|
|
516
|
+
console.log(` - ${row.reason}`);
|
|
517
|
+
}
|
|
518
|
+
if (row.recordIds.length > 0) {
|
|
519
|
+
console.log(` - records: ${row.recordIds.join(', ')}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
console.log(gateSummaryLine(rows, unclaimedObligations));
|
|
523
|
+
console.log('==========================\n');
|
|
524
|
+
}
|
|
525
|
+
/** GF-24 observability: obligations nobody claimed + records nobody claimed. */
|
|
526
|
+
printRegistryMismatches(obligations, records) {
|
|
527
|
+
const claimed = new Set(this.rows.flatMap((row) => row.claims));
|
|
528
|
+
if (obligations !== null) {
|
|
529
|
+
const unclaimed = obligations.obligations
|
|
530
|
+
.filter((entry) => !claimed.has(entry.id))
|
|
531
|
+
.map((entry) => entry.id)
|
|
532
|
+
.sort();
|
|
533
|
+
if (unclaimed.length > 0) {
|
|
534
|
+
console.warn(`[gateforge] obligations without any claim (will grade missing): ${unclaimed.join(', ')}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const claimedPairs = new Set(this.rows.flatMap((row) => row.claims.map((claim) => `${claim}\u0000${row.testId}`)));
|
|
538
|
+
const orphans = records
|
|
539
|
+
.filter((record) => !claimedPairs.has(`${record.obligationId}\u0000${record.testId}`))
|
|
540
|
+
.map((record) => `${record.obligationId} (${record.testId})`)
|
|
541
|
+
.sort();
|
|
542
|
+
if (orphans.length > 0) {
|
|
543
|
+
console.warn(`[gateforge] witness records with no matching claim (claim-registry mismatch, GF-24): ${orphans.join(', ')}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Playwright custom reporters MUST be the module's default export.
|
|
548
|
+
export default GateforgeReporter;
|
|
549
|
+
/**
|
|
550
|
+
* Reads the CLI-derived advisory route inventory (`http-routes.json`,
|
|
551
|
+
* written by `test-gates` beside the obligations document). Returns
|
|
552
|
+
* null when absent or malformed — the core resolver then returns its
|
|
553
|
+
* blocking missing-context result for HTTP rows. Advisory only: the
|
|
554
|
+
* authoritative CLI recomputes this list from source.
|
|
555
|
+
*
|
|
556
|
+
* Args:
|
|
557
|
+
* stateDir: absolute run-state directory.
|
|
558
|
+
*
|
|
559
|
+
* Returns:
|
|
560
|
+
* readonly HttpRouteCandidate[] | null: the advisory inventory or null.
|
|
561
|
+
*/
|
|
562
|
+
function readHttpRoutes(stateDir) {
|
|
563
|
+
let raw;
|
|
564
|
+
try {
|
|
565
|
+
raw = readFileSync(join(stateDir, 'http-routes.json'), 'utf8');
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
let document;
|
|
571
|
+
try {
|
|
572
|
+
document = JSON.parse(raw);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
if (typeof document !== 'object' || document === null || Array.isArray(document))
|
|
578
|
+
return null;
|
|
579
|
+
const routes = document['routes'];
|
|
580
|
+
if (!Array.isArray(routes))
|
|
581
|
+
return null;
|
|
582
|
+
const candidates = [];
|
|
583
|
+
for (const entry of routes) {
|
|
584
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
|
|
585
|
+
return null;
|
|
586
|
+
const candidate = entry;
|
|
587
|
+
if (typeof candidate['resourceId'] !== 'string' ||
|
|
588
|
+
typeof candidate['method'] !== 'string' ||
|
|
589
|
+
typeof candidate['canonicalPath'] !== 'string') {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
candidates.push({
|
|
593
|
+
resourceId: candidate['resourceId'],
|
|
594
|
+
method: candidate['method'],
|
|
595
|
+
canonicalPath: candidate['canonicalPath'],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return candidates;
|
|
599
|
+
}
|
|
600
|
+
/** Reads the obligations document path from options/env (null when unset/broken). */
|
|
601
|
+
function readObligationsRaw(obligationsPath) {
|
|
602
|
+
const path = obligationsPath ?? process.env[ENV_OBLIGATIONS] ?? null;
|
|
603
|
+
if (path === null || path === '')
|
|
604
|
+
return null;
|
|
605
|
+
try {
|
|
606
|
+
return readFileSync(path, 'utf8');
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Loads the CLI-written claim-injections document (Phase 4): a
|
|
614
|
+
* reconciliation key → obligation ids map for sidecar/native-mapped
|
|
615
|
+
* tests. Malformed documents contribute NOTHING (never a partial map) —
|
|
616
|
+
* injected claims are declarations, and a broken declaration must not
|
|
617
|
+
* silently redirect evidence.
|
|
618
|
+
*
|
|
619
|
+
* Args:
|
|
620
|
+
* stateDir: absolute run-state directory, when wired.
|
|
621
|
+
*
|
|
622
|
+
* Returns:
|
|
623
|
+
* Map<string, string[]>: reconciliation key → sorted obligation ids.
|
|
624
|
+
*/
|
|
625
|
+
function readClaimInjections(stateDir) {
|
|
626
|
+
const out = new Map();
|
|
627
|
+
if (stateDir === undefined || stateDir === '')
|
|
628
|
+
return out;
|
|
629
|
+
let raw;
|
|
630
|
+
try {
|
|
631
|
+
raw = readFileSync(join(stateDir, CLAIM_INJECTIONS_FILE), 'utf8');
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return out; // absent: annotation-only run (the common case)
|
|
635
|
+
}
|
|
636
|
+
let document;
|
|
637
|
+
try {
|
|
638
|
+
document = JSON.parse(raw);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
if (typeof document !== 'object' || document === null || Array.isArray(document))
|
|
644
|
+
return out;
|
|
645
|
+
const injections = document['injections'];
|
|
646
|
+
if (typeof injections !== 'object' || injections === null || Array.isArray(injections))
|
|
647
|
+
return out;
|
|
648
|
+
for (const [key, value] of Object.entries(injections)) {
|
|
649
|
+
if (!Array.isArray(value))
|
|
650
|
+
continue;
|
|
651
|
+
const claims = value.filter((claim) => typeof claim === 'string' && claim.length > 0);
|
|
652
|
+
if (claims.length === 0)
|
|
653
|
+
continue;
|
|
654
|
+
out.set(key, [...new Set(claims)].sort());
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
/** Writes one GF-canonical JSON state artifact. */
|
|
659
|
+
function writeJson(stateDir, name, value) {
|
|
660
|
+
mkdirSync(stateDir, { recursive: true });
|
|
661
|
+
writeFileSync(join(stateDir, name), `${canonicalOf(value)}\n`, 'utf8');
|
|
662
|
+
}
|
|
663
|
+
/** Authenticated GET helper for reporter-side witness calls. */
|
|
664
|
+
async function authenticatedFetch(url, path, token) {
|
|
665
|
+
const response = await fetch(`${url}${path}`, {
|
|
666
|
+
headers: { 'x-gateforge-run': token, accept: 'application/json' },
|
|
667
|
+
});
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
throw new Error(`witness ${path} answered HTTP ${response.status}`);
|
|
670
|
+
}
|
|
671
|
+
return response.json();
|
|
672
|
+
}
|
|
673
|
+
//# sourceMappingURL=reporter.js.map
|