@dogfood-lab/ingest 1.8.0 → 1.10.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/lib/integrity.js +15 -1
- package/lib/unsafe-segment.js +15 -7
- package/load-context.js +344 -55
- package/package.json +1 -1
- package/persist.js +250 -5
- package/rebuild-indexes.js +18 -2
- package/run.js +525 -44
- package/verify-chain.js +253 -17
package/persist.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* duplicate detection by run_id, directory creation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, mkdirSync, writeFileSync, renameSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, renameSync, openSync, closeSync, unlinkSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { join, dirname, relative, sep } from 'node:path';
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
|
|
@@ -14,6 +14,8 @@ import { validateRecord } from './validate-record.js';
|
|
|
14
14
|
import { isUnsafeSegment } from './lib/unsafe-segment.js';
|
|
15
15
|
import { submissionDigest } from './lib/integrity.js';
|
|
16
16
|
import { readChainHead, appendChainEntry } from './lib/chain-manifest.js';
|
|
17
|
+
import { parseRejectionReason } from '@dogfood-lab/verify';
|
|
18
|
+
import { SUPPORTED_SCHEMA_VERSIONS } from '@dogfood-lab/schemas';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Error thrown when writeRecord loses a TOCTOU race for the same canonical path.
|
|
@@ -29,6 +31,40 @@ export class DuplicateRunIdError extends Error {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when writeRecord's SECOND computeRecordPath() call — the one
|
|
36
|
+
* that computes the real write target, reached only AFTER validateRecord()
|
|
37
|
+
* has already confirmed the record is schema-valid — still cannot produce a
|
|
38
|
+
* safe path.
|
|
39
|
+
*
|
|
40
|
+
* F-bbbe2e1f: computeRecordPath()'s isUnsafeSegment check (lib/unsafe-segment.js)
|
|
41
|
+
* is STRICTER than the record schema's own repo pattern
|
|
42
|
+
* (`^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$`), which permits an embedded `..` or a
|
|
43
|
+
* lone `.` segment (e.g. `../etc`). A record can therefore pass
|
|
44
|
+
* validateRecord() above and still fail here — this is never a schema
|
|
45
|
+
* problem, it is the traversal backstop firing on schema-valid input. Pre-fix
|
|
46
|
+
* that let a bare, unclassified computeRecordPath Error (no `.code`) escape
|
|
47
|
+
* uncaught — the exact shape F-a37d36f5 eliminated at the FIRST
|
|
48
|
+
* computeRecordPath call (the isDuplicate probe below) but not this second
|
|
49
|
+
* one, three lines after validateRecord(). Classifying it here matches
|
|
50
|
+
* RecordValidationError's discipline and stays fail-closed: nothing below
|
|
51
|
+
* this point has touched the filesystem yet (mkdirSync/openSync/writeFileSync
|
|
52
|
+
* all come after), so no partial write is possible either way.
|
|
53
|
+
*/
|
|
54
|
+
export class UnsafeRecordPathError extends Error {
|
|
55
|
+
constructor(record, cause) {
|
|
56
|
+
super(
|
|
57
|
+
`record passed schema validation but its path could not be safely computed ` +
|
|
58
|
+
`(repo: ${record.repo}, run_id: ${record.run_id}): ${cause.message}`,
|
|
59
|
+
{ cause }
|
|
60
|
+
);
|
|
61
|
+
this.name = 'UnsafeRecordPathError';
|
|
62
|
+
this.code = 'UNSAFE_RECORD_PATH';
|
|
63
|
+
this.repo = record.repo;
|
|
64
|
+
this.runId = record.run_id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
/**
|
|
33
69
|
* Compute the canonical file path for a persisted record.
|
|
34
70
|
*
|
|
@@ -43,7 +79,15 @@ export function computeRecordPath(record, repoRoot) {
|
|
|
43
79
|
const status = record.verification?.status;
|
|
44
80
|
const base = status === 'rejected' ? 'records/_rejected' : 'records';
|
|
45
81
|
|
|
46
|
-
|
|
82
|
+
// V2-CROSS-BO-003 (F-54e5fde7 family): strictly two segments — the old
|
|
83
|
+
// destructure silently dropped a third segment (`group/subgroup/project`
|
|
84
|
+
// filed under `group/subgroup/`), sharding the record into the WRONG
|
|
85
|
+
// repo's path. Fail closed instead.
|
|
86
|
+
const segments = (record.repo || '').split('/');
|
|
87
|
+
if (segments.length !== 2) {
|
|
88
|
+
throw new Error(`invalid repo format: ${record.repo}`);
|
|
89
|
+
}
|
|
90
|
+
const [org, repo] = segments;
|
|
47
91
|
if (!org || !repo) {
|
|
48
92
|
throw new Error(`invalid repo format: ${record.repo}`);
|
|
49
93
|
}
|
|
@@ -80,9 +124,180 @@ export function computeRecordPath(record, repoRoot) {
|
|
|
80
124
|
return join(repoRoot, base, org, repo, year, month, day, filename);
|
|
81
125
|
}
|
|
82
126
|
|
|
127
|
+
/**
|
|
128
|
+
* F-4036ae25: a prior `_rejected` record whose rejection_reasons are ALL
|
|
129
|
+
* retryable-class is "the submitter can fix this and resubmit", not an
|
|
130
|
+
* unappealable verdict about the run — the same doctrine that already exempts
|
|
131
|
+
* an unfilable (`_skipPersist`) rejection from consuming its run_id. Narrowing
|
|
132
|
+
* `_skipPersist` in verify/index.js means a filable-but-rejected submission
|
|
133
|
+
* now DOES persist (the fleet-wide audit trail F-4036ae25 restores), so this
|
|
134
|
+
* is the other half of that fix: persisting the evidence must not resurrect
|
|
135
|
+
* the exact run_id-poisoning pathology F-82429f90 eliminated for
|
|
136
|
+
* validator-crash faults.
|
|
137
|
+
*
|
|
138
|
+
* F-f8952a50 (wave 10): this used to check `parseRejectionReason(r).prefix
|
|
139
|
+
* === 'schema:'` — ONE of the ten prefixes parse-rejection.js's own "Prefix
|
|
140
|
+
* taxonomy" documents under `class: 'submission-bad'` ("the submitter fixes
|
|
141
|
+
* the payload and resubmits"). Proven live with `repo:mismatch` (a pure
|
|
142
|
+
* identity/addressing mistake — the run happened, only the repo/run_url
|
|
143
|
+
* pairing was mis-stated): a corrected resubmission's payload never even
|
|
144
|
+
* reached verify() a second time, because both isDuplicate() call sites
|
|
145
|
+
* treated the stale `repo:` rejection as an unconditional block — see
|
|
146
|
+
* f-0f9e4077-retry-collision-duplicate-rejection.test.js's 'F-f8952a50'
|
|
147
|
+
* describe block for the end-to-end proof.
|
|
148
|
+
*
|
|
149
|
+
* The fix reads `parseRejectionReason(r).retryable` — the classifier's OWN
|
|
150
|
+
* per-prefix routing decision (see parse-rejection.js's file header for the
|
|
151
|
+
* full split) — instead of re-deriving a prefix allowlist here. A future
|
|
152
|
+
* prefix parse-rejection.js adds is automatically retryable or not with no
|
|
153
|
+
* edit to this file. This is intentionally NARROWER than "every submission-bad
|
|
154
|
+
* class": `retryable` is false for `policy:` and `provenance:` even though
|
|
155
|
+
* both are class `submission-bad` — those two prefixes are a rendered VERDICT
|
|
156
|
+
* on the run's own reported content (see parse-rejection.js), and letting a
|
|
157
|
+
* resubmission retry past one would let a submitter launder a genuinely-bad
|
|
158
|
+
* run into an accepted one by resubmitting different self-reported content
|
|
159
|
+
* under the same run_id. That boundary is independently pinned end-to-end by
|
|
160
|
+
* schema-invalid-skip-persist.test.js's "REGRESSION GUARD" test and the
|
|
161
|
+
* "persist-a-verdict doctrine is preserved" describe block — this fix must
|
|
162
|
+
* not (and, via the per-prefix flag, does not) touch it.
|
|
163
|
+
*
|
|
164
|
+
* `'operational'` / `'ingest'` / `'unknown'`-class reasons are always
|
|
165
|
+
* `retryable: false` too (see parse-rejection.js). Most `'operational'`-class
|
|
166
|
+
* reasons are not actually reachable here in production (F-82429f90 and its
|
|
167
|
+
* provenance-fault:/scenario-fetch-fault: siblings THROW instead of
|
|
168
|
+
* persisting a `_rejected` record — see runValidator in verify/index.js).
|
|
169
|
+
*
|
|
170
|
+
* F-51780da9 (wave 22, confirming audit of F-be0deacd): `CONTRACT_SCHEMA_TOO_NEW:`
|
|
171
|
+
* is the one documented exception where the frozen `retryable: false` is NOT
|
|
172
|
+
* the whole story. `validators/schema-version.js` RETURNS it as an ordinary
|
|
173
|
+
* rejection string rather than throwing, so a too-new-major submission
|
|
174
|
+
* genuinely does persist to `_rejected/` and IS read by this function.
|
|
175
|
+
* `retryable` is a STATIC per-prefix classification stamped at parse time —
|
|
176
|
+
* it has no way to observe that testing-os has SINCE been upgraded past the
|
|
177
|
+
* declared major, so trusting it alone lets one stale TOO_NEW rejection
|
|
178
|
+
* permanently poison a run_id even after the exact condition it was
|
|
179
|
+
* rejecting no longer holds (proven live: seed a `_rejected` record with a
|
|
180
|
+
* TOO_NEW reason declaring a major the CURRENT build already understands —
|
|
181
|
+
* pre-fix this function still returns `false` forever, because it never asks
|
|
182
|
+
* whether the stored major is still actually too new). `reevaluateTooNewRetry`
|
|
183
|
+
* below is the correction: for this ONE prefix, re-derive whether the STORED
|
|
184
|
+
* rejection's declared major is still above the CURRENT build's
|
|
185
|
+
* `SUPPORTED_SCHEMA_VERSIONS.<contract>.maxMajor`, instead of trusting the
|
|
186
|
+
* frozen boolean. The other nine prefixes were swept for the same trap (any
|
|
187
|
+
* retryable semantics that depend on mutable environment rather than the
|
|
188
|
+
* submission itself) and none qualify: schema:/policy-config:/repo:/
|
|
189
|
+
* submission-contains-verifier-field:/unsafe-record-path:/steps[<id>]:/
|
|
190
|
+
* CONTRACT_SCHEMA_TOO_OLD: are already retryable:true (an already-permissive
|
|
191
|
+
* prefix has no "stuck forever" direction to fix); policy:/provenance: are a
|
|
192
|
+
* DELIBERATE, permanent anti-gaming verdict on the run's own reported
|
|
193
|
+
* content, not an environment-dependent snapshot that goes stale;
|
|
194
|
+
* provenance-fault:/scenario-fetch-fault:/submission-malformed:/
|
|
195
|
+
* VALIDATOR_FAULT_*: all throw rather than persist (never reach this
|
|
196
|
+
* function); scenario-load: is evaluated against an immutable git
|
|
197
|
+
* commit_sha, not a build-version boundary, so the same committed content at
|
|
198
|
+
* the same ref parses the same way forever — there is no analogous integer
|
|
199
|
+
* ceiling to re-derive the way TOO_NEW's `maxMajor` comparison allows.
|
|
200
|
+
*
|
|
201
|
+
* Any read/parse failure fails closed (blocking): a corrupted or unreadable
|
|
202
|
+
* evidence file must never silently unblock a path collision. The same
|
|
203
|
+
* failure mode covers a CONTRACT_SCHEMA_TOO_NEW: reason whose shape
|
|
204
|
+
* `reevaluateTooNewRetry` cannot parse, or whose contract key is no longer
|
|
205
|
+
* registered in `SUPPORTED_SCHEMA_VERSIONS` — both fail closed to "still
|
|
206
|
+
* blocking" rather than guessing.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} rejectedPath - Absolute path already confirmed to exist.
|
|
209
|
+
* @returns {boolean} true when every rejection reason is individually
|
|
210
|
+
* retryable (see parseRejectionReason's `retryable` field), OR — for
|
|
211
|
+
* CONTRACT_SCHEMA_TOO_NEW: specifically — no longer applicable because
|
|
212
|
+
* testing-os has since been upgraded past the declared major.
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* F-51780da9: matches a `CONTRACT_SCHEMA_TOO_NEW:` reason string emitted by
|
|
217
|
+
* `validators/schema-version.js` — `CONTRACT_SCHEMA_TOO_NEW: <contract>
|
|
218
|
+
* schema v<major>.<minor>.<patch> ...` — capturing the contract key and the
|
|
219
|
+
* declared MAJOR, the two pieces of information needed to re-ask, against
|
|
220
|
+
* the CURRENT build, the exact question the stored rejection answered
|
|
221
|
+
* against a possibly-older one. Anchored to the literal TOO_NEW prefix only
|
|
222
|
+
* (never TOO_OLD:, a distinct literal) and requires a trailing space after
|
|
223
|
+
* the three-part version so it matches both the full production message
|
|
224
|
+
* (`... but this build supports v...`) and a shortened test fixture
|
|
225
|
+
* (`... — upgrade testing-os`) without over-matching a malformed variant.
|
|
226
|
+
*/
|
|
227
|
+
const TOO_NEW_DECLARED_VERSION = /^CONTRACT_SCHEMA_TOO_NEW: (\S+) schema v(\d+)\.\d+\.\d+ /;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* F-51780da9: re-evaluate a single STORED `CONTRACT_SCHEMA_TOO_NEW:` reason
|
|
231
|
+
* against the CURRENT build's `SUPPORTED_SCHEMA_VERSIONS`, instead of
|
|
232
|
+
* trusting the frozen `retryable: false` parse-rejection.js stamps on this
|
|
233
|
+
* prefix. `validators/schema-version.js` emits this prefix precisely when
|
|
234
|
+
* `declaredMajor > supported.maxMajor`; this re-runs that identical
|
|
235
|
+
* comparison against whatever `maxMajor` is TODAY, which a testing-os
|
|
236
|
+
* upgrade may have since raised past the stored declared major.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} reason - a single rejection_reasons[] entry.
|
|
239
|
+
* @returns {boolean} `true` when the declared major is no longer above the
|
|
240
|
+
* current build's supported ceiling (the rejection has been resolved by a
|
|
241
|
+
* since-applied upgrade); `false` when it is still genuinely too new, the
|
|
242
|
+
* contract key is unrecognized, or `reason` is not a
|
|
243
|
+
* CONTRACT_SCHEMA_TOO_NEW: reason at all — every non-match fails closed to
|
|
244
|
+
* "still blocking" rather than guessing.
|
|
245
|
+
*/
|
|
246
|
+
function reevaluateTooNewRetry(reason) {
|
|
247
|
+
const match = typeof reason === 'string' ? reason.match(TOO_NEW_DECLARED_VERSION) : null;
|
|
248
|
+
if (!match) return false;
|
|
249
|
+
const [, contract, declaredMajorStr] = match;
|
|
250
|
+
const supported = SUPPORTED_SCHEMA_VERSIONS[contract];
|
|
251
|
+
if (!supported) return false;
|
|
252
|
+
return Number(declaredMajorStr) <= supported.maxMajor;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isRetryableRejection(rejectedPath) {
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = JSON.parse(readFileSync(rejectedPath, 'utf-8'));
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const reasons = parsed?.verification?.rejection_reasons;
|
|
263
|
+
if (parsed?.verification?.status !== 'rejected' || !Array.isArray(reasons) || reasons.length === 0) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// F-51780da9: a reason counts as retryable either via the classifier's own
|
|
267
|
+
// static per-prefix flag, or — for CONTRACT_SCHEMA_TOO_NEW: specifically —
|
|
268
|
+
// via the re-derived, current-environment check above. Every other prefix
|
|
269
|
+
// is unaffected: reevaluateTooNewRetry returns false for any reason it
|
|
270
|
+
// does not recognize, so `.every()`'s existing behavior is unchanged for
|
|
271
|
+
// schema:/policy:/repo:/provenance:/etc.
|
|
272
|
+
return reasons.every(r => parseRejectionReason(r).retryable === true || reevaluateTooNewRetry(r));
|
|
273
|
+
}
|
|
274
|
+
|
|
83
275
|
/**
|
|
84
276
|
* Check if a record with this run_id already exists (accepted or rejected).
|
|
85
277
|
*
|
|
278
|
+
* F-0f9e4077 (wave-6 regression, fixed wave-8): computeRecordPath() is keyed
|
|
279
|
+
* on (run_id, date, accepted|rejected) — never on rejection_reasons content.
|
|
280
|
+
* So once a `_rejected` record occupies a path, ANY new record whose OWN
|
|
281
|
+
* status is STILL 'rejected' is headed to that exact same path no matter
|
|
282
|
+
* which violation it reports (an unexpected field with a different name, a
|
|
283
|
+
* different value, even a byte-identical resubmission all collide
|
|
284
|
+
* identically — computeRecordPath never looks at content). The
|
|
285
|
+
* isRetryableRejection carve-out below exists so a stale retryable-class
|
|
286
|
+
* rejection (F-f8952a50: any prefix parse-rejection.js's taxonomy flags
|
|
287
|
+
* `retryable: true` — shape/addressing mistakes, not just schema:) does not
|
|
288
|
+
* block a DIFFERENT-status (now accepted) record from reaching ITS OWN,
|
|
289
|
+
* different path — it was never a promise that the
|
|
290
|
+
* REJECTED path specifically is free. Pre-fix, skipping the status check let
|
|
291
|
+
* an uncorrected retry fall through as "not a duplicate," reach
|
|
292
|
+
* writeRecord()'s exclusive-create, and throw DuplicateRunIdError — a
|
|
293
|
+
* purely sequential, single-writer collision masquerading as the two-writer
|
|
294
|
+
* TOCTOU race that error class exists for. Gating on
|
|
295
|
+
* `record.verification.status === 'accepted'` restores the carve-out to
|
|
296
|
+
* exactly the case it can actually help (a record now headed elsewhere)
|
|
297
|
+
* while making a still-rejected collision an unconditional, ordinary
|
|
298
|
+
* duplicate — so writeRecord() short-circuits before ever attempting the
|
|
299
|
+
* write, and no throw is reachable for this class anymore.
|
|
300
|
+
*
|
|
86
301
|
* @param {string} runId
|
|
87
302
|
* @param {object} record - The record (used for repo/timing to compute path)
|
|
88
303
|
* @param {string} repoRoot
|
|
@@ -97,7 +312,14 @@ export function isDuplicate(runId, record, repoRoot) {
|
|
|
97
312
|
// Check rejected path
|
|
98
313
|
const rejectedRecord = { ...record, verification: { ...record.verification, status: 'rejected' } };
|
|
99
314
|
const rejectedPath = computeRecordPath(rejectedRecord, repoRoot);
|
|
100
|
-
if (existsSync(rejectedPath))
|
|
315
|
+
if (existsSync(rejectedPath)) {
|
|
316
|
+
// A record that is itself still rejected can only ever land at THIS
|
|
317
|
+
// path — asking whether the OLD occupant is retryable is moot when the
|
|
318
|
+
// NEW attempt has nowhere else to go. Only a record now bound for the
|
|
319
|
+
// accepted path (real forward progress) gets to ask that question.
|
|
320
|
+
if (record.verification?.status !== 'accepted') return true;
|
|
321
|
+
return !isRetryableRejection(rejectedPath);
|
|
322
|
+
}
|
|
101
323
|
|
|
102
324
|
return false;
|
|
103
325
|
}
|
|
@@ -122,7 +344,21 @@ export function isDuplicate(runId, record, repoRoot) {
|
|
|
122
344
|
* @throws {DuplicateRunIdError} when a concurrent writer won the race
|
|
123
345
|
*/
|
|
124
346
|
export function writeRecord(record, repoRoot) {
|
|
125
|
-
|
|
347
|
+
// F-a37d36f5: family sibling of the ingest.js pre-check fix — writeRecord
|
|
348
|
+
// carries its OWN premature isDuplicate call, reached even when the
|
|
349
|
+
// caller's own duplicate check was skipped or already cleared. A record
|
|
350
|
+
// whose repo/run_id computeRecordPath rejects (invalid format, unsafe
|
|
351
|
+
// segment) cannot possibly collide with an existing file, so treating the
|
|
352
|
+
// throw as "not a duplicate" here is the same semantically-free
|
|
353
|
+
// short-circuit as the run.js fix — it lets validateRecord() below be the
|
|
354
|
+
// authoritative judge instead of a raw path-computation throw.
|
|
355
|
+
let duplicate;
|
|
356
|
+
try {
|
|
357
|
+
duplicate = isDuplicate(record.run_id, record, repoRoot);
|
|
358
|
+
} catch {
|
|
359
|
+
duplicate = false;
|
|
360
|
+
}
|
|
361
|
+
if (duplicate) {
|
|
126
362
|
const path = computeRecordPath(record, repoRoot);
|
|
127
363
|
return { path, written: false };
|
|
128
364
|
}
|
|
@@ -152,7 +388,16 @@ export function writeRecord(record, repoRoot) {
|
|
|
152
388
|
// schema is the contract every downstream consumer relies on.
|
|
153
389
|
validateRecord(record);
|
|
154
390
|
|
|
155
|
-
|
|
391
|
+
// F-bbbe2e1f: see UnsafeRecordPathError's doc comment. Wrap this SECOND
|
|
392
|
+
// computeRecordPath() call — the first is inside isDuplicate() above,
|
|
393
|
+
// already guarded by F-a37d36f5 — so a schema-valid-but-unsafe repo (e.g.
|
|
394
|
+
// `../etc`) throws a classified error instead of a bare one.
|
|
395
|
+
let path;
|
|
396
|
+
try {
|
|
397
|
+
path = computeRecordPath(record, repoRoot);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
throw new UnsafeRecordPathError(record, err);
|
|
400
|
+
}
|
|
156
401
|
const dir = dirname(path);
|
|
157
402
|
|
|
158
403
|
mkdirSync(dir, { recursive: true });
|
package/rebuild-indexes.js
CHANGED
|
@@ -281,13 +281,29 @@ export function rebuildIndexes(repoRoot, options = {}) {
|
|
|
281
281
|
|
|
282
282
|
// --- latest-by-repo.json ---
|
|
283
283
|
// Keyed by repo, then product_surface. Only accepted records count.
|
|
284
|
-
|
|
284
|
+
//
|
|
285
|
+
// F-89b7dcd5: `record.repo` and `sr.product_surface` come from loadRecord()
|
|
286
|
+
// (JSON.parse only, no schema gate — see its JSDoc) rather than validated
|
|
287
|
+
// submission input, so a hand-committed record with repo: '__proto__' would
|
|
288
|
+
// otherwise resolve `latestByRepo['__proto__']` to Object.prototype itself
|
|
289
|
+
// (truthy, on a plain `{}`), skip the init branch below, and land the
|
|
290
|
+
// surface write ON Object.prototype — real global prototype pollution for
|
|
291
|
+
// the rest of this process. Object.create(null) removes the prototype
|
|
292
|
+
// chain entirely: `latestByRepo['__proto__']` is a plain (absent) data
|
|
293
|
+
// property, not the special accessor. JSON.stringify (below, at the
|
|
294
|
+
// commitGroupRename call) serializes a null-prototype object identically
|
|
295
|
+
// to a plain one, so this costs nothing on the write side. Not reachable
|
|
296
|
+
// from the ingest write path today (writeRecord's validateRecord() gate
|
|
297
|
+
// constrains repo's shape before persist.js ever writes a file), so this
|
|
298
|
+
// is defense-in-depth against a hand-committed or otherwise out-of-band
|
|
299
|
+
// record file, not a live vulnerability.
|
|
300
|
+
const latestByRepo = Object.create(null);
|
|
285
301
|
|
|
286
302
|
for (const record of allRecords) {
|
|
287
303
|
if (record.verification?.status !== 'accepted') continue;
|
|
288
304
|
|
|
289
305
|
const repo = record.repo;
|
|
290
|
-
if (!latestByRepo[repo]) latestByRepo[repo] =
|
|
306
|
+
if (!latestByRepo[repo]) latestByRepo[repo] = Object.create(null);
|
|
291
307
|
|
|
292
308
|
for (const sr of record.scenario_results || []) {
|
|
293
309
|
const surface = sr.product_surface;
|