@kylecheng3146/agent-ops 0.5.3 → 0.6.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.
Files changed (93) hide show
  1. package/README.md +48 -0
  2. package/dist/packages/cli/src/args.js +165 -2
  3. package/dist/packages/cli/src/bin.js +64 -17
  4. package/dist/packages/cli/src/cli.js +33 -6
  5. package/dist/packages/cli/src/commands/advance.js +23 -3
  6. package/dist/packages/cli/src/commands/review-show.js +1 -1
  7. package/dist/packages/cli/src/commands/review.js +100 -9
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +61 -17
  10. package/dist/packages/cli/src/commands/verify.js +1 -0
  11. package/dist/packages/cli/src/context.js +63 -1
  12. package/dist/packages/cli/src/hook-process.js +31 -1
  13. package/dist/packages/cli/src/owned-run-step.js +65 -0
  14. package/dist/packages/cli/src/parallel-deps.js +18 -5
  15. package/dist/packages/cli/src/run-deps.js +314 -0
  16. package/dist/packages/cli/src/run-entry.js +943 -0
  17. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  18. package/dist/packages/cli/src/run-step-entry.js +58 -0
  19. package/dist/runtime/src/config/explain.js +6 -0
  20. package/dist/runtime/src/config/hash.js +1 -1
  21. package/dist/runtime/src/config/merge.js +9 -1
  22. package/dist/runtime/src/contracts.js +2 -2
  23. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  24. package/dist/runtime/src/install/doctor.js +2 -0
  25. package/dist/runtime/src/parallel/finish.js +85 -22
  26. package/dist/runtime/src/parallel/guard.js +5 -1
  27. package/dist/runtime/src/parallel/integrate.js +288 -10
  28. package/dist/runtime/src/parallel/manage.js +4 -1
  29. package/dist/runtime/src/parallel/receipt.js +71 -5
  30. package/dist/runtime/src/parallel/service.js +41 -1
  31. package/dist/runtime/src/review/attestation.js +36 -11
  32. package/dist/runtime/src/review/execute.js +24 -1
  33. package/dist/runtime/src/review/packet.js +35 -1
  34. package/dist/runtime/src/review/runner.js +12 -1
  35. package/dist/runtime/src/review/scope.js +18 -1
  36. package/dist/runtime/src/review/slots.js +35 -10
  37. package/dist/runtime/src/run/authorization.js +60 -0
  38. package/dist/runtime/src/run/budget.js +30 -0
  39. package/dist/runtime/src/run/controls.js +139 -0
  40. package/dist/runtime/src/run/convergence.js +177 -0
  41. package/dist/runtime/src/run/hosts/claude.js +180 -0
  42. package/dist/runtime/src/run/hosts/codex.js +279 -0
  43. package/dist/runtime/src/run/hosts/index.js +4 -0
  44. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  45. package/dist/runtime/src/run/hosts/types.js +1 -0
  46. package/dist/runtime/src/run/hosts/util.js +136 -0
  47. package/dist/runtime/src/run/integration.js +204 -0
  48. package/dist/runtime/src/run/macOS.js +189 -0
  49. package/dist/runtime/src/run/ownership.js +22 -0
  50. package/dist/runtime/src/run/planning.js +25 -0
  51. package/dist/runtime/src/run/policy-transition.js +1076 -0
  52. package/dist/runtime/src/run/policy.js +176 -0
  53. package/dist/runtime/src/run/ratchet.js +71 -0
  54. package/dist/runtime/src/run/scheduler.js +260 -0
  55. package/dist/runtime/src/run/service.js +425 -0
  56. package/dist/runtime/src/run/supervisor.js +635 -0
  57. package/dist/runtime/src/run/transport.js +159 -0
  58. package/dist/runtime/src/run/types.js +1 -0
  59. package/dist/runtime/src/run/usage.js +81 -0
  60. package/dist/runtime/src/run/verification.js +64 -0
  61. package/dist/runtime/src/run/worker-gate.js +48 -0
  62. package/dist/runtime/src/schema/acceptance.js +91 -0
  63. package/dist/runtime/src/schema/validate.js +67 -7
  64. package/dist/runtime/src/task/completion.js +14 -2
  65. package/dist/runtime/src/task/contract.js +17 -0
  66. package/dist/runtime/src/task/pin-finding.js +81 -0
  67. package/dist/runtime/src/task/service.js +316 -13
  68. package/dist/runtime/src/task/store.js +57 -7
  69. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  70. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  71. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  72. package/dist/runtime/src/verify/adapters/index.js +5 -0
  73. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  74. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  75. package/dist/runtime/src/verify/adapters/node.js +254 -0
  76. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  77. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  78. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  79. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  80. package/dist/runtime/src/verify/evidence.js +5 -2
  81. package/dist/runtime/src/verify/service.js +24 -8
  82. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  83. package/dist/runtime/src/verify/spawn.js +13 -2
  84. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  85. package/docs/en/spec/loop-engineering.md +49 -0
  86. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  87. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  88. package/package.json +1 -1
  89. package/schemas/config.schema.json +221 -19
  90. package/schemas/evidence.schema.json +129 -6
  91. package/schemas/task.schema.json +265 -6
  92. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  93. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -0,0 +1,254 @@
1
+ import { resolve } from "node:path";
2
+ import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
3
+ import { attempts, hasRetry, lower, object, result, run, string, terminal, terminalEvidence } from "./shared.js";
4
+ function eventsFromInput(input) {
5
+ if (typeof input !== "string")
6
+ return { events: input, diagnostics: [] };
7
+ const document = parseJsonDocument(input);
8
+ if (Array.isArray(document))
9
+ return { events: document, diagnostics: [] };
10
+ const lines = parseJsonLines(input);
11
+ if (lines.values.length > 0 || lines.invalidLines.length > 0) {
12
+ return {
13
+ events: lines.values,
14
+ diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`)
15
+ };
16
+ }
17
+ return document === undefined
18
+ ? { events: [], diagnostics: ["malformed-output"] }
19
+ : { events: Array.isArray(document) ? document : [document], diagnostics: [] };
20
+ }
21
+ function eventData(value) {
22
+ return object(value.data) ?? value;
23
+ }
24
+ function eventType(value) {
25
+ return lower(value.type ?? value.event ?? value.kind);
26
+ }
27
+ function checkId(data) {
28
+ const file = string(data.file ?? data.filePath ?? data.filename);
29
+ const ancestors = Array.isArray(data.ancestorNames)
30
+ ? data.ancestorNames.filter((item) => typeof item === "string")
31
+ : Array.isArray(data.nesting)
32
+ ? data.nesting.filter((item) => typeof item === "string")
33
+ : Array.isArray(data.ancestorTitles)
34
+ ? data.ancestorTitles.filter((item) => typeof item === "string")
35
+ : [];
36
+ const name = string(data.name ?? data.title ?? data.fullName);
37
+ if (name === undefined)
38
+ return undefined;
39
+ const line = typeof data.line === "number" && Number.isSafeInteger(data.line) ? data.line : undefined;
40
+ const column = typeof data.column === "number" && Number.isSafeInteger(data.column) ? data.column : undefined;
41
+ try {
42
+ return stableCheckId([
43
+ file ?? "<node-test>",
44
+ ...ancestors,
45
+ name,
46
+ ...(line === undefined || column === undefined ? [] : [`@${line}:${column}`])
47
+ ]);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ function assertionFailure(data) {
54
+ if (nonAssertionFailure(data) !== undefined)
55
+ return false;
56
+ if (data.assertion === true || data.isAssertion === true)
57
+ return true;
58
+ const explicit = lower(data.failureClass ?? data.failureKind ?? data.errorType);
59
+ if (explicit === "assertion" || explicit === "assertion-failed" || explicit === "test-assertion") {
60
+ return true;
61
+ }
62
+ const details = object(data.details);
63
+ const error = object(data.error) ?? object(details?.error);
64
+ const cause = object(error?.cause);
65
+ const errorName = lower(error?.name ?? error?.type);
66
+ const errorCode = lower(error?.code);
67
+ const causeName = lower(cause?.name ?? cause?.type);
68
+ const causeCode = lower(cause?.code);
69
+ return errorName === "assertionerror" || errorCode === "err_assertion" ||
70
+ causeName === "assertionerror" || causeCode === "err_assertion";
71
+ }
72
+ function nonAssertionFailure(data) {
73
+ const details = object(data.details);
74
+ const kind = lower(data.kind ?? data.scope ?? data.type ?? details?.type);
75
+ if (kind === "hook")
76
+ return "hook-error";
77
+ if (kind === "fixture")
78
+ return "fixture-error";
79
+ if (kind === "collection" || kind === "root" || kind === "suite")
80
+ return "collection-error";
81
+ if (data.timeout === true)
82
+ return "timeout";
83
+ const error = object(data.error) ?? object(details?.error);
84
+ const nativeFailure = lower(error?.failureType);
85
+ if (nativeFailure === "hookfailed")
86
+ return "hook-error";
87
+ if (nativeFailure === "testtimeoutfailure")
88
+ return "timeout";
89
+ if (nativeFailure === "cancelledbyparent" || nativeFailure === "testaborted")
90
+ return "cancelled";
91
+ if (nativeFailure !== undefined && nativeFailure !== "testcodefailure")
92
+ return "infrastructure-error";
93
+ const explicit = lower(data.failureClass ?? data.failureKind ?? data.errorType);
94
+ if (explicit !== undefined && !["assertion", "assertion-failed", "test-assertion"].includes(explicit)) {
95
+ return explicit;
96
+ }
97
+ return undefined;
98
+ }
99
+ function eventEvidence(type, data, id) {
100
+ const sequence = string(data.sequence ?? data.id ?? data.testId);
101
+ return [`node:${type}:${id}`, ...(sequence === undefined ? [] : [`sequence:${sequence}`])];
102
+ }
103
+ /** Collect node:test TestsStream/custom reporter events without parsing stdout text. */
104
+ export function collectNodeAcceptance(input, options) {
105
+ const parsed = eventsFromInput(input);
106
+ const results = [];
107
+ const seenCheckIds = new Set();
108
+ const duplicateCheckIds = new Set();
109
+ const pendingComplete = new Map();
110
+ const diagnostics = [...parsed.diagnostics];
111
+ let completed = false;
112
+ let failedSummary = false;
113
+ let completionEvidence = [];
114
+ // start/pass/fail follow definition order, even for concurrent suites.
115
+ // Execution-ordered complete events cannot supply unambiguous lineage.
116
+ const stacks = new Map();
117
+ const append = (id, data, evidence, attemptCount, passed) => {
118
+ const failure = hasRetry(data) ? "retry" : (data.skip === true || typeof data.skip === "string") ? "skipped"
119
+ : (data.todo === true || typeof data.todo === "string") ? "todo" : nonAssertionFailure(data);
120
+ results.push(failure !== undefined
121
+ ? result(options, "node:test", id, "UNKNOWN", evidence, failure, attemptCount)
122
+ : passed === true ? result(options, "node:test", id, "PASS", evidence, "none", attemptCount)
123
+ : passed === false && assertionFailure(data)
124
+ ? result(options, "node:test", id, "FAIL", evidence, "assertion-failed", attemptCount)
125
+ : result(options, "node:test", id, "UNKNOWN", evidence, passed === undefined ? "incomplete-output" : "infrastructure-error", attemptCount));
126
+ if (seenCheckIds.has(id))
127
+ duplicateCheckIds.add(id);
128
+ seenCheckIds.add(id);
129
+ };
130
+ for (const raw of parsed.events) {
131
+ const value = object(raw);
132
+ if (value === undefined) {
133
+ diagnostics.push("malformed-event");
134
+ continue;
135
+ }
136
+ const type = eventType(value);
137
+ const rawData = eventData(value);
138
+ const depth = rawData.nesting;
139
+ const rawFile = string(rawData.file);
140
+ const rawName = string(rawData.name);
141
+ let ancestors;
142
+ if (typeof depth === "number" && Number.isSafeInteger(depth) && depth >= 0 && depth <= 128 && rawFile !== undefined) {
143
+ const stack = stacks.get(rawFile) ?? [];
144
+ if (stack.length >= depth && !Array.isArray(rawData.ancestorNames) && !Array.isArray(rawData.ancestorTitles)) {
145
+ ancestors = stack.slice(0, depth);
146
+ }
147
+ if (type === "test:start" && rawName !== undefined) {
148
+ stack.length = depth;
149
+ stack.push(rawName);
150
+ stacks.set(rawFile, stack);
151
+ }
152
+ }
153
+ const data = ancestors === undefined ? rawData : { ...rawData, ancestorNames: ancestors };
154
+ if (terminal(value) || (type === "test:complete" &&
155
+ (value.completed === true || data.completed === true || value.terminal === true))) {
156
+ completed = true;
157
+ completionEvidence = [terminalEvidence(value, options.executionId)];
158
+ continue;
159
+ }
160
+ if (type === undefined) {
161
+ diagnostics.push("missing-event-type");
162
+ continue;
163
+ }
164
+ if (type === "test:summary" && data.success === false)
165
+ failedSummary = true;
166
+ if (type === "test:enqueue" || type === "test:dequeue" ||
167
+ type === "test:start" || type === "test:diagnostic" || type === "test:summary" ||
168
+ type === "test:coverage" || type === "test:stdout" || type === "test:stderr") {
169
+ if (type === "test:diagnostic" && (data.error !== undefined || data.failureClass !== undefined)) {
170
+ diagnostics.push(`node-diagnostic:${string(data.failureClass) ?? "infrastructure-error"}`);
171
+ }
172
+ continue;
173
+ }
174
+ const details = object(data.details);
175
+ const file = string(data.file ?? data.filePath);
176
+ const name = string(data.name);
177
+ const summary = lower(data.type ?? details?.type) === "suite" ||
178
+ (file !== undefined && name !== undefined && resolve(name) === resolve(file));
179
+ if (summary) {
180
+ const error = object(details?.error);
181
+ // A failed child already has its own result. Independent suite/file
182
+ // failures still poison the run; summaries never become check evidence.
183
+ if (error !== undefined && lower(error.failureType) !== "subtestsfailed") {
184
+ diagnostics.push(`node-summary:${nonAssertionFailure(data) ?? "infrastructure-error"}`);
185
+ }
186
+ continue;
187
+ }
188
+ if (type === "test:complete" && typeof data.nesting === "number")
189
+ continue;
190
+ if (type === "test:plan" || type === "plan") {
191
+ const count = data.count;
192
+ const nesting = typeof data.nesting === "number" ? data.nesting : 0;
193
+ if (nesting === 0 && typeof count === "number" && Number.isSafeInteger(count) && count >= 0) {
194
+ completed = true;
195
+ completionEvidence = [`node:test:plan:${count}`];
196
+ }
197
+ else if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
198
+ diagnostics.push("malformed-plan");
199
+ }
200
+ continue;
201
+ }
202
+ const id = checkId(data);
203
+ if (id === undefined) {
204
+ diagnostics.push(`missing-check-id:${type}`);
205
+ continue;
206
+ }
207
+ const evidence = eventEvidence(type, data, id);
208
+ const attemptCount = attempts(data.attempts, hasRetry(data) ? 2 : 1);
209
+ if (type === "test:complete" || type === "test-complete") {
210
+ if (!seenCheckIds.has(id)) {
211
+ pendingComplete.set(id, { data, type, evidence, attempts: attemptCount });
212
+ }
213
+ continue;
214
+ }
215
+ if (hasRetry(data)) {
216
+ append(id, data, evidence, attemptCount);
217
+ continue;
218
+ }
219
+ if (type === "test:skip" || type === "test-skip" || type === "skip") {
220
+ append(id, { ...data, skip: true }, evidence, attemptCount);
221
+ continue;
222
+ }
223
+ if (type === "test:todo" || type === "test-todo" || type === "todo") {
224
+ append(id, { ...data, todo: true }, evidence, attemptCount);
225
+ continue;
226
+ }
227
+ if (type === "test:pass" || type === "test-pass" || type === "pass" ||
228
+ (type === "test:result" && lower(data.status) === "passed")) {
229
+ append(id, data, evidence, attemptCount, true);
230
+ continue;
231
+ }
232
+ if (type === "test:fail" || type === "test-fail" || type === "fail" ||
233
+ (type === "test:result" && lower(data.status) === "failed")) {
234
+ append(id, data, evidence, attemptCount, false);
235
+ continue;
236
+ }
237
+ diagnostics.push(`unsupported-event:${type}`);
238
+ }
239
+ for (const [id, pending] of pendingComplete.entries()) {
240
+ if (seenCheckIds.has(id))
241
+ continue;
242
+ const details = object(pending.data.details);
243
+ append(id, pending.data, pending.evidence, pending.attempts, typeof details?.passed === "boolean" ? details.passed : undefined);
244
+ }
245
+ for (let i = 0; i < results.length; i++) {
246
+ if (duplicateCheckIds.has(results[i].checkId)) {
247
+ results[i] = { ...results[i], status: "UNKNOWN", failureClass: "duplicate-check" };
248
+ }
249
+ }
250
+ if (failedSummary && results.every(result => result.status === "PASS")) {
251
+ diagnostics.push("unexplained-summary-failure");
252
+ }
253
+ return run(options, "node:test", results, completed, completionEvidence, diagnostics);
254
+ }
@@ -0,0 +1,188 @@
1
+ import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
2
+ import { array, hasRetry, lower, number, object, result, run, string, terminalEvidence } from "./shared.js";
3
+ function inputEvents(input) {
4
+ if (Array.isArray(input))
5
+ return { events: input, diagnostics: [] };
6
+ if (typeof input !== "string") {
7
+ const value = object(input);
8
+ if (value === undefined)
9
+ return { events: [], diagnostics: ["malformed-output"] };
10
+ return { events: [value], diagnostics: [] };
11
+ }
12
+ const document = parseJsonDocument(input);
13
+ if (Array.isArray(document))
14
+ return { events: document, diagnostics: [] };
15
+ if (object(document) !== undefined) {
16
+ const value = object(document);
17
+ return { events: [value], diagnostics: [] };
18
+ }
19
+ const lines = parseJsonLines(input);
20
+ return {
21
+ events: lines.values,
22
+ diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`)
23
+ };
24
+ }
25
+ function eventType(value) {
26
+ return lower(value.type ?? value.event ?? value.kind);
27
+ }
28
+ function nodeId(value) {
29
+ return string(value.nodeid ?? value.nodeId ?? value.testId ?? value.name ?? value.title);
30
+ }
31
+ function phaseValue(value) {
32
+ return object(value.report) ?? object(value.result) ?? value;
33
+ }
34
+ function phaseName(value) {
35
+ return lower(value.when ?? value.phase ?? value.stage) ?? "unknown";
36
+ }
37
+ function outcome(value) {
38
+ return lower(value.outcome ?? value.status ?? value.state);
39
+ }
40
+ function assertionFailure(value) {
41
+ if (value.assertion === true || value.isAssertion === true)
42
+ return true;
43
+ const type = lower(value.errorType ?? value.exceptionType ?? value.errorName);
44
+ return type === "assertionerror" || type === "assertion-error";
45
+ }
46
+ function addRecord(grouped, raw, diagnostics) {
47
+ const id = nodeId(raw);
48
+ if (id === undefined) {
49
+ diagnostics.push("missing-check-id");
50
+ return;
51
+ }
52
+ let stable;
53
+ try {
54
+ stable = stableCheckId([id]);
55
+ }
56
+ catch {
57
+ diagnostics.push("missing-check-id");
58
+ return;
59
+ }
60
+ const phase = phaseValue(raw) ?? raw;
61
+ const evidenceId = string(raw.id ?? raw.reportId ?? raw.nodeid ?? raw.nodeId) ?? stable;
62
+ const attemptCount = number(raw.attempts ?? phase.attempts);
63
+ const list = grouped.get(stable) ?? [];
64
+ list.push({
65
+ phase: phaseName(phase),
66
+ outcome: outcome(phase),
67
+ assertion: assertionFailure(phase),
68
+ xfail: phase.wasxfail === true ||
69
+ (typeof phase.wasxfail === "string" && phase.wasxfail.length > 0) ||
70
+ phase.xfail === true || outcome(phase) === "xfailed" || outcome(phase) === "xpassed",
71
+ retry: hasRetry(raw) || hasRetry(phase),
72
+ attempts: attemptCount !== undefined && attemptCount > 0 ? attemptCount : null,
73
+ evidence: `pytest:${stable}:${evidenceId}`,
74
+ infrastructure: (phase.error !== undefined && phase.error !== null) ||
75
+ (phase.crash !== undefined && phase.crash !== null && phase.crash !== false)
76
+ });
77
+ grouped.set(stable, list);
78
+ }
79
+ function appendReportTests(report, grouped, diagnostics) {
80
+ const tests = array(report.tests) ?? [];
81
+ for (const raw of tests) {
82
+ const test = object(raw);
83
+ if (test === undefined) {
84
+ diagnostics.push("malformed-test");
85
+ continue;
86
+ }
87
+ const call = object(test.call);
88
+ const setup = object(test.setup);
89
+ const teardown = object(test.teardown);
90
+ if (setup !== undefined)
91
+ addRecord(grouped, { ...test, ...setup, phase: "setup" }, diagnostics);
92
+ if (call !== undefined)
93
+ addRecord(grouped, { ...test, ...call, phase: "call" }, diagnostics);
94
+ else
95
+ addRecord(grouped, test, diagnostics);
96
+ if (teardown !== undefined)
97
+ addRecord(grouped, { ...test, ...teardown, phase: "teardown" }, diagnostics);
98
+ }
99
+ }
100
+ /** Collect a structured pytest hook stream or pytest-json-report-like document. */
101
+ export function collectPytestAcceptance(input, options) {
102
+ const parsed = inputEvents(input);
103
+ const grouped = new Map();
104
+ const diagnostics = [...parsed.diagnostics];
105
+ let completed = false;
106
+ let completionEvidence = [];
107
+ for (const raw of parsed.events) {
108
+ const value = object(raw);
109
+ if (value === undefined) {
110
+ diagnostics.push("malformed-event");
111
+ continue;
112
+ }
113
+ if (Array.isArray(value.tests)) {
114
+ appendReportTests(value, grouped, diagnostics);
115
+ if (value.completed !== false && value.interrupted !== true) {
116
+ completed = true;
117
+ completionEvidence = ["pytest:report:complete"];
118
+ }
119
+ continue;
120
+ }
121
+ const type = eventType(value);
122
+ if (["session-finished", "session-finish", "complete", "completed", "finished", "run-complete"].includes(type ?? "") &&
123
+ value.completed !== false && value.interrupted !== true) {
124
+ completed = true;
125
+ completionEvidence = [terminalEvidence(value, options.executionId)];
126
+ continue;
127
+ }
128
+ if (type === "session-error" || type === "internal-error" || type === "collection-error") {
129
+ diagnostics.push(`pytest:${type}`);
130
+ continue;
131
+ }
132
+ if (type === "test-report" || type === "runtest" || type === "test") {
133
+ addRecord(grouped, value, diagnostics);
134
+ continue;
135
+ }
136
+ diagnostics.push(`unsupported-event:${type ?? "unknown"}`);
137
+ }
138
+ const results = [];
139
+ for (const [id, records] of grouped.entries()) {
140
+ const refs = records.map((record) => record.evidence);
141
+ const attemptsCount = records.reduce((max, record) => {
142
+ if (max === null || record.attempts === null)
143
+ return null;
144
+ return Math.max(max, record.attempts);
145
+ }, 1);
146
+ if (records.some((record) => record.retry || (record.attempts !== null && record.attempts > 1))) {
147
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "retry", attemptsCount === null ? null : Math.max(attemptsCount, 2)));
148
+ continue;
149
+ }
150
+ const setupOrTeardownFailure = records.some((record) => (record.phase === "setup" || record.phase === "teardown") &&
151
+ record.outcome !== "passed" && record.outcome !== "pass");
152
+ if (setupOrTeardownFailure || records.some((record) => record.infrastructure)) {
153
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "fixture-error", attemptsCount));
154
+ continue;
155
+ }
156
+ const phases = records.map(record => record.phase);
157
+ if (new Set(phases).size !== phases.length) {
158
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "duplicate-check", attemptsCount));
159
+ continue;
160
+ }
161
+ const call = records.find((record) => record.phase === "call");
162
+ if (phases.join(",") !== "setup,call,teardown" || call === undefined) {
163
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "incomplete-output", attemptsCount));
164
+ continue;
165
+ }
166
+ if (call.xfail) {
167
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "xfail", attemptsCount));
168
+ }
169
+ else if (call.outcome === "passed" || call.outcome === "pass") {
170
+ results.push(result(options, "pytest", id, "PASS", refs, "none", attemptsCount));
171
+ }
172
+ else if (call.outcome === "failed" || call.outcome === "fail") {
173
+ results.push(call.assertion
174
+ ? result(options, "pytest", id, "FAIL", refs, "assertion-failed", attemptsCount)
175
+ : result(options, "pytest", id, "UNKNOWN", refs, "infrastructure-error", attemptsCount));
176
+ }
177
+ else if (call.outcome === "skipped" || call.outcome === "skip") {
178
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "skipped", attemptsCount));
179
+ }
180
+ else if (call.outcome === "xfailed" || call.outcome === "xpassed") {
181
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "xfail", attemptsCount));
182
+ }
183
+ else {
184
+ results.push(result(options, "pytest", id, "UNKNOWN", refs, "incomplete-output", attemptsCount));
185
+ }
186
+ }
187
+ return run(options, "pytest", results, completed, completionEvidence, diagnostics);
188
+ }
@@ -0,0 +1,84 @@
1
+ import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
2
+ import { attempts, lower, object, result, run, string, terminal, terminalEvidence } from "./shared.js";
3
+ function inputEvents(input) {
4
+ if (Array.isArray(input))
5
+ return { events: input, diagnostics: [] };
6
+ if (typeof input !== "string") {
7
+ const value = object(input);
8
+ return value === undefined
9
+ ? { events: [], diagnostics: ["malformed-output"] }
10
+ : { events: [value], diagnostics: [] };
11
+ }
12
+ const document = parseJsonDocument(input);
13
+ if (Array.isArray(document))
14
+ return { events: document, diagnostics: [] };
15
+ if (object(document) !== undefined)
16
+ return { events: [document], diagnostics: [] };
17
+ const lines = parseJsonLines(input);
18
+ return {
19
+ events: lines.values,
20
+ diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`)
21
+ };
22
+ }
23
+ function status(value) {
24
+ const normalized = lower(value);
25
+ if (normalized === "pass" || normalized === "passed" || normalized === "ok")
26
+ return "PASS";
27
+ if (normalized === "fail" || normalized === "failed")
28
+ return "FAIL";
29
+ return "UNKNOWN";
30
+ }
31
+ /**
32
+ * Collect the stdlib harness' JSON-lines output. Native libtest text without
33
+ * this explicit protocol is intentionally unsupported instead of guessed.
34
+ */
35
+ export function collectRustAcceptance(input, options) {
36
+ const parsed = inputEvents(input);
37
+ const results = [];
38
+ const diagnostics = [...parsed.diagnostics];
39
+ let completed = false;
40
+ let completionEvidence = [];
41
+ for (const raw of parsed.events) {
42
+ const value = object(raw);
43
+ if (value === undefined) {
44
+ diagnostics.push("malformed-event");
45
+ continue;
46
+ }
47
+ const type = lower(value.type ?? value.event ?? value.kind);
48
+ if (terminal(value) || type === "completed" || type === "complete") {
49
+ completed = true;
50
+ completionEvidence = [terminalEvidence(value, options.executionId)];
51
+ continue;
52
+ }
53
+ if (type !== "check" && type !== "assertion" && type !== "result") {
54
+ diagnostics.push(`unsupported-event:${type ?? "unknown"}`);
55
+ continue;
56
+ }
57
+ const rawId = string(value.checkId ?? value.testId ?? value.name);
58
+ if (rawId === undefined) {
59
+ diagnostics.push("missing-check-id");
60
+ continue;
61
+ }
62
+ let checkId;
63
+ try {
64
+ checkId = stableCheckId([rawId]);
65
+ }
66
+ catch {
67
+ diagnostics.push("missing-check-id");
68
+ continue;
69
+ }
70
+ const checkStatus = status(value.status ?? value.outcome ?? value.state);
71
+ const failureClass = lower(value.failureClass ?? value.failureKind) ??
72
+ (checkStatus === "PASS" ? "none" : checkStatus === "FAIL" ? "infrastructure-error" : "unsupported-output");
73
+ const evidence = Array.isArray(value.evidence)
74
+ ? value.evidence.filter((item) => typeof item === "string")
75
+ : [`rust:${checkId}`];
76
+ try {
77
+ results.push(result(options, "rust-harness", checkId, checkStatus, evidence, failureClass, attempts(value.attempts, null)));
78
+ }
79
+ catch {
80
+ diagnostics.push(`malformed-result:${checkId}`);
81
+ }
82
+ }
83
+ return run(options, "rust-harness", results, completed, completionEvidence, diagnostics);
84
+ }
@@ -0,0 +1,82 @@
1
+ import { ACCEPTANCE_FAILURE, makeAcceptanceCheck, makeAcceptanceRun } from "../acceptance-protocol.js";
2
+ export function object(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value)
4
+ ? value
5
+ : undefined;
6
+ }
7
+ export function string(value) {
8
+ return typeof value === "string" && value.length > 0 ? value : undefined;
9
+ }
10
+ export function number(value) {
11
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
12
+ }
13
+ export function array(value) {
14
+ return Array.isArray(value) ? value : undefined;
15
+ }
16
+ export function attempts(value, fallback = 1) {
17
+ const parsed = number(value);
18
+ return parsed === undefined ? fallback : parsed;
19
+ }
20
+ export function result(options, framework, checkId, status, evidence, failureClass = status === "PASS" ? "none" : ACCEPTANCE_FAILURE.infrastructure, attemptCount = 1) {
21
+ return makeAcceptanceCheck({
22
+ executionId: options.executionId,
23
+ framework,
24
+ frameworkVersion: options.frameworkVersion,
25
+ checkId,
26
+ phase: options.phase,
27
+ status,
28
+ failureClass,
29
+ attempts: attemptCount,
30
+ evidence
31
+ });
32
+ }
33
+ export function unknownResult(options, framework, checkId, failureClass, evidence = ["adapter:unknown"], attemptCount = 1) {
34
+ return result(options, framework, checkId, "UNKNOWN", evidence, failureClass, attemptCount);
35
+ }
36
+ export function run(options, framework, results, completed, completionEvidence, diagnostics = []) {
37
+ return makeAcceptanceRun({
38
+ executionId: options.executionId,
39
+ framework,
40
+ frameworkVersion: options.frameworkVersion,
41
+ phase: options.phase,
42
+ results,
43
+ completed,
44
+ completionEvidence,
45
+ diagnostics
46
+ });
47
+ }
48
+ export function lower(value) {
49
+ const text = string(value);
50
+ return text?.toLowerCase();
51
+ }
52
+ export function hasRetry(value) {
53
+ if (value.retry === true || value.retried === true)
54
+ return true;
55
+ const retryCount = number(value.retryCount);
56
+ if (retryCount !== undefined && retryCount > 0)
57
+ return true;
58
+ const repeatCount = number(value.repeatCount);
59
+ if (repeatCount !== undefined && repeatCount > 0)
60
+ return true;
61
+ const rerun = number(value.rerun);
62
+ if (rerun !== undefined && rerun > 0)
63
+ return true;
64
+ const invocations = number(value.invocations);
65
+ if (invocations !== undefined && invocations > 1)
66
+ return true;
67
+ if (Array.isArray(value.retryReasons) && value.retryReasons.length > 0)
68
+ return true;
69
+ const attemptsValue = number(value.attempts);
70
+ return attemptsValue !== undefined && attemptsValue > 1;
71
+ }
72
+ export function terminal(value) {
73
+ if (value.completed === true || value.complete === true || value.terminal === true)
74
+ return true;
75
+ const type = lower(value.type ?? value.event);
76
+ return type === "complete" || type === "completed" || type === "finished" ||
77
+ type === "session-finished" || type === "run-complete";
78
+ }
79
+ export function terminalEvidence(value, fallback) {
80
+ const id = string(value.executionId) ?? string(value.id) ?? fallback;
81
+ return `terminal:${id}`;
82
+ }