@quolu/lattice 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lattice.mjs +12 -6
- package/package.json +1 -1
- package/src/cli-command-kind.mjs +6 -0
- package/src/cli-failure-class.mjs +34 -0
- package/src/cli-help.mjs +2 -1
- package/src/runtime-error-reporting.mjs +63 -31
- package/src/runtime-errors.mjs +34 -3
- package/src/todo-cli.mjs +3 -1
package/bin/lattice.mjs
CHANGED
|
@@ -185,8 +185,9 @@ await scheduleRuntimeErrorReport();
|
|
|
185
185
|
* 送信を有効にしていない端末では、設定fileの有無を1回見るだけで終わる。hookは打鍵ごとに走るので見ない。
|
|
186
186
|
*/
|
|
187
187
|
async function scheduleRuntimeErrorReport() {
|
|
188
|
+
// `reporting verify`の直後は送らない。受け口は1分に1回まで受けるので、続けて送ると断られる。
|
|
188
189
|
if (help !== null || args[0] === '--version' || args[0] === 'hooks'
|
|
189
|
-
|| (args[0] === 'runtime-errors' && args[1] === 'report')) return;
|
|
190
|
+
|| (args[0] === 'runtime-errors' && (args[1] === 'report' || (args[1] === 'reporting' && args[2] === 'verify')))) return;
|
|
190
191
|
try {
|
|
191
192
|
const { runtimeErrorReportingEnabled } = await import('../src/runtime-errors.mjs');
|
|
192
193
|
if (!runtimeErrorReportingEnabled()) return;
|
|
@@ -202,11 +203,10 @@ async function scheduleRuntimeErrorReport() {
|
|
|
202
203
|
|
|
203
204
|
// typed契約の外へ漏れた例外を、opt-inのruntime error記録へ残す。どの面で・どの種類の例外で
|
|
204
205
|
// 落ちたかを固定語彙の分類(safe_context)で添える——error_codeだけでは原因を追えなかった。
|
|
206
|
+
// 取消は記録せず、通信の失敗が漏れたものは内部故障と別の記録になる(ADR 0196)。
|
|
205
207
|
function observeInternalFailure(error, commandKind) {
|
|
206
|
-
pendingObservation = import('../src/runtime-errors.mjs').then(({
|
|
207
|
-
|
|
208
|
-
version: packageJson.version, safeContext: runtimeErrorSafeContext({ commandKind, error }),
|
|
209
|
-
});
|
|
208
|
+
pendingObservation = import('../src/runtime-errors.mjs').then(({ observeEscapedCliFailure }) => {
|
|
209
|
+
observeEscapedCliFailure({ error, commandKind, version: packageJson.version });
|
|
210
210
|
}).catch(() => {});
|
|
211
211
|
}
|
|
212
212
|
|
|
@@ -222,7 +222,7 @@ function workingDirectory() {
|
|
|
222
222
|
async function runRuntimeErrorsCli(rest) {
|
|
223
223
|
const runtimeErrors = await import('../src/runtime-errors.mjs');
|
|
224
224
|
const usage = () => {
|
|
225
|
-
process.stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2', code: 'USAGE', message: 'usage: lattice runtime-errors <snapshot [--after-cursor N] [--limit N]|ack <cursor>|diagnostics|resolve <fingerprint>|reopen <fingerprint>|compact|report|reporting <status|enable|disable>> --json' })}\n`);
|
|
225
|
+
process.stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2', code: 'USAGE', message: 'usage: lattice runtime-errors <snapshot [--after-cursor N] [--limit N]|ack <cursor>|diagnostics|resolve <fingerprint>|reopen <fingerprint>|compact|report|reporting <status|enable|disable|verify>> --json' })}\n`);
|
|
226
226
|
return 2;
|
|
227
227
|
};
|
|
228
228
|
const options = { version: packageJson.version };
|
|
@@ -251,6 +251,12 @@ async function runRuntimeErrorsCli(rest) {
|
|
|
251
251
|
result = runtimeErrors.setRuntimeErrorStatus(words[1], words[0] === 'resolve' ? 'resolved' : 'open', options);
|
|
252
252
|
} else if (words[0] === 'compact' && words.length === 1) {
|
|
253
253
|
result = runtimeErrors.compactRuntimeErrors(options);
|
|
254
|
+
} else if (words[0] === 'reporting' && words.length === 2 && words[1] === 'verify') {
|
|
255
|
+
// 空の報告を1通送って経路を確かめる。受領まで確かめられた時だけexit 0。
|
|
256
|
+
const reporting = await import('../src/runtime-error-reporting.mjs');
|
|
257
|
+
result = await reporting.verifyRuntimeErrorReporting(options);
|
|
258
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
259
|
+
return result.outcome === 'verified' ? 0 : 1;
|
|
254
260
|
} else if (words[0] === 'reporting' && words.length === 2 && ['status', 'enable', 'disable'].includes(words[1])) {
|
|
255
261
|
const reporting = await import('../src/runtime-error-reporting.mjs');
|
|
256
262
|
result = words[1] === 'status' ? reporting.runtimeErrorReportingStatus(options)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
package/src/cli-command-kind.mjs
CHANGED
|
@@ -12,6 +12,12 @@ const RUNTIME_SURFACES = Object.freeze({
|
|
|
12
12
|
event: Object.freeze(['verify']),
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* storeも端末の状態も書き換えないと確かめた面(ADR 0196)。通信の失敗が漏れた記録の重大度を決める時に使う
|
|
17
|
+
* ——ここに在る面は、落ちても失うものが無く、打ち直せば戻る。確かめていない面は載せない。
|
|
18
|
+
*/
|
|
19
|
+
export const READ_ONLY_COMMAND_KINDS = new Set(['run.list', 'todo.status', 'todo.verify']);
|
|
20
|
+
|
|
15
21
|
export function cliCommandKind(argv, surfaces = RUNTIME_SURFACES) {
|
|
16
22
|
if (!Array.isArray(argv)) return 'other';
|
|
17
23
|
const [surface, subcommand] = argv;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* typed契約の外へ漏れた例外を、取消・通信の失敗・それ以外に分ける(ADR 0196)。
|
|
3
|
+
*
|
|
4
|
+
* runtime error記録の入口が使う。取消は故障として記録しない。通信の失敗が漏れたものは、内部故障とは
|
|
5
|
+
* 別の記録にする——原因が通信でも、型つきの答えにできず落ちたのは製品の対処不良で、直すのは回線ではなく
|
|
6
|
+
* その面の通信失敗の受け方である。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Nodeとundiciが通信の失敗に付けるcode。端末内のportの取り合い(EADDRINUSE)と、子processとのpipeでも
|
|
10
|
+
// 起きるEPIPEは、通信の失敗に数えない。
|
|
11
|
+
const TRANSPORT_CODES = new Set([
|
|
12
|
+
'ECONNREFUSED', 'ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EHOSTUNREACH', 'EHOSTDOWN', 'ENETUNREACH',
|
|
13
|
+
'ENETDOWN', 'ENOTFOUND', 'EAI_AGAIN', 'ENOTCONN',
|
|
14
|
+
'ERR_SOCKET_CONNECTION_TIMEOUT', 'ERR_SOCKET_CLOSED',
|
|
15
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET', 'UND_ERR_CLOSED',
|
|
16
|
+
]);
|
|
17
|
+
const CAUSE_DEPTH = 4;
|
|
18
|
+
|
|
19
|
+
/** 例外と、その`cause`の連なり。`fetch failed`(TypeError)は、通信のcodeを`cause`の側に持つ。 */
|
|
20
|
+
export function failureCauseChain(error) {
|
|
21
|
+
const chain = [];
|
|
22
|
+
for (let current = error; current !== null && typeof current === 'object' && chain.length < CAUSE_DEPTH;
|
|
23
|
+
current = current.cause) chain.push(current);
|
|
24
|
+
return chain;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `cancelled`(利用者や呼び出し元の取消)・`transport`(通信の失敗)・`internal`(それ以外)。 */
|
|
28
|
+
export function cliFailureClass(error) {
|
|
29
|
+
const chain = failureCauseChain(error);
|
|
30
|
+
if (chain.some((entry) => entry.name === 'AbortError' || entry.code === 'ABORT_ERR')) return 'cancelled';
|
|
31
|
+
// `AbortSignal.timeout`の時間切れは`TimeoutError`で届く。
|
|
32
|
+
if (chain.some((entry) => entry.name === 'TimeoutError' || TRANSPORT_CODES.has(entry.code))) return 'transport';
|
|
33
|
+
return 'internal';
|
|
34
|
+
}
|
package/src/cli-help.mjs
CHANGED
|
@@ -198,6 +198,7 @@ Commands:
|
|
|
198
198
|
compact
|
|
199
199
|
report 未受領の記録をBugHubの受け口へ送る(送信を有効にした端末だけ)
|
|
200
200
|
reporting <status|enable|disable>
|
|
201
|
+
reporting verify 空の報告を1通送り、合鍵と受け口までの経路を確かめる(記録には触れない)
|
|
201
202
|
`,
|
|
202
203
|
bridge: `Usage: lattice bridge <command> [options] --json
|
|
203
204
|
|
|
@@ -315,7 +316,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
315
316
|
'runtime-errors reopen': 'runtime-errors reopen <fingerprint> --json',
|
|
316
317
|
'runtime-errors compact': 'runtime-errors compact --json',
|
|
317
318
|
'runtime-errors report': 'runtime-errors report --json',
|
|
318
|
-
'runtime-errors reporting': 'runtime-errors reporting <status|enable|disable> --json',
|
|
319
|
+
'runtime-errors reporting': 'runtime-errors reporting <status|enable|disable|verify> --json',
|
|
319
320
|
'bridge setup': 'bridge setup --listen <IP> [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...] --json',
|
|
320
321
|
'bridge reconfigure': 'bridge reconfigure [--listen <IP>] [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...] --json',
|
|
321
322
|
'bridge status': 'bridge status --json',
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
* - 受領済みにするのは、200・`accepted: true`・`report_id`一致・応答の署名一致がそろった時だけ。
|
|
32
32
|
* そろわなければ「届いたか不明」として記録を未受領のまま残し、後から今の累計を送り直す。
|
|
33
33
|
* - 送るのは`runtime-errors snapshot`が出す項目だけ。message本文・path・引数・stackは記録に無い。
|
|
34
|
+
* - `reporting verify`は、記録を載せない空の報告を1通送って、合鍵・署名・受け口までの経路を確かめる。
|
|
35
|
+
* 受け口は受領を記録するだけで、issueも通知も動かない。storeの記録と受領の印には触れない。
|
|
34
36
|
*/
|
|
35
37
|
|
|
36
38
|
const REPORT_SCHEMA_VERSION = '1.0';
|
|
@@ -275,6 +277,41 @@ function parseJson(text) {
|
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 報告を1通、署名して送り、受領を確かめる。`accepted`を返すのは、200・`accepted: true`・`report_id`一致・
|
|
282
|
+
* 応答の署名一致がそろった時だけ。戻り値に秘密・key_id・宛先は入れない。
|
|
283
|
+
*/
|
|
284
|
+
async function sendReport({ url, keyId, secret }, snapshot, options) {
|
|
285
|
+
const now = nowMs(options);
|
|
286
|
+
const ts = Math.floor(now / 1000);
|
|
287
|
+
// `observed_at`は、載せる記録のどの時刻よりも前にしない(BugHubは観測より後の記録を断る)。秒へ切り捨てると、
|
|
288
|
+
// 記録と同じ1秒の中で送った時に記録より前へずれる。`ts`とは同じ時刻から作り、差はBugHubの許す幅(10分)に収まる。
|
|
289
|
+
const observed = Math.max(now, ...snapshot.runtime_errors.map((record) => Date.parse(record.last_seen)),
|
|
290
|
+
...snapshot.resolutions.map((resolution) => Date.parse(resolution.resolved_at)));
|
|
291
|
+
const report = buildRuntimeErrorReport({ snapshot, version: options.version ?? 'unknown',
|
|
292
|
+
reportId: options.reportId ?? randomUUID(), observedAt: new Date(observed).toISOString() });
|
|
293
|
+
const bodyBytes = Buffer.from(JSON.stringify(report), 'utf8');
|
|
294
|
+
if (bodyBytes.length > REPORT_MAX_BYTES) return { outcome: 'too_large', reason: 'report_too_large', http_status: null };
|
|
295
|
+
|
|
296
|
+
const response = await post(url, {
|
|
297
|
+
'Content-Type': 'application/json',
|
|
298
|
+
'Content-Length': String(bodyBytes.length),
|
|
299
|
+
Authorization: `BugHub-HMAC-SHA256 key_id=${keyId}, ts=${ts}, sig=${signReport(secret, String(ts), bodyBytes)}`,
|
|
300
|
+
}, bodyBytes, options.timeoutMs ?? REQUEST_TIMEOUT_MS);
|
|
301
|
+
|
|
302
|
+
if (response.status === null) return { outcome: 'unconfirmed', reason: response.reason, http_status: null };
|
|
303
|
+
const body = parseJson(response.body);
|
|
304
|
+
if (response.status === 200) {
|
|
305
|
+
if (body === null || body.accepted !== true || body.report_id !== report.report_id
|
|
306
|
+
|| !receiptSignatureMatches(secret, report.report_id, body.received_at, body.sig)) {
|
|
307
|
+
return { outcome: 'unconfirmed', reason: 'receipt_not_verified', http_status: 200 };
|
|
308
|
+
}
|
|
309
|
+
return { outcome: 'accepted', reason: null, http_status: 200 };
|
|
310
|
+
}
|
|
311
|
+
const code = typeof body?.error === 'string' && REJECTION_CODE.test(body.error) ? body.error : 'unrecognized_response';
|
|
312
|
+
return { outcome: response.status >= 500 ? 'unconfirmed' : 'rejected', reason: code, http_status: response.status };
|
|
313
|
+
}
|
|
314
|
+
|
|
278
315
|
/**
|
|
279
316
|
* 未受領の記録を1回送る。`options.auto`の時だけ送る時機の制限を見る(手で打った`report`は見ない
|
|
280
317
|
* ——多すぎればBugHubが429で断る)。戻り値に秘密・key_id・宛先は入れない。
|
|
@@ -305,13 +342,6 @@ export async function reportRuntimeErrors(options = {}) {
|
|
|
305
342
|
if (reason !== null) return result('throttled', { reason });
|
|
306
343
|
}
|
|
307
344
|
|
|
308
|
-
// `ts`と`observed_at`は同じ時刻から作る(BugHubは10分より離れた組を断る)。
|
|
309
|
-
const ts = Math.floor(nowMs(options) / 1000);
|
|
310
|
-
const report = buildRuntimeErrorReport({ snapshot: pending, version: options.version ?? 'unknown',
|
|
311
|
-
reportId: options.reportId ?? randomUUID(), observedAt: new Date(ts * 1000).toISOString() });
|
|
312
|
-
const bodyBytes = Buffer.from(JSON.stringify(report), 'utf8');
|
|
313
|
-
if (bodyBytes.length > REPORT_MAX_BYTES) return result('rejected', { reason: 'report_too_large', sent });
|
|
314
|
-
|
|
315
345
|
const finish = (outcome, extra = {}) => {
|
|
316
346
|
try {
|
|
317
347
|
writeDelivery(options, { schema: DELIVERY_SCHEMA, last_attempt_at: new Date(nowMs(options)).toISOString(),
|
|
@@ -320,31 +350,33 @@ export async function reportRuntimeErrors(options = {}) {
|
|
|
320
350
|
return result(outcome, { sent, acknowledged_through: pending.cursor.acknowledged_through, ...extra });
|
|
321
351
|
};
|
|
322
352
|
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (response.status === 200) {
|
|
333
|
-
if (body === null || body.accepted !== true || body.report_id !== report.report_id
|
|
334
|
-
|| !receiptSignatureMatches(secret, report.report_id, body.received_at, body.sig)) {
|
|
335
|
-
return finish('unconfirmed', { reason: 'receipt_not_verified', http_status: 200 });
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
const acknowledged = acknowledgeRuntimeErrors(pending.cursor.next, options);
|
|
339
|
-
return finish('delivered', { http_status: 200, acknowledged_through: acknowledged.cursor.acknowledged_through });
|
|
340
|
-
} catch {
|
|
341
|
-
// 届いているが受領済みを残せなかった。次の送信が同じ累計を送り直す(BugHubは二重に数えない)。
|
|
342
|
-
return finish('unconfirmed', { reason: 'ack_not_recorded', http_status: 200 });
|
|
343
|
-
}
|
|
353
|
+
const answer = await sendReport(credential.credential, pending, options);
|
|
354
|
+
if (answer.outcome === 'too_large') return result('rejected', { reason: 'report_too_large', sent });
|
|
355
|
+
if (answer.outcome !== 'accepted') return finish(answer.outcome, { reason: answer.reason, http_status: answer.http_status });
|
|
356
|
+
try {
|
|
357
|
+
const acknowledged = acknowledgeRuntimeErrors(pending.cursor.next, options);
|
|
358
|
+
return finish('delivered', { http_status: 200, acknowledged_through: acknowledged.cursor.acknowledged_through });
|
|
359
|
+
} catch {
|
|
360
|
+
// 届いているが受領済みを残せなかった。次の送信が同じ累計を送り直す(BugHubは二重に数えない)。
|
|
361
|
+
return finish('unconfirmed', { reason: 'ack_not_recorded', http_status: 200 });
|
|
344
362
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 記録を載せない空の報告を1通送り、合鍵・署名・受け口までの経路を確かめる(`reporting verify`)。
|
|
367
|
+
* 送る条件は`report`と同じで、送信を有効にしていない端末と合鍵の無い端末はnetworkへ触れない。
|
|
368
|
+
* storeの記録・受領の印・送る時機の記録には触れない。受け口は端末×製品ごとに1分に1回まで受けるので、
|
|
369
|
+
* 直前に送っていれば429で断られる。
|
|
370
|
+
*/
|
|
371
|
+
export async function verifyRuntimeErrorReporting(options = {}) {
|
|
372
|
+
if (!runtimeErrorCollectionSupported(options)) return result('unsupported');
|
|
373
|
+
if (!runtimeErrorReportingEnabled(options)) return result('disabled');
|
|
374
|
+
const credential = readProductCredential(options);
|
|
375
|
+
if (credential.status === 'missing') return result('credential_missing');
|
|
376
|
+
if (credential.status !== 'ok') return result('credential_unsafe', { reason: credential.reason });
|
|
377
|
+
const answer = await sendReport(credential.credential, { runtime_errors: [], resolutions: [] }, options);
|
|
378
|
+
if (answer.outcome === 'accepted') return result('verified', { http_status: 200 });
|
|
379
|
+
return result(answer.outcome, { reason: answer.reason, http_status: answer.http_status });
|
|
348
380
|
}
|
|
349
381
|
|
|
350
382
|
/**
|
package/src/runtime-errors.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
import { homedir, platform as hostPlatform, arch as hostArch } from 'node:os';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
|
|
8
|
+
import { READ_ONLY_COMMAND_KINDS } from './cli-command-kind.mjs';
|
|
9
|
+
import { cliFailureClass, failureCauseChain } from './cli-failure-class.mjs';
|
|
8
10
|
import {
|
|
9
11
|
daclIsOwnerOnly, daclIsProtected, isAclScratchName, readWindowsDacl, restrictWindowsDirToOwner, windowsSelfSid,
|
|
10
12
|
} from './windows-owner-only.mjs';
|
|
@@ -40,6 +42,9 @@ const definitions = Object.freeze({
|
|
|
40
42
|
'LATTICE.RUN_STORE_IO_FAILED': { component: 'run_store', severity: 'high', template: 'Lattice run store IO failed' },
|
|
41
43
|
'LATTICE.EVENT_CHAIN_INTEGRITY_FAILED': { component: 'event_store', severity: 'high', template: 'Lattice run event chain integrity check failed' },
|
|
42
44
|
'LATTICE.CLI_INTERNAL_FAILED': { component: 'cli', severity: 'high', template: 'Lattice CLI crashed outside the typed error contract' },
|
|
45
|
+
// 通信の失敗が型つき契約の外へ漏れた記録(ADR 0196)。直すのは回線ではなく、その面の通信失敗の受け方。
|
|
46
|
+
// 重大度は下の`severityOf`が、落ちた面で失うものがあるかで決める。
|
|
47
|
+
'LATTICE.CLI_TRANSPORT_UNHANDLED': { component: 'cli', severity: 'high', template: 'Lattice CLI let a communication failure escape the typed error contract' },
|
|
43
48
|
'LATTICE.MCP_SERVER_FAILED': { component: 'mcp', severity: 'high', template: 'Lattice MCP server failed' },
|
|
44
49
|
});
|
|
45
50
|
|
|
@@ -169,13 +174,28 @@ const validSafeContext = (value) => plain(value) && exact(value, SAFE_CONTEXT_KE
|
|
|
169
174
|
*/
|
|
170
175
|
export function runtimeErrorSafeContext({ commandKind, error } = {}) {
|
|
171
176
|
const errorKind = error?.constructor?.name;
|
|
177
|
+
// `fetch failed`のように、codeを`cause`の側に持つ例外がある。連なりの中で最初に語彙へ合うcodeを載せる。
|
|
178
|
+
const causeCode = failureCauseChain(error).map((entry) => entry.code)
|
|
179
|
+
.find((code) => typeof code === 'string' && CAUSE_CODE.test(code));
|
|
172
180
|
return {
|
|
173
181
|
command_kind: validCommandKind(commandKind) ? commandKind : 'other',
|
|
174
182
|
error_kind: ERROR_KINDS.has(errorKind) ? errorKind : 'other',
|
|
175
|
-
cause_code:
|
|
183
|
+
cause_code: causeCode ?? 'none',
|
|
176
184
|
};
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
/**
|
|
188
|
+
* 記録の重大度。error_codeだけでは決めない記録がある(ADR 0196): 通信の失敗が漏れた記録は、落ちた面が
|
|
189
|
+
* 何も書き換えないと確かめてある時だけ`warn`——その1回が止まっただけで、失うものが無く、打ち直せば戻る。
|
|
190
|
+
* 書き換える面と、確かめていない面は`high`のまま(結果が分からず、不整合や重複を否定できない)。
|
|
191
|
+
* 分類と面から決まる値なので、同じfingerprintの記録は必ず同じ重大度になる。
|
|
192
|
+
*/
|
|
193
|
+
function severityOf(code, safeContext = null) {
|
|
194
|
+
if (code === 'LATTICE.CLI_TRANSPORT_UNHANDLED' && safeContext !== null
|
|
195
|
+
&& READ_ONLY_COMMAND_KINDS.has(safeContext.command_kind)) return 'warn';
|
|
196
|
+
return definitions[code].severity;
|
|
197
|
+
}
|
|
198
|
+
|
|
179
199
|
/**
|
|
180
200
|
* 旧記録(safe_context無し)はerror_codeだけで決まる式のまま。新しい記録は3つの分類も含める
|
|
181
201
|
* ——原因が違えば別の記録になる。式はdotagentsのadapterが再計算して照合する契約で、
|
|
@@ -269,7 +289,7 @@ function validate(store) {
|
|
|
269
289
|
const definition = definitions[record.error_code];
|
|
270
290
|
if (!definition || record.product !== PRODUCT || !validVersion(record.product_version)
|
|
271
291
|
|| record.component !== definition.component || record.message_template !== definition.template
|
|
272
|
-
|| record.severity !==
|
|
292
|
+
|| record.severity !== severityOf(record.error_code, safeContext) || record.fingerprint !== fingerprintOf(record.error_code, safeContext)
|
|
273
293
|
|| seen.has(record.fingerprint) || !Number.isSafeInteger(record.count) || record.count < 1
|
|
274
294
|
|| !validTime(record.first_seen) || !validTime(record.last_seen)
|
|
275
295
|
|| Date.parse(record.first_seen) > Date.parse(record.last_seen)
|
|
@@ -442,7 +462,7 @@ export function recordRuntimeError(code, options = {}) {
|
|
|
442
462
|
if (store.records.length >= MAX_RECORDS) throw Error('store_overflow');
|
|
443
463
|
store.records.push({
|
|
444
464
|
product: PRODUCT, product_version: version, component: definition.component, error_code: code,
|
|
445
|
-
message_template: definition.template, severity:
|
|
465
|
+
message_template: definition.template, severity: severityOf(code, safeContext), fingerprint: key,
|
|
446
466
|
count: 1, first_seen: time, last_seen: time, state_schema_version: STATE_VERSION,
|
|
447
467
|
os, arch, status: 'open', resolved_at: null, reason_code: null, sequence,
|
|
448
468
|
safe_context: { ...safeContext },
|
|
@@ -462,6 +482,17 @@ export function observeRuntimeError(code, options = {}) {
|
|
|
462
482
|
}
|
|
463
483
|
}
|
|
464
484
|
|
|
485
|
+
/**
|
|
486
|
+
* CLIのtyped契約の外へ漏れた例外を記録する(process境界のbinが呼ぶ)。取消は故障ではないので記録しない。
|
|
487
|
+
* 通信の失敗が漏れたものは内部故障と別の記録にし、重大度を面で決める(ADR 0196)。
|
|
488
|
+
*/
|
|
489
|
+
export function observeEscapedCliFailure({ error, commandKind, ...options }) {
|
|
490
|
+
const failureClass = cliFailureClass(error);
|
|
491
|
+
if (failureClass === 'cancelled') return;
|
|
492
|
+
observeRuntimeError(failureClass === 'transport' ? 'LATTICE.CLI_TRANSPORT_UNHANDLED' : 'LATTICE.CLI_INTERNAL_FAILED',
|
|
493
|
+
{ ...options, safeContext: runtimeErrorSafeContext({ commandKind, error }) });
|
|
494
|
+
}
|
|
495
|
+
|
|
465
496
|
export function acknowledgeRuntimeErrors(cursor, options = {}) {
|
|
466
497
|
requireCursor(cursor);
|
|
467
498
|
if (!collectionEnabled(options)) return snapshot({ ...options, afterCursor: cursor });
|
package/src/todo-cli.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cliCommandKind } from './cli-command-kind.mjs';
|
|
2
|
+
import { cliFailureClass } from './cli-failure-class.mjs';
|
|
2
3
|
import { gitSync } from './git-process.mjs';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import {
|
|
@@ -4253,7 +4254,8 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env,
|
|
|
4253
4254
|
} catch (error) {
|
|
4254
4255
|
if (error instanceof TodoStoreError || (typeof error?.code === 'string'
|
|
4255
4256
|
&& error.detail !== null && typeof error.detail === 'object')) return typedFailure(stderr, error);
|
|
4256
|
-
|
|
4257
|
+
// 漏れた`fetch failed`もTypeErrorで届く。呼び出し側の契約違反と答えず、内部故障として観測口へ渡す。
|
|
4258
|
+
if (error instanceof TypeError && cliFailureClass(error) !== 'transport') {
|
|
4257
4259
|
return typedFailure(stderr, {
|
|
4258
4260
|
code: 'CONTRACT_VIOLATION',
|
|
4259
4261
|
message: error.message,
|