@dogfood-lab/ingest 1.3.1 → 1.4.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/package.json +1 -1
- package/rebuild-indexes.js +44 -4
- package/run.js +79 -15
package/package.json
CHANGED
package/rebuild-indexes.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
28
|
-
import { join, relative } from 'node:path';
|
|
28
|
+
import { join, relative, sep } from 'node:path';
|
|
29
29
|
import { randomBytes } from 'node:crypto';
|
|
30
30
|
|
|
31
31
|
import { logStage as sharedLogStage } from '@dogfood-lab/dogfood-swarm/lib/log-stage.js';
|
|
@@ -49,14 +49,44 @@ function logStage(stage, fields = {}) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Recursively find all .json files under a directory.
|
|
51
51
|
*
|
|
52
|
+
* d3-ingest-B005 (Stage C humanization): the `readdirSync` is wrapped so an
|
|
53
|
+
* unreadable subtree (EACCES, ENOTDIR, a Windows lock — the same class
|
|
54
|
+
* renameWithRetry defends against) degrades to "that subtree's records are
|
|
55
|
+
* missing from this rebuild" instead of throwing and aborting the WHOLE scan.
|
|
56
|
+
* This restores parity with the per-FILE tolerance of `loadRecord` just below
|
|
57
|
+
* (module header: "does NOT crash on a single bad file") and mirrors the
|
|
58
|
+
* in-repo precedent in `packages/portfolio/lib/parse-regression-pins.js`'s
|
|
59
|
+
* `walkSourceFiles`. The skip is NOT silent: a structured
|
|
60
|
+
* `logStage('warn', { kind: 'dir_unreadable', path })` NDJSON line makes it
|
|
61
|
+
* greppable alongside the rest of the pipeline (`"kind":"dir_unreadable"`), so
|
|
62
|
+
* the operator can see which subtree was dropped and why the index is partial.
|
|
63
|
+
* Exported so the guard is unit-testable in isolation (the single wrapped
|
|
64
|
+
* `readdirSync` serves both the top-level call and every recursive descent).
|
|
65
|
+
*
|
|
52
66
|
* @param {string} dir
|
|
53
67
|
* @returns {string[]}
|
|
54
68
|
*/
|
|
55
|
-
function findJsonFiles(dir) {
|
|
69
|
+
export function findJsonFiles(dir) {
|
|
56
70
|
const results = [];
|
|
57
71
|
if (!existsSync(dir)) return results;
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// Unreadable directory (locked / permission-restricted / not a dir).
|
|
78
|
+
// Skip this subtree rather than sinking the whole rebuild, but emit a
|
|
79
|
+
// structured, greppable warn naming the path so the partial index is
|
|
80
|
+
// explained — not a silent swallow.
|
|
81
|
+
logStage('warn', {
|
|
82
|
+
kind: 'dir_unreadable',
|
|
83
|
+
reason: err && err.code ? err.code : 'readdir_failed',
|
|
84
|
+
path: dir,
|
|
85
|
+
error: err && err.message ? err.message : String(err),
|
|
86
|
+
});
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
for (const entry of entries) {
|
|
61
91
|
const fullPath = join(dir, entry.name);
|
|
62
92
|
if (entry.isDirectory()) {
|
|
@@ -115,7 +145,17 @@ export function rebuildIndexes(repoRoot, options = {}) {
|
|
|
115
145
|
const skipped = [];
|
|
116
146
|
|
|
117
147
|
for (const f of [...acceptedFiles, ...rejectedFiles]) {
|
|
118
|
-
|
|
148
|
+
// SEED-1 (d3-ingest-001) — posixify at the serialization boundary.
|
|
149
|
+
// `relative()` returns OS-native separators (backslashes on win32). This
|
|
150
|
+
// value becomes `record._path` and is serialized verbatim into all three
|
|
151
|
+
// committed index `path` fields (latest-by-repo / failing / stale), which
|
|
152
|
+
// downstream consumers read as raw.githubusercontent.com URL fragments
|
|
153
|
+
// (docs/policy-contract.md Gate F) — a backslash there is a broken URL.
|
|
154
|
+
// Normalize ONCE here, at the single source the whole family flows through,
|
|
155
|
+
// so every serialized path is forward-slash regardless of host OS. Mirrors
|
|
156
|
+
// the canonical transform proven in
|
|
157
|
+
// packages/portfolio/lib/parse-regression-pins.js:75. NEVER a win32-skip.
|
|
158
|
+
const relPath = relative(repoRoot, f).split(sep).join('/');
|
|
119
159
|
const { record, error } = loadRecord(f);
|
|
120
160
|
if (error) {
|
|
121
161
|
corrupted.push({ path: relPath, error });
|
package/run.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - regenerate indexes
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { resolve, dirname } from 'node:path';
|
|
20
|
+
import { resolve, dirname, sep } from 'node:path';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { randomBytes } from 'node:crypto';
|
|
23
23
|
|
|
@@ -30,6 +30,27 @@ import { rebuildIndexes } from './rebuild-indexes.js';
|
|
|
30
30
|
|
|
31
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* SEED-1 (d3-ingest-003) — posixify a path-shaped value at the operator/log
|
|
35
|
+
* SERIALIZATION boundary. `computeRecordPath`/`writeRecord` return OS-native
|
|
36
|
+
* paths (backslash-separated, absolute, on win32) because those values are
|
|
37
|
+
* also used for real filesystem operations. But the moment a path crosses into
|
|
38
|
+
* CLI JSON output, NDJSON log lines, or a downstream report
|
|
39
|
+
* (dogfood-swarm persist.js records `report.dogfood.path`), it must be
|
|
40
|
+
* forward-slash so operators and log pivots see one canonical shape across
|
|
41
|
+
* OSes — and so a copy-paste into a raw.githubusercontent URL is not a broken
|
|
42
|
+
* link. We posixify ONLY here, at the emit sites, leaving the returned fs paths
|
|
43
|
+
* OS-native for the filesystem layer. Mirrors the boundary-normalize doctrine
|
|
44
|
+
* already used in rebuild-indexes.js and parse-regression-pins.js. NEVER a
|
|
45
|
+
* win32-skip.
|
|
46
|
+
*
|
|
47
|
+
* @param {string|null} p
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
function posixifyPath(p) {
|
|
51
|
+
return typeof p === 'string' ? p.split(sep).join('/') : p;
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
/**
|
|
34
55
|
* Emit a single structured stage-transition log line via the shared helper.
|
|
35
56
|
*
|
|
@@ -238,7 +259,10 @@ export async function ingest(submission, options) {
|
|
|
238
259
|
logStage('persist_complete', {
|
|
239
260
|
submission_id: submissionId,
|
|
240
261
|
correlation_id,
|
|
241
|
-
path
|
|
262
|
+
// d3-ingest-003: posixify at the log boundary — `path` is OS-native from
|
|
263
|
+
// writeRecord (used for the fs write); the NDJSON log surface gets forward
|
|
264
|
+
// slashes so log pivots are byte-identical across OSes.
|
|
265
|
+
path: posixifyPath(path),
|
|
242
266
|
written,
|
|
243
267
|
duplicate: !written,
|
|
244
268
|
duration_ms: Date.now() - persistStart
|
|
@@ -281,11 +305,12 @@ export async function ingest(submission, options) {
|
|
|
281
305
|
failed_stage: 'rebuild_indexes',
|
|
282
306
|
message: err.message,
|
|
283
307
|
stack: truncatedStack,
|
|
284
|
-
|
|
308
|
+
// d3-ingest-003: operator-facing path → posixify at the log boundary.
|
|
309
|
+
record_persisted_at: posixifyPath(path),
|
|
285
310
|
recovery: 'next ingest will trigger a full rebuild of indexes/'
|
|
286
311
|
});
|
|
287
312
|
console.error(
|
|
288
|
-
`WARNING: record persisted at ${path}, but index rebuild failed: ${err.message}\n` +
|
|
313
|
+
`WARNING: record persisted at ${posixifyPath(path)}, but index rebuild failed: ${err.message}\n` +
|
|
289
314
|
` indexes/ may be stale until next ingest. To force rebuild now, re-run any test ingest.\n` +
|
|
290
315
|
` stack: ${stackPreview}`
|
|
291
316
|
);
|
|
@@ -429,7 +454,10 @@ export async function verifyOnly(submission, options) {
|
|
|
429
454
|
submission_id: submissionId,
|
|
430
455
|
correlation_id,
|
|
431
456
|
status: record.verification?.status ?? null,
|
|
432
|
-
|
|
457
|
+
// d3-ingest-003: posixify at the log boundary. The returned
|
|
458
|
+
// `would_persist_to` below stays OS-native so callers that resolve it
|
|
459
|
+
// against the filesystem keep a real fs path.
|
|
460
|
+
would_persist_to: posixifyPath(would_persist_to)
|
|
433
461
|
});
|
|
434
462
|
|
|
435
463
|
return { record, would_persist_to, verify_only: true };
|
|
@@ -467,7 +495,19 @@ const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(__dirname
|
|
|
467
495
|
|
|
468
496
|
if (isMain) {
|
|
469
497
|
const args = process.argv.slice(2);
|
|
470
|
-
|
|
498
|
+
// SEED-2 (d3-ingest-002) — make the CLI's repoRoot overridable so callers
|
|
499
|
+
// (notably dogfood-swarm's commands/persist.js execSync, and any test
|
|
500
|
+
// harness) can redirect every record write + index rebuild into a sandbox
|
|
501
|
+
// instead of the REAL working tree. Without this the only way to sandbox was
|
|
502
|
+
// a brittle source-copy of this file (the setupTempRunJs run.js-copy in
|
|
503
|
+
// d1b-001-cli-toplevel-error-event.test.js) that rewrote __dirname's `../..`
|
|
504
|
+
// walk. A production caller passes the real root explicitly; a test passes a
|
|
505
|
+
// temp dir; the default preserves the historical behavior when the env var
|
|
506
|
+
// is unset. resolve() makes a relative override absolute so the downstream
|
|
507
|
+
// join()s stay anchored.
|
|
508
|
+
const repoRoot = process.env.INGEST_REPO_ROOT
|
|
509
|
+
? resolve(process.env.INGEST_REPO_ROOT)
|
|
510
|
+
: resolve(__dirname, '../..');
|
|
471
511
|
|
|
472
512
|
// Parse CLI flags
|
|
473
513
|
let submissionJson;
|
|
@@ -476,9 +516,28 @@ if (isMain) {
|
|
|
476
516
|
const positionalArgs = [];
|
|
477
517
|
|
|
478
518
|
for (let i = 0; i < args.length; i++) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
519
|
+
// Accept BOTH the space form (`--flag value`) and the equals form
|
|
520
|
+
// (`--flag=value`). The prior parser matched only the space form, so a
|
|
521
|
+
// caller passing `--provenance=stub --file=...` (the shape dogfood-swarm's
|
|
522
|
+
// commands/persist.js builds for its execSync invocation) fell through to
|
|
523
|
+
// positionalArgs — `--provenance` then read as missing and the CLI exited 2
|
|
524
|
+
// ("--provenance flag is required"), silently breaking `swarm persist
|
|
525
|
+
// --ingest` on every platform. (dogfood-swarm self-audit follow-up.)
|
|
526
|
+
let arg = args[i];
|
|
527
|
+
let inlineValue = null;
|
|
528
|
+
if (arg.startsWith('--')) {
|
|
529
|
+
const eq = arg.indexOf('=');
|
|
530
|
+
if (eq !== -1) {
|
|
531
|
+
inlineValue = arg.slice(eq + 1);
|
|
532
|
+
arg = arg.slice(0, eq);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const hasValue = inlineValue !== null || args[i + 1] !== undefined;
|
|
536
|
+
const takeValue = () => (inlineValue !== null ? inlineValue : args[++i]);
|
|
537
|
+
|
|
538
|
+
if (arg === '--provenance' && hasValue) {
|
|
539
|
+
provenanceMode = takeValue();
|
|
540
|
+
} else if (arg === '--file' && hasValue) {
|
|
482
541
|
const { readFileSync } = await import('node:fs');
|
|
483
542
|
// D1B-001 family (operator-legibility): a --file read failure
|
|
484
543
|
// (ENOENT/EACCES) routes through the structured error event and exits 2
|
|
@@ -488,7 +547,7 @@ if (isMain) {
|
|
|
488
547
|
// correlation id here (the same pivot the JSON.parse catch uses when
|
|
489
548
|
// there is no submission to derive a run_id from yet).
|
|
490
549
|
try {
|
|
491
|
-
submissionJson = readFileSync(resolve(
|
|
550
|
+
submissionJson = readFileSync(resolve(takeValue()), 'utf-8');
|
|
492
551
|
} catch (err) {
|
|
493
552
|
emitCliErrorEvent({
|
|
494
553
|
failedStage: 'cli_read_file',
|
|
@@ -498,9 +557,9 @@ if (isMain) {
|
|
|
498
557
|
});
|
|
499
558
|
process.exit(2);
|
|
500
559
|
}
|
|
501
|
-
} else if (
|
|
502
|
-
submissionJson =
|
|
503
|
-
} else if (
|
|
560
|
+
} else if (arg === '--payload' && hasValue) {
|
|
561
|
+
submissionJson = takeValue();
|
|
562
|
+
} else if (arg === '--verify-only') {
|
|
504
563
|
// F-252714-058: dry-run the pipeline without writing or rebuilding
|
|
505
564
|
// indexes. CI / operators preview what WOULD have been persisted.
|
|
506
565
|
verifyOnlyFlag = true;
|
|
@@ -625,7 +684,10 @@ if (isMain) {
|
|
|
625
684
|
status: result.record.verification.status,
|
|
626
685
|
run_id: result.record.run_id ?? null,
|
|
627
686
|
verdict: result.record.overall_verdict?.verified ?? null,
|
|
628
|
-
|
|
687
|
+
// d3-ingest-003: posixify path-shaped CLI output so the operator
|
|
688
|
+
// contract is identical across OSes (a Windows backslash here breaks
|
|
689
|
+
// any downstream URL-build/string-match).
|
|
690
|
+
would_persist_to: posixifyPath(result.would_persist_to),
|
|
629
691
|
verify_only: true,
|
|
630
692
|
rejection_reasons: result.record.verification.rejection_reasons ?? []
|
|
631
693
|
}));
|
|
@@ -648,7 +710,9 @@ if (isMain) {
|
|
|
648
710
|
status: result.record.verification.status,
|
|
649
711
|
run_id: result.record.run_id ?? null,
|
|
650
712
|
verdict: result.record.overall_verdict?.verified ?? null,
|
|
651
|
-
|
|
713
|
+
// d3-ingest-003: posixify path-shaped CLI output (same family as
|
|
714
|
+
// would_persist_to above). dogfood-swarm's persist.js pivots on this.
|
|
715
|
+
path: posixifyPath(result.path),
|
|
652
716
|
written: result.written,
|
|
653
717
|
rejection_reasons: result.record.verification.rejection_reasons ?? []
|
|
654
718
|
}));
|