@patronage/software-factory 1.0.0-alpha.5 → 1.0.0-alpha.6
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/index.d.ts +27 -37
- package/dist/index.js +20 -333
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -145,8 +145,6 @@ interface HqIngestDependencies {
|
|
|
145
145
|
* `timeoutMs`: see the note there.
|
|
146
146
|
*/
|
|
147
147
|
journalFlushBudgetMs?: number;
|
|
148
|
-
/** @deprecated Legacy sidecars are now streamed directly; retained for API compatibility. */
|
|
149
|
-
sidecarReclaimMs?: number;
|
|
150
148
|
/**
|
|
151
149
|
* Setup budget for callers that own the process lifetime — honored AS GIVEN,
|
|
152
150
|
* above the `DEFAULT_HQ_INGEST_TIMEOUT_MS` ceiling that clamps `timeoutMs`.
|
|
@@ -184,12 +182,6 @@ interface HqSpoolEntryOutcome {
|
|
|
184
182
|
kind: string;
|
|
185
183
|
spool: string;
|
|
186
184
|
/**
|
|
187
|
-
* `migrated` belongs to the legacy JSONL journal only: the row was moved
|
|
188
|
-
* into the current spool without being delivered. The spool pass that runs
|
|
189
|
-
* after it in the same drain supersedes that line with a real outcome when
|
|
190
|
-
* it gets to the row; a `migrated` line that survives the run means the row
|
|
191
|
-
* is still waiting.
|
|
192
|
-
*
|
|
193
185
|
* `undeliverable` is the one terminal verdict (#445). Every other status
|
|
194
186
|
* describes a moment: HQ was unreachable, HQ refused this content today, the
|
|
195
187
|
* row moved. Retrying is meaningful for all of them. A wrong-origin entry is
|
|
@@ -198,13 +190,11 @@ interface HqSpoolEntryOutcome {
|
|
|
198
190
|
* run. Leaving it spooled asks the operator to retry something that provably
|
|
199
191
|
* cannot succeed, and the count it inflates is the one doctor goes red on.
|
|
200
192
|
*/
|
|
201
|
-
status: "delivered" | "duplicate" | "
|
|
193
|
+
status: "delivered" | "duplicate" | "rejected" | "undeliverable" | "unreachable";
|
|
202
194
|
}
|
|
203
195
|
interface HqSpoolFlushInput {
|
|
204
196
|
clientId: string;
|
|
205
197
|
clientSecret: string;
|
|
206
|
-
/** Repository root whose legacy `.factory-memory` spool is also drained. */
|
|
207
|
-
cwd: string;
|
|
208
198
|
/** The profile's HQ origin; entries recorded against another are refused. */
|
|
209
199
|
endpoint: string;
|
|
210
200
|
/** Operator-named spool directories; replaces the default two locations. */
|
|
@@ -239,13 +229,13 @@ interface HqSpoolFlushSummary {
|
|
|
239
229
|
interface HqSpoolWorkCount {
|
|
240
230
|
/**
|
|
241
231
|
* The earliest moment learned across pending spool files (their own write
|
|
242
|
-
* time)
|
|
232
|
+
* time). Absent only when
|
|
243
233
|
* `pending` is `0`, or when every timestamp source was unreadable within
|
|
244
234
|
* budget — an estimate for doctor's remediation message (#394), never a
|
|
245
235
|
* precise audit trail.
|
|
246
236
|
*/
|
|
247
237
|
oldestQueuedAt?: string;
|
|
248
|
-
/** Spooled events
|
|
238
|
+
/** Spooled events waiting in the locations below. */
|
|
249
239
|
pending: number;
|
|
250
240
|
/** Locations that exist and hold spooled work. */
|
|
251
241
|
spools: string[];
|
|
@@ -266,7 +256,7 @@ interface HqSpoolWorkCount {
|
|
|
266
256
|
* same locations `flushHqSpool` drains are inspected here, read-only: no
|
|
267
257
|
* directory is created, nothing is secured, and nothing is delivered.
|
|
268
258
|
*/
|
|
269
|
-
declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "
|
|
259
|
+
declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "explicitDirectories" | "repository">, dependencies?: {
|
|
270
260
|
budgetMs?: number;
|
|
271
261
|
env?: NodeJS.ProcessEnv;
|
|
272
262
|
}): Promise<HqSpoolWorkCount>;
|
|
@@ -275,7 +265,7 @@ interface HqSpoolOrphan {
|
|
|
275
265
|
/** The `hq-retry-spool` directory itself, ready to pass to `--dir`. */
|
|
276
266
|
directory: string;
|
|
277
267
|
oldestQueuedAt?: string;
|
|
278
|
-
/** Spooled events
|
|
268
|
+
/** Spooled events waiting there. */
|
|
279
269
|
pending: number;
|
|
280
270
|
/**
|
|
281
271
|
* The location exists but could not be listed. As everywhere else in this
|
|
@@ -753,8 +743,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
753
743
|
generationId: z.ZodNumber;
|
|
754
744
|
headSha: z.ZodString;
|
|
755
745
|
outcome: z.ZodEnum<{
|
|
756
|
-
active: "active";
|
|
757
746
|
stale: "stale";
|
|
747
|
+
active: "active";
|
|
758
748
|
}>;
|
|
759
749
|
reason: z.ZodString;
|
|
760
750
|
recordedAt: z.ZodISODateTime;
|
|
@@ -1234,8 +1224,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1234
1224
|
"run-interior-cycle": "run-interior-cycle";
|
|
1235
1225
|
}>;
|
|
1236
1226
|
stage: z.ZodEnum<{
|
|
1237
|
-
gate: "gate";
|
|
1238
1227
|
interior: "interior";
|
|
1228
|
+
gate: "gate";
|
|
1239
1229
|
"interior-complete": "interior-complete";
|
|
1240
1230
|
}>;
|
|
1241
1231
|
}, z.core.$strip>>;
|
|
@@ -1251,10 +1241,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1251
1241
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1252
1242
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1253
1243
|
status: z.ZodEnum<{
|
|
1254
|
-
blocked: "blocked";
|
|
1255
1244
|
"not-required": "not-required";
|
|
1256
|
-
|
|
1245
|
+
blocked: "blocked";
|
|
1257
1246
|
current: "current";
|
|
1247
|
+
stale: "stale";
|
|
1258
1248
|
missing: "missing";
|
|
1259
1249
|
}>;
|
|
1260
1250
|
}, z.core.$strip>;
|
|
@@ -1263,10 +1253,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1263
1253
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1264
1254
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1265
1255
|
status: z.ZodEnum<{
|
|
1266
|
-
blocked: "blocked";
|
|
1267
1256
|
"not-required": "not-required";
|
|
1268
|
-
|
|
1257
|
+
blocked: "blocked";
|
|
1269
1258
|
current: "current";
|
|
1259
|
+
stale: "stale";
|
|
1270
1260
|
missing: "missing";
|
|
1271
1261
|
}>;
|
|
1272
1262
|
}, z.core.$strip>>;
|
|
@@ -1277,8 +1267,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1277
1267
|
docsOnlyDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
1278
1268
|
docsOnlyVerifiedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1279
1269
|
prVerify: z.ZodEnum<{
|
|
1280
|
-
stale: "stale";
|
|
1281
1270
|
passed: "passed";
|
|
1271
|
+
stale: "stale";
|
|
1282
1272
|
missing: "missing";
|
|
1283
1273
|
}>;
|
|
1284
1274
|
trivialDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1778,10 +1768,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
|
|
|
1778
1768
|
//#region src/review-proof-applicability.d.ts
|
|
1779
1769
|
declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
|
|
1780
1770
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
1781
|
-
blocked: "blocked";
|
|
1782
1771
|
"not-required": "not-required";
|
|
1783
|
-
|
|
1772
|
+
blocked: "blocked";
|
|
1784
1773
|
current: "current";
|
|
1774
|
+
stale: "stale";
|
|
1785
1775
|
missing: "missing";
|
|
1786
1776
|
}>;
|
|
1787
1777
|
type ReviewStatus = z.infer<typeof reviewStatusSchema>;
|
|
@@ -2187,9 +2177,9 @@ declare const retroEnvelopeV1Schema: z.ZodObject<{
|
|
|
2187
2177
|
kind: z.ZodLiteral<"retro-envelope">;
|
|
2188
2178
|
outcome: z.ZodOptional<z.ZodObject<{
|
|
2189
2179
|
status: z.ZodEnum<{
|
|
2190
|
-
blocked: "blocked";
|
|
2191
2180
|
success: "success";
|
|
2192
2181
|
fail: "fail";
|
|
2182
|
+
blocked: "blocked";
|
|
2193
2183
|
"ship-with-followups": "ship-with-followups";
|
|
2194
2184
|
}>;
|
|
2195
2185
|
verdict: z.ZodOptional<z.ZodString>;
|
|
@@ -2949,8 +2939,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
2949
2939
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
2950
2940
|
}, z.core.$strict>>;
|
|
2951
2941
|
stage: z.ZodEnum<{
|
|
2952
|
-
gate: "gate";
|
|
2953
2942
|
interior: "interior";
|
|
2943
|
+
gate: "gate";
|
|
2954
2944
|
}>;
|
|
2955
2945
|
usage: z.ZodOptional<z.ZodObject<{
|
|
2956
2946
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3002,8 +2992,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3002
2992
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
3003
2993
|
}, z.core.$strict>>;
|
|
3004
2994
|
stage: z.ZodEnum<{
|
|
3005
|
-
gate: "gate";
|
|
3006
2995
|
interior: "interior";
|
|
2996
|
+
gate: "gate";
|
|
3007
2997
|
}>;
|
|
3008
2998
|
usage: z.ZodOptional<z.ZodObject<{
|
|
3009
2999
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3046,8 +3036,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3046
3036
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
3047
3037
|
}, z.core.$strict>>;
|
|
3048
3038
|
stage: z.ZodEnum<{
|
|
3049
|
-
gate: "gate";
|
|
3050
3039
|
interior: "interior";
|
|
3040
|
+
gate: "gate";
|
|
3051
3041
|
}>;
|
|
3052
3042
|
usage: z.ZodOptional<z.ZodObject<{
|
|
3053
3043
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3077,7 +3067,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3077
3067
|
staleRepeats: number;
|
|
3078
3068
|
};
|
|
3079
3069
|
observedAt: string;
|
|
3080
|
-
stage: "
|
|
3070
|
+
stage: "interior" | "gate";
|
|
3081
3071
|
issue?: number | undefined;
|
|
3082
3072
|
pr?: number | undefined;
|
|
3083
3073
|
threadId?: string | undefined;
|
|
@@ -3333,8 +3323,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3333
3323
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3334
3324
|
observedAt: z.ZodString;
|
|
3335
3325
|
purpose: z.ZodEnum<{
|
|
3336
|
-
review: "review";
|
|
3337
3326
|
code: "code";
|
|
3327
|
+
review: "review";
|
|
3338
3328
|
}>;
|
|
3339
3329
|
slotResolution: z.ZodObject<{
|
|
3340
3330
|
effort: z.ZodEnum<{
|
|
@@ -3412,8 +3402,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3412
3402
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3413
3403
|
observedAt: z.ZodString;
|
|
3414
3404
|
purpose: z.ZodEnum<{
|
|
3415
|
-
review: "review";
|
|
3416
3405
|
code: "code";
|
|
3406
|
+
review: "review";
|
|
3417
3407
|
}>;
|
|
3418
3408
|
slotResolution: z.ZodObject<{
|
|
3419
3409
|
effort: z.ZodEnum<{
|
|
@@ -3482,8 +3472,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3482
3472
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3483
3473
|
observedAt: z.ZodString;
|
|
3484
3474
|
purpose: z.ZodEnum<{
|
|
3485
|
-
review: "review";
|
|
3486
3475
|
code: "code";
|
|
3476
|
+
review: "review";
|
|
3487
3477
|
}>;
|
|
3488
3478
|
slotResolution: z.ZodObject<{
|
|
3489
3479
|
effort: z.ZodEnum<{
|
|
@@ -3555,7 +3545,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3555
3545
|
exitCode: number | null;
|
|
3556
3546
|
harness: string;
|
|
3557
3547
|
observedAt: string;
|
|
3558
|
-
purpose: "
|
|
3548
|
+
purpose: "code" | "review";
|
|
3559
3549
|
slotResolution: {
|
|
3560
3550
|
effort: "high" | "low" | "medium" | "xhigh";
|
|
3561
3551
|
engine: string;
|
|
@@ -4076,13 +4066,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4076
4066
|
reviews: {
|
|
4077
4067
|
correctness: {
|
|
4078
4068
|
required: boolean;
|
|
4079
|
-
status: "
|
|
4069
|
+
status: "not-required" | "blocked" | "current" | "stale" | "missing";
|
|
4080
4070
|
reviewedHeadSha?: string | undefined;
|
|
4081
4071
|
reviewedPatchId?: string | undefined;
|
|
4082
4072
|
};
|
|
4083
4073
|
security?: {
|
|
4084
4074
|
required: boolean;
|
|
4085
|
-
status: "
|
|
4075
|
+
status: "not-required" | "blocked" | "current" | "stale" | "missing";
|
|
4086
4076
|
reviewedHeadSha?: string | undefined;
|
|
4087
4077
|
reviewedPatchId?: string | undefined;
|
|
4088
4078
|
} | undefined;
|
|
@@ -4090,7 +4080,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4090
4080
|
schemaVersion: 1;
|
|
4091
4081
|
verification: {
|
|
4092
4082
|
command: "patronage-factory pr:verify";
|
|
4093
|
-
prVerify: "
|
|
4083
|
+
prVerify: "passed" | "stale" | "missing";
|
|
4094
4084
|
docsOnlyDeltaAccepted?: boolean | undefined;
|
|
4095
4085
|
docsOnlyVerifiedHeadSha?: string | undefined;
|
|
4096
4086
|
trivialDeltaAccepted?: boolean | undefined;
|
|
@@ -4133,7 +4123,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4133
4123
|
interior: number;
|
|
4134
4124
|
};
|
|
4135
4125
|
nextAction: "run-gate-cycle" | "accept-nonblocking-findings" | "escalate-to-triage" | "ready-for-human" | "advance-to-gate" | "run-interior-cycle";
|
|
4136
|
-
stage: "
|
|
4126
|
+
stage: "interior" | "gate" | "interior-complete";
|
|
4137
4127
|
forcedTransition?: "gate-cap-exhausted" | "interior-cap-reached" | undefined;
|
|
4138
4128
|
} | undefined;
|
|
4139
4129
|
reviewRuns?: PrReviewResult[] | undefined;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __exportAll } from "./chunk-pbuEa-1d.js";
|
|
3
|
-
import { appendFileSync, constants, cpSync,
|
|
3
|
+
import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
6
|
import path from "node:path";
|
|
@@ -9,7 +9,6 @@ import { z } from "zod";
|
|
|
9
9
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
10
10
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
11
11
|
import os, { homedir } from "node:os";
|
|
12
|
-
import { createInterface } from "node:readline";
|
|
13
12
|
import { setImmediate } from "node:timers";
|
|
14
13
|
import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
14
|
import { Worker } from "node:worker_threads";
|
|
@@ -18,7 +17,7 @@ import { parse } from "yaml";
|
|
|
18
17
|
import { promisify } from "node:util";
|
|
19
18
|
import { GitHubApiError, mintInstallationToken } from "@patronage/factory-ci";
|
|
20
19
|
//#region package.json
|
|
21
|
-
var version = "1.0.0-alpha.
|
|
20
|
+
var version = "1.0.0-alpha.6";
|
|
22
21
|
//#endregion
|
|
23
22
|
//#region src/review-rungs.ts
|
|
24
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -1348,7 +1347,6 @@ function formatUserConfigError(configPath, error) {
|
|
|
1348
1347
|
}
|
|
1349
1348
|
const DEFAULT_HQ_TRANSPORT_TIMEOUT_MS = 2500;
|
|
1350
1349
|
const HQ_RETRY_SPOOL_DIRNAME = "hq-retry-spool";
|
|
1351
|
-
const HQ_RETRY_JOURNAL_BASENAME = "hq-retry-journal.jsonl";
|
|
1352
1350
|
/**
|
|
1353
1351
|
* Appended to a spooled event that can never be delivered (#445).
|
|
1354
1352
|
*
|
|
@@ -1389,9 +1387,6 @@ const SPOOL_REPOSITORY_MARKER = ".repository-identity";
|
|
|
1389
1387
|
* and the extra level is slack for a key scheme that nests one deeper.
|
|
1390
1388
|
*/
|
|
1391
1389
|
const HQ_SPOOL_SWEEP_MAX_DEPTH = 4;
|
|
1392
|
-
`${HQ_RETRY_JOURNAL_BASENAME}`;
|
|
1393
|
-
`${HQ_RETRY_SPOOL_DIRNAME}`;
|
|
1394
|
-
const LEGACY_FACTORY_MEMORY_DIRNAME = ".factory-memory";
|
|
1395
1390
|
const HQ_SPOOL_STATE_SEGMENTS = ["patronage-factory", "hq-spool"];
|
|
1396
1391
|
const MAX_HQ_CONFIG_BYTES = 1024 * 1024;
|
|
1397
1392
|
const MAX_HQ_INGEST_PAYLOAD_BYTES = 256 * 1024;
|
|
@@ -1503,12 +1498,6 @@ const repositorySpoolLayout = (repository, env = process.env) => ({
|
|
|
1503
1498
|
HQ_RETRY_SPOOL_DIRNAME
|
|
1504
1499
|
]
|
|
1505
1500
|
});
|
|
1506
|
-
/** Read-only drain source for spools written before the relocation. */
|
|
1507
|
-
const legacySpoolLayout = (cwd) => ({
|
|
1508
|
-
create: false,
|
|
1509
|
-
root: cwd,
|
|
1510
|
-
segments: [LEGACY_FACTORY_MEMORY_DIRNAME, HQ_RETRY_SPOOL_DIRNAME]
|
|
1511
|
-
});
|
|
1512
1501
|
/**
|
|
1513
1502
|
* An operator-named directory (`hq:flush --dir`). Accepts either the spool
|
|
1514
1503
|
* itself or the directory holding it, so a `.factory-memory` path and a
|
|
@@ -1580,30 +1569,10 @@ const secureSpoolLayout = async (layout, deadline) => {
|
|
|
1580
1569
|
const memoryIdentity = identities.at(-2);
|
|
1581
1570
|
if (spool === void 0 || memory === void 0 || spoolIdentity === void 0 || memoryIdentity === void 0 || !await directoryIdentityMatches(memory, memoryIdentity, deadline) || !await directoryIdentityMatches(spool, spoolIdentity, deadline)) return;
|
|
1582
1571
|
return {
|
|
1583
|
-
memory,
|
|
1584
|
-
memoryIdentity,
|
|
1585
1572
|
spool,
|
|
1586
1573
|
spoolIdentity
|
|
1587
1574
|
};
|
|
1588
1575
|
};
|
|
1589
|
-
/**
|
|
1590
|
-
* The legacy JSONL journal lives one level above the spool. Securing it on its
|
|
1591
|
-
* own lets a pre-spool `.factory-memory` (journal but no spool directory)
|
|
1592
|
-
* drain without the read path creating anything inside the worktree.
|
|
1593
|
-
*/
|
|
1594
|
-
const secureJournalDirectory = async (layout, deadline) => {
|
|
1595
|
-
const segments = layout.segments.slice(0, -1);
|
|
1596
|
-
if (segments.length === 0) return;
|
|
1597
|
-
const chain = await secureChain(layout, segments, deadline);
|
|
1598
|
-
if (chain === void 0) return;
|
|
1599
|
-
const directory = chain.directories.at(-1);
|
|
1600
|
-
const identity = chain.identities.at(-1);
|
|
1601
|
-
if (directory === void 0 || identity === void 0 || !await directoryIdentityMatches(directory, identity, deadline)) return;
|
|
1602
|
-
return {
|
|
1603
|
-
directory,
|
|
1604
|
-
identity
|
|
1605
|
-
};
|
|
1606
|
-
};
|
|
1607
1576
|
const readDirectoryIdentity = async (directory, deadline) => {
|
|
1608
1577
|
const opened = await openWithin(directory, constants.O_RDONLY + constants.O_DIRECTORY + constants.O_NOFOLLOW, remainingMs(deadline));
|
|
1609
1578
|
if (opened.status !== "fulfilled") return;
|
|
@@ -2333,225 +2302,6 @@ const replayCloseoutSpool = async (layout, endpoint, clientId, clientSecret, req
|
|
|
2333
2302
|
if (!await mutateBoundDirectory(spool, spoolIdentity, performance.now() + flushBudgetMs, () => outcome.ok ? unlink(claimPath) : rename(claimPath, eventPath))) return;
|
|
2334
2303
|
}
|
|
2335
2304
|
};
|
|
2336
|
-
const writeLegacyCursor = async (directory, directoryIdentity, cursorPath, offset, deadline) => {
|
|
2337
|
-
if (path.dirname(cursorPath) !== directory || !await directoryIdentityMatches(directory, directoryIdentity, deadline)) return false;
|
|
2338
|
-
const temporary = `${cursorPath}.${randomUUID()}.tmp`;
|
|
2339
|
-
const opened = await openWithin(temporary, constants.O_CREAT + constants.O_EXCL + constants.O_WRONLY + constants.O_NOFOLLOW, remainingMs(deadline), 384);
|
|
2340
|
-
if (opened.status !== "fulfilled") return false;
|
|
2341
|
-
const wrote = await settleWithin((async () => {
|
|
2342
|
-
await opened.value.writeFile(String(offset), "utf-8");
|
|
2343
|
-
await opened.value.sync();
|
|
2344
|
-
})(), remainingMs(deadline));
|
|
2345
|
-
const closed = await settleWithin(opened.value.close(), remainingMs(deadline));
|
|
2346
|
-
if (wrote.status !== "fulfilled" || closed.status !== "fulfilled") {
|
|
2347
|
-
await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => unlink(temporary));
|
|
2348
|
-
return false;
|
|
2349
|
-
}
|
|
2350
|
-
if (!await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => rename(temporary, cursorPath))) {
|
|
2351
|
-
await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => unlink(temporary));
|
|
2352
|
-
return false;
|
|
2353
|
-
}
|
|
2354
|
-
return await syncBoundDirectory(directory, directoryIdentity, deadline);
|
|
2355
|
-
};
|
|
2356
|
-
const readLegacyCursor = async (cursorPath, deadline) => {
|
|
2357
|
-
const file = await readBoundedTextFileNoFollow(cursorPath, deadline, 128);
|
|
2358
|
-
if (file === void 0) return 0;
|
|
2359
|
-
const offset = Number(file.contents);
|
|
2360
|
-
return Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
|
|
2361
|
-
};
|
|
2362
|
-
const claimLegacySource = async (sourcePath, target, directory, directoryIdentity, deadline) => {
|
|
2363
|
-
if (path.dirname(sourcePath) !== directory || !await directoryIdentityMatches(directory, directoryIdentity, deadline)) return;
|
|
2364
|
-
const metadata = await settleWithin(lstat(sourcePath), remainingMs(deadline));
|
|
2365
|
-
if (metadata.status !== "fulfilled" || !metadata.value.isFile() || metadata.value.isSymbolicLink()) return;
|
|
2366
|
-
const claimPath = `${target}.legacy-claim-${randomUUID()}`;
|
|
2367
|
-
if (!await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => rename(sourcePath, claimPath))) return;
|
|
2368
|
-
return claimPath;
|
|
2369
|
-
};
|
|
2370
|
-
const releaseLegacyClaim = async (claimPath, target, directory, directoryIdentity, deadline) => {
|
|
2371
|
-
if (path.dirname(claimPath) !== directory) return false;
|
|
2372
|
-
const readyPath = `${target}.legacy-ready-${randomUUID()}`;
|
|
2373
|
-
return mutateBoundDirectory(directory, directoryIdentity, deadline, async () => rename(claimPath, readyPath));
|
|
2374
|
-
};
|
|
2375
|
-
async function replayRetryJournal(layout, writeLayout, endpoint, clientId, clientSecret, request, dependencies, transportBudgetMs, setupDeadline, replayDeadline, options = {}) {
|
|
2376
|
-
const maxEntries = options.maxEntries ?? MAX_HQ_REPLAY_ENTRIES;
|
|
2377
|
-
const { report } = options;
|
|
2378
|
-
const secured = await settleWithin(secureJournalDirectory(layout, setupDeadline), remainingMs(setupDeadline));
|
|
2379
|
-
if (secured.status !== "fulfilled" || secured.value === void 0) return;
|
|
2380
|
-
const { directory, identity: directoryIdentity } = secured.value;
|
|
2381
|
-
const target = path.join(directory, HQ_RETRY_JOURNAL_BASENAME);
|
|
2382
|
-
const baseName = path.basename(target);
|
|
2383
|
-
const listing = await settleWithin(readdir(directory), remainingMs(setupDeadline));
|
|
2384
|
-
if (listing.status !== "fulfilled") return;
|
|
2385
|
-
let delivered = 0;
|
|
2386
|
-
let corruptDropped = 0;
|
|
2387
|
-
const sources = listing.value.filter((name) => name === baseName || name.startsWith(`${baseName}.legacy-ready-`) || name.startsWith(`${baseName}.draining-`) || name.startsWith(`${baseName}.legacy-claim-`)).filter((name) => !name.includes(".cursor")).toSorted();
|
|
2388
|
-
for (const name of sources) {
|
|
2389
|
-
if (delivered >= maxEntries || remainingMs(replayDeadline) <= 0) break;
|
|
2390
|
-
const sourcePath = path.join(directory, name);
|
|
2391
|
-
if (name.startsWith(`${baseName}.draining-`) || name.startsWith(`${baseName}.legacy-claim-`)) {
|
|
2392
|
-
const metadata = await settleWithin(lstat(sourcePath), remainingMs(setupDeadline));
|
|
2393
|
-
if (metadata.status !== "fulfilled" || !metadata.value.isFile() || metadata.value.isSymbolicLink() || Date.now() - Math.max(metadata.value.mtimeMs, metadata.value.ctimeMs) < STALE_SPOOL_ARTIFACT_MS) continue;
|
|
2394
|
-
}
|
|
2395
|
-
const claimPath = await claimLegacySource(sourcePath, target, directory, directoryIdentity, setupDeadline);
|
|
2396
|
-
if (claimPath === void 0) continue;
|
|
2397
|
-
const opened = await openWithin(claimPath, constants.O_RDONLY + constants.O_NOFOLLOW, remainingMs(setupDeadline));
|
|
2398
|
-
if (opened.status !== "fulfilled") {
|
|
2399
|
-
await releaseLegacyClaim(claimPath, target, directory, directoryIdentity, setupDeadline);
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
const sourceMetadata = await settleWithin(opened.value.stat(), remainingMs(setupDeadline));
|
|
2403
|
-
if (sourceMetadata.status !== "fulfilled" || !sourceMetadata.value.isFile()) {
|
|
2404
|
-
await settleWithin(opened.value.close(), remainingMs(setupDeadline));
|
|
2405
|
-
await releaseLegacyClaim(claimPath, target, directory, directoryIdentity, setupDeadline);
|
|
2406
|
-
continue;
|
|
2407
|
-
}
|
|
2408
|
-
const cursorPath = `${target}.legacy-cursor-${String(sourceMetadata.value.dev)}-${String(sourceMetadata.value.ino)}`;
|
|
2409
|
-
let offset = await readLegacyCursor(cursorPath, setupDeadline);
|
|
2410
|
-
if (offset >= sourceMetadata.value.size) {
|
|
2411
|
-
await settleWithin(opened.value.close(), remainingMs(setupDeadline));
|
|
2412
|
-
const current = await settleWithin(lstat(claimPath), remainingMs(setupDeadline));
|
|
2413
|
-
if (current.status === "fulfilled" && current.value.isFile() && !current.value.isSymbolicLink() && current.value.dev === sourceMetadata.value.dev && current.value.ino === sourceMetadata.value.ino && current.value.size === sourceMetadata.value.size) {
|
|
2414
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(claimPath));
|
|
2415
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(cursorPath));
|
|
2416
|
-
}
|
|
2417
|
-
continue;
|
|
2418
|
-
}
|
|
2419
|
-
const abort = new AbortController();
|
|
2420
|
-
const streamBudget = Math.min(remainingMs(setupDeadline), remainingMs(replayDeadline));
|
|
2421
|
-
const abortTimer = setTimeout(() => abort.abort(), Math.max(0, streamBudget));
|
|
2422
|
-
abortTimer.unref();
|
|
2423
|
-
const stream = createReadStream(claimPath, {
|
|
2424
|
-
autoClose: false,
|
|
2425
|
-
encoding: "utf-8",
|
|
2426
|
-
fd: opened.value.fd,
|
|
2427
|
-
signal: abort.signal,
|
|
2428
|
-
start: offset
|
|
2429
|
-
});
|
|
2430
|
-
const lines = createInterface({
|
|
2431
|
-
crlfDelay: Infinity,
|
|
2432
|
-
input: stream
|
|
2433
|
-
});
|
|
2434
|
-
let reachedEof = true;
|
|
2435
|
-
try {
|
|
2436
|
-
for await (const line of lines) {
|
|
2437
|
-
if (delivered >= maxEntries || remainingMs(replayDeadline) <= 0 || remainingMs(setupDeadline) <= 0) {
|
|
2438
|
-
reachedEof = false;
|
|
2439
|
-
break;
|
|
2440
|
-
}
|
|
2441
|
-
const nextOffset = Math.min(sourceMetadata.value.size, offset + Buffer.byteLength(line, "utf-8") + 1);
|
|
2442
|
-
if (line.trim().length === 0) {
|
|
2443
|
-
offset = nextOffset;
|
|
2444
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2445
|
-
reachedEof = false;
|
|
2446
|
-
break;
|
|
2447
|
-
}
|
|
2448
|
-
continue;
|
|
2449
|
-
}
|
|
2450
|
-
let parsed;
|
|
2451
|
-
try {
|
|
2452
|
-
parsed = JSON.parse(line);
|
|
2453
|
-
} catch {
|
|
2454
|
-
corruptDropped += 1;
|
|
2455
|
-
offset = nextOffset;
|
|
2456
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2457
|
-
reachedEof = false;
|
|
2458
|
-
break;
|
|
2459
|
-
}
|
|
2460
|
-
continue;
|
|
2461
|
-
}
|
|
2462
|
-
if (!isReplayableEntry(parsed)) {
|
|
2463
|
-
offset = nextOffset;
|
|
2464
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2465
|
-
reachedEof = false;
|
|
2466
|
-
break;
|
|
2467
|
-
}
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
const entryEndpoint = validatedEndpoint(parsed.endpoint);
|
|
2471
|
-
let handled = false;
|
|
2472
|
-
const row = {
|
|
2473
|
-
eventId: parsed.event.eventId,
|
|
2474
|
-
kind: parsed.event.kind
|
|
2475
|
-
};
|
|
2476
|
-
let rowOutcome = {
|
|
2477
|
-
detail: `recorded endpoint ${entryEndpoint?.origin ?? "(unusable)"} is not this repository's authorized HQ origin ${endpoint.origin}`,
|
|
2478
|
-
...row,
|
|
2479
|
-
spool: directory,
|
|
2480
|
-
status: "migrated"
|
|
2481
|
-
};
|
|
2482
|
-
if (entryEndpoint?.origin === endpoint.origin) {
|
|
2483
|
-
rowOutcome = {
|
|
2484
|
-
...rowOutcome,
|
|
2485
|
-
detail: "event could not be serialized for delivery"
|
|
2486
|
-
};
|
|
2487
|
-
let body;
|
|
2488
|
-
try {
|
|
2489
|
-
body = JSON.stringify(parsed.event);
|
|
2490
|
-
} catch {
|
|
2491
|
-
body = void 0;
|
|
2492
|
-
}
|
|
2493
|
-
if (body !== void 0) {
|
|
2494
|
-
const outcome = await attemptTransport(request, endpoint, clientId, clientSecret, body, Math.min(transportBudgetMs, remainingMs(replayDeadline)), report !== void 0);
|
|
2495
|
-
handled = outcome.ok;
|
|
2496
|
-
if (handled) delivered += 1;
|
|
2497
|
-
rowOutcome = outcome.ok ? {
|
|
2498
|
-
...row,
|
|
2499
|
-
spool: directory,
|
|
2500
|
-
status: outcome.duplicate === true ? "duplicate" : "delivered"
|
|
2501
|
-
} : {
|
|
2502
|
-
...row,
|
|
2503
|
-
detail: outcome.detail === void 0 ? outcome.reason : `${outcome.reason}: ${outcome.detail}`,
|
|
2504
|
-
spool: directory,
|
|
2505
|
-
status: drainFailureStatus(outcome.reason)
|
|
2506
|
-
};
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
if (!handled) {
|
|
2510
|
-
const retained = await appendCloseoutSpool(writeLayout, parsed, performance.now() + journalFlushBudgetFor(dependencies));
|
|
2511
|
-
if (retained === void 0) options.onMigrate?.();
|
|
2512
|
-
if (retained !== void 0) {
|
|
2513
|
-
report?.({
|
|
2514
|
-
...rowOutcome,
|
|
2515
|
-
detail: `${rowOutcome.detail ?? rowOutcome.status}; migration into the current spool ${retained}`,
|
|
2516
|
-
status: "unreachable"
|
|
2517
|
-
});
|
|
2518
|
-
reachedEof = false;
|
|
2519
|
-
break;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
report?.(rowOutcome);
|
|
2523
|
-
offset = nextOffset;
|
|
2524
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2525
|
-
reachedEof = false;
|
|
2526
|
-
break;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
} catch {
|
|
2530
|
-
reachedEof = false;
|
|
2531
|
-
} finally {
|
|
2532
|
-
clearTimeout(abortTimer);
|
|
2533
|
-
abort.abort();
|
|
2534
|
-
lines.close();
|
|
2535
|
-
stream.destroy();
|
|
2536
|
-
await settleWithin(opened.value.close(), remainingMs(setupDeadline));
|
|
2537
|
-
}
|
|
2538
|
-
if (reachedEof && offset >= sourceMetadata.value.size) {
|
|
2539
|
-
const current = await settleWithin(lstat(claimPath), remainingMs(setupDeadline));
|
|
2540
|
-
if (current.status === "fulfilled" && current.value.isFile() && !current.value.isSymbolicLink() && current.value.dev === sourceMetadata.value.dev && current.value.ino === sourceMetadata.value.ino && current.value.size === sourceMetadata.value.size && await directoryIdentityMatches(directory, directoryIdentity, setupDeadline)) {
|
|
2541
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(claimPath));
|
|
2542
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(cursorPath));
|
|
2543
|
-
}
|
|
2544
|
-
} else await releaseLegacyClaim(claimPath, target, directory, directoryIdentity, setupDeadline);
|
|
2545
|
-
}
|
|
2546
|
-
await reportReplayOutcome(dependencies, delivered, corruptDropped);
|
|
2547
|
-
}
|
|
2548
|
-
async function reportReplayOutcome(dependencies, delivered, corruptDropped) {
|
|
2549
|
-
if (delivered <= 0 && corruptDropped <= 0) return;
|
|
2550
|
-
const parts = [];
|
|
2551
|
-
if (delivered > 0) parts.push(`${delivered} queued event(s) delivered from the retry journal`);
|
|
2552
|
-
if (corruptDropped > 0) parts.push(`${corruptDropped} unparseable journal line(s) dropped (partial write recovered, #120)`);
|
|
2553
|
-
await reportDiagnostic(dependencies, performance.now() + journalFlushBudgetFor(dependencies), parts.join("; "), delivered > 0 ? "HQ ingest confirmed" : "HQ ingest recovered");
|
|
2554
|
-
}
|
|
2555
2305
|
async function deliverHqIngest(input, dependencies, setupDeadline, timeoutMs, transportBudgetMs) {
|
|
2556
2306
|
let config;
|
|
2557
2307
|
try {
|
|
@@ -2595,7 +2345,6 @@ async function deliverHqIngest(input, dependencies, setupDeadline, timeoutMs, tr
|
|
|
2595
2345
|
owner: input.profile.repository.owner,
|
|
2596
2346
|
repo: input.profile.repository.name
|
|
2597
2347
|
}, env);
|
|
2598
|
-
const readLayouts = [writeLayout, legacySpoolLayout(input.cwd)];
|
|
2599
2348
|
const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
2600
2349
|
const makeEventId = dependencies.randomUUID ?? randomUUID;
|
|
2601
2350
|
let event;
|
|
@@ -2666,10 +2415,7 @@ async function deliverHqIngest(input, dependencies, setupDeadline, timeoutMs, tr
|
|
|
2666
2415
|
throw new Error(reason);
|
|
2667
2416
|
}
|
|
2668
2417
|
const replayDeadline = performance.now() + timeoutMs;
|
|
2669
|
-
|
|
2670
|
-
await replayCloseoutSpool(readLayout, endpoint, clientId, clientSecret, request, transportBudgetMs, replayDeadline, replayDeadline, journalFlushBudgetFor(dependencies), persistedEventPath);
|
|
2671
|
-
await replayRetryJournal(readLayout, writeLayout, endpoint, clientId, clientSecret, request, dependencies, transportBudgetMs, replayDeadline, replayDeadline);
|
|
2672
|
-
}
|
|
2418
|
+
await replayCloseoutSpool(writeLayout, endpoint, clientId, clientSecret, request, transportBudgetMs, replayDeadline, replayDeadline, journalFlushBudgetFor(dependencies), persistedEventPath);
|
|
2673
2419
|
const outcome = await attemptTransport(request, endpoint, clientId, clientSecret, body, transportBudgetMs);
|
|
2674
2420
|
if (!outcome.ok) {
|
|
2675
2421
|
({reason, unconfirmed} = outcome);
|
|
@@ -2748,20 +2494,7 @@ async function awaitPendingHqIngest() {
|
|
|
2748
2494
|
const DEFAULT_HQ_FLUSH_BUDGET_MS = 600 * 1e3;
|
|
2749
2495
|
/** ENOENT is the only filesystem answer that means "this location is absent". */
|
|
2750
2496
|
const isMissingEntryError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
2751
|
-
const
|
|
2752
|
-
const journalLineTimestamps = (contents) => {
|
|
2753
|
-
const timestamps = [];
|
|
2754
|
-
for (const line of contents.split("\n")) {
|
|
2755
|
-
const trimmed = line.trim();
|
|
2756
|
-
if (trimmed === "") continue;
|
|
2757
|
-
try {
|
|
2758
|
-
const parsed = JSON.parse(trimmed);
|
|
2759
|
-
if (typeof parsed.failedAt === "string") timestamps.push(parsed.failedAt);
|
|
2760
|
-
} catch {}
|
|
2761
|
-
}
|
|
2762
|
-
return timestamps;
|
|
2763
|
-
};
|
|
2764
|
-
const countRemainingSpoolWork = async (spools, drainLayouts, deadline) => {
|
|
2497
|
+
const countRemainingSpoolWork = async (spools, deadline) => {
|
|
2765
2498
|
let remaining = 0;
|
|
2766
2499
|
let unlistableScans = 0;
|
|
2767
2500
|
let oldestMs;
|
|
@@ -2790,27 +2523,6 @@ const countRemainingSpoolWork = async (spools, drainLayouts, deadline) => {
|
|
|
2790
2523
|
if (probed.status === "fulfilled") noteTimestamp(probed.value.mtime.toISOString());
|
|
2791
2524
|
}
|
|
2792
2525
|
}
|
|
2793
|
-
for (const layout of drainLayouts) {
|
|
2794
|
-
const journalDir = path.dirname(layoutPath(layout));
|
|
2795
|
-
const journalListing = await settleWithin(readdir(journalDir), remainingMs(deadline));
|
|
2796
|
-
if (journalListing.status !== "fulfilled") {
|
|
2797
|
-
await countUnlistable(journalDir);
|
|
2798
|
-
continue;
|
|
2799
|
-
}
|
|
2800
|
-
const journalNames = journalListing.value.filter((name) => name === "hq-retry-journal.jsonl" || name.startsWith(`hq-retry-journal.jsonl.legacy-ready-`) || name.startsWith(`hq-retry-journal.jsonl.legacy-claim-`) || name.startsWith(`hq-retry-journal.jsonl.draining-`));
|
|
2801
|
-
for (const name of journalNames) {
|
|
2802
|
-
const journalFilePath = path.join(journalDir, name);
|
|
2803
|
-
const read = await settleWithin(readFile(journalFilePath, "utf-8"), remainingMs(deadline));
|
|
2804
|
-
if (read.status === "fulfilled") {
|
|
2805
|
-
remaining += journalRowCount(read.value);
|
|
2806
|
-
for (const timestamp of journalLineTimestamps(read.value)) noteTimestamp(timestamp);
|
|
2807
|
-
continue;
|
|
2808
|
-
}
|
|
2809
|
-
remaining += 1;
|
|
2810
|
-
const probed = await settleWithin(lstat(journalFilePath), remainingMs(deadline));
|
|
2811
|
-
if (probed.status === "fulfilled") noteTimestamp(probed.value.mtime.toISOString());
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
2526
|
return {
|
|
2815
2527
|
...oldestMs === void 0 ? {} : { oldestQueuedAt: new Date(oldestMs).toISOString() },
|
|
2816
2528
|
remaining,
|
|
@@ -2831,10 +2543,10 @@ const DEFAULT_HQ_SPOOL_INSPECT_BUDGET_MS = 5e3;
|
|
|
2831
2543
|
*/
|
|
2832
2544
|
async function countHqSpoolWork(input, dependencies = {}) {
|
|
2833
2545
|
const env = dependencies.env ?? process.env;
|
|
2834
|
-
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [repositorySpoolLayout(input.repository, env)
|
|
2546
|
+
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [repositorySpoolLayout(input.repository, env)];
|
|
2835
2547
|
const deadline = performance.now() + (dependencies.budgetMs ?? DEFAULT_HQ_SPOOL_INSPECT_BUDGET_MS);
|
|
2836
2548
|
const paths = layouts.map((layout) => layoutPath(layout));
|
|
2837
|
-
const { oldestQueuedAt, remaining, unlistableScans } = await countRemainingSpoolWork(paths,
|
|
2549
|
+
const { oldestQueuedAt, remaining, unlistableScans } = await countRemainingSpoolWork(paths, deadline);
|
|
2838
2550
|
const existing = await Promise.all(paths.map(async (spool) => {
|
|
2839
2551
|
return (await settleWithin(lstat(spool), remainingMs(deadline))).status === "fulfilled" ? spool : void 0;
|
|
2840
2552
|
}));
|
|
@@ -3002,7 +2714,7 @@ async function sweepHqSpoolOrphans(input, dependencies = {}) {
|
|
|
3002
2714
|
});
|
|
3003
2715
|
continue;
|
|
3004
2716
|
}
|
|
3005
|
-
const counted = await countRemainingSpoolWork([spool],
|
|
2717
|
+
const counted = await countRemainingSpoolWork([spool], deadline);
|
|
3006
2718
|
if (counted.remaining === 0 && counted.unlistableScans === 0) continue;
|
|
3007
2719
|
orphans.push({
|
|
3008
2720
|
directory: spool,
|
|
@@ -3022,7 +2734,7 @@ async function sweepHqSpoolOrphans(input, dependencies = {}) {
|
|
|
3022
2734
|
});
|
|
3023
2735
|
continue;
|
|
3024
2736
|
}
|
|
3025
|
-
const counted = await countRemainingSpoolWork([spool],
|
|
2737
|
+
const counted = await countRemainingSpoolWork([spool], deadline);
|
|
3026
2738
|
if (counted.remaining === 0 && counted.unlistableScans === 0) continue;
|
|
3027
2739
|
orphans.push({
|
|
3028
2740
|
directory: spool,
|
|
@@ -3042,8 +2754,8 @@ async function sweepHqSpoolOrphans(input, dependencies = {}) {
|
|
|
3042
2754
|
* This is the deliberate counterpart to the sink's advisory emit path: the
|
|
3043
2755
|
* caller has already resolved credentials explicitly, so there is no cap, no
|
|
3044
2756
|
* daemon, and no background retry — one pass, bounded, over the repo-keyed
|
|
3045
|
-
* location
|
|
3046
|
-
*
|
|
2757
|
+
* location (or the operator's `--dir`). Delivery itself is the sink's own
|
|
2758
|
+
* replay, so there is exactly one POST path.
|
|
3047
2759
|
*/
|
|
3048
2760
|
async function flushHqSpool(input, dependencies = {}) {
|
|
3049
2761
|
const endpoint = validatedIngestEndpoint(input.endpoint);
|
|
@@ -3056,8 +2768,7 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
3056
2768
|
journalFlushBudgetMs: dependencies.journalFlushBudgetMs
|
|
3057
2769
|
});
|
|
3058
2770
|
const deadline = performance.now() + (dependencies.budgetMs ?? DEFAULT_HQ_FLUSH_BUDGET_MS);
|
|
3059
|
-
const
|
|
3060
|
-
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [writeLayout, legacySpoolLayout(input.cwd)];
|
|
2771
|
+
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [repositorySpoolLayout(input.repository, env)];
|
|
3061
2772
|
const outcomeById = /* @__PURE__ */ new Map();
|
|
3062
2773
|
let rejectedFiles = 0;
|
|
3063
2774
|
let unreachableFiles = 0;
|
|
@@ -3066,31 +2777,13 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
3066
2777
|
outcomeById.set(outcome.eventId, outcome);
|
|
3067
2778
|
dependencies.report?.(outcome);
|
|
3068
2779
|
};
|
|
3069
|
-
const journalReport = (outcome) => {
|
|
3070
|
-
if (outcome.status === "unreachable" && outcome.detail?.includes("migration into the current spool")) unreachableFiles += 1;
|
|
3071
|
-
record(outcome);
|
|
3072
|
-
};
|
|
3073
2780
|
const spoolReport = (outcome) => {
|
|
3074
2781
|
if (outcome.status === "rejected") rejectedFiles += 1;
|
|
3075
2782
|
if (outcome.status === "unreachable") unreachableFiles += 1;
|
|
3076
2783
|
record(outcome);
|
|
3077
2784
|
};
|
|
3078
2785
|
const spools = [];
|
|
3079
|
-
|
|
3080
|
-
const journalDrainOptions = {
|
|
3081
|
-
maxEntries: Number.POSITIVE_INFINITY,
|
|
3082
|
-
onMigrate: () => {
|
|
3083
|
-
migratedIntoWriteLayout = true;
|
|
3084
|
-
},
|
|
3085
|
-
report: journalReport
|
|
3086
|
-
};
|
|
3087
|
-
for (const layout of layouts) await replayRetryJournal(layout, writeLayout, endpoint, input.clientId, input.clientSecret, request, {
|
|
3088
|
-
env,
|
|
3089
|
-
fetch: request,
|
|
3090
|
-
journalFlushBudgetMs: flushBudgetMs
|
|
3091
|
-
}, transportBudgetMs, deadline, deadline, journalDrainOptions);
|
|
3092
|
-
const drainLayouts = migratedIntoWriteLayout && !layouts.some((layout) => layoutPath(layout) === layoutPath(writeLayout)) ? [...layouts, writeLayout] : layouts;
|
|
3093
|
-
for (const layout of drainLayouts) {
|
|
2786
|
+
for (const layout of layouts) {
|
|
3094
2787
|
await replayCloseoutSpool(layout, endpoint, input.clientId, input.clientSecret, request, transportBudgetMs, deadline, deadline, flushBudgetMs, void 0, {
|
|
3095
2788
|
maxEntries: Number.POSITIVE_INFINITY,
|
|
3096
2789
|
report: spoolReport
|
|
@@ -3100,7 +2793,7 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
3100
2793
|
if ((await settleWithin(lstat(layoutPath(layout)), remainingMs(deadline))).status === "fulfilled") unsecurableSpools += 1;
|
|
3101
2794
|
} else spools.push(secured.spool);
|
|
3102
2795
|
}
|
|
3103
|
-
const { remaining, unlistableScans } = await countRemainingSpoolWork(spools,
|
|
2796
|
+
const { remaining, unlistableScans } = await countRemainingSpoolWork(spools, deadline);
|
|
3104
2797
|
const outcomes = [...outcomeById.values()];
|
|
3105
2798
|
const count = (status) => outcomes.filter((outcome) => outcome.status === status).length;
|
|
3106
2799
|
const rejected = count("rejected");
|
|
@@ -12128,10 +11821,10 @@ const describeOrphans = (orphans) => {
|
|
|
12128
11821
|
return ` ${orphans.length} spool location(s) hold this repository's evidence under an earlier key it no longer resolves to — a key-format change strands evidence there where no drain looks: ${described}. Drain each with \`psf hq:flush --dir <path>\`.`;
|
|
12129
11822
|
};
|
|
12130
11823
|
/**
|
|
12131
|
-
* Red when HQ evidence is waiting for this repository: its own spool or
|
|
12132
|
-
*
|
|
12133
|
-
*
|
|
12134
|
-
*
|
|
11824
|
+
* Red when HQ evidence is waiting for this repository: its own spool or a
|
|
11825
|
+
* sibling location written under an earlier key of its own that it no longer
|
|
11826
|
+
* resolves to (#420). Another repository's spool under the shared root is that
|
|
11827
|
+
* repository's business, never this check's (#446).
|
|
12135
11828
|
*
|
|
12136
11829
|
* Reuses `countHqSpoolWork` (#414) — the same read-only, credential-free
|
|
12137
11830
|
* inspection `hq:flush` itself consults before ever resolving a secret — so
|
|
@@ -12147,13 +11840,10 @@ const describeOrphans = (orphans) => {
|
|
|
12147
11840
|
async function hqSpoolDoctorCheck(input, dependencies = {}) {
|
|
12148
11841
|
const countSpool = dependencies.countSpool ?? countHqSpoolWork;
|
|
12149
11842
|
const sweepOrphans = dependencies.sweepOrphans ?? sweepHqSpoolOrphans;
|
|
12150
|
-
const [counted, swept] = await Promise.all([countSpool({
|
|
12151
|
-
cwd: input.cwd,
|
|
12152
|
-
repository: input.repository
|
|
12153
|
-
}, { env: input.env }), sweepOrphans({ repository: input.repository }, { env: input.env })]);
|
|
11843
|
+
const [counted, swept] = await Promise.all([countSpool({ repository: input.repository }, { env: input.env }), sweepOrphans({ repository: input.repository }, { env: input.env })]);
|
|
12154
11844
|
const orphanNote = swept.orphans.length === 0 ? "" : describeOrphans(swept.orphans);
|
|
12155
11845
|
if (counted.pending === 0 && counted.unlistable === 0 && swept.orphans.length === 0) return {
|
|
12156
|
-
message: "HQ spool
|
|
11846
|
+
message: "HQ spool is empty; no evidence is waiting to be drained.",
|
|
12157
11847
|
name: HQ_SPOOL_CHECK_NAME,
|
|
12158
11848
|
status: "ok"
|
|
12159
11849
|
};
|
|
@@ -12357,7 +12047,6 @@ async function doctorProjectProfile(input = {}) {
|
|
|
12357
12047
|
const cwd = input.cwd ?? process.cwd();
|
|
12358
12048
|
const userConfig = resolveDoctorUserConfig(env, input.userConfig);
|
|
12359
12049
|
const [hqSpoolCheck, hqRetroReadbackCheck] = await Promise.all([hqSpoolDoctorCheck({
|
|
12360
|
-
cwd,
|
|
12361
12050
|
env,
|
|
12362
12051
|
repository: {
|
|
12363
12052
|
owner: profile.repository.owner,
|
|
@@ -13035,7 +12724,6 @@ async function runHqFlush(args, dependencies = {}) {
|
|
|
13035
12724
|
};
|
|
13036
12725
|
const explicitDirectories = args.dir && args.dir.length > 0 ? { explicitDirectories: args.dir } : {};
|
|
13037
12726
|
const pending = await (dependencies.countSpool ?? countHqSpoolWork)({
|
|
13038
|
-
cwd: args.cwd,
|
|
13039
12727
|
...explicitDirectories,
|
|
13040
12728
|
repository
|
|
13041
12729
|
}, { env });
|
|
@@ -13080,7 +12768,6 @@ async function runHqFlush(args, dependencies = {}) {
|
|
|
13080
12768
|
...await (dependencies.flush ?? flushHqSpool)({
|
|
13081
12769
|
clientId: resolution.credentials.clientId,
|
|
13082
12770
|
clientSecret: resolution.credentials.clientSecret,
|
|
13083
|
-
cwd: args.cwd,
|
|
13084
12771
|
endpoint: profile.hq.endpoint,
|
|
13085
12772
|
...explicitDirectories,
|
|
13086
12773
|
repository
|
|
@@ -13107,7 +12794,7 @@ function renderHqFlush(result) {
|
|
|
13107
12794
|
`hq:flush ${result.endpoint}`,
|
|
13108
12795
|
...result.spools.length === 0 ? ["no spool directory found"] : result.spools.map((spool) => `spool: ${spool}`),
|
|
13109
12796
|
...result.outcomes.map(outcomeLine),
|
|
13110
|
-
`delivered ${result.delivered}, duplicate ${result.duplicate}, rejected ${result.rejected}, undeliverable ${result.undeliverable}, unreachable ${result.unreachable}
|
|
12797
|
+
`delivered ${result.delivered}, duplicate ${result.duplicate}, rejected ${result.rejected}, undeliverable ${result.undeliverable}, unreachable ${result.unreachable}`,
|
|
13111
12798
|
...result.undeliverable > 0 ? [`${result.undeliverable} event(s) can never be delivered and were dispositioned in place, renamed with \`.undeliverable\` and left readable; they no longer count as work waiting.`] : [],
|
|
13112
12799
|
result.incomplete ? `INCOMPLETE: ${result.remaining} event(s) still spooled; the drain did not finish. Run hq:flush again.` : `spool drained: ${result.remaining} event(s) remain (rejections stay until HQ accepts them)`,
|
|
13113
12800
|
...orphanLines(result.orphans)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.6",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.6"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
"profile:schema": "tsx scripts/generate-profile-schema.ts",
|
|
78
78
|
"pretest": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
79
79
|
"test": "vitest run",
|
|
80
|
+
"pretest:profile": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
81
|
+
"test:profile": "tsx scripts/vitest-profile.ts",
|
|
80
82
|
"pretest:stress": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
81
83
|
"test:stress": "node scripts/stress-tests.mjs",
|
|
82
84
|
"pretypecheck": "bash ../scripts/ensure-worktree-bootstrap.sh",
|