onto-mcp 0.4.11 → 0.4.13

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 (139) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +153 -0
  3. package/.onto/domains/accounting-kr/competency_qs.md +153 -153
  4. package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
  5. package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
  6. package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
  7. package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +202 -49
  8. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
  9. package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
  10. package/.onto/processes/review/nesting-batch-worker-contract.md +1 -1
  11. package/.onto/processes/review/productized-live-path.md +9 -5
  12. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  13. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  14. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  15. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  16. package/README.md +64 -1
  17. package/dist/cli.js +11 -1
  18. package/dist/core-api/reconstruct-api.js +679 -18
  19. package/dist/core-api/review-api.js +70 -5
  20. package/dist/core-api/review-progress.js +7 -0
  21. package/dist/core-api/runtime-observation.js +7 -0
  22. package/dist/core-runtime/artifact-io.js +131 -0
  23. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
  24. package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
  25. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  26. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  27. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  28. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  29. package/dist/core-runtime/cli/run-seats.js +18 -0
  30. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  31. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  32. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  33. package/dist/core-runtime/discovery/host-detection.js +71 -22
  34. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  35. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  36. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  37. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  38. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  39. package/dist/core-runtime/discovery/supported-models.js +289 -25
  40. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  46. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  47. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  48. package/dist/core-runtime/effort-calibration-ingest.js +326 -0
  49. package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
  50. package/dist/core-runtime/effort-calibration-review.js +22 -0
  51. package/dist/core-runtime/effort-calibration-sweep.js +110 -0
  52. package/dist/core-runtime/effort-frontier.js +134 -0
  53. package/dist/core-runtime/llm/claude-bin.js +77 -0
  54. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  55. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  56. package/dist/core-runtime/llm/llm-caller.js +316 -35
  57. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  58. package/dist/core-runtime/llm/model-switcher.js +1 -1
  59. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  60. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  61. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  62. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  63. package/dist/core-runtime/onboard/cli-host.js +5 -2
  64. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  65. package/dist/core-runtime/onboard/host-target.js +28 -3
  66. package/dist/core-runtime/onboard/register.js +54 -12
  67. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  68. package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
  69. package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
  70. package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
  71. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  72. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  73. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  74. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  75. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  76. package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
  77. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  78. package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
  79. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  80. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  81. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  82. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  83. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  84. package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
  85. package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
  86. package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
  87. package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
  88. package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
  89. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  90. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
  91. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  92. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
  93. package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
  94. package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
  95. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
  96. package/dist/core-runtime/reconstruct/record.js +49 -3
  97. package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
  98. package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
  99. package/dist/core-runtime/reconstruct/run.js +5409 -517
  100. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
  101. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
  102. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
  103. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  104. package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
  105. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
  106. package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
  107. package/dist/core-runtime/review/continuation-plan.js +12 -5
  108. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  109. package/dist/core-runtime/review/materializers.js +100 -11
  110. package/dist/core-runtime/review/nesting-batch.js +6 -2
  111. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  112. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  113. package/dist/core-runtime/review/review-artifact-utils.js +219 -7
  114. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  115. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  116. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  117. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  118. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  119. package/dist/core-runtime/route-identity.js +182 -0
  120. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  121. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  122. package/dist/core-runtime/target-material-kind.js +58 -0
  123. package/dist/mcp/server.js +27 -5
  124. package/dist/mcp/tool-schemas.js +8 -0
  125. package/dist/tui/app.js +167 -0
  126. package/dist/tui/data/event-follower.js +116 -0
  127. package/dist/tui/data/node-detail.js +42 -0
  128. package/dist/tui/data/projection-poll.js +26 -0
  129. package/dist/tui/data/session-discovery.js +65 -0
  130. package/dist/tui/index.js +108 -0
  131. package/dist/tui/screens/log.js +25 -0
  132. package/dist/tui/screens/node-detail.js +17 -0
  133. package/dist/tui/screens/session-selector.js +26 -0
  134. package/dist/tui/screens/workflow-tree.js +90 -0
  135. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  136. package/dist/tui/view-model/review-adapter.js +270 -0
  137. package/dist/tui/view-model/tree-view-model.js +19 -0
  138. package/package.json +20 -3
  139. package/settings.example.json +77 -16
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Dispatch limit/transport circuit breaker — pure policy logic (no I/O, no
3
+ * timers of its own, no LLM calls).
4
+ *
5
+ * 설계 B (development-records/design/20260704-review-unit-resubmit-and-limit-breaker-design.md §4):
6
+ * an unattended batch must not throw a retry storm at a dead rate limit and
7
+ * lose items. The repo has no common dispatch surface (§8 재앵커링), so the
8
+ * policy is injected per loop. Currently wired: the reconstruct semantic-map
9
+ * judgment loop and the review lens/stance fan-out pools — both the flat
10
+ * per-unit loops AND the nested-workers first-attempt batch (§4-1: a batch-window
11
+ * SUCCESS is recorded skipped so a stale batch success never resets the streak,
12
+ * while a batch-window FAILURE is classified like any failure; the
13
+ * directly-observed flat retries drive the streak).
14
+ *
15
+ * 1. backoff first — a per-item failure counts toward the breaker only after
16
+ * the item's bounded backoff retries are exhausted. Providers surface 429s
17
+ * as opaque message strings (no Retry-After anywhere), so the schedule is
18
+ * a capped exponential.
19
+ * 2. systemic detection — only consecutive FINAL item failures of a systemic
20
+ * class (rate_limit / auth / transport) across DISTINCT items count;
21
+ * threshold N via settings. A success resets the streak.
22
+ * 3. poison item — an item that keeps failing while the streak stays below N
23
+ * is dead-lettered (complete-with-failure) and the batch continues.
24
+ * 4. breaker trip — the loop halts the batch and persists the incomplete-item
25
+ * list (fallback provider swap is a deferred later cut).
26
+ * 5. recovery re-dispatch targets exactly the persisted incomplete set. For
27
+ * reconstruct semantic-map, stage-local resume validates the same-batch
28
+ * partition plus prior census/sidecar truth before reusing retained rows
29
+ * and re-dispatching incomplete rows.
30
+ *
31
+ * File I/O (persisting the artifact), timestamps, and halt mechanics stay in
32
+ * the wiring; this module owns classification, the backoff schedule, the
33
+ * breaker state machine, and the artifact projection.
34
+ */
35
+ import path from "node:path";
36
+ import { readStructuredDispatchFailureClass, readStructuredDispatchFailureEvidence, } from "./structured-dispatch-error.js";
37
+ const RATE_LIMIT_PATTERNS = [
38
+ "429",
39
+ "rate limit",
40
+ "limit reached",
41
+ "rate_limit",
42
+ "too many requests",
43
+ "overloaded",
44
+ "session limit",
45
+ "usage limit",
46
+ "quota",
47
+ "retry-after",
48
+ "retry_after",
49
+ ];
50
+ const AUTH_PATTERNS = [
51
+ "401",
52
+ "403",
53
+ "unauthorized",
54
+ "forbidden",
55
+ "invalid api key",
56
+ "invalid x-api-key",
57
+ "authentication",
58
+ "not logged in",
59
+ ];
60
+ /** Single source for the transient-transport message patterns shared with the
61
+ * review runner's per-unit retry decision (isTransientExecutorFailureMessage)
62
+ * — the two consumers differ only in their extras. */
63
+ export const TRANSIENT_TRANSPORT_MESSAGE_PATTERNS = [
64
+ "stream disconnected before completion",
65
+ "connection reset by peer",
66
+ "error sending request",
67
+ "failed to connect to websocket",
68
+ "transport channel closed",
69
+ "http/request failed",
70
+ "request failed after",
71
+ ];
72
+ const TRANSPORT_PATTERNS = [
73
+ ...TRANSIENT_TRANSPORT_MESSAGE_PATTERNS,
74
+ "timed out",
75
+ "timeout",
76
+ "econnrefused",
77
+ "econnreset",
78
+ "etimedout",
79
+ "socket hang up",
80
+ "fetch failed",
81
+ ];
82
+ /**
83
+ * Classify a failure message into a systemic dispatch class, or null for
84
+ * item-local failures (malformed output, validation rejection, …) that must
85
+ * never trip the batch breaker. Message-based by necessity: both adapter
86
+ * families flatten provider status into strings (재앵커링 노트 (4)).
87
+ */
88
+ export function classifySystemicDispatchFailure(message) {
89
+ if (typeof message !== "string" || message.length === 0)
90
+ return null;
91
+ const normalized = message.toLowerCase();
92
+ if (RATE_LIMIT_PATTERNS.some((pattern) => normalized.includes(pattern))) {
93
+ return "rate_limit";
94
+ }
95
+ if (AUTH_PATTERNS.some((pattern) => normalized.includes(pattern))) {
96
+ return "auth";
97
+ }
98
+ if (TRANSPORT_PATTERNS.some((pattern) => normalized.includes(pattern))) {
99
+ return "transport";
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Classify a dispatch ERROR: structured provider status first (the SDK
105
+ * adapters rethrow the original error with `.status` preserved), message
106
+ * substrings as the fallback for the CLI/worker adapters that flatten
107
+ * everything into text.
108
+ */
109
+ export function classifyDispatchError(error) {
110
+ const structured = readStructuredDispatchFailureEvidence(error);
111
+ if (structured)
112
+ return structured.failure_class;
113
+ const status = error?.status;
114
+ if (typeof status === "number") {
115
+ if (status === 429)
116
+ return "rate_limit";
117
+ if (status === 401 || status === 403)
118
+ return "auth";
119
+ if (status >= 500)
120
+ return "transport";
121
+ }
122
+ return classifySystemicDispatchFailure(error instanceof Error ? error.message : String(error));
123
+ }
124
+ /** Marker stamped by {@link runWithDispatchBackoff} on errors that came from
125
+ * an actual provider dispatch. Item-level attribution MUST read the marker
126
+ * (readDispatchFailureClass) instead of re-classifying arbitrary error text —
127
+ * deterministic stage errors embed content-derived text (sheet names, row
128
+ * ranges) that substring patterns would misread as systemic. */
129
+ const DISPATCH_FAILURE_CLASS = Symbol.for("onto.dispatch_failure_class");
130
+ function markDispatchFailureClass(error, failureClass) {
131
+ if (error !== null && typeof error === "object") {
132
+ error[DISPATCH_FAILURE_CLASS] = failureClass;
133
+ }
134
+ }
135
+ /** Systemic class of a dispatch-marked error; null for unmarked errors
136
+ * (deterministic/stage-local throws) and marked-but-item-local failures. */
137
+ export function readDispatchFailureClass(error) {
138
+ const structured = readStructuredDispatchFailureEvidence(error);
139
+ if (structured)
140
+ return readStructuredDispatchFailureClass(error);
141
+ if (error === null || typeof error !== "object")
142
+ return null;
143
+ const failureClass = error[DISPATCH_FAILURE_CLASS];
144
+ return failureClass === "rate_limit" || failureClass === "auth" || failureClass === "transport"
145
+ ? failureClass
146
+ : null;
147
+ }
148
+ /** Capped exponential backoff (no jitter — deterministic for replay/tests).
149
+ * attempt is 0-based: delay before retry #1 is initialMs. */
150
+ export function dispatchBackoffDelayMs(args) {
151
+ const exponential = args.initialMs * 2 ** Math.max(0, args.attempt);
152
+ const bounded = Math.min(args.capMs, exponential);
153
+ return Number.isFinite(bounded) && bounded > 0 ? Math.floor(bounded) : args.capMs;
154
+ }
155
+ /**
156
+ * Breaker state machine over one batch. The loop reports each item's FINAL
157
+ * outcome (after its bounded retries); the machine answers "has this become
158
+ * systemic?" and keeps the dead-letter/completion bookkeeping the wiring
159
+ * persists.
160
+ *
161
+ * Poison-vs-systemic attribution rule: a systemic-class failure is held
162
+ * PENDING until the batch proves the provider lane is alive (a later item
163
+ * succeeds) — only then is it a poison item (reproduced on that item alone)
164
+ * and dead-lettered. If the streak instead reaches the threshold, the batch
165
+ * trips and the pending items stay in the INCOMPLETE set: they were victims
166
+ * of the outage and must be re-dispatched on recovery (규칙 5), not
167
+ * complete-with-failure. Item-local failures (null class) dead-letter
168
+ * immediately and say nothing about the provider, so they neither extend nor
169
+ * reset the systemic streak.
170
+ */
171
+ export class DispatchBreakerState {
172
+ policy;
173
+ pendingSystemic = [];
174
+ trip = null;
175
+ completed = [];
176
+ deadLetter = [];
177
+ constructor(policy) {
178
+ this.policy = policy;
179
+ }
180
+ /** Report a REAL dispatch success — the only event that proves the
181
+ * provider lane is alive and may reclassify pending systemic failures as
182
+ * poison. Items that made no successful provider call must use
183
+ * {@link recordItemSkipped} instead. */
184
+ recordItemSuccess(itemId) {
185
+ this.completed.push(itemId);
186
+ // Attribution freezes at trip: a CONCURRENT pool (review lens/stance)
187
+ // can deliver an in-flight success after the trip decision, and letting
188
+ // it reclassify the pending outage victims as poison would dead-letter
189
+ // them OUT of the incomplete recovery set (규칙 5 위반). The late unit
190
+ // itself still counts as completed.
191
+ if (this.trip !== null)
192
+ return;
193
+ // Concurrent pool (opt-in, F1): during a concurrent burst a success does
194
+ // NOT prove the whole lane is alive (a partial rate-limit yields some 200s
195
+ // and some 429s), and letting completion order decide which pending victims
196
+ // become poison makes the trip/classification non-deterministic. In this
197
+ // mode systemic victims stay pending — the trip is count-based and
198
+ // order-independent; un-tripped victims end as incomplete (review recovery
199
+ // re-derives from the frontier either way). Sequential callers omit the flag
200
+ // and keep the poison-via-later-success attribution.
201
+ if (this.policy.concurrent)
202
+ return;
203
+ // The provider lane is alive: pending systemic failures were item-scoped
204
+ // after all — poison, dead-lettered.
205
+ for (const entry of this.pendingSystemic)
206
+ this.deadLetter.push(entry);
207
+ this.pendingSystemic = [];
208
+ }
209
+ /** Report an item that owes no dispatch (structural skip, budget cap, all
210
+ * subsumed): completed for recovery-set purposes, but it proves NOTHING
211
+ * about the provider lane — the systemic streak and pending attribution
212
+ * are untouched. Conflating this with success let one interleaved skip
213
+ * reset an outage streak and write its victims off as poison. */
214
+ recordItemSkipped(itemId) {
215
+ this.completed.push(itemId);
216
+ }
217
+ /** Report an item's FINAL failure (per-item budget exhausted). Returns the
218
+ * trip state when this failure crosses the systemic threshold. */
219
+ recordItemFailure(entry) {
220
+ if (entry.failure_class === null) {
221
+ // Item-local failure class: dead-letter, never breaker fuel.
222
+ this.deadLetter.push(entry);
223
+ return null;
224
+ }
225
+ if (!this.pendingSystemic.some((pending) => pending.item_id === entry.item_id)) {
226
+ // Post-trip in-flight systemic failures still join the pending set —
227
+ // they are outage victims and belong to the incomplete recovery set.
228
+ this.pendingSystemic.push(entry);
229
+ }
230
+ if (this.policy.enabled &&
231
+ this.trip === null &&
232
+ this.pendingSystemic.length >= this.policy.systemic_threshold) {
233
+ // The FIRST crossing is the trip authority; later records must not
234
+ // rewrite its count. Concurrent-mode guarantee (F1): the trip DECISION
235
+ // (bool), `consecutive_item_count`, and the completed/dead-letter/
236
+ // incomplete SETS are order-independent (pendingSystemic is never
237
+ // flushed). `failure_class` is NOT: the trip fires early on the
238
+ // first-N-to-complete prefix, so a mixed-class burst labels the trip by
239
+ // whichever systemic class happened to cross. It is a best-effort
240
+ // diagnostic label (halt_reason / disclosure), never recovery-relevant,
241
+ // and is left as the crossing item's class — a deterministic full-batch
242
+ // class summary, if ever needed, belongs at end-of-batch over the full
243
+ // pending set, not at this early trip point (F1 후속).
244
+ this.trip = {
245
+ failure_class: entry.failure_class,
246
+ consecutive_item_count: this.pendingSystemic.length,
247
+ threshold: this.policy.systemic_threshold,
248
+ };
249
+ return this.trip;
250
+ }
251
+ return null;
252
+ }
253
+ tripped() {
254
+ return this.trip;
255
+ }
256
+ completedItemIds() {
257
+ return this.completed;
258
+ }
259
+ deadLetterEntries() {
260
+ return this.deadLetter;
261
+ }
262
+ }
263
+ export class DispatchBreakerTrippedError extends Error {
264
+ trip;
265
+ constructor(trip, incompleteArtifactPath, options) {
266
+ super(`dispatch breaker tripped: ${trip.failure_class} failed ${trip.consecutive_item_count} consecutive items (threshold ${trip.threshold}) — batch halted, incomplete items persisted for exact re-dispatch${incompleteArtifactPath ? ` (${incompleteArtifactPath})` : ""}${options?.fallbackOutcomePath ? `; fallback outcome: ${options.fallbackOutcomePath}` : ""}`);
267
+ this.name = "DispatchBreakerTrippedError";
268
+ this.trip = trip;
269
+ if (options?.structuredContributors) {
270
+ this.structuredContributors = options.structuredContributors.map((entry) => structuredClone(entry));
271
+ }
272
+ if (options?.fallbackOutcomePath) {
273
+ this.fallbackOutcomePath = options.fallbackOutcomePath;
274
+ }
275
+ }
276
+ }
277
+ /**
278
+ * Run one dispatch under the backoff-first rule (규칙 1): systemic-class
279
+ * errors are retried with capped exponential backoff up to the per-item
280
+ * attempt cap; item-local errors (null class) never retry here — the item's
281
+ * own semantics own those. On exhaustion the LAST error is rethrown so the
282
+ * caller can classify it and report the item's FINAL outcome to
283
+ * {@link DispatchBreakerState}. `sleep` is injected so fixtures stay
284
+ * deterministic and fast.
285
+ */
286
+ export async function runWithDispatchBackoff(args) {
287
+ const maxAttempts = Math.max(1, args.policy.per_call_max_attempts);
288
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
289
+ try {
290
+ return await args.dispatch();
291
+ }
292
+ catch (error) {
293
+ const message = error instanceof Error ? error.message : String(error);
294
+ const failureClass = classifyDispatchError(error);
295
+ markDispatchFailureClass(error, failureClass);
296
+ const retryable = failureClass !== null && attempt + 1 < maxAttempts;
297
+ if (!retryable)
298
+ throw error;
299
+ const delayMs = dispatchBackoffDelayMs({
300
+ attempt,
301
+ initialMs: args.policy.backoff_initial_ms,
302
+ capMs: args.policy.backoff_cap_ms,
303
+ });
304
+ await args.onRetry?.({
305
+ label: args.label,
306
+ attempt: attempt + 1,
307
+ delayMs,
308
+ failureClass,
309
+ message,
310
+ });
311
+ await args.sleep(delayMs);
312
+ }
313
+ }
314
+ // Unreachable: the loop always returns/throws on the final attempt.
315
+ throw new Error(`runWithDispatchBackoff fell through for ${args.label}`);
316
+ }
317
+ /** Session-root location of the dead-letter/incomplete artifact — part of the
318
+ * F-B3 recovery contract (재디스패치 집합의 진실 위치), shared by every wired
319
+ * pipeline (reconstruct semantic-map, review lens/stance). One batch trips at
320
+ * most once per run (trip is terminal), so a fixed per-session path holds the
321
+ * latest batch's end state; the `pipeline`/`batch_label` fields identify it. */
322
+ export function dispatchIncompleteArtifactPath(sessionRoot) {
323
+ return path.join(sessionRoot, "dispatch-incomplete.yaml");
324
+ }
325
+ export function isDispatchIncompleteArtifact(value) {
326
+ const candidate = value;
327
+ return Boolean(candidate &&
328
+ typeof candidate === "object" &&
329
+ candidate.schema_version === "1" &&
330
+ typeof candidate.pipeline === "string" &&
331
+ typeof candidate.batch_label === "string" &&
332
+ candidate.breaker &&
333
+ typeof candidate.breaker === "object" &&
334
+ typeof candidate.breaker.tripped === "boolean" &&
335
+ typeof candidate.breaker.threshold === "number" &&
336
+ Array.isArray(candidate.completed_item_ids) &&
337
+ candidate.completed_item_ids.every((id) => typeof id === "string") &&
338
+ Array.isArray(candidate.dead_letter) &&
339
+ candidate.dead_letter.every((entry) => entry && typeof entry === "object" && typeof entry.item_id === "string") &&
340
+ Array.isArray(candidate.incomplete_item_ids) &&
341
+ candidate.incomplete_item_ids.every((id) => typeof id === "string"));
342
+ }
343
+ function assertUniqueItemIds(itemIds, label) {
344
+ const seen = new Set();
345
+ for (const itemId of itemIds) {
346
+ if (seen.has(itemId)) {
347
+ throw new Error(`dispatch incomplete artifact: duplicate ${label} item_id '${itemId}'`);
348
+ }
349
+ seen.add(itemId);
350
+ }
351
+ }
352
+ /** Deterministic projection of a batch's end state — the wiring persists it
353
+ * on breaker trip AND on normal completion (rule 6 observability), so a
354
+ * recovery run always has the exact re-dispatch set. This overload accepts a
355
+ * fully-expanded partition so recovery runs can persist retained prior work +
356
+ * current retry work without pretending the live breaker state observed every
357
+ * retained item. `createdAt` is supplied by the caller (runtime owns timestamps). */
358
+ export function buildDispatchIncompleteArtifactFromPartition(args) {
359
+ assertUniqueItemIds(args.plannedItemIds, "planned");
360
+ assertUniqueItemIds(args.completedItemIds, "completed");
361
+ assertUniqueItemIds(args.deadLetter.map((entry) => entry.item_id), "dead_letter");
362
+ const planned = new Set(args.plannedItemIds);
363
+ const completed = new Set(args.completedItemIds);
364
+ const deadLettered = new Set(args.deadLetter.map((entry) => entry.item_id));
365
+ const unknownCompleted = args.completedItemIds.filter((itemId) => !planned.has(itemId));
366
+ const unknownDeadLetter = args.deadLetter
367
+ .map((entry) => entry.item_id)
368
+ .filter((itemId) => !planned.has(itemId));
369
+ const overlapping = args.completedItemIds.filter((itemId) => deadLettered.has(itemId));
370
+ if (unknownCompleted.length > 0 || unknownDeadLetter.length > 0 || overlapping.length > 0) {
371
+ throw new Error([
372
+ "dispatch incomplete artifact: invalid full-batch partition",
373
+ unknownCompleted.length > 0
374
+ ? `unknown completed item_ids=${unknownCompleted.join(",")}`
375
+ : null,
376
+ unknownDeadLetter.length > 0
377
+ ? `unknown dead_letter item_ids=${unknownDeadLetter.join(",")}`
378
+ : null,
379
+ overlapping.length > 0 ? `overlapping item_ids=${overlapping.join(",")}` : null,
380
+ ].filter((part) => part !== null).join("; "));
381
+ }
382
+ return {
383
+ schema_version: "1",
384
+ pipeline: args.pipeline,
385
+ batch_label: args.batchLabel,
386
+ created_at: args.createdAt,
387
+ breaker: args.breaker,
388
+ completed_item_ids: args.plannedItemIds.filter((itemId) => completed.has(itemId)),
389
+ dead_letter: args.plannedItemIds
390
+ .filter((itemId) => deadLettered.has(itemId))
391
+ .map((itemId) => args.deadLetter.find((entry) => entry.item_id === itemId)),
392
+ incomplete_item_ids: args.plannedItemIds.filter((itemId) => !completed.has(itemId) && !deadLettered.has(itemId)),
393
+ };
394
+ }
395
+ /** State-machine convenience wrapper for normal first-attempt batches. */
396
+ export function buildDispatchIncompleteArtifact(args) {
397
+ const trip = args.state.tripped();
398
+ return buildDispatchIncompleteArtifactFromPartition({
399
+ pipeline: args.pipeline,
400
+ batchLabel: args.batchLabel,
401
+ createdAt: args.createdAt,
402
+ plannedItemIds: args.plannedItemIds,
403
+ completedItemIds: args.state.completedItemIds(),
404
+ deadLetter: args.state.deadLetterEntries(),
405
+ breaker: {
406
+ tripped: trip !== null,
407
+ failure_class: trip?.failure_class ?? null,
408
+ consecutive_item_count: trip?.consecutive_item_count ?? null,
409
+ threshold: args.state.policy.systemic_threshold,
410
+ },
411
+ });
412
+ }
@@ -0,0 +1,33 @@
1
+ const CAPABILITY_ROWS = [
2
+ {
3
+ execution_adapter: "openai_sdk",
4
+ adapter_package_version: "6.39.0",
5
+ protocol_version: "openai_responses_v1",
6
+ endpoint_kind: "official_sdk",
7
+ capabilities: {
8
+ structured_failure_evidence: true,
9
+ counted_adapter_requests: true,
10
+ sdk_retry_zero: true,
11
+ invoke_once: true,
12
+ },
13
+ },
14
+ {
15
+ execution_adapter: "anthropic_sdk",
16
+ adapter_package_version: "0.99.0",
17
+ protocol_version: "anthropic_messages_v1",
18
+ endpoint_kind: "official_sdk",
19
+ capabilities: {
20
+ structured_failure_evidence: true,
21
+ counted_adapter_requests: true,
22
+ sdk_retry_zero: true,
23
+ invoke_once: true,
24
+ },
25
+ },
26
+ ];
27
+ export function resolveDispatchFallbackAdapterCapabilities(args) {
28
+ const row = CAPABILITY_ROWS.find((candidate) => candidate.execution_adapter === args.executionAdapter &&
29
+ candidate.adapter_package_version === args.adapterPackageVersion &&
30
+ candidate.protocol_version === args.protocolVersion &&
31
+ candidate.endpoint_kind === "official_sdk");
32
+ return row ? { ...row.capabilities } : null;
33
+ }