@akagilnc/pi-workflow-roles 0.1.5478 → 0.1.5504
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/acp-host/production-host.js +4945 -4980
- package/dist/analyst-gate-cycles-read.js +11 -29
- package/dist/analyst-ledger.js +10 -12
- package/dist/archivist-record-entry.js +4 -3
- package/dist/archivist-record-pointer.js +59 -32
- package/dist/atomic-write.js +2 -2
- package/dist/auditor-dossier-tool.js +8 -4
- package/dist/auditor-soul.js +2 -1
- package/dist/collector-config.js +6 -5
- package/dist/doctor-evidence.js +5 -1
- package/dist/engine-detour-usage.js +4 -10
- package/dist/headless-host/description.js +251 -0
- package/dist/headless-host/production-host.js +2787 -2658
- package/dist/ledger-session-read.js +5 -0
- package/dist/migrate-book-topology.js +129 -100
- package/dist/navigator-public-session.js +13 -17
- package/dist/notary-source-run.js +7 -15
- package/dist/packaged-role-registry.js +0 -12
- package/dist/pi/durable-principal.js +2 -3
- package/dist/pi/role-turn-host.js +27 -13
- package/dist/public-cli/instruction-seat-run.js +4 -4
- package/dist/public-cli/invocation.js +17 -45
- package/dist/public-cli/main.js +1074 -1085
- package/dist/public-cli/run-lifecycle.js +17 -47
- package/dist/public-cli/settlement.js +28 -128
- package/dist/public-cli/terminal.js +0 -12
- package/dist/role-run-placement.js +52 -5
- package/dist/role-run-relocation.js +39 -2
- package/dist/run-terminal-artifacts.js +2 -6
- package/dist/run-ticket-number.js +33 -4
- package/dist/session-assistant-usage.js +9 -18
- package/dist/session-identity.js +3 -4
- package/dist/sitian-appender.js +20 -6
- package/dist/submission-ledger.js +4 -4
- package/dist/ticket-provenance-contracts.js +2 -8
- package/dist/ticket-provenance.js +5 -4
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +2 -2
- package/src/analyst-gate-cycles-read.ts +11 -34
- package/src/analyst-ledger.ts +14 -14
- package/src/archivist-record-entry.ts +4 -3
- package/src/archivist-record-pointer.ts +70 -34
- package/src/atomic-write.ts +3 -1
- package/src/auditor-dossier-tool.ts +8 -4
- package/src/auditor-soul.ts +2 -1
- package/src/collector-config.ts +5 -3
- package/src/doctor-evidence.ts +5 -1
- package/src/engine-detour-usage.ts +10 -13
- package/src/factory-board.ts +16 -81
- package/src/headless-host/description.ts +248 -1
- package/src/headless-host/production-host.ts +1 -2
- package/src/headless-host/role-turn-host.ts +10 -9
- package/src/ledger-session-read.ts +6 -0
- package/src/navigator-public-session.ts +13 -18
- package/src/navigator-work-context.ts +3 -1
- package/src/notary-role.ts +2 -1
- package/src/notary-source-run.ts +7 -17
- package/src/packaged-role-registry.ts +0 -15
- package/src/pi/durable-principal.ts +2 -4
- package/src/pi/role-turn-host.ts +35 -12
- package/src/public-cli/instruction-seat-run.ts +4 -4
- package/src/public-cli/invocation.ts +29 -57
- package/src/public-cli/run-lifecycle.ts +20 -43
- package/src/public-cli/settlement.ts +30 -173
- package/src/public-cli/terminal.ts +0 -60
- package/src/role-envelope.ts +4 -2
- package/src/role-run-placement.ts +63 -4
- package/src/role-run-relocation.ts +39 -2
- package/src/role-runtime.ts +40 -59
- package/src/run-terminal-artifacts.ts +3 -5
- package/src/run-ticket-number.ts +41 -3
- package/src/session-assistant-usage.ts +8 -20
- package/src/session-identity.ts +3 -4
- package/src/sitian-appender.ts +20 -12
- package/src/submission-ledger.ts +4 -4
- package/src/ticket-provenance-contracts.ts +3 -6
- package/src/ticket-provenance.ts +5 -6
- package/src/ticket-snapshot.ts +3 -2
- package/src/ticket-trajectory.ts +85 -99
package/src/notary-source-run.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./activation-ledger-topology.ts";
|
|
15
15
|
import type { NotarySourceRunLocator } from "./notary-contracts.ts";
|
|
16
16
|
import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
|
|
17
|
+
import { formatRunLeaf, parseRunLeaf } from "./role-run-placement.ts";
|
|
17
18
|
|
|
18
19
|
async function readRetainedIdentity(runDirectory: string) {
|
|
19
20
|
try {
|
|
@@ -26,11 +27,6 @@ async function readRetainedIdentity(runDirectory: string) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
// Run directory leaf: <runId>@<role>. Production uses uuidv7 runIds; offline tracers
|
|
30
|
-
// may use deterministic non-uuid leaves. Role token stays identifier-shaped.
|
|
31
|
-
const RUN_DIR_NAME =
|
|
32
|
-
/^([A-Za-z0-9][A-Za-z0-9._-]*)@([A-Za-z][A-Za-z0-9_-]*)$/;
|
|
33
|
-
|
|
34
30
|
export class NotarySourceRunError extends Error {
|
|
35
31
|
constructor(message: string, options?: ErrorOptions) {
|
|
36
32
|
super(message, options);
|
|
@@ -38,12 +34,6 @@ export class NotarySourceRunError extends Error {
|
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
function parseRunDirectoryName(name: string): { runId: string; role: string } | undefined {
|
|
42
|
-
const match = RUN_DIR_NAME.exec(name);
|
|
43
|
-
if (match === null) return undefined;
|
|
44
|
-
return { runId: match[1]!, role: match[2]! };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
async function requireRunDirectory(candidate: string, display: string): Promise<string> {
|
|
48
38
|
let real: string;
|
|
49
39
|
try {
|
|
@@ -68,7 +58,7 @@ async function requireRunDirectory(candidate: string, display: string): Promise<
|
|
|
68
58
|
`notary --source-run must be a run directory: ${display}`,
|
|
69
59
|
);
|
|
70
60
|
}
|
|
71
|
-
const identity =
|
|
61
|
+
const identity = parseRunLeaf(basename(real));
|
|
72
62
|
if (identity === undefined) {
|
|
73
63
|
throw new NotarySourceRunError(
|
|
74
64
|
`notary --source-run must be named <runId>@<role>: ${basename(real)}`,
|
|
@@ -102,13 +92,13 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
102
92
|
const bookRunsRoot = join(bookDirectory, "runs");
|
|
103
93
|
|
|
104
94
|
let candidate: string;
|
|
105
|
-
const bare =
|
|
95
|
+
const bare = parseRunLeaf(raw);
|
|
106
96
|
if (bare !== undefined && !raw.includes("/") && !raw.includes("\\")) {
|
|
107
97
|
candidate = (await findRunDirectoryById(options.home, bare.runId, bookKey, bare.role))
|
|
108
|
-
?? join(bookRunsRoot,
|
|
98
|
+
?? join(bookRunsRoot, formatRunLeaf(bare.runId, bare.role));
|
|
109
99
|
} else {
|
|
110
100
|
candidate = isAbsolute(raw) ? raw : resolve(options.projectRoot, raw);
|
|
111
|
-
const identity =
|
|
101
|
+
const identity = parseRunLeaf(basename(candidate));
|
|
112
102
|
const subjectDirectory = dirname(dirname(candidate));
|
|
113
103
|
const isLegacyUnboundLocator =
|
|
114
104
|
identity !== undefined &&
|
|
@@ -126,7 +116,7 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
126
116
|
}
|
|
127
117
|
|
|
128
118
|
const real = await requireRunDirectory(candidate, raw);
|
|
129
|
-
const identity =
|
|
119
|
+
const identity = parseRunLeaf(basename(real))!;
|
|
130
120
|
|
|
131
121
|
const bookIdentity = physicalPathIdentity(bookDirectory);
|
|
132
122
|
const parentIdentity = physicalPathIdentity(dirname(real));
|
|
@@ -176,7 +166,7 @@ export async function loadNotarySourceRunLocator(
|
|
|
176
166
|
path: string,
|
|
177
167
|
): Promise<NotarySourceRunLocator> {
|
|
178
168
|
const real = await requireRunDirectory(path, path);
|
|
179
|
-
const identity =
|
|
169
|
+
const identity = parseRunLeaf(basename(real))!;
|
|
180
170
|
const runState = await readRetainedIdentity(real);
|
|
181
171
|
if (runState === undefined) {
|
|
182
172
|
throw new NotarySourceRunError(
|
|
@@ -334,8 +334,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
334
334
|
sourceRunStored: "raw",
|
|
335
335
|
argvResult: "source-run",
|
|
336
336
|
sameParent: "source-locator",
|
|
337
|
-
/** Infrastructure failure at this stage has no accepted gate cycle to project. */
|
|
338
|
-
skipGateOnInfrastructureStage: true,
|
|
339
337
|
/** 符宝郎 is a review officer and inherits the gatekeeper model. */
|
|
340
338
|
reviewOfficer: true,
|
|
341
339
|
provinceConfig: true,
|
|
@@ -443,8 +441,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
443
441
|
sameParent: "gate-pointer",
|
|
444
442
|
/** Resume restores an optional source-run path onto the admitted face. */
|
|
445
443
|
resumeSourcePath: true,
|
|
446
|
-
/** Infrastructure failure at this stage has no accepted gate cycle to project. */
|
|
447
|
-
skipGateOnInfrastructureStage: true,
|
|
448
444
|
/** 台院 is a review officer and inherits the gatekeeper model. */
|
|
449
445
|
reviewOfficer: true,
|
|
450
446
|
provinceConfig: true,
|
|
@@ -476,8 +472,6 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
476
472
|
presentSettled: "always",
|
|
477
473
|
summonResume: true,
|
|
478
474
|
admission: "instruction",
|
|
479
|
-
/** Infrastructure failure at this stage has no accepted gate cycle to project. */
|
|
480
|
-
skipGateOnInfrastructureStage: true,
|
|
481
475
|
sameParent: "none",
|
|
482
476
|
/** Province model root. Officers name this seat via modelInheritsFrom. */
|
|
483
477
|
provinceConfig: true,
|
|
@@ -725,15 +719,6 @@ export function packagedAuditToolName(role: string): string | undefined {
|
|
|
725
719
|
return record.auditTool;
|
|
726
720
|
}
|
|
727
721
|
|
|
728
|
-
/** Infrastructure-failure stage that must not be re-read as an accepted gate cycle. */
|
|
729
|
-
export function packagedSkipsGateOnInfrastructureStage(stage: unknown): boolean {
|
|
730
|
-
return typeof stage === "string" && PUBLIC_ROLE_RECORDS.some((record) =>
|
|
731
|
-
"skipGateOnInfrastructureStage" in record
|
|
732
|
-
&& record.skipGateOnInfrastructureStage === true
|
|
733
|
-
&& record.role === stage
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
722
|
/** Durable officer entry whose name is the seat role. */
|
|
738
723
|
export function packagedDurableOfficerEntry(officer: unknown): boolean {
|
|
739
724
|
return typeof officer === "string" && PUBLIC_ROLE_RECORDS.some((record) =>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
|
|
3
1
|
import type {
|
|
4
2
|
DurablePrincipal,
|
|
5
3
|
DurablePrincipalAuthority,
|
|
@@ -8,7 +6,7 @@ import type {
|
|
|
8
6
|
} from "../host-contracts.ts";
|
|
9
7
|
import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
|
|
10
8
|
import { resolveActivationLedgerHome } from "../activation-ledger-topology.ts";
|
|
11
|
-
import { roleRunPlacement } from "../role-run-placement.ts";
|
|
9
|
+
import { roleRunPlacement, sessionFileIn } from "../role-run-placement.ts";
|
|
12
10
|
|
|
13
11
|
type PiDurablePrincipal = DurablePrincipal & {
|
|
14
12
|
readonly sessionDirectory: string;
|
|
@@ -69,7 +67,7 @@ export const piDurablePrincipalAuthority: DurablePrincipalAuthority = {
|
|
|
69
67
|
sessionFile:
|
|
70
68
|
typeof record.sessionFile === "string" && record.sessionFile.trim() !== ""
|
|
71
69
|
? record.sessionFile
|
|
72
|
-
:
|
|
70
|
+
: sessionFileIn(record.sessionDirectory),
|
|
73
71
|
};
|
|
74
72
|
},
|
|
75
73
|
};
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { constants } from "node:fs";
|
|
8
|
-
import { access, appendFile,
|
|
8
|
+
import { access, appendFile, realpath } from "node:fs/promises";
|
|
9
9
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
10
|
import { platform } from "node:process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
@@ -25,6 +25,7 @@ import { ExplicitInternalActivationError } from "../host-contracts.ts";
|
|
|
25
25
|
import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.ts";
|
|
26
26
|
import { projectActivationFlags } from "../role-activation-flags.ts";
|
|
27
27
|
import { encodeUserDialogueStdin } from "../user-dialogue-stdin.ts";
|
|
28
|
+
import { readStrictPiSessionJsonl } from "../ledger-session-read.ts";
|
|
28
29
|
import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
|
|
29
30
|
import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
|
|
30
31
|
import { RECEIPT_DELIVERY_LIMIT_ENV } from "../receipt-delivery-policy.ts";
|
|
@@ -462,9 +463,38 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
462
463
|
|
|
463
464
|
import { sitianReport } from "../sitian-facade.ts";
|
|
464
465
|
|
|
466
|
+
/** Last non-session entry id. Session headers are not parents of custom lines. */
|
|
467
|
+
export function piSessionCustomParentId(
|
|
468
|
+
entries: readonly { id?: unknown; type?: unknown }[],
|
|
469
|
+
): string | null {
|
|
470
|
+
let parentId: string | null = null;
|
|
471
|
+
for (const entry of entries) {
|
|
472
|
+
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
473
|
+
}
|
|
474
|
+
return parentId;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** One Pi custom JSONL line: type, customType, data, id, parentId, timestamp. */
|
|
478
|
+
export function formatPiSessionCustomEntry(input: {
|
|
479
|
+
readonly customType: string;
|
|
480
|
+
readonly data: unknown;
|
|
481
|
+
readonly parentId: string | null;
|
|
482
|
+
readonly timestamp: string;
|
|
483
|
+
}): string {
|
|
484
|
+
return `${JSON.stringify({
|
|
485
|
+
type: "custom",
|
|
486
|
+
customType: input.customType,
|
|
487
|
+
data: input.data,
|
|
488
|
+
id: randomUUID(),
|
|
489
|
+
parentId: input.parentId,
|
|
490
|
+
timestamp: input.timestamp,
|
|
491
|
+
})}\n`;
|
|
492
|
+
}
|
|
493
|
+
|
|
465
494
|
/**
|
|
466
495
|
* Append one custom JSONL entry to the durable principal's session file.
|
|
467
496
|
* Pi session codec only — AK artifact O_EXCL retention stays in public-cli.
|
|
497
|
+
* Read is raw JSON.parse (SyntaxError propagates). Sitian full-traversal stays elsewhere.
|
|
468
498
|
*/
|
|
469
499
|
export async function appendPiSessionCustomEntry(
|
|
470
500
|
authority: DurablePrincipalAuthority,
|
|
@@ -473,21 +503,14 @@ export async function appendPiSessionCustomEntry(
|
|
|
473
503
|
data: unknown,
|
|
474
504
|
): Promise<void> {
|
|
475
505
|
const { sessionFile } = authority.decode(principal);
|
|
476
|
-
const
|
|
477
|
-
let parentId: string | null = null;
|
|
478
|
-
for (const line of text.trim().split("\n").filter(Boolean)) {
|
|
479
|
-
const entry = JSON.parse(line) as { id?: unknown; type?: unknown };
|
|
480
|
-
if (typeof entry.id === "string" && entry.type !== "session") parentId = entry.id;
|
|
481
|
-
}
|
|
506
|
+
const entries = await readStrictPiSessionJsonl(sessionFile) as { id?: unknown; type?: unknown }[];
|
|
482
507
|
const timestamp = new Date().toISOString();
|
|
483
|
-
const pointerLine =
|
|
484
|
-
type: "custom",
|
|
508
|
+
const pointerLine = formatPiSessionCustomEntry({
|
|
485
509
|
customType,
|
|
486
510
|
data,
|
|
487
|
-
|
|
488
|
-
parentId,
|
|
511
|
+
parentId: piSessionCustomParentId(entries),
|
|
489
512
|
timestamp,
|
|
490
|
-
})
|
|
513
|
+
});
|
|
491
514
|
await appendFile(sessionFile, pointerLine, "utf8");
|
|
492
515
|
sitianReport({
|
|
493
516
|
level: "event",
|
|
@@ -83,7 +83,7 @@ import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
|
|
|
83
83
|
import {
|
|
84
84
|
presentStructuralRejection,
|
|
85
85
|
readBoundSessionEntries,
|
|
86
|
-
|
|
86
|
+
attachPostAuditCountersignFact,
|
|
87
87
|
trySettlePublicSeat,
|
|
88
88
|
type SettlementCourtScope,
|
|
89
89
|
} from "./settlement.ts";
|
|
@@ -1399,10 +1399,10 @@ async function auditSubmittedRole(
|
|
|
1399
1399
|
}
|
|
1400
1400
|
}
|
|
1401
1401
|
// The turn already settled this court (payloads, usage, autoResumeCount, history).
|
|
1402
|
-
// Audit only adds
|
|
1403
|
-
//
|
|
1402
|
+
// Audit only adds, for 中书省, the officer fact. A second settle would
|
|
1403
|
+
// republish the same attempt.
|
|
1404
1404
|
if (turn.terminal === undefined) throw new Error(`audited ${admitted.role} submission did not settle`);
|
|
1405
|
-
const terminal = await
|
|
1405
|
+
const terminal = await attachPostAuditCountersignFact(
|
|
1406
1406
|
admitted,
|
|
1407
1407
|
env.principalAuthority,
|
|
1408
1408
|
turn.terminal,
|
|
@@ -27,6 +27,7 @@ import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
|
|
|
27
27
|
import {
|
|
28
28
|
ensureRoleRunDirectory,
|
|
29
29
|
ensureRoleRunPlacement,
|
|
30
|
+
formatRunLeaf,
|
|
30
31
|
isUnboundRunDirectory,
|
|
31
32
|
listBookRunDirectories,
|
|
32
33
|
roleRunArtifactsDirectory,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
} from "../packaged-role-registry.ts";
|
|
52
53
|
import {
|
|
53
54
|
isSafePositiveTicketNumber,
|
|
55
|
+
parseTicketNumber,
|
|
54
56
|
readBoardTicketNumber,
|
|
55
57
|
requireSafePositiveTicketNumber,
|
|
56
58
|
} from "../run-ticket-number.ts";
|
|
@@ -600,7 +602,7 @@ export async function relocateAdmittedRunToTicket(
|
|
|
600
602
|
const childRunIds = parentPage.childDiaristRunIds;
|
|
601
603
|
for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
|
|
602
604
|
if (typeof childRunId !== "string") continue;
|
|
603
|
-
const childDirectory = join(dirname(oldRunDirectory),
|
|
605
|
+
const childDirectory = join(dirname(oldRunDirectory), formatRunLeaf(childRunId, "diarist"));
|
|
604
606
|
// A child that already filed under a ticket keeps its own assertion.
|
|
605
607
|
if (!existsSync(childDirectory)) continue;
|
|
606
608
|
const childTarget = roleRunPlacement(ledgerHome, {
|
|
@@ -759,20 +761,22 @@ export async function recordLaunchedRolePackageIdentity(
|
|
|
759
761
|
});
|
|
760
762
|
}
|
|
761
763
|
|
|
764
|
+
/** Trim, then the shared ticket spelling. Callers keep their own diagnostic text. */
|
|
765
|
+
function rejectUnlessTicketNumber(raw: string, message: string): number {
|
|
766
|
+
const parsed = parseTicketNumber(raw.trim());
|
|
767
|
+
if (parsed === undefined) throw new CliUsageError(message);
|
|
768
|
+
return parsed;
|
|
769
|
+
}
|
|
770
|
+
|
|
762
771
|
/** Positive ticket number for analyst query-scope face (and shared integer parse). */
|
|
763
772
|
export function parsePositiveTicketNumber(
|
|
764
773
|
raw: string,
|
|
765
774
|
flag: string,
|
|
766
775
|
): number {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const value = Number(trimmed);
|
|
772
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
773
|
-
throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
|
|
774
|
-
}
|
|
775
|
-
return value;
|
|
776
|
+
return rejectUnlessTicketNumber(
|
|
777
|
+
raw,
|
|
778
|
+
`${flag} must be a positive integer, got ${raw}`,
|
|
779
|
+
);
|
|
776
780
|
}
|
|
777
781
|
|
|
778
782
|
/**
|
|
@@ -1650,7 +1654,10 @@ export async function admitPublicRole(
|
|
|
1650
1654
|
}
|
|
1651
1655
|
let caseIdentity: DoctorCaseIdentity;
|
|
1652
1656
|
try {
|
|
1653
|
-
const patient = await loadDoctorCase(
|
|
1657
|
+
const patient = await loadDoctorCase(
|
|
1658
|
+
caseRunsPath,
|
|
1659
|
+
parsed.runs === undefined ? undefined : placed.projectRoot,
|
|
1660
|
+
);
|
|
1654
1661
|
if (patient.identity.issueNumber !== issueNumber) {
|
|
1655
1662
|
throw new CliUsageError(
|
|
1656
1663
|
`doctor case issue ${patient.identity.issueNumber} does not match --issue ${issueNumber}`,
|
|
@@ -2311,32 +2318,21 @@ function isGitRemoteMissing(error: unknown): boolean {
|
|
|
2311
2318
|
return status === 2;
|
|
2312
2319
|
}
|
|
2313
2320
|
|
|
2314
|
-
/** Positive Issue number grammar shared with Doctor case path identity. */
|
|
2315
|
-
const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
2316
|
-
|
|
2317
|
-
/** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
|
|
2318
|
-
/** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
|
|
2319
|
-
const DOCTOR_CASE_RUNS_PATH_PATTERN =
|
|
2320
|
-
/\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
|
|
2321
|
-
|
|
2322
2321
|
/**
|
|
2323
2322
|
* Parse a positive Issue number for public Doctor admission.
|
|
2324
2323
|
* Leading zeros and non-integers are structural rejects.
|
|
2325
2324
|
*/
|
|
2326
2325
|
export function parseDoctorIssueNumber(raw: string): number {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
);
|
|
2332
|
-
}
|
|
2333
|
-
return Number(trimmed);
|
|
2326
|
+
return rejectUnlessTicketNumber(
|
|
2327
|
+
raw,
|
|
2328
|
+
`doctor --issue must be a positive integer, got ${raw}`,
|
|
2329
|
+
);
|
|
2334
2330
|
}
|
|
2335
2331
|
|
|
2336
2332
|
/**
|
|
2337
2333
|
* Resolve the retained Doctor case runs root from Issue identity.
|
|
2338
|
-
* Default is the #78 book locator; optional --runs must stay project-confined
|
|
2339
|
-
*
|
|
2334
|
+
* Default is the #78 book locator; optional --runs must stay project-confined.
|
|
2335
|
+
* loadDoctorCase owns retained-root grammar at IO; admission matches its case identity.
|
|
2340
2336
|
*/
|
|
2341
2337
|
export async function resolveDoctorCaseRunsPath(options: {
|
|
2342
2338
|
home: string;
|
|
@@ -2388,18 +2384,6 @@ export async function resolveDoctorCaseRunsPath(options: {
|
|
|
2388
2384
|
);
|
|
2389
2385
|
}
|
|
2390
2386
|
|
|
2391
|
-
const normalized = real.split(sep).join("/");
|
|
2392
|
-
const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
|
|
2393
|
-
if (!match) {
|
|
2394
|
-
throw new CliUsageError(
|
|
2395
|
-
"doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory",
|
|
2396
|
-
);
|
|
2397
|
-
}
|
|
2398
|
-
if (Number(match[1]) !== options.issueNumber) {
|
|
2399
|
-
throw new CliUsageError(
|
|
2400
|
-
`doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`,
|
|
2401
|
-
);
|
|
2402
|
-
}
|
|
2403
2387
|
return real;
|
|
2404
2388
|
}
|
|
2405
2389
|
|
|
@@ -2477,8 +2461,6 @@ export type AdmitMergerInvocationOptions = {
|
|
|
2477
2461
|
assertedTicketNumber?: number;
|
|
2478
2462
|
};
|
|
2479
2463
|
|
|
2480
|
-
const ANALYST_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
2481
|
-
|
|
2482
2464
|
/**
|
|
2483
2465
|
* Parse a positive ticket / issue number for public analyst admission.
|
|
2484
2466
|
* Leading zeros and non-integers are structural rejects (same face as #176).
|
|
@@ -2488,20 +2470,10 @@ export function parseAnalystTicketNumber(
|
|
|
2488
2470
|
raw: string,
|
|
2489
2471
|
flag: string = "--ticket",
|
|
2490
2472
|
): number {
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
);
|
|
2496
|
-
}
|
|
2497
|
-
const value = Number(trimmed);
|
|
2498
|
-
// Digit-only strings beyond MAX_SAFE_INTEGER round or become Infinity — reject.
|
|
2499
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2500
|
-
throw new CliUsageError(
|
|
2501
|
-
`analyst ${flag} must be a positive integer, got ${raw}`,
|
|
2502
|
-
);
|
|
2503
|
-
}
|
|
2504
|
-
return value;
|
|
2473
|
+
return rejectUnlessTicketNumber(
|
|
2474
|
+
raw,
|
|
2475
|
+
`analyst ${flag} must be a positive integer, got ${raw}`,
|
|
2476
|
+
);
|
|
2505
2477
|
}
|
|
2506
2478
|
|
|
2507
2479
|
/**
|
|
@@ -2535,7 +2507,7 @@ export function parseAnalystCohortIssueToken(
|
|
|
2535
2507
|
const sep = trimmed.lastIndexOf(":");
|
|
2536
2508
|
if (sep > 0) {
|
|
2537
2509
|
const rhs = trimmed.slice(sep + 1);
|
|
2538
|
-
if (
|
|
2510
|
+
if (parseTicketNumber(rhs) !== undefined) {
|
|
2539
2511
|
const bookKey = trimmed.slice(0, sep);
|
|
2540
2512
|
if (bookKey.trim() === "") {
|
|
2541
2513
|
throw new CliUsageError(
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
activationBookDirectory,
|
|
17
17
|
resolveActivationLedgerHome,
|
|
18
18
|
} from "../activation-ledger-topology.ts";
|
|
19
|
-
import { listBookRunDirectories } from "../role-run-placement.ts";
|
|
19
|
+
import { findRoleRunDirectory, listBookRunDirectories, parseRunLeaf, sessionFileIn } from "../role-run-placement.ts";
|
|
20
20
|
import { isSafePositiveTicketNumber } from "../run-ticket-number.ts";
|
|
21
21
|
import { CliUsageError } from "./cli-errors.ts";
|
|
22
22
|
import type { FixerPhase } from "../package-contracts/fixer-output.ts";
|
|
@@ -25,6 +25,7 @@ import { parseCollectorRepository } from "../collector-config.ts";
|
|
|
25
25
|
import type { DoctorCaseIdentity } from "../doctor-contracts.ts";
|
|
26
26
|
import type { NotarySourceRunLocator } from "../notary-contracts.ts";
|
|
27
27
|
import {
|
|
28
|
+
retainedRunPathsMatch,
|
|
28
29
|
rewriteAdmittedRoleRunPage,
|
|
29
30
|
rewriteRunDirectoryPathValue,
|
|
30
31
|
} from "../role-run-relocation.ts";
|
|
@@ -915,31 +916,12 @@ export async function findRunDirectoryById(
|
|
|
915
916
|
if (errorCodeOf(error) === "ENOENT") return undefined;
|
|
916
917
|
throw error;
|
|
917
918
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
runDirectories = await listBookRunDirectories(bookDir);
|
|
925
|
-
} catch (error) {
|
|
926
|
-
if (errorCodeOf(error) === "ENOENT") continue;
|
|
927
|
-
throw error;
|
|
928
|
-
}
|
|
929
|
-
for (const runDirectory of runDirectories) {
|
|
930
|
-
const entry = basename(runDirectory);
|
|
931
|
-
if (
|
|
932
|
-
(onlyRole === undefined && (entry === `${runId}@judge` || entry.startsWith(`${runId}@`))) ||
|
|
933
|
-
entry === `${runId}@${onlyRole}`
|
|
934
|
-
) {
|
|
935
|
-
matches.push(runDirectory);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if (matches.length === 0) return undefined;
|
|
940
|
-
if (matches.length === 1) return matches[0];
|
|
941
|
-
throw new Error(
|
|
942
|
-
`ambiguous role run id ${runId}: ${matches.join(", ")}`,
|
|
919
|
+
return findRoleRunDirectory(
|
|
920
|
+
bookKeys
|
|
921
|
+
.filter((bookKey) => onlyBookKey === undefined || bookKey === onlyBookKey)
|
|
922
|
+
.map((bookKey) => activationBookDirectory(ledgerHome, bookKey)),
|
|
923
|
+
runId,
|
|
924
|
+
onlyRole,
|
|
943
925
|
);
|
|
944
926
|
}
|
|
945
927
|
|
|
@@ -999,7 +981,7 @@ async function runHasFormedSessionPrincipal(runDirectory: string): Promise<boole
|
|
|
999
981
|
typeof disk.principalWire.sessionFile === "string" &&
|
|
1000
982
|
disk.principalWire.sessionFile.trim() !== ""
|
|
1001
983
|
? disk.principalWire.sessionFile
|
|
1002
|
-
:
|
|
984
|
+
: sessionFileIn(disk.principalWire.sessionDirectory);
|
|
1003
985
|
try {
|
|
1004
986
|
const stat = await lstat(sessionFile);
|
|
1005
987
|
return stat.isFile() && !stat.isSymbolicLink();
|
|
@@ -1040,17 +1022,14 @@ export async function findLatestRunIdForSeatTicket(input: {
|
|
|
1040
1022
|
if (errorCodeOf(error) === "ENOENT") return undefined;
|
|
1041
1023
|
throw error;
|
|
1042
1024
|
}
|
|
1043
|
-
const suffix = `@${input.role}`;
|
|
1044
1025
|
let best: string | undefined;
|
|
1045
1026
|
for (const runDirectory of runDirectories) {
|
|
1046
|
-
const
|
|
1047
|
-
if (
|
|
1048
|
-
const runId =
|
|
1049
|
-
if (runId.length === 0) continue;
|
|
1027
|
+
const parsed = parseRunLeaf(basename(runDirectory));
|
|
1028
|
+
if (parsed === undefined || parsed.role !== input.role) continue;
|
|
1029
|
+
const runId = parsed.runId;
|
|
1050
1030
|
const parentPath = await readRunParentPath(runDirectory);
|
|
1051
|
-
//
|
|
1052
|
-
|
|
1053
|
-
if (parentPath !== input.parentRunPath && basename(parentPath ?? "") !== basename(input.parentRunPath)) continue;
|
|
1031
|
+
// Follow the parent's retained placement without collapsing distinct book/subject bindings.
|
|
1032
|
+
if (!await retainedRunPathsMatch(parentPath, input.parentRunPath, input.parentRunPath)) continue;
|
|
1054
1033
|
// Durable fact: never resume-select a provisional that never formed principal.
|
|
1055
1034
|
if (!(await runHasFormedSessionPrincipal(runDirectory))) continue;
|
|
1056
1035
|
if (best === undefined || runId > best) best = runId;
|
|
@@ -1226,7 +1205,7 @@ async function loadResumableRunRecord(
|
|
|
1226
1205
|
lens = record.lens;
|
|
1227
1206
|
}
|
|
1228
1207
|
// Collector — admitted repository/PR identity (#633 resume).
|
|
1229
|
-
if (
|
|
1208
|
+
if (isSafePositiveTicketNumber(record.prNumber)) {
|
|
1230
1209
|
prNumber = record.prNumber;
|
|
1231
1210
|
}
|
|
1232
1211
|
if (typeof record.repository === "string" && record.repository.trim() !== "") {
|
|
@@ -1245,7 +1224,7 @@ async function loadResumableRunRecord(
|
|
|
1245
1224
|
waitWindowMs = record.waitWindowMs;
|
|
1246
1225
|
}
|
|
1247
1226
|
// Doctor — admitted single-case identity (#633 resume).
|
|
1248
|
-
if (
|
|
1227
|
+
if (isSafePositiveTicketNumber(record.issueNumber)) {
|
|
1249
1228
|
issueNumber = record.issueNumber;
|
|
1250
1229
|
}
|
|
1251
1230
|
if (typeof record.caseRunsPath === "string" && record.caseRunsPath.trim() !== "") {
|
|
@@ -1256,9 +1235,7 @@ async function loadResumableRunRecord(
|
|
|
1256
1235
|
) {
|
|
1257
1236
|
const ci = record.caseIdentity as Record<string, unknown>;
|
|
1258
1237
|
if (
|
|
1259
|
-
|
|
1260
|
-
Number.isSafeInteger(ci.issueNumber) &&
|
|
1261
|
-
ci.issueNumber >= 1 &&
|
|
1238
|
+
isSafePositiveTicketNumber(ci.issueNumber) &&
|
|
1262
1239
|
typeof ci.runsPath === "string" &&
|
|
1263
1240
|
ci.runsPath.trim() !== ""
|
|
1264
1241
|
) {
|
|
@@ -1376,9 +1353,9 @@ async function loadResumableRunRecord(
|
|
|
1376
1353
|
);
|
|
1377
1354
|
}
|
|
1378
1355
|
}
|
|
1379
|
-
const sourceRunLeaf = basename(sourceRunPath ?? "")
|
|
1380
|
-
const referencedRunId = sourceRun?.runId ?? sourceRunLeaf
|
|
1381
|
-
const referencedRole = sourceRun?.role ?? sourceRunLeaf
|
|
1356
|
+
const sourceRunLeaf = parseRunLeaf(basename(sourceRunPath ?? ""));
|
|
1357
|
+
const referencedRunId = sourceRun?.runId ?? sourceRunLeaf?.runId;
|
|
1358
|
+
const referencedRole = sourceRun?.role ?? sourceRunLeaf?.role;
|
|
1382
1359
|
if (referencedRunId !== undefined && referencedRunId !== "") {
|
|
1383
1360
|
const currentSourceRunDirectory = await findRunDirectoryById(
|
|
1384
1361
|
home,
|