@agent-delivery-harness/cli 0.1.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +104 -201
- package/NOTICE +1 -1
- package/package.json +10 -5
- package/src/boundary.ts +302 -9
- package/src/command-activity.ts +75 -0
- package/src/commands/admit.ts +106 -0
- package/src/commands/check.ts +13 -2
- package/src/commands/emit-review-evidence.ts +38 -0
- package/src/commands/emit.ts +419 -0
- package/src/commands/gate.ts +127 -18
- package/src/commands/maintain.ts +203 -0
- package/src/commands/managed.ts +478 -0
- package/src/commands/prepare.ts +123 -15
- package/src/commands/record.ts +100 -8
- package/src/commands/resume.ts +53 -0
- package/src/commands/review-context.ts +19 -0
- package/src/commands/runs.ts +640 -0
- package/src/commands/save-context.ts +51 -0
- package/src/commands/submit-evidence.ts +46 -7
- package/src/commands/verify.ts +173 -5
- package/src/declared-checks.ts +50 -0
- package/src/index.ts +40 -4
- package/src/main.ts +38 -42
- package/src/ordinary-context.ts +99 -0
- package/src/provider-rails.ts +24 -0
- package/src/record-retention.ts +325 -0
- package/src/review-evidence.ts +284 -0
- package/src/run-archive-commands.ts +164 -0
- package/src/run-archive.ts +127 -0
- package/src/run-artifact-commands.ts +163 -0
- package/src/run-attachments.ts +112 -0
- package/src/run-export.ts +164 -0
- package/src/run-live.ts +102 -0
- package/src/run-projection.ts +396 -0
- package/src/run-server.ts +905 -0
- package/src/run-surface.ts +334 -0
- package/src/run-view-command.ts +77 -0
- package/src/run-view-html.ts +138 -0
- package/src/run-view-record.ts +119 -0
- package/src/run-view.ts +343 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/** Explicit acquisition capture; retained reports remain observations. */
|
|
2
|
+
import {
|
|
3
|
+
captureRunArtifact,
|
|
4
|
+
readRunArtifact,
|
|
5
|
+
validateRunEventInput,
|
|
6
|
+
type RunArtifactMetadata,
|
|
7
|
+
} from "@agent-delivery-harness/kernel";
|
|
8
|
+
import {
|
|
9
|
+
buildRunEvent,
|
|
10
|
+
oneLine,
|
|
11
|
+
resolveRunSurface,
|
|
12
|
+
runSurfaceBlocker,
|
|
13
|
+
} from "./run-surface.ts";
|
|
14
|
+
import type { CommandResult, ConfigFreeCommandContext } from "./boundary.ts";
|
|
15
|
+
const blocked = (reason: string): CommandResult => ({
|
|
16
|
+
kind: "blocked",
|
|
17
|
+
blockers: [
|
|
18
|
+
runSurfaceBlocker({
|
|
19
|
+
code: "run_artifact_unavailable",
|
|
20
|
+
summary: "The run attachment is unavailable.",
|
|
21
|
+
details: oneLine(reason),
|
|
22
|
+
remediation: {
|
|
23
|
+
id: "inspect-selected-report",
|
|
24
|
+
summary:
|
|
25
|
+
"Check the explicit report binding and selected source file, then retry capture.",
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
const record = (v: unknown): v is Record<string, unknown> =>
|
|
31
|
+
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
32
|
+
export async function runArtifactCommand(
|
|
33
|
+
context: ConfigFreeCommandContext,
|
|
34
|
+
command: "capture" | "artifact",
|
|
35
|
+
args: readonly string[],
|
|
36
|
+
): Promise<CommandResult> {
|
|
37
|
+
if (
|
|
38
|
+
command === "artifact" &&
|
|
39
|
+
(args.length < 2 ||
|
|
40
|
+
args.length > 3 ||
|
|
41
|
+
(args[2] !== undefined && args[2] !== "--json"))
|
|
42
|
+
)
|
|
43
|
+
return {
|
|
44
|
+
kind: "usage",
|
|
45
|
+
message: "Usage: runs artifact <run-id> <artifact-id> [--json]",
|
|
46
|
+
};
|
|
47
|
+
if (command === "capture" && (args.length !== 3 || args[1] !== "--json"))
|
|
48
|
+
return {
|
|
49
|
+
kind: "usage",
|
|
50
|
+
message: "Usage: runs capture <run-id> --json <request>",
|
|
51
|
+
};
|
|
52
|
+
const resolved = await resolveRunSurface(context.rootDir);
|
|
53
|
+
if (!resolved.ok) return blocked(resolved.reason);
|
|
54
|
+
const { store, commonDir } = resolved.surface;
|
|
55
|
+
const runId = args[0]!;
|
|
56
|
+
if (command === "artifact") {
|
|
57
|
+
const result = await readRunArtifact(store, runId, args[1]!);
|
|
58
|
+
if (!result.ok) return blocked(result.reason);
|
|
59
|
+
context.write(
|
|
60
|
+
args[2] === "--json"
|
|
61
|
+
? `${JSON.stringify({ spec: "run-artifact/1", labels: "self-attested observation; not admission evidence", runId, ...result })}\n`
|
|
62
|
+
: `${oneLine(Buffer.from(result.base64, "base64").toString("utf8"), 2 * 1024 * 1024)}\n`,
|
|
63
|
+
);
|
|
64
|
+
return { kind: "ok" };
|
|
65
|
+
}
|
|
66
|
+
let request: unknown;
|
|
67
|
+
try {
|
|
68
|
+
if (Buffer.byteLength(args[2]!, "utf8") > 16384)
|
|
69
|
+
return blocked("capture request exceeds 16 KiB");
|
|
70
|
+
request = JSON.parse(args[2]!);
|
|
71
|
+
} catch {
|
|
72
|
+
return { kind: "usage", message: "Capture request must be JSON." };
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
!record(request) ||
|
|
76
|
+
Object.keys(request).sort().join(",") !==
|
|
77
|
+
"artifact,eventId,report,sourcePath,sourceRoot" ||
|
|
78
|
+
typeof request["sourceRoot"] !== "string" ||
|
|
79
|
+
typeof request["sourcePath"] !== "string" ||
|
|
80
|
+
typeof request["eventId"] !== "string" ||
|
|
81
|
+
!record(request["artifact"]) ||
|
|
82
|
+
!record(request["report"])
|
|
83
|
+
)
|
|
84
|
+
return {
|
|
85
|
+
kind: "usage",
|
|
86
|
+
message:
|
|
87
|
+
"Capture requires artifact, report, eventId, sourceRoot and sourcePath.",
|
|
88
|
+
};
|
|
89
|
+
const artifact = request["artifact"];
|
|
90
|
+
const report = request["report"];
|
|
91
|
+
const binding = {
|
|
92
|
+
activityId: artifact["activityId"],
|
|
93
|
+
attemptId: artifact["attemptId"],
|
|
94
|
+
candidateTreeSha: artifact["candidateTreeSha"],
|
|
95
|
+
...(artifact["roundId"] === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: { roundId: artifact["roundId"] }),
|
|
98
|
+
...(artifact["round"] === undefined ? {} : { round: artifact["round"] }),
|
|
99
|
+
...(artifact["lensId"] === undefined ? {} : { lensId: artifact["lensId"] }),
|
|
100
|
+
};
|
|
101
|
+
const journal = await store.read(runId);
|
|
102
|
+
if (!journal.ok || journal.events[0]?.version !== "run-event/2")
|
|
103
|
+
return blocked(
|
|
104
|
+
"capture requires an existing run-event/2 run; start an explicit successor for a legacy run",
|
|
105
|
+
);
|
|
106
|
+
const make = (suffix: string, kind: string, payload: unknown) => {
|
|
107
|
+
const event = buildRunEvent({
|
|
108
|
+
runId,
|
|
109
|
+
commonDir,
|
|
110
|
+
kind,
|
|
111
|
+
role: "executor",
|
|
112
|
+
payload,
|
|
113
|
+
version: "run-event/2",
|
|
114
|
+
eventId: `${request["eventId"]}-${suffix}`,
|
|
115
|
+
});
|
|
116
|
+
return event;
|
|
117
|
+
};
|
|
118
|
+
const artifactEvent = make("artifact", "artifact.referenced", artifact);
|
|
119
|
+
const reportEvent = make("report", "report.referenced", {
|
|
120
|
+
...report,
|
|
121
|
+
...binding,
|
|
122
|
+
availability: "referenced",
|
|
123
|
+
artifactId: artifact["artifactId"],
|
|
124
|
+
});
|
|
125
|
+
// Report input cannot override the capture's binding or carry success fields.
|
|
126
|
+
if (
|
|
127
|
+
Object.keys(report).some(
|
|
128
|
+
(k) =>
|
|
129
|
+
!["reportId", "role", "originatingReportId", "findingId"].includes(k),
|
|
130
|
+
) ||
|
|
131
|
+
!validateRunEventInput(artifactEvent).ok ||
|
|
132
|
+
!validateRunEventInput(reportEvent).ok
|
|
133
|
+
)
|
|
134
|
+
return blocked("invalid report or artifact event binding");
|
|
135
|
+
const result = await captureRunArtifact({
|
|
136
|
+
store,
|
|
137
|
+
runId,
|
|
138
|
+
metadata: artifact as unknown as RunArtifactMetadata,
|
|
139
|
+
sourceRoot: request["sourceRoot"],
|
|
140
|
+
sourcePath: request["sourcePath"],
|
|
141
|
+
});
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
const unavailable = make("unavailable", "report.referenced", {
|
|
144
|
+
...report,
|
|
145
|
+
...binding,
|
|
146
|
+
availability: "unavailable",
|
|
147
|
+
reason: result.reason,
|
|
148
|
+
});
|
|
149
|
+
await store.append(runId, unavailable, { reuseExistingTimestamp: true });
|
|
150
|
+
return blocked(result.reason);
|
|
151
|
+
}
|
|
152
|
+
for (const event of [artifactEvent, reportEvent]) {
|
|
153
|
+
const appended = await store.append(runId, event, { reuseExistingTimestamp: true });
|
|
154
|
+
if (!appended.ok)
|
|
155
|
+
return blocked(
|
|
156
|
+
`bytes retained but reference append refused: ${appended.rejections.map((r) => r.message).join("; ")}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
context.write(
|
|
160
|
+
`${JSON.stringify({ spec: "run-artifact-capture/1", runId, artifactId: result.metadata.artifactId, digest: result.metadata.digest, availability: "referenced", authority: "observation" })}\n`,
|
|
161
|
+
);
|
|
162
|
+
return { kind: "ok" };
|
|
163
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Bounded observational attachment transport; no path is followed by this parser. */
|
|
2
|
+
import {
|
|
3
|
+
canonicalize,
|
|
4
|
+
sha256Hex,
|
|
5
|
+
portableArtifactContents,
|
|
6
|
+
applySecretDiscipline,
|
|
7
|
+
MAX_PORTABLE_ARTIFACTS,
|
|
8
|
+
MAX_PORTABLE_EVIDENCE_BYTES,
|
|
9
|
+
type RunArtifactMetadata,
|
|
10
|
+
type RunEvent,
|
|
11
|
+
} from "@agent-delivery-harness/kernel";
|
|
12
|
+
export interface ArchivedAttachment {
|
|
13
|
+
readonly metadata: RunArtifactMetadata;
|
|
14
|
+
readonly availability: "retained" | "unavailable";
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
readonly code?: "missing" | "access_refused";
|
|
17
|
+
}
|
|
18
|
+
export interface RunAttachments {
|
|
19
|
+
readonly entries: readonly ArchivedAttachment[];
|
|
20
|
+
readonly blobs: Readonly<Record<string, string>>;
|
|
21
|
+
}
|
|
22
|
+
const record = (v: unknown): v is Record<string, unknown> =>
|
|
23
|
+
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
24
|
+
export function referencedAttachments(
|
|
25
|
+
events: readonly RunEvent[],
|
|
26
|
+
): Map<string, RunArtifactMetadata> | undefined {
|
|
27
|
+
const entries = new Map<string, RunArtifactMetadata>();
|
|
28
|
+
for (const event of events) {
|
|
29
|
+
if (event.kind !== "artifact.referenced") continue;
|
|
30
|
+
const metadata = event.payload as unknown as RunArtifactMetadata;
|
|
31
|
+
const prior = entries.get(metadata.artifactId);
|
|
32
|
+
if (prior && canonicalize(prior) !== canonicalize(metadata))
|
|
33
|
+
return undefined;
|
|
34
|
+
entries.set(metadata.artifactId, metadata);
|
|
35
|
+
}
|
|
36
|
+
return entries;
|
|
37
|
+
}
|
|
38
|
+
export function validateRunAttachments(
|
|
39
|
+
value: unknown,
|
|
40
|
+
events: readonly RunEvent[],
|
|
41
|
+
): value is RunAttachments {
|
|
42
|
+
if (
|
|
43
|
+
!record(value) ||
|
|
44
|
+
Object.keys(value).sort().join(",") !== "blobs,entries" ||
|
|
45
|
+
!Array.isArray(value["entries"]) ||
|
|
46
|
+
!record(value["blobs"]) ||
|
|
47
|
+
Buffer.byteLength(JSON.stringify(value), "utf8") >
|
|
48
|
+
MAX_PORTABLE_EVIDENCE_BYTES
|
|
49
|
+
)
|
|
50
|
+
return false;
|
|
51
|
+
const refs = referencedAttachments(events);
|
|
52
|
+
if (
|
|
53
|
+
!refs ||
|
|
54
|
+
refs.size > MAX_PORTABLE_ARTIFACTS ||
|
|
55
|
+
value["entries"].length !== refs.size
|
|
56
|
+
)
|
|
57
|
+
return false;
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
const needed = new Set<string>();
|
|
60
|
+
const read = portableArtifactContents(value["blobs"]);
|
|
61
|
+
if (read.blockers.length) return false;
|
|
62
|
+
for (const entry of value["entries"]) {
|
|
63
|
+
if (!record(entry) || !record(entry["metadata"])) return false;
|
|
64
|
+
const metadata = entry["metadata"] as unknown as RunArtifactMetadata;
|
|
65
|
+
if (
|
|
66
|
+
seen.has(metadata.artifactId) ||
|
|
67
|
+
!refs.has(metadata.artifactId) ||
|
|
68
|
+
canonicalize(metadata) !== canonicalize(refs.get(metadata.artifactId))
|
|
69
|
+
)
|
|
70
|
+
return false;
|
|
71
|
+
seen.add(metadata.artifactId);
|
|
72
|
+
if (entry["availability"] === "unavailable") {
|
|
73
|
+
if (
|
|
74
|
+
Object.keys(entry).sort().join(",") !==
|
|
75
|
+
"availability,code,metadata,reason" ||
|
|
76
|
+
!["missing", "access_refused"].includes(String(entry["code"])) ||
|
|
77
|
+
typeof entry["reason"] !== "string" ||
|
|
78
|
+
!entry["reason"].length ||
|
|
79
|
+
entry["reason"].length > 512
|
|
80
|
+
)
|
|
81
|
+
return false;
|
|
82
|
+
} else if (entry["availability"] === "retained") {
|
|
83
|
+
if (Object.keys(entry).sort().join(",") !== "availability,metadata")
|
|
84
|
+
return false;
|
|
85
|
+
const observed = read.observations.get(metadata.digest);
|
|
86
|
+
if (!observed || observed.base64 === undefined) return false;
|
|
87
|
+
const bytes = Buffer.from(observed.base64, "base64");
|
|
88
|
+
if (
|
|
89
|
+
bytes.length !== metadata.sizeBytes ||
|
|
90
|
+
sha256Hex(bytes) !== metadata.digest
|
|
91
|
+
)
|
|
92
|
+
return false;
|
|
93
|
+
let structured: unknown = null;
|
|
94
|
+
try {
|
|
95
|
+
structured = JSON.parse(bytes.toString("utf8"));
|
|
96
|
+
} catch {}
|
|
97
|
+
if (
|
|
98
|
+
!applySecretDiscipline(
|
|
99
|
+
{ text: bytes.toString("utf8"), structured },
|
|
100
|
+
new Set(),
|
|
101
|
+
).ok
|
|
102
|
+
)
|
|
103
|
+
return false;
|
|
104
|
+
needed.add(metadata.digest);
|
|
105
|
+
} else return false;
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
Object.keys(value["blobs"]).length === needed.size &&
|
|
109
|
+
Object.keys(value["blobs"]).every((key) => needed.has(key)) &&
|
|
110
|
+
applySecretDiscipline(value["entries"], new Set()).ok
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateRunAttachments,
|
|
3
|
+
type RunAttachments,
|
|
4
|
+
} from "./run-attachments.ts";
|
|
5
|
+
/** Retained observability, projected and checked by the same product code. */
|
|
6
|
+
import {
|
|
7
|
+
canonicalize,
|
|
8
|
+
MAX_PORTABLE_RECORD_BYTES,
|
|
9
|
+
applySecretDiscipline,
|
|
10
|
+
evaluateRunJournal,
|
|
11
|
+
RUN_STORE_ID,
|
|
12
|
+
validateRunEvent,
|
|
13
|
+
type RunEvent,
|
|
14
|
+
} from "@agent-delivery-harness/kernel";
|
|
15
|
+
import {
|
|
16
|
+
READOUT_LABELS,
|
|
17
|
+
projectCosts,
|
|
18
|
+
projectRunProgress,
|
|
19
|
+
readoutOf,
|
|
20
|
+
summarize,
|
|
21
|
+
} from "./run-projection.ts";
|
|
22
|
+
|
|
23
|
+
export function buildRunExport(input: {
|
|
24
|
+
readonly runId: string;
|
|
25
|
+
readonly events: readonly RunEvent[];
|
|
26
|
+
readonly rootDir?: string;
|
|
27
|
+
readonly refusedAppends?: readonly unknown[];
|
|
28
|
+
}) {
|
|
29
|
+
const v2 = input.events[0]?.version === "run-event/2";
|
|
30
|
+
return {
|
|
31
|
+
spec: v2
|
|
32
|
+
? ("delivery-run-export/2" as const)
|
|
33
|
+
: ("delivery-run-export/1" as const),
|
|
34
|
+
labels: READOUT_LABELS,
|
|
35
|
+
runId: input.runId,
|
|
36
|
+
events: input.events,
|
|
37
|
+
summary: summarize(input.events),
|
|
38
|
+
costs: projectCosts(input.events),
|
|
39
|
+
readout: readoutOf(
|
|
40
|
+
input.events,
|
|
41
|
+
evaluateRunJournal(input.events),
|
|
42
|
+
input.rootDir,
|
|
43
|
+
),
|
|
44
|
+
refusedAppends: input.refusedAppends ?? [],
|
|
45
|
+
// Exported progress is a historical projection at the last observation,
|
|
46
|
+
// never a claim about whether the producer is executing now.
|
|
47
|
+
...(v2
|
|
48
|
+
? {
|
|
49
|
+
progress: projectRunProgress(
|
|
50
|
+
input.events,
|
|
51
|
+
input.events.at(-1)?.at ?? "",
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type DeliveryRunExport = ReturnType<typeof buildRunExport> & {
|
|
59
|
+
readonly attachments?: RunAttachments;
|
|
60
|
+
};
|
|
61
|
+
export type RunExportParseResult =
|
|
62
|
+
| { readonly ok: true; readonly value: DeliveryRunExport }
|
|
63
|
+
| { readonly ok: false; readonly code: "run_export_invalid" };
|
|
64
|
+
|
|
65
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
66
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
|
|
68
|
+
/** Validate observations and recompute totals; this never establishes admission. */
|
|
69
|
+
export function parseRunExport(text: string): RunExportParseResult {
|
|
70
|
+
const invalid = { ok: false, code: "run_export_invalid" } as const;
|
|
71
|
+
try {
|
|
72
|
+
if (Buffer.byteLength(text, "utf8") > MAX_PORTABLE_RECORD_BYTES)
|
|
73
|
+
return invalid;
|
|
74
|
+
const value: unknown = JSON.parse(text);
|
|
75
|
+
if (
|
|
76
|
+
!isRecord(value) ||
|
|
77
|
+
!["delivery-run-export/1", "delivery-run-export/2"].includes(
|
|
78
|
+
String(value["spec"]),
|
|
79
|
+
) ||
|
|
80
|
+
value["labels"] !== READOUT_LABELS ||
|
|
81
|
+
typeof value["runId"] !== "string" ||
|
|
82
|
+
value["runId"].length > 128 ||
|
|
83
|
+
!RUN_STORE_ID.test(value["runId"]) ||
|
|
84
|
+
!Array.isArray(value["events"]) ||
|
|
85
|
+
!Array.isArray(value["refusedAppends"]) ||
|
|
86
|
+
!isRecord(value["readout"])
|
|
87
|
+
)
|
|
88
|
+
return invalid;
|
|
89
|
+
const v2 = value["spec"] === "delivery-run-export/2";
|
|
90
|
+
const hasAttachments = Object.prototype.hasOwnProperty.call(
|
|
91
|
+
value,
|
|
92
|
+
"attachments",
|
|
93
|
+
);
|
|
94
|
+
if (hasAttachments && (!v2 || !applySecretDiscipline(value, new Set()).ok))
|
|
95
|
+
return invalid;
|
|
96
|
+
if (
|
|
97
|
+
Object.keys(value)
|
|
98
|
+
.filter((key) => key !== "attachments")
|
|
99
|
+
.sort()
|
|
100
|
+
.join(",") !==
|
|
101
|
+
(v2
|
|
102
|
+
? "costs,events,labels,progress,readout,refusedAppends,runId,spec,summary"
|
|
103
|
+
: "costs,events,labels,readout,refusedAppends,runId,spec,summary")
|
|
104
|
+
)
|
|
105
|
+
return invalid;
|
|
106
|
+
for (const [index, event] of value["events"].entries()) {
|
|
107
|
+
if (
|
|
108
|
+
!validateRunEvent(event).ok ||
|
|
109
|
+
!isRecord(event) ||
|
|
110
|
+
event["runId"] !== value["runId"] ||
|
|
111
|
+
event["seq"] !== index + 1 ||
|
|
112
|
+
event["version"] !== (v2 ? "run-event/2" : "run-event/1")
|
|
113
|
+
)
|
|
114
|
+
return invalid;
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
hasAttachments &&
|
|
118
|
+
!validateRunAttachments(
|
|
119
|
+
value["attachments"],
|
|
120
|
+
value["events"] as RunEvent[],
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return invalid;
|
|
124
|
+
const expected = buildRunExport({
|
|
125
|
+
runId: value["runId"],
|
|
126
|
+
events: value["events"] as RunEvent[],
|
|
127
|
+
});
|
|
128
|
+
if (
|
|
129
|
+
expected.spec !== value["spec"] ||
|
|
130
|
+
canonicalize(value["progress"] ?? null) !==
|
|
131
|
+
canonicalize(expected.progress ?? null)
|
|
132
|
+
)
|
|
133
|
+
return invalid;
|
|
134
|
+
if (
|
|
135
|
+
canonicalize(value["summary"]) !== canonicalize(expected.summary) ||
|
|
136
|
+
canonicalize(value["costs"]) !== canonicalize(expected.costs)
|
|
137
|
+
)
|
|
138
|
+
return invalid;
|
|
139
|
+
// The optional note describes config presence in the exporting workspace;
|
|
140
|
+
// it cannot be recomputed in a different checkout and grants no authority.
|
|
141
|
+
const { note, ...readout } = value["readout"];
|
|
142
|
+
if (
|
|
143
|
+
(note !== undefined && typeof note !== "string") ||
|
|
144
|
+
canonicalize(readout) !== canonicalize(expected.readout)
|
|
145
|
+
)
|
|
146
|
+
return invalid;
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
value: {
|
|
150
|
+
...expected,
|
|
151
|
+
...(hasAttachments
|
|
152
|
+
? { attachments: value["attachments"] as RunAttachments }
|
|
153
|
+
: {}),
|
|
154
|
+
refusedAppends: value["refusedAppends"],
|
|
155
|
+
readout: {
|
|
156
|
+
...expected.readout,
|
|
157
|
+
...(note === undefined ? {} : { note }),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
return invalid;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/run-live.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Fixed, hash-authorized enhancement. No executor content enters this script. */
|
|
2
|
+
export const RUN_LIVE_SCRIPT = String.raw`(() => {
|
|
3
|
+
const root = document.querySelector('main');
|
|
4
|
+
const toggle = document.querySelector('[data-live-toggle]');
|
|
5
|
+
const status = document.querySelector('[data-live-status]');
|
|
6
|
+
if (!root || !toggle || !status) return;
|
|
7
|
+
let paused = false;
|
|
8
|
+
let timer;
|
|
9
|
+
let interactingUntil = 0;
|
|
10
|
+
const interval = Number(root.dataset.pollSeconds) * 1000;
|
|
11
|
+
const interact = () => { interactingUntil = Date.now() + 1500; };
|
|
12
|
+
for (const event of ['pointerdown', 'keydown', 'wheel', 'touchstart']) {
|
|
13
|
+
document.addEventListener(event, interact, { passive: true });
|
|
14
|
+
}
|
|
15
|
+
toggle.addEventListener('click', event => {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
paused = !paused;
|
|
18
|
+
toggle.textContent = paused ? 'Resume live updates' : 'Pause updates';
|
|
19
|
+
status.textContent = paused ? 'Updates paused' : 'Live';
|
|
20
|
+
});
|
|
21
|
+
// Preserve existing nodes wherever possible, including focused controls and
|
|
22
|
+
// native disclosure state. Stable keys keep newly inserted cards from taking
|
|
23
|
+
// the identity of an earlier card the operator is reading.
|
|
24
|
+
const key = node => node.nodeType === 1 ? node.id || node.getAttribute('data-key') : null;
|
|
25
|
+
function patch(current, next) {
|
|
26
|
+
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
|
|
27
|
+
current.replaceWith(next.cloneNode(true));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (current.nodeType !== 1) {
|
|
31
|
+
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (current.hasAttribute('data-live-controls')) return;
|
|
35
|
+
for (const attr of [...current.attributes]) {
|
|
36
|
+
if (current.tagName === 'DETAILS' && attr.name === 'open') continue;
|
|
37
|
+
if (!next.hasAttribute(attr.name)) current.removeAttribute(attr.name);
|
|
38
|
+
}
|
|
39
|
+
for (const attr of next.attributes) {
|
|
40
|
+
if (current.tagName !== 'DETAILS' || attr.name !== 'open') current.setAttribute(attr.name, attr.value);
|
|
41
|
+
}
|
|
42
|
+
const children = [...current.childNodes];
|
|
43
|
+
const used = new Set();
|
|
44
|
+
let cursor = current.firstChild;
|
|
45
|
+
for (const incoming of next.childNodes) {
|
|
46
|
+
const incomingKey = key(incoming);
|
|
47
|
+
const existing = incomingKey
|
|
48
|
+
? children.find(child => !used.has(child) && key(child) === incomingKey)
|
|
49
|
+
: children.find(child => !used.has(child) && !key(child) && child.nodeType === incoming.nodeType && child.nodeName === incoming.nodeName);
|
|
50
|
+
if (existing) {
|
|
51
|
+
used.add(existing);
|
|
52
|
+
if (existing !== cursor) current.insertBefore(existing, cursor);
|
|
53
|
+
patch(existing, incoming);
|
|
54
|
+
cursor = existing.nextSibling;
|
|
55
|
+
} else {
|
|
56
|
+
const added = incoming.cloneNode(true);
|
|
57
|
+
current.insertBefore(added, cursor);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const child of children) if (!used.has(child)) child.remove();
|
|
61
|
+
}
|
|
62
|
+
const busy = () => Date.now() < interactingUntil || !window.getSelection()?.isCollapsed;
|
|
63
|
+
async function poll() {
|
|
64
|
+
if (paused || document.hidden || busy()) {
|
|
65
|
+
timer = setTimeout(poll, interval);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(location.pathname, { cache: 'no-store', signal: AbortSignal.timeout(10000) });
|
|
70
|
+
if (!response.ok) throw Error('read');
|
|
71
|
+
const page = new DOMParser().parseFromString(await response.text(), 'text/html');
|
|
72
|
+
const next = page.querySelector('main');
|
|
73
|
+
if (!next) throw Error('read');
|
|
74
|
+
if (paused || document.hidden || busy()) { timer = setTimeout(poll, interval); return; }
|
|
75
|
+
const focused = document.activeElement;
|
|
76
|
+
if (focused && focused !== document.body && !focused.closest('[data-live-controls]')) {
|
|
77
|
+
const context = focused.closest('[id], [data-key]');
|
|
78
|
+
const replacement = context && [...next.querySelectorAll('[id], [data-key]')].find(element => key(element) === key(context));
|
|
79
|
+
// A completed attempt may move into collapsed history. Keep the current
|
|
80
|
+
// reading snapshot until focus leaves it, rather than removing the
|
|
81
|
+
// focused control or presenting a stale duplicate as current work.
|
|
82
|
+
if (!replacement || context.closest('section')?.id !== replacement.closest('section')?.id) {
|
|
83
|
+
status.textContent = 'New update ready · Finish reading to apply';
|
|
84
|
+
timer = setTimeout(poll, interval);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const anchor = [...root.querySelectorAll('[id], [data-key]')].find(element => element.getBoundingClientRect().top >= 0 && element.getBoundingClientRect().top < window.innerHeight && element.getBoundingClientRect().height > 0);
|
|
89
|
+
const offset = anchor?.getBoundingClientRect().top;
|
|
90
|
+
const scroll = window.scrollY;
|
|
91
|
+
patch(root, next);
|
|
92
|
+
if (focused?.isConnected && document.activeElement !== focused) focused.focus({ preventScroll: true });
|
|
93
|
+
if (anchor?.isConnected && offset !== undefined) window.scrollBy(0, anchor.getBoundingClientRect().top - offset);
|
|
94
|
+
else window.scrollTo(0, scroll);
|
|
95
|
+
status.textContent = next.dataset.live === 'true' ? 'Live' : 'Saved observations';
|
|
96
|
+
if (next.dataset.live !== 'true') { toggle.hidden = true; return; }
|
|
97
|
+
} catch { status.textContent = 'Update unavailable · Retrying'; }
|
|
98
|
+
timer = setTimeout(poll, interval);
|
|
99
|
+
}
|
|
100
|
+
timer = setTimeout(poll, interval);
|
|
101
|
+
window.addEventListener('pagehide', () => clearTimeout(timer), { once: true });
|
|
102
|
+
})();`;
|