wowbagger 0.1.0-alpha.1
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/CHANGELOG.md +94 -0
- package/LICENSE +201 -0
- package/README.md +464 -0
- package/adapters/claude-code/entrypoint.js +19 -0
- package/adapters/claude-code/wowbagger-adapter.json +25 -0
- package/adapters/codex/entrypoint.js +11 -0
- package/adapters/codex/wowbagger-adapter.json +25 -0
- package/adapters/opencode/entrypoint.js +11 -0
- package/adapters/opencode/wowbagger-adapter.json +25 -0
- package/bin/wowbagger.js +7 -0
- package/package.json +51 -0
- package/skills/wowbagger/SKILL.md +136 -0
- package/src/adapter/approval.js +135 -0
- package/src/adapter/bootstrap.js +43 -0
- package/src/adapter/context.js +34 -0
- package/src/adapter/core-probe.js +231 -0
- package/src/adapter/describe.js +383 -0
- package/src/adapter/entrypoint-main.js +335 -0
- package/src/adapter/entrypoint-path.js +103 -0
- package/src/adapter/handoff.js +124 -0
- package/src/adapter/instructions.js +106 -0
- package/src/adapter/invoke.js +294 -0
- package/src/adapter/limits.js +26 -0
- package/src/adapter/manifest.js +93 -0
- package/src/adapter/messages.js +15 -0
- package/src/adapter/paths.js +88 -0
- package/src/adapter/process-outcome.js +1116 -0
- package/src/adapter/schema-helpers.js +60 -0
- package/src/claim-capabilities.js +54 -0
- package/src/claim-coordinator.js +85 -0
- package/src/claim-journal.js +236 -0
- package/src/claim-operations.js +138 -0
- package/src/claim-publication.js +739 -0
- package/src/claim-request.js +140 -0
- package/src/claim-store.js +198 -0
- package/src/cli.js +1130 -0
- package/src/dependencies.js +3 -0
- package/src/git-reconciliation.js +62 -0
- package/src/ledger.js +296 -0
- package/src/mint.js +32 -0
- package/src/mutation.js +1979 -0
- package/src/namespace.js +35 -0
- package/src/ready.js +85 -0
- package/src/request.js +246 -0
- package/src/schema-migration.js +300 -0
- package/src/validate.js +1208 -0
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { parseLedgerItemSource } from '../ledger.js';
|
|
4
|
+
import {
|
|
5
|
+
createCandidateSource,
|
|
6
|
+
validateCreateRequest,
|
|
7
|
+
validateTransitionRequest,
|
|
8
|
+
} from '../mutation.js';
|
|
9
|
+
import { normalizeJsonValue, parseJsonRequest } from '../request.js';
|
|
10
|
+
import { validateLedger } from '../validate.js';
|
|
11
|
+
import { CORE_COMMAND_ORDER, CORE_CONTRACT_VERSION, verifyCoreProbe } from './core-probe.js';
|
|
12
|
+
import { isSafeLogicalPath } from './paths.js';
|
|
13
|
+
import { hasExactMembers, isNonNegativeSafeInteger, sameJson } from './schema-helpers.js';
|
|
14
|
+
|
|
15
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
16
|
+
const WOWBAGGER_ID = /^wb_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
17
|
+
const CONTROL_CHARACTER = /[\u0000-\u001F\u007F]/;
|
|
18
|
+
const MESSAGES = Object.freeze({
|
|
19
|
+
'mutation-outcome-unknown': 'The mutation may have been applied; inspect current state before retrying.',
|
|
20
|
+
'output-limit-exceeded': 'The core output exceeded the requested bound.',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const CORE_ERROR_EXIT_CODES = new Map([
|
|
24
|
+
['invalid-request', 2],
|
|
25
|
+
['item-not-found', 2],
|
|
26
|
+
['transition-precondition-failed', 2],
|
|
27
|
+
['patch-precondition-failed', 2],
|
|
28
|
+
['candidate-invalid', 2],
|
|
29
|
+
['ledger-invalid', 3],
|
|
30
|
+
['revision-conflict', 4],
|
|
31
|
+
['lock-held', 4],
|
|
32
|
+
['id-collision', 4],
|
|
33
|
+
['path-collision', 4],
|
|
34
|
+
['atomic-scope-required', 5],
|
|
35
|
+
['capability-unavailable', 5],
|
|
36
|
+
['operation-failed', 6],
|
|
37
|
+
['post-commit-recovery-required', 6],
|
|
38
|
+
['write-outcome-unknown', 6],
|
|
39
|
+
]);
|
|
40
|
+
const MUTATION_ERROR_STATES = new Map([
|
|
41
|
+
['post-commit-recovery-required', 'committed'],
|
|
42
|
+
['write-outcome-unknown', 'unknown'],
|
|
43
|
+
]);
|
|
44
|
+
const CORE_ERROR_CODES_BY_COMMAND = Object.freeze({
|
|
45
|
+
inspect: new Set(['invalid-request', 'item-not-found', 'ledger-invalid']),
|
|
46
|
+
create: new Set([
|
|
47
|
+
'invalid-request', 'ledger-invalid', 'lock-held', 'id-collision', 'path-collision',
|
|
48
|
+
'candidate-invalid', 'capability-unavailable', 'operation-failed',
|
|
49
|
+
'post-commit-recovery-required', 'write-outcome-unknown',
|
|
50
|
+
]),
|
|
51
|
+
transition: new Set([
|
|
52
|
+
'invalid-request', 'item-not-found', 'ledger-invalid', 'lock-held',
|
|
53
|
+
'revision-conflict', 'atomic-scope-required', 'transition-precondition-failed',
|
|
54
|
+
'candidate-invalid', 'operation-failed', 'post-commit-recovery-required',
|
|
55
|
+
'write-outcome-unknown',
|
|
56
|
+
]),
|
|
57
|
+
patch: new Set([
|
|
58
|
+
'invalid-request', 'item-not-found', 'ledger-invalid', 'lock-held',
|
|
59
|
+
'revision-conflict', 'patch-precondition-failed', 'candidate-invalid',
|
|
60
|
+
'operation-failed', 'post-commit-recovery-required', 'write-outcome-unknown',
|
|
61
|
+
]),
|
|
62
|
+
});
|
|
63
|
+
const INVALID_REQUEST_CODES = new Set([
|
|
64
|
+
'invalid-json', 'duplicate-key', 'missing-member', 'unknown-member', 'invalid-type',
|
|
65
|
+
'invalid-value', 'missing-argument', 'repeated-argument', 'unknown-argument',
|
|
66
|
+
]);
|
|
67
|
+
const TRANSITION_ISSUE_MESSAGES = Object.freeze({
|
|
68
|
+
'date-before-created': 'Transition date must not be earlier than the current created date.',
|
|
69
|
+
'date-before-updated': 'Transition date must not be earlier than the current updated date.',
|
|
70
|
+
'invalid-edge': 'The requested lifecycle edge is not allowed for this item.',
|
|
71
|
+
'live-dependencies': 'Completion requires an empty depends_on list.',
|
|
72
|
+
'nonterminal-children': 'Epic completion requires every direct child to be done or killed.',
|
|
73
|
+
});
|
|
74
|
+
const TRANSITION_ISSUE_FIELDS = Object.freeze({
|
|
75
|
+
'date-before-created': 'date',
|
|
76
|
+
'date-before-updated': 'date',
|
|
77
|
+
'invalid-edge': 'to_status',
|
|
78
|
+
'live-dependencies': 'depends_on',
|
|
79
|
+
'nonterminal-children': 'parent',
|
|
80
|
+
});
|
|
81
|
+
const PATCH_ISSUE_MESSAGES = Object.freeze({
|
|
82
|
+
'date-before-created': 'Patch date must not be earlier than the current created date.',
|
|
83
|
+
'date-before-updated': 'Patch date must not be earlier than the current updated date.',
|
|
84
|
+
});
|
|
85
|
+
const TRANSITION_BLOCKER_FIELDS = Object.freeze({
|
|
86
|
+
'dependent-cleanup': 'depends_on',
|
|
87
|
+
'dependent-disposition': 'depends_on',
|
|
88
|
+
'child-disposition': 'parent',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function error(code, details) {
|
|
92
|
+
return { code, message: MESSAGES[code] ?? `The adapter refused the operation (${code}).`, details };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function plainObject(value) {
|
|
96
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function decodeCanonicalBase64(value) {
|
|
100
|
+
if (typeof value !== 'string' || value.includes('\n') || value.includes('\r')) return null;
|
|
101
|
+
const bytes = Buffer.from(value, 'base64');
|
|
102
|
+
return bytes.toString('base64') === value ? bytes : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decodeUtf8(bytes) {
|
|
106
|
+
try {
|
|
107
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isCalendarDate(value) {
|
|
114
|
+
if (typeof value !== 'string') return false;
|
|
115
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
116
|
+
if (!match) return false;
|
|
117
|
+
const year = Number(match[1]);
|
|
118
|
+
const month = Number(match[2]);
|
|
119
|
+
const day = Number(match[3]);
|
|
120
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
121
|
+
const monthLengths = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
|
|
122
|
+
31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
123
|
+
return day <= monthLengths[month - 1];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isCoreRfc3339Utc(value) {
|
|
127
|
+
if (typeof value !== 'string') return false;
|
|
128
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z$/.exec(value);
|
|
129
|
+
return match !== null && isCalendarDate(match[1])
|
|
130
|
+
&& Number(match[2]) <= 23 && Number(match[3]) <= 59 && Number(match[4]) <= 59;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function nonEmptyTrimmedString(value) {
|
|
134
|
+
return typeof value === 'string' && value.trim().length > 0 && !CONTROL_CHARACTER.test(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function nonEmptyControlFreeString(value) {
|
|
138
|
+
return typeof value === 'string' && value.length > 0 && !CONTROL_CHARACTER.test(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isSafeLedgerDisplayPath(value) {
|
|
142
|
+
return isSafeLogicalPath(value) && value !== '.' && value.endsWith('.md');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isSafeArtifactPath(value) {
|
|
146
|
+
return isSafeLogicalPath(value) && value !== '.' && [...value].length <= 1024;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compareText(left, right) {
|
|
150
|
+
const leftPoints = [...left];
|
|
151
|
+
const rightPoints = [...right];
|
|
152
|
+
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index += 1) {
|
|
153
|
+
const difference = leftPoints[index].codePointAt(0) - rightPoints[index].codePointAt(0);
|
|
154
|
+
if (difference !== 0) return difference;
|
|
155
|
+
}
|
|
156
|
+
return leftPoints.length - rightPoints.length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isOrdered(values, compare) {
|
|
160
|
+
return values.every((value, index) => index === 0 || compare(values[index - 1], value) <= 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isJsonPointer(value) {
|
|
164
|
+
if (typeof value !== 'string' || CONTROL_CHARACTER.test(value)) return false;
|
|
165
|
+
return value === '' || /^\/(?:[^~]|~[01])*(?:\/(?:[^~]|~[01])*)*$/.test(value);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isMutationCommand(command) {
|
|
169
|
+
return command === 'create' || command === 'transition' || command === 'patch';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function observationIssue(process) {
|
|
173
|
+
if (!hasExactMembers(process, [
|
|
174
|
+
'started', 'process_tree_contained', 'orphaned', 'exit_code', 'signal', 'timed_out',
|
|
175
|
+
'stdout_complete', 'stderr_complete', 'stdout_base64', 'stderr_base64',
|
|
176
|
+
])) return 'members';
|
|
177
|
+
for (const member of [
|
|
178
|
+
'started', 'process_tree_contained', 'orphaned', 'timed_out', 'stdout_complete', 'stderr_complete',
|
|
179
|
+
]) {
|
|
180
|
+
if (typeof process[member] !== 'boolean') return member;
|
|
181
|
+
}
|
|
182
|
+
if (process.signal !== null && (typeof process.signal !== 'string' || process.signal.length === 0)) return 'signal';
|
|
183
|
+
if (process.exit_code !== null && !isNonNegativeSafeInteger(process.exit_code)) return 'exit_code';
|
|
184
|
+
const stdout = decodeCanonicalBase64(process.stdout_base64);
|
|
185
|
+
const stderr = decodeCanonicalBase64(process.stderr_base64);
|
|
186
|
+
if (stdout === null) return 'stdout_base64';
|
|
187
|
+
if (stderr === null) return 'stderr_base64';
|
|
188
|
+
if (!process.started) {
|
|
189
|
+
if (process.exit_code !== null || process.signal !== null || process.timed_out
|
|
190
|
+
|| !process.process_tree_contained || process.orphaned
|
|
191
|
+
|| !process.stdout_complete || !process.stderr_complete
|
|
192
|
+
|| stdout.length > 0 || stderr.length > 0) return 'not-started-state';
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if ((process.timed_out || process.signal !== null) && process.exit_code !== null) {
|
|
196
|
+
return 'contradictory-exit-state';
|
|
197
|
+
}
|
|
198
|
+
const incomplete = process.timed_out || process.signal !== null
|
|
199
|
+
|| !process.process_tree_contained || process.orphaned
|
|
200
|
+
|| !process.stdout_complete || !process.stderr_complete;
|
|
201
|
+
return process.exit_code === null && !incomplete ? 'exit_code' : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hasRequiredFinalLf(bytes) {
|
|
205
|
+
if (bytes.length <= 1 || bytes[bytes.length - 1] !== 0x0A
|
|
206
|
+
|| bytes[bytes.length - 2] === 0x0A || bytes[bytes.length - 2] === 0x0D) return false;
|
|
207
|
+
let inString = false;
|
|
208
|
+
let escaped = false;
|
|
209
|
+
for (let index = 0; index < bytes.length - 1; index += 1) {
|
|
210
|
+
const byte = bytes[index];
|
|
211
|
+
if (inString) {
|
|
212
|
+
if (escaped) escaped = false;
|
|
213
|
+
else if (byte === 0x5C) escaped = true;
|
|
214
|
+
else if (byte === 0x22) inString = false;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (byte === 0x22) inString = true;
|
|
218
|
+
else if (byte === 0x09 || byte === 0x0A || byte === 0x0D || byte === 0x20) return false;
|
|
219
|
+
}
|
|
220
|
+
return !inString && !escaped;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validValidationErrors(value) {
|
|
224
|
+
return Array.isArray(value) && value.every((error) => hasExactMembers(error, [
|
|
225
|
+
'path', 'field', 'code', 'message',
|
|
226
|
+
])
|
|
227
|
+
&& nonEmptyControlFreeString(error.path)
|
|
228
|
+
&& nonEmptyControlFreeString(error.field)
|
|
229
|
+
&& nonEmptyControlFreeString(error.code)
|
|
230
|
+
&& nonEmptyControlFreeString(error.message))
|
|
231
|
+
&& isOrdered(value, compareValidationErrors);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function compareValidationErrors(left, right) {
|
|
235
|
+
return compareText(left.path, right.path)
|
|
236
|
+
|| compareText(left.field, right.field)
|
|
237
|
+
|| compareText(left.code, right.code)
|
|
238
|
+
|| compareText(left.message, right.message);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validCoreValidateEnvelope(value, exitCode) {
|
|
242
|
+
return hasExactMembers(value, ['valid', 'errors'])
|
|
243
|
+
&& typeof value.valid === 'boolean'
|
|
244
|
+
&& validValidationErrors(value.errors)
|
|
245
|
+
&& exitCode === (value.valid ? 0 : 1)
|
|
246
|
+
&& (value.valid ? value.errors.length === 0 : value.errors.length > 0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function validCoreReadyEnvelope(value, exitCode, responseContext) {
|
|
250
|
+
const coreRequest = responseCoreRequest(responseContext, 'ready');
|
|
251
|
+
if (coreRequest === null) return false;
|
|
252
|
+
if (value?.valid === true) {
|
|
253
|
+
return hasExactMembers(value, ['as_of', 'valid', 'ready'])
|
|
254
|
+
&& isCalendarDate(value.as_of)
|
|
255
|
+
&& (coreRequest === undefined || value.as_of === coreRequest.as_of)
|
|
256
|
+
&& validCoreRelationList(value.ready)
|
|
257
|
+
&& exitCode === 0;
|
|
258
|
+
}
|
|
259
|
+
return value?.valid === false
|
|
260
|
+
&& hasExactMembers(value, ['valid', 'errors'])
|
|
261
|
+
&& validValidationErrors(value.errors) && value.errors.length > 0 && exitCode === 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validCoreCapabilitiesEnvelope(value, exitCode) {
|
|
265
|
+
return verifyCoreProbe({
|
|
266
|
+
core: {
|
|
267
|
+
required_core_contract_version: CORE_CONTRACT_VERSION,
|
|
268
|
+
commands: [...CORE_COMMAND_ORDER],
|
|
269
|
+
},
|
|
270
|
+
optional_features: {
|
|
271
|
+
claims: value?.result?.operations?.work_claim?.supported === true,
|
|
272
|
+
policy: false,
|
|
273
|
+
},
|
|
274
|
+
}, value).ok && exitCode === 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function validCoreReadEnvelope(value, command, exitCode, responseContext) {
|
|
278
|
+
const coreRequest = responseCoreRequest(responseContext, command);
|
|
279
|
+
if (coreRequest === null) return false;
|
|
280
|
+
if (!plainObject(value)
|
|
281
|
+
|| value.command !== command || value.contract_version !== CORE_CONTRACT_VERSION) return false;
|
|
282
|
+
if (value.ok === true) {
|
|
283
|
+
return hasExactMembers(value, ['ok', 'command', 'contract_version', 'result'])
|
|
284
|
+
&& hasExactMembers(value.result, ['item'])
|
|
285
|
+
&& validCoreItemShape(value.result.item)
|
|
286
|
+
&& (coreRequest === undefined || value.result.item.id === coreRequest.id)
|
|
287
|
+
&& exitCode === 0;
|
|
288
|
+
}
|
|
289
|
+
return value.ok === false
|
|
290
|
+
&& hasExactMembers(value, ['ok', 'command', 'contract_version', 'error'])
|
|
291
|
+
&& validCoreErrorAtExit(value.error, command, exitCode, responseContext);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function validCoreItemShape(value) {
|
|
295
|
+
if (!hasExactMembers(value, [
|
|
296
|
+
'id', 'path', 'revision', 'source_encoding', 'source_media_type',
|
|
297
|
+
'source_base64', 'core', 'body',
|
|
298
|
+
])) return false;
|
|
299
|
+
if (!hasExactMembers(value.core, [
|
|
300
|
+
'schema_version', 'id', 'title', 'kind', 'status', 'created', 'updated',
|
|
301
|
+
'provenance', 'depends_on', 'related',
|
|
302
|
+
], [
|
|
303
|
+
'parent', 'snoozed_until', 'completed', 'killed', 'archived', 'number',
|
|
304
|
+
'priority', 'decisions',
|
|
305
|
+
])) return false;
|
|
306
|
+
if (!WOWBAGGER_ID.test(value.id)
|
|
307
|
+
|| !isSafeLedgerDisplayPath(value.path)
|
|
308
|
+
|| !DIGEST.test(value.revision)
|
|
309
|
+
|| value.source_encoding !== 'base64'
|
|
310
|
+
|| value.source_media_type !== 'text/markdown; charset=utf-8'
|
|
311
|
+
|| typeof value.body !== 'string'
|
|
312
|
+
|| !validCoreView(value.core)) return false;
|
|
313
|
+
const source = decodeCanonicalBase64(value.source_base64);
|
|
314
|
+
if (source === null || sha256(source) !== value.revision) return false;
|
|
315
|
+
const sourceText = decodeUtf8(source);
|
|
316
|
+
if (sourceText === null) return false;
|
|
317
|
+
const parsed = parseLedgerItemSource(sourceText);
|
|
318
|
+
const sourceCore = parsed.error ? null : sourceCoreView(parsed.data);
|
|
319
|
+
return sourceCore !== null
|
|
320
|
+
&& value.id === value.core.id
|
|
321
|
+
&& value.body === parsed.body
|
|
322
|
+
&& sameJson(value.core, sourceCore)
|
|
323
|
+
&& validReturnedItemSemantics(value.path, parsed.data);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function validCoreView(value) {
|
|
327
|
+
if (!plainObject(value)
|
|
328
|
+
|| (value.schema_version !== 1 && value.schema_version !== 2)
|
|
329
|
+
|| !WOWBAGGER_ID.test(value.id)
|
|
330
|
+
|| !nonEmptyTrimmedString(value.title)
|
|
331
|
+
|| !new Set(['task', 'epic']).has(value.kind)
|
|
332
|
+
|| !new Set(['triage', 'backlog', 'in-progress', 'done', 'killed', 'archived']).has(value.status)
|
|
333
|
+
|| !isCalendarDate(value.created)
|
|
334
|
+
|| !isCalendarDate(value.updated)
|
|
335
|
+
|| value.updated < value.created
|
|
336
|
+
|| !hasExactMembers(value.provenance, ['source', 'recorded_at'])
|
|
337
|
+
|| !nonEmptyTrimmedString(value.provenance.source)
|
|
338
|
+
|| !isCoreRfc3339Utc(value.provenance.recorded_at)
|
|
339
|
+
|| !validCoreRelationList(value.depends_on)
|
|
340
|
+
|| !validCoreRelationList(value.related)) return false;
|
|
341
|
+
|
|
342
|
+
for (const field of ['parent']) {
|
|
343
|
+
if (Object.hasOwn(value, field) && !WOWBAGGER_ID.test(value[field])) return false;
|
|
344
|
+
}
|
|
345
|
+
for (const field of ['snoozed_until', 'completed', 'killed', 'archived']) {
|
|
346
|
+
if (Object.hasOwn(value, field) && !isCalendarDate(value[field])) return false;
|
|
347
|
+
}
|
|
348
|
+
if (Object.hasOwn(value, 'number')
|
|
349
|
+
&& (!Number.isSafeInteger(value.number) || value.number < 1)) return false;
|
|
350
|
+
if (Object.hasOwn(value, 'priority')
|
|
351
|
+
&& (!Number.isSafeInteger(value.priority) || value.priority < 0)) return false;
|
|
352
|
+
if (!validCoreTerminalDates(value)) return false;
|
|
353
|
+
return !Object.hasOwn(value, 'decisions') || validCoreDecisions(value.decisions);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function validCoreRelationList(value) {
|
|
357
|
+
return Array.isArray(value)
|
|
358
|
+
&& value.every((id) => WOWBAGGER_ID.test(id))
|
|
359
|
+
&& new Set(value).size === value.length;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function validCoreTerminalDates(value) {
|
|
363
|
+
const terminalFields = ['completed', 'killed', 'archived'];
|
|
364
|
+
const required = { done: 'completed', killed: 'killed', archived: 'archived' }[value.status] ?? null;
|
|
365
|
+
if (required === null) return terminalFields.every((field) => !Object.hasOwn(value, field));
|
|
366
|
+
return terminalFields.every((field) => field === required
|
|
367
|
+
? Object.hasOwn(value, field) && value[field] === value.updated
|
|
368
|
+
: !Object.hasOwn(value, field));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function validCoreDecisions(value) {
|
|
372
|
+
if (!Array.isArray(value)) return false;
|
|
373
|
+
return value.every((decision) => {
|
|
374
|
+
if (!hasExactMembers(decision, ['action', 'date', 'summary', 'rationale'], ['rollup'])
|
|
375
|
+
|| !new Set([
|
|
376
|
+
'accept', 'complete', 'kill', 'archive', 'restore', 'replace-dependency',
|
|
377
|
+
'waive-dependency', 'reparent', 'record',
|
|
378
|
+
]).has(decision.action)
|
|
379
|
+
|| !isCalendarDate(decision.date)
|
|
380
|
+
|| !nonEmptyTrimmedString(decision.summary)
|
|
381
|
+
|| !nonEmptyTrimmedString(decision.rationale)) return false;
|
|
382
|
+
if (!Object.hasOwn(decision, 'rollup')) return true;
|
|
383
|
+
return Array.isArray(decision.rollup)
|
|
384
|
+
&& decision.rollup.every((entry) => hasExactMembers(entry, ['id', 'status'])
|
|
385
|
+
&& WOWBAGGER_ID.test(entry.id)
|
|
386
|
+
&& new Set(['done', 'killed']).has(entry.status));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function sourceCoreView(data) {
|
|
391
|
+
if (!plainObject(data)) return null;
|
|
392
|
+
const core = {};
|
|
393
|
+
for (const field of [
|
|
394
|
+
'schema_version', 'id', 'title', 'kind', 'status', 'created', 'updated',
|
|
395
|
+
]) {
|
|
396
|
+
if (!Object.hasOwn(data, field)) return null;
|
|
397
|
+
core[field] = data[field];
|
|
398
|
+
}
|
|
399
|
+
if (!plainObject(data.provenance)
|
|
400
|
+
|| !Object.hasOwn(data.provenance, 'source')
|
|
401
|
+
|| !Object.hasOwn(data.provenance, 'recorded_at')
|
|
402
|
+
|| !Array.isArray(data.depends_on)
|
|
403
|
+
|| (Object.hasOwn(data, 'related') && !Array.isArray(data.related))) return null;
|
|
404
|
+
core.provenance = {
|
|
405
|
+
source: data.provenance.source,
|
|
406
|
+
recorded_at: data.provenance.recorded_at,
|
|
407
|
+
};
|
|
408
|
+
core.depends_on = data.depends_on;
|
|
409
|
+
core.related = data.related ?? [];
|
|
410
|
+
for (const field of ['parent', 'snoozed_until', 'completed', 'killed', 'archived', 'number', 'priority']) {
|
|
411
|
+
if (Object.hasOwn(data, field)) core[field] = data[field];
|
|
412
|
+
}
|
|
413
|
+
if (Object.hasOwn(data, 'decisions')) {
|
|
414
|
+
if (!Array.isArray(data.decisions)) return null;
|
|
415
|
+
core.decisions = [];
|
|
416
|
+
for (const decision of data.decisions) {
|
|
417
|
+
if (!plainObject(decision)
|
|
418
|
+
|| !['action', 'date', 'summary', 'rationale'].every((field) => Object.hasOwn(decision, field))) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const normalized = {
|
|
422
|
+
action: decision.action,
|
|
423
|
+
date: decision.date,
|
|
424
|
+
summary: decision.summary,
|
|
425
|
+
rationale: decision.rationale,
|
|
426
|
+
};
|
|
427
|
+
if (Object.hasOwn(decision, 'rollup')) {
|
|
428
|
+
if (!Array.isArray(decision.rollup)) return null;
|
|
429
|
+
normalized.rollup = [];
|
|
430
|
+
for (const entry of decision.rollup) {
|
|
431
|
+
if (!plainObject(entry)) return null;
|
|
432
|
+
normalized.rollup.push({ id: entry.id, status: entry.status });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
core.decisions.push(normalized);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return core;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function semanticSupportItems(data) {
|
|
442
|
+
const parentIds = new Set();
|
|
443
|
+
const referenceIds = new Set();
|
|
444
|
+
const rollupChildren = new Map();
|
|
445
|
+
if (Object.hasOwn(data, 'parent') && data.parent !== data.id) parentIds.add(data.parent);
|
|
446
|
+
for (const relation of ['depends_on', 'related']) {
|
|
447
|
+
for (const id of data[relation] ?? []) {
|
|
448
|
+
if (id !== data.id) referenceIds.add(id);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const rollup = matchingEpicRollup(data);
|
|
452
|
+
for (const entry of rollup) {
|
|
453
|
+
if (entry.id !== data.id && !rollupChildren.has(entry.id)) {
|
|
454
|
+
rollupChildren.set(entry.id, entry.status);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const ids = new Set([...parentIds, ...referenceIds, ...rollupChildren.keys()]);
|
|
459
|
+
return [...ids].sort().map((id) => {
|
|
460
|
+
if (parentIds.has(id)) return semanticSupportItem(id, { kind: 'epic' });
|
|
461
|
+
if (rollupChildren.has(id)) {
|
|
462
|
+
return semanticSupportItem(id, {
|
|
463
|
+
status: rollupChildren.get(id),
|
|
464
|
+
parent: data.id,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return semanticSupportItem(id);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function matchingEpicRollup(data) {
|
|
472
|
+
if (data.kind !== 'epic' || data.status !== 'done' || !Array.isArray(data.decisions)) return [];
|
|
473
|
+
const decision = data.decisions.find((candidate) => candidate?.action === 'complete'
|
|
474
|
+
&& candidate.date === data.completed && Array.isArray(candidate.rollup));
|
|
475
|
+
return decision?.rollup ?? [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function semanticSupportItem(id, { kind = 'task', status = 'backlog', parent = null } = {}) {
|
|
479
|
+
const date = dateFromWowbaggerId(id);
|
|
480
|
+
const terminal = {
|
|
481
|
+
done: ['completed', 'complete'],
|
|
482
|
+
killed: ['killed', 'kill'],
|
|
483
|
+
archived: ['archived', 'archive'],
|
|
484
|
+
}[status] ?? null;
|
|
485
|
+
const data = {
|
|
486
|
+
schema_version: 1,
|
|
487
|
+
id,
|
|
488
|
+
title: `Adapter semantic support ${id}`,
|
|
489
|
+
kind,
|
|
490
|
+
status,
|
|
491
|
+
created: date,
|
|
492
|
+
updated: date,
|
|
493
|
+
provenance: {
|
|
494
|
+
source: 'adapter-semantic-support',
|
|
495
|
+
recorded_at: `${date}T00:00:00Z`,
|
|
496
|
+
},
|
|
497
|
+
depends_on: [],
|
|
498
|
+
related: [],
|
|
499
|
+
...(parent === null ? {} : { parent }),
|
|
500
|
+
};
|
|
501
|
+
if (terminal) {
|
|
502
|
+
const [field, action] = terminal;
|
|
503
|
+
data[field] = date;
|
|
504
|
+
data.decisions = [{
|
|
505
|
+
action,
|
|
506
|
+
date,
|
|
507
|
+
summary: `Support ${action} decision.`,
|
|
508
|
+
rationale: 'Synthetic relation support for adapter response validation.',
|
|
509
|
+
...(kind === 'epic' && status === 'done' ? { rollup: [] } : {}),
|
|
510
|
+
}];
|
|
511
|
+
}
|
|
512
|
+
return { path: `.adapter-semantic-support/${id}.md`, data };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function dateFromWowbaggerId(id) {
|
|
516
|
+
const alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
517
|
+
let milliseconds = 0;
|
|
518
|
+
for (const character of id.slice(3, 13)) {
|
|
519
|
+
milliseconds = (milliseconds * 32) + alphabet.indexOf(character);
|
|
520
|
+
}
|
|
521
|
+
return new Date(milliseconds).toISOString().slice(0, 10);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function validReturnedItemSemantics(itemPath, data) {
|
|
525
|
+
const ledger = {
|
|
526
|
+
errors: [],
|
|
527
|
+
items: [
|
|
528
|
+
{ path: itemPath, data },
|
|
529
|
+
...semanticSupportItems(data),
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
return !validateLedger(ledger).errors.some((internal) => internal.path === itemPath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function validCoreMutationEnvelope(value, command, exitCode, responseContext) {
|
|
536
|
+
const mutationRequest = responseMutationRequest(responseContext, command);
|
|
537
|
+
const canonicalMutationRequest = hasCanonicalMutationRequest(responseContext, command);
|
|
538
|
+
if (!plainObject(value)
|
|
539
|
+
|| value.command !== command || value.contract_version !== CORE_CONTRACT_VERSION) return false;
|
|
540
|
+
if (value.ok === true) {
|
|
541
|
+
return canonicalMutationRequest && value.state === 'committed'
|
|
542
|
+
&& hasExactMembers(value, ['ok', 'command', 'contract_version', 'state', 'result'])
|
|
543
|
+
&& hasExactMembers(value.result, ['item'])
|
|
544
|
+
&& validCoreItemShape(value.result.item)
|
|
545
|
+
&& validMutationResultCorrelation(value.result.item, command, mutationRequest)
|
|
546
|
+
&& exitCode === 0;
|
|
547
|
+
}
|
|
548
|
+
return value.ok === false
|
|
549
|
+
&& new Set(['unchanged', 'committed', 'unknown']).has(value.state)
|
|
550
|
+
&& hasExactMembers(value, ['ok', 'command', 'contract_version', 'state', 'error'])
|
|
551
|
+
&& validCoreErrorAtExit(value.error, command, exitCode, responseContext)
|
|
552
|
+
&& value.state === (MUTATION_ERROR_STATES.get(value.error.code) ?? 'unchanged');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function responseCoreRequest(responseContext, command) {
|
|
556
|
+
if (responseContext === null || responseContext === undefined) return undefined;
|
|
557
|
+
const coreRequest = responseContext.core_request;
|
|
558
|
+
return plainObject(coreRequest) && coreRequest.command === command ? coreRequest : null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function responseMutationInput(responseContext, command) {
|
|
562
|
+
const coreRequest = responseCoreRequest(responseContext, command);
|
|
563
|
+
if (coreRequest === undefined) return undefined;
|
|
564
|
+
if (coreRequest === null || !isMutationCommand(command)) return null;
|
|
565
|
+
if (!Object.hasOwn(responseContext, 'mutation_input')) return undefined;
|
|
566
|
+
return responseContext.mutation_input instanceof Uint8Array
|
|
567
|
+
? responseContext.mutation_input
|
|
568
|
+
: null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function responseMutationRequest(responseContext, command) {
|
|
572
|
+
const coreRequest = responseCoreRequest(responseContext, command);
|
|
573
|
+
if (coreRequest === undefined) return undefined;
|
|
574
|
+
if (coreRequest === null || !isMutationCommand(command)) return null;
|
|
575
|
+
const mutationInput = responseMutationInput(responseContext, command);
|
|
576
|
+
if (mutationInput !== undefined) {
|
|
577
|
+
if (mutationInput === null) return null;
|
|
578
|
+
const parsed = parseJsonRequest(mutationInput);
|
|
579
|
+
return plainObject(parsed.value) ? parsed.value : null;
|
|
580
|
+
}
|
|
581
|
+
return plainObject(responseContext.mutation_request) ? responseContext.mutation_request : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function responseItemId(responseContext, command) {
|
|
585
|
+
if (command === 'inspect') {
|
|
586
|
+
const coreRequest = responseCoreRequest(responseContext, command);
|
|
587
|
+
if (coreRequest === undefined) return undefined;
|
|
588
|
+
return WOWBAGGER_ID.test(coreRequest?.id) ? coreRequest.id : null;
|
|
589
|
+
}
|
|
590
|
+
const mutationRequest = responseMutationRequest(responseContext, command);
|
|
591
|
+
if (mutationRequest === undefined) return undefined;
|
|
592
|
+
return WOWBAGGER_ID.test(mutationRequest?.id) ? mutationRequest.id : null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function responseExpectedRevision(responseContext, command) {
|
|
596
|
+
const mutationRequest = responseMutationRequest(responseContext, command);
|
|
597
|
+
if (mutationRequest === undefined) return undefined;
|
|
598
|
+
return (command === 'transition' || command === 'patch') && DIGEST.test(mutationRequest?.expected_revision)
|
|
599
|
+
? mutationRequest.expected_revision
|
|
600
|
+
: null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function expectedCreateCandidateRevision(responseContext, command) {
|
|
604
|
+
if (command !== 'create') return null;
|
|
605
|
+
const mutationRequest = responseMutationRequest(responseContext, command);
|
|
606
|
+
if (mutationRequest === undefined) return undefined;
|
|
607
|
+
if (!hasCanonicalMutationRequest(responseContext, command)) return null;
|
|
608
|
+
try {
|
|
609
|
+
return sha256(createCandidateSource(mutationRequest));
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function hasCanonicalMutationRequest(responseContext, command) {
|
|
616
|
+
const mutationInput = responseMutationInput(responseContext, command);
|
|
617
|
+
if (mutationInput !== undefined) {
|
|
618
|
+
if (mutationInput === null) return false;
|
|
619
|
+
const parsed = parseJsonRequest(mutationInput);
|
|
620
|
+
if (parsed.issues.length > 0 || !plainObject(parsed.value)) return false;
|
|
621
|
+
if (command === 'create') return validateCreateRequest(parsed.value).length === 0;
|
|
622
|
+
if (command === 'transition') return validateTransitionRequest(parsed.value).length === 0;
|
|
623
|
+
if (command === 'patch') return validPatchRequest(parsed.value);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
const mutationRequest = responseMutationRequest(responseContext, command);
|
|
627
|
+
if (mutationRequest === undefined) return true;
|
|
628
|
+
if (!plainObject(mutationRequest)) return false;
|
|
629
|
+
if (command === 'create') return validateCreateRequest(mutationRequest).length === 0;
|
|
630
|
+
if (command === 'transition') return validateTransitionRequest(mutationRequest).length === 0;
|
|
631
|
+
if (command === 'patch') return validPatchRequest(mutationRequest);
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function validMutationResultCorrelation(item, command, mutationRequest) {
|
|
636
|
+
if (mutationRequest === undefined) return true;
|
|
637
|
+
if (command === 'create') return validCreateResultCorrelation(item, mutationRequest);
|
|
638
|
+
if (command === 'patch') return validPatchResultCorrelation(item, mutationRequest);
|
|
639
|
+
return validTransitionResultCorrelation(item, mutationRequest);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function validCreateResultCorrelation(item, request) {
|
|
643
|
+
if (!plainObject(request) || !plainObject(request.item)
|
|
644
|
+
|| item.id !== request.id || item.path !== `${request.id}.md` || item.body !== request.body
|
|
645
|
+
|| item.core.schema_version !== 1 || item.core.status !== 'triage'
|
|
646
|
+
|| item.core.title !== request.item.title || item.core.kind !== request.item.kind
|
|
647
|
+
|| !sameJson(item.core.provenance, {
|
|
648
|
+
source: request.item.provenance?.source,
|
|
649
|
+
recorded_at: request.item.provenance?.recorded_at,
|
|
650
|
+
})
|
|
651
|
+
|| !sameJson(item.core.depends_on, request.item.depends_on)
|
|
652
|
+
|| !sameJson(item.core.related, request.item.related ?? [])) return false;
|
|
653
|
+
for (const field of ['parent', 'snoozed_until']) {
|
|
654
|
+
if (Object.hasOwn(item.core, field) !== Object.hasOwn(request.item, field)
|
|
655
|
+
|| (Object.hasOwn(item.core, field) && item.core[field] !== request.item[field])) return false;
|
|
656
|
+
}
|
|
657
|
+
if (['completed', 'killed', 'archived', 'decisions'].some((field) => Object.hasOwn(item.core, field))) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
const source = decodeCanonicalBase64(item.source_base64);
|
|
661
|
+
try {
|
|
662
|
+
return source !== null && source.equals(createCandidateSource(request));
|
|
663
|
+
} catch {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function validTransitionResultCorrelation(item, request) {
|
|
669
|
+
return plainObject(request)
|
|
670
|
+
&& item.id === request.id
|
|
671
|
+
&& item.revision !== request.expected_revision
|
|
672
|
+
&& item.core.status === request.to_status
|
|
673
|
+
&& item.core.updated === request.date
|
|
674
|
+
&& (!Object.hasOwn(request, 'decision')
|
|
675
|
+
|| (plainObject(request.decision) && Array.isArray(item.core.decisions)
|
|
676
|
+
&& item.core.decisions.some((decision) => {
|
|
677
|
+
const actions = request.to_status === 'backlog'
|
|
678
|
+
? new Set(['accept', 'restore'])
|
|
679
|
+
: new Set(['complete', 'kill', 'archive']);
|
|
680
|
+
return actions.has(decision.action)
|
|
681
|
+
&& decision.date === request.date
|
|
682
|
+
&& decision.summary === request.decision.summary
|
|
683
|
+
&& decision.rationale === request.decision.rationale;
|
|
684
|
+
})));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function validPatchRequest(request) {
|
|
688
|
+
if (!hasExactMembers(request, ['id', 'expected_revision', 'date', 'set'])
|
|
689
|
+
|| !WOWBAGGER_ID.test(request.id)
|
|
690
|
+
|| !DIGEST.test(request.expected_revision)
|
|
691
|
+
|| !isCalendarDate(request.date)
|
|
692
|
+
|| !hasExactMembers(request.set, [], ['number', 'priority'])
|
|
693
|
+
|| Object.keys(request.set).length === 0) return false;
|
|
694
|
+
for (const [field, value] of Object.entries(request.set)) {
|
|
695
|
+
if (value === null) continue;
|
|
696
|
+
const minimum = field === 'number' ? 1 : 0;
|
|
697
|
+
if (parsedIntegerValue(value, minimum) === undefined) return false;
|
|
698
|
+
}
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function parsedIntegerValue(value, minimum) {
|
|
703
|
+
if (Number.isSafeInteger(value) && value >= minimum) return value;
|
|
704
|
+
if (value === null || typeof value !== 'object'
|
|
705
|
+
|| Object.getPrototypeOf(value)?.constructor?.name !== 'JsonNumber'
|
|
706
|
+
|| !hasExactMembers(value, ['source'])
|
|
707
|
+
|| typeof value.source !== 'string'
|
|
708
|
+
|| !/^(0|[1-9][0-9]*)$/.test(value.source)) return undefined;
|
|
709
|
+
const parsed = Number(value.source);
|
|
710
|
+
return Number.isSafeInteger(parsed) && parsed >= minimum ? parsed : undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function validPatchResultCorrelation(item, request) {
|
|
714
|
+
return validPatchRequest(request)
|
|
715
|
+
&& item.id === request.id
|
|
716
|
+
&& item.revision !== request.expected_revision
|
|
717
|
+
&& item.core.updated === request.date
|
|
718
|
+
&& Object.entries(request.set).every(([field, requested]) => {
|
|
719
|
+
if (requested === null) return !Object.hasOwn(item.core, field);
|
|
720
|
+
return item.core[field] === parsedIntegerValue(requested, field === 'number' ? 1 : 0);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function validCoreErrorAtExit(value, command, exitCode, responseContext) {
|
|
725
|
+
if (!hasExactMembers(value, ['code', 'message', 'details'])
|
|
726
|
+
|| !CORE_ERROR_CODES_BY_COMMAND[command]?.has(value.code)
|
|
727
|
+
|| CORE_ERROR_EXIT_CODES.get(value.code) !== exitCode
|
|
728
|
+
|| !coreErrorMessageMatches(value.code, command, value.message)) return false;
|
|
729
|
+
return validCoreErrorDetails(value.code, value.details, command, responseContext);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function coreErrorMessageMatches(code, command, message) {
|
|
733
|
+
const expected = {
|
|
734
|
+
'item-not-found': 'The requested item was not found.',
|
|
735
|
+
'ledger-invalid': 'The configured ledger is invalid.',
|
|
736
|
+
'transition-precondition-failed': 'The requested lifecycle transition failed its preconditions.',
|
|
737
|
+
'patch-precondition-failed': 'The requested patch failed its preconditions.',
|
|
738
|
+
'candidate-invalid': 'The proposed item would make the ledger invalid.',
|
|
739
|
+
'revision-conflict': 'The item changed after it was inspected.',
|
|
740
|
+
'lock-held': 'The item is locked by another cooperative Wowbagger writer.',
|
|
741
|
+
'id-collision': 'The requested item ID already exists.',
|
|
742
|
+
'path-collision': 'The default item path is occupied by a different item.',
|
|
743
|
+
'atomic-scope-required': 'The requested transition requires multi-item atomicity.',
|
|
744
|
+
'capability-unavailable': 'Atomic no-clobber publication is unavailable for this ledger.',
|
|
745
|
+
'operation-failed': 'The mutation operation failed before a commit was established.',
|
|
746
|
+
'post-commit-recovery-required': 'The item was committed, but cleanup requires recovery.',
|
|
747
|
+
'write-outcome-unknown': `The ${command} publication outcome could not be verified.`,
|
|
748
|
+
}[code] ?? `The ${command} request is invalid.`;
|
|
749
|
+
return message === expected;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function validCoreErrorDetails(code, details, command, responseContext) {
|
|
753
|
+
const expectedItemId = responseItemId(responseContext, command);
|
|
754
|
+
const expectedRevision = responseExpectedRevision(responseContext, command);
|
|
755
|
+
const expectedCreateRevision = expectedCreateCandidateRevision(responseContext, command);
|
|
756
|
+
const matchesItemId = (id) => expectedItemId === undefined || id === expectedItemId;
|
|
757
|
+
if (code !== 'invalid-request' && !hasCanonicalMutationRequest(responseContext, command)) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
switch (code) {
|
|
761
|
+
case 'invalid-request': return !hasCanonicalMutationRequest(responseContext, command)
|
|
762
|
+
&& validInvalidRequestDetails(details);
|
|
763
|
+
case 'item-not-found': return hasExactMembers(details, ['id'])
|
|
764
|
+
&& WOWBAGGER_ID.test(details.id) && matchesItemId(details.id);
|
|
765
|
+
case 'ledger-invalid': return hasExactMembers(details, ['validation_errors'])
|
|
766
|
+
&& validValidationErrors(details.validation_errors) && details.validation_errors.length > 0;
|
|
767
|
+
case 'transition-precondition-failed': return hasExactMembers(details, ['id', 'issues'])
|
|
768
|
+
&& WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
769
|
+
&& validTransitionIssues(details.issues) && details.issues.length > 0;
|
|
770
|
+
case 'patch-precondition-failed': return hasExactMembers(details, ['id', 'issues'])
|
|
771
|
+
&& WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
772
|
+
&& validPatchIssues(details.issues) && details.issues.length > 0;
|
|
773
|
+
case 'candidate-invalid': return hasExactMembers(details, ['id', 'validation_errors'])
|
|
774
|
+
&& WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
775
|
+
&& validValidationErrors(details.validation_errors) && details.validation_errors.length > 0;
|
|
776
|
+
case 'revision-conflict': return hasExactMembers(details, [
|
|
777
|
+
'id', 'expected_revision', 'actual_revision',
|
|
778
|
+
]) && WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
779
|
+
&& DIGEST.test(details.expected_revision) && DIGEST.test(details.actual_revision)
|
|
780
|
+
&& details.actual_revision !== details.expected_revision
|
|
781
|
+
&& (expectedRevision === undefined || details.expected_revision === expectedRevision);
|
|
782
|
+
case 'lock-held': return validLockHeldDetails(details) && matchesItemId(details.id);
|
|
783
|
+
case 'id-collision': return hasExactMembers(details, ['id', 'path', 'actual_revision'])
|
|
784
|
+
&& WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
785
|
+
&& isSafeLedgerDisplayPath(details.path)
|
|
786
|
+
&& DIGEST.test(details.actual_revision);
|
|
787
|
+
case 'path-collision': return validPathCollisionDetails(details) && matchesItemId(details.id)
|
|
788
|
+
&& (command !== 'create' || expectedItemId === undefined
|
|
789
|
+
|| details.path === `${expectedItemId}.md`);
|
|
790
|
+
case 'atomic-scope-required': return hasExactMembers(details, [
|
|
791
|
+
'id', 'blockers', 'precondition_issues',
|
|
792
|
+
]) && WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
793
|
+
&& validTransitionBlockers(details.blockers)
|
|
794
|
+
&& validTransitionIssues(details.precondition_issues);
|
|
795
|
+
case 'capability-unavailable': return validCapabilityUnavailableDetails(details);
|
|
796
|
+
case 'operation-failed': return validOperationFailedDetails(details) && matchesItemId(details.id);
|
|
797
|
+
case 'post-commit-recovery-required': return hasExactMembers(details, [
|
|
798
|
+
'id', 'revision', 'recovery_artifacts', 'recovery_artifacts_truncated',
|
|
799
|
+
]) && WOWBAGGER_ID.test(details.id) && matchesItemId(details.id) && DIGEST.test(details.revision)
|
|
800
|
+
&& (command !== 'create' || expectedCreateRevision === undefined
|
|
801
|
+
|| details.revision === expectedCreateRevision)
|
|
802
|
+
&& ((command !== 'transition' && command !== 'patch')
|
|
803
|
+
|| expectedRevision === undefined || details.revision !== expectedRevision)
|
|
804
|
+
&& validRecoveryArtifacts(details.recovery_artifacts, details.recovery_artifacts_truncated);
|
|
805
|
+
case 'write-outcome-unknown': return hasExactMembers(details, [
|
|
806
|
+
'id', 'recovery_artifacts', 'recovery_artifacts_truncated',
|
|
807
|
+
]) && WOWBAGGER_ID.test(details.id) && matchesItemId(details.id)
|
|
808
|
+
&& validRecoveryArtifacts(details.recovery_artifacts, details.recovery_artifacts_truncated);
|
|
809
|
+
default: return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function validInvalidRequestDetails(value) {
|
|
814
|
+
return hasExactMembers(value, ['issues']) && Array.isArray(value.issues) && value.issues.length > 0
|
|
815
|
+
&& value.issues.every((issue) => hasExactMembers(issue, ['path', 'code', 'message'])
|
|
816
|
+
&& isJsonPointer(issue.path)
|
|
817
|
+
&& INVALID_REQUEST_CODES.has(issue.code)
|
|
818
|
+
&& nonEmptyControlFreeString(issue.message))
|
|
819
|
+
&& isOrdered(value.issues, compareInvalidRequestIssues);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function compareInvalidRequestIssues(left, right) {
|
|
823
|
+
return compareText(left.path, right.path)
|
|
824
|
+
|| compareText(left.code, right.code)
|
|
825
|
+
|| compareText(left.message, right.message);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function validTransitionIssues(value) {
|
|
829
|
+
if (!Array.isArray(value)) return false;
|
|
830
|
+
return value.every((issue) => hasExactMembers(issue, ['code', 'field', 'message', 'related_ids'])
|
|
831
|
+
&& Object.hasOwn(TRANSITION_ISSUE_MESSAGES, issue.code)
|
|
832
|
+
&& issue.field === TRANSITION_ISSUE_FIELDS[issue.code]
|
|
833
|
+
&& issue.message === TRANSITION_ISSUE_MESSAGES[issue.code]
|
|
834
|
+
&& validSortedUniqueIds(issue.related_ids))
|
|
835
|
+
&& isOrdered(value, compareTransitionIssues);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function validPatchIssues(value) {
|
|
839
|
+
if (!Array.isArray(value)) return false;
|
|
840
|
+
return value.every((issue) => hasExactMembers(issue, ['code', 'field', 'message', 'related_ids'])
|
|
841
|
+
&& Object.hasOwn(PATCH_ISSUE_MESSAGES, issue.code)
|
|
842
|
+
&& issue.field === 'date'
|
|
843
|
+
&& issue.message === PATCH_ISSUE_MESSAGES[issue.code]
|
|
844
|
+
&& sameJson(issue.related_ids, []))
|
|
845
|
+
&& isOrdered(value, compareTransitionIssues);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function compareTransitionIssues(left, right) {
|
|
849
|
+
return compareText(left.code, right.code)
|
|
850
|
+
|| compareText(left.field, right.field)
|
|
851
|
+
|| compareText(left.related_ids.join('\0'), right.related_ids.join('\0'));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function validTransitionBlockers(value) {
|
|
855
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
856
|
+
const seen = new Set();
|
|
857
|
+
for (const [index, blocker] of value.entries()) {
|
|
858
|
+
if (!hasExactMembers(blocker, ['code', 'item_id', 'field'])
|
|
859
|
+
|| !Object.hasOwn(TRANSITION_BLOCKER_FIELDS, blocker.code)
|
|
860
|
+
|| !WOWBAGGER_ID.test(blocker.item_id)
|
|
861
|
+
|| blocker.field !== TRANSITION_BLOCKER_FIELDS[blocker.code]
|
|
862
|
+
|| (index > 0 && compareTransitionBlockers(value[index - 1], blocker) > 0)) return false;
|
|
863
|
+
const key = `${blocker.code}\0${blocker.item_id}\0${blocker.field}`;
|
|
864
|
+
if (seen.has(key)) return false;
|
|
865
|
+
seen.add(key);
|
|
866
|
+
}
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function compareTransitionBlockers(left, right) {
|
|
871
|
+
return compareText(left.code, right.code)
|
|
872
|
+
|| compareText(left.item_id, right.item_id)
|
|
873
|
+
|| compareText(left.field, right.field);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function validLockHeldDetails(value) {
|
|
877
|
+
if (!hasExactMembers(value, ['id', 'lock_path', 'owner', 'owner_diagnostic'])
|
|
878
|
+
|| !WOWBAGGER_ID.test(value.id)
|
|
879
|
+
|| value.lock_path !== `.wowbagger-locks/${value.id}.lock`) return false;
|
|
880
|
+
const diagnostics = new Set(['too-large', 'invalid-utf8', 'duplicate-key', 'invalid-json', 'invalid-shape']);
|
|
881
|
+
if (value.owner === null) return diagnostics.has(value.owner_diagnostic);
|
|
882
|
+
return value.owner_diagnostic === null
|
|
883
|
+
&& hasExactMembers(value.owner, ['lock_version', 'item_id', 'operation', 'writer_id', 'started_at'])
|
|
884
|
+
&& value.owner.lock_version === 1
|
|
885
|
+
&& value.owner.item_id === value.id
|
|
886
|
+
&& new Set(['create', 'transition', 'patch']).has(value.owner.operation)
|
|
887
|
+
&& typeof value.owner.writer_id === 'string'
|
|
888
|
+
&& /^[\x21-\x7e]{1,128}$/.test(value.owner.writer_id)
|
|
889
|
+
&& isCoreRfc3339Utc(value.owner.started_at);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function validPathCollisionDetails(value) {
|
|
893
|
+
if (!hasExactMembers(value, ['id', 'path', 'occupant_kind'], ['occupying_id'])
|
|
894
|
+
|| !WOWBAGGER_ID.test(value.id)
|
|
895
|
+
|| !isSafeLedgerDisplayPath(value.path)
|
|
896
|
+
|| !new Set(['item', 'directory']).has(value.occupant_kind)) return false;
|
|
897
|
+
return value.occupant_kind === 'item'
|
|
898
|
+
? Object.hasOwn(value, 'occupying_id') && WOWBAGGER_ID.test(value.occupying_id)
|
|
899
|
+
: !Object.hasOwn(value, 'occupying_id');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function validCapabilityUnavailableDetails(value) {
|
|
903
|
+
return hasExactMembers(value, [
|
|
904
|
+
'capability', 'reason', 'recovery_artifacts', 'recovery_artifacts_truncated',
|
|
905
|
+
]) && value.capability === 'atomic-no-clobber-publication'
|
|
906
|
+
&& value.reason === 'filesystem-primitive-unavailable'
|
|
907
|
+
&& validRecoveryArtifacts(value.recovery_artifacts, value.recovery_artifacts_truncated);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function validOperationFailedDetails(value) {
|
|
911
|
+
if (!hasExactMembers(value, [
|
|
912
|
+
'id', 'operation', 'reason', 'recovery_artifacts', 'recovery_artifacts_truncated',
|
|
913
|
+
]) || !WOWBAGGER_ID.test(value.id)
|
|
914
|
+
|| !new Set([
|
|
915
|
+
'lock-closure', 'prepare-temporary', 'sync-temporary', 'publish',
|
|
916
|
+
'verify-publication', 'cleanup',
|
|
917
|
+
]).has(value.operation)
|
|
918
|
+
|| !new Set(['retry-limit-exhausted', 'io-error', 'verification-failed']).has(value.reason)
|
|
919
|
+
|| !validRecoveryArtifacts(value.recovery_artifacts, value.recovery_artifacts_truncated)) return false;
|
|
920
|
+
return (value.reason !== 'retry-limit-exhausted' || value.operation === 'lock-closure')
|
|
921
|
+
&& (value.reason !== 'verification-failed'
|
|
922
|
+
|| value.operation === 'publish' || value.operation === 'verify-publication');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function validRecoveryArtifacts(value, truncated) {
|
|
926
|
+
if (!Array.isArray(value) || typeof truncated !== 'boolean' || value.length > 16
|
|
927
|
+
|| (truncated && value.length !== 16)) return false;
|
|
928
|
+
const seen = new Set();
|
|
929
|
+
for (const artifact of value) {
|
|
930
|
+
if (!hasExactMembers(artifact, ['path', 'kind', 'sha256', 'size_bytes'])
|
|
931
|
+
|| !isSafeArtifactPath(artifact.path)
|
|
932
|
+
|| !new Set(['temporary-file', 'lock-file', 'final-item']).has(artifact.kind)) return false;
|
|
933
|
+
const readable = DIGEST.test(artifact.sha256) && isNonNegativeSafeInteger(artifact.size_bytes);
|
|
934
|
+
if (!readable && !(artifact.sha256 === null && artifact.size_bytes === null)) return false;
|
|
935
|
+
const key = artifact.path;
|
|
936
|
+
if (seen.has(key)) return false;
|
|
937
|
+
seen.add(key);
|
|
938
|
+
}
|
|
939
|
+
return isOrdered(value, compareRecoveryArtifacts);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function compareRecoveryArtifacts(left, right) {
|
|
943
|
+
return compareText(left.path, right.path) || compareText(left.kind, right.kind);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function validSortedUniqueIds(value) {
|
|
947
|
+
return Array.isArray(value)
|
|
948
|
+
&& value.every((id) => WOWBAGGER_ID.test(id))
|
|
949
|
+
&& new Set(value).size === value.length
|
|
950
|
+
&& value.every((id, index) => index === 0 || value[index - 1] < id);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function sha256(bytes) {
|
|
954
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function envelopeState(process, command = null, responseContext = null) {
|
|
958
|
+
const bytes = decodeCanonicalBase64(process?.stdout_base64);
|
|
959
|
+
if (bytes === null) return { present: false, valid: false };
|
|
960
|
+
const parsed = parseJsonRequest(bytes);
|
|
961
|
+
const value = parsed.issues.length === 0
|
|
962
|
+
? normalizeJsonValue(parsed.value)
|
|
963
|
+
: null;
|
|
964
|
+
const valid = hasRequiredFinalLf(bytes) && parsed.issues.length === 0
|
|
965
|
+
&& validCoreCommandEnvelope(value, command, process?.exit_code, responseContext);
|
|
966
|
+
return {
|
|
967
|
+
present: bytes.length > 0,
|
|
968
|
+
valid,
|
|
969
|
+
mutation_state: valid && isMutationCommand(command)
|
|
970
|
+
? value.state
|
|
971
|
+
: null,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function validCoreCommandEnvelope(value, command, exitCode, responseContext) {
|
|
976
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
977
|
+
if (command === 'validate') return validCoreValidateEnvelope(value, exitCode);
|
|
978
|
+
if (command === 'ready') return validCoreReadyEnvelope(value, exitCode, responseContext);
|
|
979
|
+
if (command === 'capabilities') return validCoreCapabilitiesEnvelope(value, exitCode);
|
|
980
|
+
if (command === 'inspect') return validCoreReadEnvelope(value, command, exitCode, responseContext);
|
|
981
|
+
if (isMutationCommand(command)) {
|
|
982
|
+
return validCoreMutationEnvelope(value, command, exitCode, responseContext);
|
|
983
|
+
}
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function processSummary(process, command, responseContext) {
|
|
988
|
+
const envelope = envelopeState(process, command, responseContext);
|
|
989
|
+
return {
|
|
990
|
+
started: process?.started ?? null,
|
|
991
|
+
process_tree_contained: process?.process_tree_contained ?? null,
|
|
992
|
+
orphaned: process?.orphaned ?? null,
|
|
993
|
+
exit_code: process?.exit_code ?? null,
|
|
994
|
+
signal: process?.signal ?? null,
|
|
995
|
+
timed_out: process?.timed_out ?? null,
|
|
996
|
+
stdout_complete: process?.stdout_complete ?? null,
|
|
997
|
+
stderr_complete: process?.stderr_complete ?? null,
|
|
998
|
+
core_envelope_present: envelope.present,
|
|
999
|
+
core_envelope_valid: envelope.valid,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function capturedStreamsOverLimit(process, stdoutLimit, stderrLimit) {
|
|
1004
|
+
const streams = [];
|
|
1005
|
+
for (const [stream, limit] of [
|
|
1006
|
+
['stdout', stdoutLimit],
|
|
1007
|
+
['stderr', stderrLimit],
|
|
1008
|
+
]) {
|
|
1009
|
+
if (limit === undefined) continue;
|
|
1010
|
+
if (!isNonNegativeSafeInteger(limit)) return ['stdout', 'stderr'];
|
|
1011
|
+
const bytes = decodeCanonicalBase64(process[`${stream}_base64`]);
|
|
1012
|
+
if (bytes !== null && bytes.length > limit) streams.push(stream);
|
|
1013
|
+
}
|
|
1014
|
+
return streams;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function mutationUnknown(base, command, itemId, expectedRevision, process, processIssue, responseContext) {
|
|
1018
|
+
const recovery = command === 'create'
|
|
1019
|
+
? {
|
|
1020
|
+
action: 'inspect-caller-known-id',
|
|
1021
|
+
validate_ledger_first: true,
|
|
1022
|
+
retry: 'only-after-item-not-found-and-audited-artifact-recovery',
|
|
1023
|
+
}
|
|
1024
|
+
: {
|
|
1025
|
+
action: 'validate-inspect-and-compare-revision',
|
|
1026
|
+
expected_revision: expectedRevision,
|
|
1027
|
+
retry: 'never-before-current-state-review',
|
|
1028
|
+
};
|
|
1029
|
+
const details = { command, item_id: itemId, recovery };
|
|
1030
|
+
if (processIssue) details.process_issue = processIssue;
|
|
1031
|
+
return {
|
|
1032
|
+
...base,
|
|
1033
|
+
mutation_outcome: 'unknown',
|
|
1034
|
+
error: error('mutation-outcome-unknown', details),
|
|
1035
|
+
process: processSummary(process, command, responseContext),
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export function mapProcessOutcome({
|
|
1040
|
+
adapter_contract_version: adapterContractVersion,
|
|
1041
|
+
request_id: requestId,
|
|
1042
|
+
command,
|
|
1043
|
+
core_request: coreRequest = null,
|
|
1044
|
+
mutation_request: mutationRequest = null,
|
|
1045
|
+
mutation_input: mutationInput,
|
|
1046
|
+
item_id: itemId,
|
|
1047
|
+
expected_revision: expectedRevision,
|
|
1048
|
+
stdout_limit_bytes: stdoutLimit,
|
|
1049
|
+
stderr_limit_bytes: stderrLimit,
|
|
1050
|
+
process,
|
|
1051
|
+
}) {
|
|
1052
|
+
const base = { ok: false, adapter_contract_version: adapterContractVersion, request_id: requestId };
|
|
1053
|
+
const mutation = isMutationCommand(command);
|
|
1054
|
+
const responseContext = coreRequest === null
|
|
1055
|
+
? null
|
|
1056
|
+
: {
|
|
1057
|
+
core_request: coreRequest,
|
|
1058
|
+
mutation_request: mutationRequest,
|
|
1059
|
+
...(mutationInput === undefined ? {} : { mutation_input: mutationInput }),
|
|
1060
|
+
};
|
|
1061
|
+
const issue = observationIssue(process);
|
|
1062
|
+
if (issue) {
|
|
1063
|
+
// A contradicted or unsigned observation on a mutation can never prove the
|
|
1064
|
+
// mutation did not apply, so it is always outcome-unknown. The launchState
|
|
1065
|
+
// 'not-started' is only reachable when issue is null (a clean non-start), so
|
|
1066
|
+
// every truthy issue on a mutation maps to unknown regardless of `started`.
|
|
1067
|
+
if (mutation) {
|
|
1068
|
+
return mutationUnknown(base, command, itemId, expectedRevision, process, issue, responseContext);
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
...base,
|
|
1072
|
+
error: error('core-observation-incomplete', { reason: 'invalid-process-observation', member: issue }),
|
|
1073
|
+
process: processSummary(process, command, responseContext),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (!process.started) {
|
|
1077
|
+
return { ...base, error: error('core-launch-failed', {}), process: processSummary(process, command, responseContext) };
|
|
1078
|
+
}
|
|
1079
|
+
const envelope = envelopeState(process, command, responseContext);
|
|
1080
|
+
const overLimitStreams = capturedStreamsOverLimit(process, stdoutLimit, stderrLimit);
|
|
1081
|
+
const ambiguous = process.timed_out || process.signal !== null
|
|
1082
|
+
|| !process.process_tree_contained || process.orphaned
|
|
1083
|
+
|| !process.stdout_complete || !process.stderr_complete || overLimitStreams.length > 0
|
|
1084
|
+
|| !envelope.present || !envelope.valid
|
|
1085
|
+
|| envelope.mutation_state === 'unknown';
|
|
1086
|
+
if (mutation && ambiguous) {
|
|
1087
|
+
return mutationUnknown(base, command, itemId, expectedRevision, process, null, responseContext);
|
|
1088
|
+
}
|
|
1089
|
+
if (process.timed_out) {
|
|
1090
|
+
return { ...base, error: error('core-timeout', {}), process: processSummary(process, command, responseContext) };
|
|
1091
|
+
}
|
|
1092
|
+
if (process.signal !== null) {
|
|
1093
|
+
return { ...base, error: error('core-signaled', { signal: process.signal }), process: processSummary(process, command, responseContext) };
|
|
1094
|
+
}
|
|
1095
|
+
if (!process.process_tree_contained || process.orphaned) {
|
|
1096
|
+
return { ...base, error: error('core-observation-incomplete', {}), process: processSummary(process, command, responseContext) };
|
|
1097
|
+
}
|
|
1098
|
+
if (!process.stdout_complete || !process.stderr_complete || overLimitStreams.length > 0) {
|
|
1099
|
+
const streams = new Set(overLimitStreams);
|
|
1100
|
+
if (!process.stdout_complete) streams.add('stdout');
|
|
1101
|
+
if (!process.stderr_complete) streams.add('stderr');
|
|
1102
|
+
return {
|
|
1103
|
+
...base,
|
|
1104
|
+
error: error('output-limit-exceeded', { streams: [...streams] }),
|
|
1105
|
+
process: processSummary(process, command, responseContext),
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
if (!envelope.present || !envelope.valid) {
|
|
1109
|
+
return {
|
|
1110
|
+
...base,
|
|
1111
|
+
error: error('core-protocol-error', { envelope_present: envelope.present, envelope_valid: envelope.valid }),
|
|
1112
|
+
process: processSummary(process, command, responseContext),
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|