@ngockhoale/ukit 3.3.3 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/manifests/engineConformance.yaml +17 -1
  3. package/manifests/hostCapabilities.yaml +68 -1
  4. package/manifests/platform.full.yaml +138 -0
  5. package/manifests/platform.user.yaml +255 -3
  6. package/package.json +1 -1
  7. package/scripts/bench/subagent-orchestrator-corpus.mjs +275 -0
  8. package/scripts/bench/subagent-orchestrator-eval.mjs +565 -0
  9. package/scripts/probe/codex-capability-probe.mjs +169 -0
  10. package/src/cli/commands/doctor.js +168 -0
  11. package/src/cli/commands/indexTools.js +7 -0
  12. package/src/cli/commands/metrics.js +66 -2
  13. package/src/cli/commands/playbook.js +4 -4
  14. package/src/cli/commands/vm.js +49 -8
  15. package/src/core/agentRuntime/adapters.js +328 -27
  16. package/src/core/agentRuntime/artifacts.js +89 -0
  17. package/src/core/agentRuntime/context.js +345 -1
  18. package/src/core/agentRuntime/contract.js +296 -0
  19. package/src/core/agentRuntime/eventStore.js +176 -0
  20. package/src/core/agentRuntime/shadowRun.js +481 -5
  21. package/src/core/agentRuntime/telemetry.js +121 -0
  22. package/src/core/observability/emit/lifecycle.js +68 -1
  23. package/src/core/observability/emit/sessionBoot.js +393 -0
  24. package/src/core/observability/privacy/allowlist.js +10 -1
  25. package/src/core/observability/schema/registry.js +10 -0
  26. package/src/core/runtimeConfig.js +133 -0
  27. package/src/core/userPlaybooks.js +18 -3
  28. package/src/decision/registry.js +19 -0
  29. package/src/diagnostics/feedbackEvents.js +7 -4
  30. package/src/diagnostics/routeOutcomes.js +51 -6
  31. package/src/diagnostics/skillAccuracy.js +43 -3
  32. package/src/index/crossCheckMatrix.js +412 -0
  33. package/src/index/fixLoopEscalation.js +453 -0
  34. package/src/index/playbookRegistry.js +691 -0
  35. package/src/index/reviewPolicy.js +368 -0
  36. package/src/index/routeResolver.js +915 -0
  37. package/src/index/sessionHistoryExtractor.js +359 -0
  38. package/src/index/taskRouting.js +764 -581
  39. package/src/index/tierSelection.js +308 -0
  40. package/src/index/verificationMap.js +404 -0
  41. package/template_project/.claude/hooks/observability-emit.mjs +14 -0
  42. package/template_project/.claude/hooks/record-execution.mjs +19 -1
  43. package/template_project/.claude/hooks/skill-router.sh +691 -25
  44. package/template_project/.claude/hooks/verification-guard.sh +230 -1
  45. package/template_project/.claude/settings.json +2 -2
  46. package/template_project/.claude/ukit/index/cross-check-matrix.mjs +415 -0
  47. package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +456 -0
  48. package/template_project/.claude/ukit/index/playbook-registry.mjs +690 -0
  49. package/template_project/.claude/ukit/index/review-panel-aggregate.mjs +20 -2
  50. package/template_project/.claude/ukit/index/review-policy.mjs +376 -0
  51. package/template_project/.claude/ukit/index/route-resolver.mjs +1059 -0
  52. package/template_project/.claude/ukit/index/route-task.mjs +1253 -846
  53. package/template_project/.claude/ukit/index/session-history-extractor.mjs +362 -0
  54. package/template_project/.claude/ukit/index/tier-selection.mjs +309 -0
  55. package/template_project/.claude/ukit/index/verification-map.mjs +403 -0
  56. package/template_project/.claude/ukit/index/worktree-sweep.mjs +195 -0
  57. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +789 -11
  58. package/template_project/.claude/ukit/runtime/observability-emit.mjs +1102 -0
  59. package/template_project/.claude/ukit/runtime/reinject-context.mjs +9 -1
  60. package/template_project/.claude/ukit/runtime/resumable-run.mjs +149 -5
  61. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +323 -6
  62. package/template_project/.codex/README.md +8 -0
  63. package/template_project/.omp/hooks/pre/ukit-bridge.js +8 -1
  64. package/template_project/ukit/README.md +1 -1
  65. package/template_project/ukit/storage/config.json +20 -0
  66. package/template_user/playbooks/architecture-decision.md +28 -0
  67. package/template_user/playbooks/autonomous-run.md +43 -0
  68. package/template_user/playbooks/autopilot-full.md +59 -0
  69. package/template_user/playbooks/autopilot-stack.md +54 -0
  70. package/template_user/playbooks/babysit.md +39 -0
  71. package/template_user/playbooks/bug-fix.md +3 -1
  72. package/template_user/playbooks/{issue-implementation.md → feature-implementation.md} +4 -2
  73. package/template_user/playbooks/hillclimb.md +44 -0
  74. package/template_user/playbooks/investigation.md +21 -0
  75. package/template_user/playbooks/migration.md +21 -0
  76. package/template_user/playbooks/open-pr.md +48 -0
  77. package/template_user/playbooks/orchestrate.md +45 -0
  78. package/template_user/playbooks/performance.md +33 -0
  79. package/template_user/playbooks/prototype.md +28 -0
  80. package/template_user/playbooks/refactor.md +19 -0
  81. package/template_user/playbooks/release.md +28 -0
  82. package/template_user/playbooks/runtime-forensics.md +23 -0
  83. package/template_user/playbooks/session-pickup.md +31 -0
  84. package/template_user/playbooks/shipping.md +53 -0
  85. package/template_user/playbooks/skill-evaluation.md +48 -0
  86. package/template_user/playbooks/small-feature.md +20 -0
  87. package/template_user/playbooks/verification-map.json +153 -0
  88. package/template_user/playbooks/verification.md +22 -0
  89. package/template_user/playbooks/worktree-cleanup.md +37 -0
@@ -0,0 +1,412 @@
1
+ // crossCheckMatrix.js — adaptive cross-check depth matrix (BL-016 /
2
+ // SPEC FR-004 / ARCH §Adaptive Cross-Check).
3
+ //
4
+ // Verification depth scales with risk instead of being uniform — a separate
5
+ // dimension from implementation depth (fast path vs full workflow). This
6
+ // module is deterministic and pure: it resolves
7
+ //
8
+ // change-risk × implementer-reliability × evidence-quality
9
+ // → none | runnable | review-round | escalate
10
+ //
11
+ // and the result feeds the review policy's `escalationDepth` input slot
12
+ // (src/index/reviewPolicy.js), which maps depth → action floor.
13
+ //
14
+ // Contract (SPEC §8):
15
+ // resolveCrossCheckDepth({riskFloor?, rigor?, implementerReliability?,
16
+ // evidenceQuality?, reviewer?, implementer?, runnable?, role?, model?,
17
+ // project?, overrides?}) → {depth, reasons[]}
18
+ //
19
+ // Invariants from ARCH §Adaptive Cross-Check:
20
+ // * high change-risk overrides implementer reliability — a strong model on
21
+ // an auth/data-path change still escalates to mandatory review.
22
+ // * a reviewer of the SAME model family is not independent review — a
23
+ // same-family re-read never satisfies a review-round; the depth demotes
24
+ // to `runnable` with the limit stated in reasons[].
25
+ // * no reviewer → `runnable` where possible + honest stated limit; the
26
+ // caller reports INCONCLUSIVE when nothing is runnable (the depth enum
27
+ // cannot return INCONCLUSIVE — it names a depth, not a verdict).
28
+ // * implementer-reliability is a joined outcome signal, never a model-name
29
+ // verdict — absent or unrecognized input resolves to baseline-neutral
30
+ // 'unknown' (GAP M13).
31
+ // * `crossCheck.{role|model}×{project}` maintainer overrides may tighten or
32
+ // loosen within the enum; malformed overrides are ignored, never fatal.
33
+ //
34
+ // Parity: tests/consistency/crossCheckParity.test.js locks this module and
35
+ // the installed mirror to identical exports, identical matrix rows, and
36
+ // identical {depth, reasons[]} on shared fixtures.
37
+
38
+ // Ordered by severity — override clamping and floor comparisons rely on the
39
+ // index order, so consumers must never reorder this list.
40
+ export const CROSS_CHECK_DEPTHS = Object.freeze([
41
+ 'none',
42
+ 'runnable',
43
+ 'review-round',
44
+ 'escalate',
45
+ ]);
46
+
47
+ export const CROSS_CHECK_RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);
48
+
49
+ export const CROSS_CHECK_RELIABILITY_LEVELS = Object.freeze([
50
+ 'trusted',
51
+ 'needs-oversight',
52
+ 'unknown',
53
+ ]);
54
+
55
+ export const CROSS_CHECK_EVIDENCE_QUALITIES = Object.freeze([
56
+ 'strong',
57
+ 'thin',
58
+ 'missing-hooks',
59
+ ]);
60
+
61
+ // The ARCH §Adaptive Cross-Check matrix verbatim (7 rows). 'any' is a
62
+ // wildcard. First matching row wins, which gives the two 'any'-wildcard rows
63
+ // their precedence: high-risk escalate beats everything; missing-hooks
64
+ // degrade applies to whatever remains.
65
+ export const CROSS_CHECK_MATRIX = Object.freeze([
66
+ { risk: 'low', reliability: 'trusted', evidence: 'strong', depth: 'none' },
67
+ { risk: 'low', reliability: 'needs-oversight', evidence: 'thin', depth: 'review-round' },
68
+ { risk: 'low', reliability: 'unknown', evidence: 'strong', depth: 'runnable' },
69
+ { risk: 'medium', reliability: 'trusted', evidence: 'strong', depth: 'runnable' },
70
+ { risk: 'medium', reliability: 'needs-oversight', evidence: 'any', depth: 'review-round' },
71
+ { risk: 'high', reliability: 'any', evidence: 'any', depth: 'escalate' },
72
+ { risk: 'any', reliability: 'any', evidence: 'missing-hooks', depth: 'runnable' },
73
+ ]);
74
+
75
+ // Cells the matrix does not list resolve conservatively: needs-oversight on
76
+ // a low/medium-risk change is the row-2/row-5 signal family, so it still
77
+ // earns a review round; trusted and baseline-neutral reliability never drop
78
+ // below `runnable` on unlisted cells — thin evidence can never justify `none`.
79
+ const RELIABILITY_RESIDUAL = Object.freeze({
80
+ 'needs-oversight': 'review-round',
81
+ });
82
+
83
+ // Bounded maintainer override shapes: a bound leaf carries only these keys
84
+ // (validated in src/core/runtimeConfig.js); all other top-level keys of the
85
+ // section are reserved.
86
+ const BOUND_KEYS = new Set(['depth', 'minDepth', 'maxDepth']);
87
+ const CROSS_CHECK_SECTION_KEYS = new Set(['enabled', 'byRole', 'byModel']);
88
+
89
+ const MAX_ID_LENGTH = 96;
90
+
91
+ const RISK_ALIASES = Object.freeze({
92
+ high: 'high',
93
+ 'high-risk': 'high',
94
+ critical: 'high',
95
+ medium: 'medium',
96
+ moderate: 'medium',
97
+ low: 'low',
98
+ none: 'low',
99
+ trivial: 'low',
100
+ });
101
+
102
+ const RELIABILITY_ALIASES = Object.freeze({
103
+ trusted: 'trusted',
104
+ 'needs-oversight': 'needs-oversight',
105
+ unknown: 'unknown',
106
+ baseline: 'unknown',
107
+ 'baseline-neutral': 'unknown',
108
+ });
109
+
110
+ // Receipt classes attesting real-run/surface evidence vs build-only or
111
+ // claimed-only work (ARCH: evidence-quality is about receipt classes present,
112
+ // never about what the agent asserts).
113
+ const STRONG_EVIDENCE_TOKENS = new Set([
114
+ 'strong',
115
+ 'surface-check',
116
+ 'render-observation',
117
+ 'io-run',
118
+ 'run-receipt',
119
+ 'real-run',
120
+ 'verification-evidence',
121
+ 'write-evidence',
122
+ ]);
123
+ const THIN_EVIDENCE_TOKENS = new Set([
124
+ 'thin',
125
+ 'build-only',
126
+ 'claimed',
127
+ 'claimed-only',
128
+ 'log',
129
+ ]);
130
+ const MISSING_HOOKS_TOKENS = new Set([
131
+ 'missing-hooks',
132
+ 'unavailable',
133
+ 'cannot-produce',
134
+ ]);
135
+
136
+ function isPlainObject(value) {
137
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
138
+ }
139
+
140
+ function depthIndex(depth) {
141
+ return CROSS_CHECK_DEPTHS.indexOf(depth);
142
+ }
143
+
144
+ function boundedId(value) {
145
+ return typeof value === 'string' && value.trim()
146
+ ? value.trim().slice(0, MAX_ID_LENGTH)
147
+ : null;
148
+ }
149
+
150
+ // ─── input normalization ────────────────────────────────────────────────────
151
+
152
+ // riskFloor arrives as the route record's {floor:'none'|'high-risk', codes[]},
153
+ // a bare enum/code string, or a level name. `rigor` (R0-R4) only ever RAISES
154
+ // the floor: R3+ ceremony on a non-floor route still counts as medium risk —
155
+ // the router asked for deep work, so verification follows.
156
+ function normalizeRisk(riskFloor, rigor) {
157
+ let risk = 'low';
158
+ if (typeof riskFloor === 'string') {
159
+ risk = RISK_ALIASES[riskFloor.trim().toLowerCase()] ?? 'low';
160
+ } else if (isPlainObject(riskFloor)) {
161
+ const floor = typeof riskFloor.floor === 'string'
162
+ ? riskFloor.floor.trim().toLowerCase()
163
+ : null;
164
+ if (RISK_ALIASES[floor]) {
165
+ risk = RISK_ALIASES[floor];
166
+ } else if (typeof riskFloor.risk === 'string') {
167
+ risk = RISK_ALIASES[riskFloor.risk.trim().toLowerCase()] ?? 'low';
168
+ }
169
+ }
170
+ if (risk === 'low' && typeof rigor === 'string') {
171
+ const level = /^r(\d)$/i.exec(rigor.trim());
172
+ if (level && Number(level[1]) >= 3) risk = 'medium';
173
+ }
174
+ return risk;
175
+ }
176
+
177
+ // Reliability is joined outcome data, never a model-name verdict: anything
178
+ // absent or unrecognized collapses to baseline-neutral 'unknown'.
179
+ function normalizeReliability(value) {
180
+ if (typeof value !== 'string') return 'unknown';
181
+ return RELIABILITY_ALIASES[value.trim().toLowerCase()] ?? 'unknown';
182
+ }
183
+
184
+ function normalizeEvidenceQuality(value) {
185
+ if (value == null) return 'thin';
186
+ if (Array.isArray(value)) {
187
+ if (value.length === 0) return 'thin';
188
+ const tokens = value
189
+ .filter((token) => typeof token === 'string' && token.trim())
190
+ .map((token) => token.trim().toLowerCase());
191
+ if (tokens.some((token) => MISSING_HOOKS_TOKENS.has(token))) return 'missing-hooks';
192
+ if (tokens.some((token) => STRONG_EVIDENCE_TOKENS.has(token))) return 'strong';
193
+ return 'thin';
194
+ }
195
+ if (typeof value === 'string') {
196
+ const token = value.trim().toLowerCase();
197
+ if (MISSING_HOOKS_TOKENS.has(token)) return 'missing-hooks';
198
+ if (STRONG_EVIDENCE_TOKENS.has(token)) return 'strong';
199
+ return 'thin';
200
+ }
201
+ if (isPlainObject(value)) {
202
+ if (typeof value.quality === 'string') return normalizeEvidenceQuality(value.quality);
203
+ if (Array.isArray(value.classes)) return normalizeEvidenceQuality(value.classes);
204
+ }
205
+ return 'thin';
206
+ }
207
+
208
+ // ─── reviewer independence ──────────────────────────────────────────────────
209
+ // A review-round only counts when a reviewer of a DIFFERENT model family
210
+ // exists — same-family re-reads and self re-reads are not independent review
211
+ // (SPEC FR-004). A reviewer descriptor that supplies no family cannot be
212
+ // proven same or different; it is treated as independent but the assumption
213
+ // is named in the reasons so the receipt stays honest.
214
+ function reviewerIndependence(reviewer, implementer) {
215
+ if (!isPlainObject(reviewer)) {
216
+ return { independent: false, reason: 'no reviewer available' };
217
+ }
218
+ const reviewerFamily = boundedId(reviewer.modelFamily) ?? boundedId(reviewer.family);
219
+ const reviewerModel = boundedId(reviewer.model) ?? boundedId(reviewer.id) ?? boundedId(reviewer.name);
220
+ const implementerFamily = isPlainObject(implementer)
221
+ ? (boundedId(implementer.modelFamily) ?? boundedId(implementer.family))
222
+ : null;
223
+ const implementerModel = isPlainObject(implementer)
224
+ ? (boundedId(implementer.model) ?? boundedId(implementer.id) ?? boundedId(implementer.name))
225
+ : null;
226
+ if (reviewerModel && implementerModel && reviewerModel === implementerModel) {
227
+ return { independent: false, reason: 'reviewer is the implementer re-reading its own diff — not independent' };
228
+ }
229
+ if (reviewerFamily && implementerFamily && reviewerFamily === implementerFamily) {
230
+ return { independent: false, reason: 'same-model-family reviewer is not independent review' };
231
+ }
232
+ return {
233
+ independent: true,
234
+ reason: reviewerFamily
235
+ ? 'different-model-family reviewer available'
236
+ : 'reviewer family unproven — assuming independent',
237
+ };
238
+ }
239
+
240
+ // ─── maintainer overrides ───────────────────────────────────────────────────
241
+ // `crossCheck.{role|model}×{project}` bounded overrides, per ARCH §Config:
242
+ // enum-only, shipped ON. A bound leaf is {depth|minDepth|maxDepth}; an entry
243
+ // may also be a project map {projectId|'*': bound}. byRole wins over byModel;
244
+ // a project-named leaf wins over the '*' leaf of the same entry. Unknown
245
+ // shapes are ignored with a reason — a maintainer typo must never crash the
246
+ // route nor invent a depth outside the enum.
247
+ function boundLeaf(entry) {
248
+ if (!isPlainObject(entry)) return null;
249
+ if (Object.keys(entry).some((key) => BOUND_KEYS.has(key))) return entry;
250
+ return null;
251
+ }
252
+
253
+ function resolveOverrideBound(overrides, { role = null, model = null, project = null }) {
254
+ if (!isPlainObject(overrides) || overrides.enabled === false) return null;
255
+ const maps = [
256
+ ['role', role, overrides.byRole],
257
+ ['model', model, overrides.byModel],
258
+ ];
259
+ for (const [kind, name, map] of maps) {
260
+ if (!name || !isPlainObject(map)) continue;
261
+ const entry = map[name];
262
+ if (entry === undefined) continue;
263
+ const direct = boundLeaf(entry);
264
+ if (direct) {
265
+ return { bound: direct, label: `${kind} "${name}"` };
266
+ }
267
+ if (isPlainObject(entry)) {
268
+ const projectLeaf = project ? boundLeaf(entry[project]) : null;
269
+ const starLeaf = boundLeaf(entry['*']);
270
+ const picked = projectLeaf ?? starLeaf;
271
+ if (picked) {
272
+ return {
273
+ bound: picked,
274
+ label: `${kind} "${name}"${projectLeaf ? ` project "${project}"` : ''}`,
275
+ };
276
+ }
277
+ }
278
+ return { bound: null, label: `${kind} "${name}"` };
279
+ }
280
+ // A bare bound ({depth:…} / {minDepth:…} / {maxDepth:…}) applies directly.
281
+ const direct = boundLeaf(overrides);
282
+ if (direct) return { bound: direct, label: 'crossCheck' };
283
+ return null;
284
+ }
285
+
286
+ // ─── resolver ───────────────────────────────────────────────────────────────
287
+
288
+ export function resolveCrossCheckDepth({
289
+ riskFloor = null,
290
+ rigor = null,
291
+ implementerReliability = null,
292
+ evidenceQuality = null,
293
+ reviewer = undefined,
294
+ implementer = null,
295
+ runnable = true,
296
+ role = null,
297
+ model = null,
298
+ project = null,
299
+ overrides = null,
300
+ } = {}) {
301
+ const reasons = [];
302
+ const risk = normalizeRisk(riskFloor, rigor);
303
+ const reliability = normalizeReliability(implementerReliability);
304
+ const evidence = normalizeEvidenceQuality(evidenceQuality);
305
+
306
+ if (implementerReliability == null
307
+ || (typeof implementerReliability === 'string'
308
+ && RELIABILITY_ALIASES[implementerReliability.trim().toLowerCase()] === undefined)) {
309
+ reasons.push(
310
+ 'implementerReliability unresolved — defaulting to project-baseline neutral '
311
+ + '("unknown"); reliability is a joined outcome signal, never a model-name verdict',
312
+ );
313
+ }
314
+
315
+ let depth = null;
316
+ let decidingRow = null;
317
+ for (const row of CROSS_CHECK_MATRIX) {
318
+ if ((row.risk === 'any' || row.risk === risk)
319
+ && (row.reliability === 'any' || row.reliability === reliability)
320
+ && (row.evidence === 'any' || row.evidence === evidence)) {
321
+ depth = row.depth;
322
+ decidingRow = row;
323
+ break;
324
+ }
325
+ }
326
+
327
+ if (depth) {
328
+ const index = CROSS_CHECK_MATRIX.indexOf(decidingRow) + 1;
329
+ reasons.push(
330
+ `matrix row ${index}: risk=${decidingRow.risk === 'any' ? risk : decidingRow.risk} × `
331
+ + `reliability=${decidingRow.reliability === 'any' ? reliability : decidingRow.reliability} × `
332
+ + `evidence=${decidingRow.evidence === 'any' ? evidence : decidingRow.evidence} → ${decidingRow.depth}`,
333
+ );
334
+ if (decidingRow.risk === 'high') {
335
+ reasons.push('high change-risk overrides implementer reliability — mandatory review regardless of track record');
336
+ }
337
+ if (decidingRow.evidence === 'missing-hooks') {
338
+ reasons.push(
339
+ 'engine cannot produce a needed receipt class (missing hooks) — runnable '
340
+ + 'checks where possible + honest stated limit; INCONCLUSIVE if nothing runnable',
341
+ );
342
+ }
343
+ } else {
344
+ depth = RELIABILITY_RESIDUAL[reliability] ?? 'runnable';
345
+ reasons.push(
346
+ `no verbatim matrix row for risk=${risk} × reliability=${reliability} × evidence=${evidence} — `
347
+ + `residual rule resolves ${depth} (needs-oversight still earns a review round; `
348
+ + 'trusted/baseline never drops below runnable)',
349
+ );
350
+ }
351
+
352
+ // Reviewer independence only demotes the review-round depth: the matrix
353
+ // chose it for the signal, but without a different-family reviewer a
354
+ // same-family or self re-read is not independent review. 'escalate' is a
355
+ // mandatory hand-off — it never silently downgrades; the reason names the
356
+ // missing reviewer so the consumer escalates to a human or another lane.
357
+ const independence = reviewerIndependence(reviewer, implementer);
358
+ if (depth === 'review-round' && !independence.independent) {
359
+ reasons.push(`${independence.reason} — review-round demoted to runnable checks + stated limit`);
360
+ depth = 'runnable';
361
+ if (runnable === false) {
362
+ reasons.push('nothing runnable — report INCONCLUSIVE');
363
+ }
364
+ } else if (depth === 'review-round') {
365
+ reasons.push(independence.reason);
366
+ } else if (depth === 'escalate' && !independence.independent) {
367
+ reasons.push(
368
+ `${independence.reason} — escalate stands: mandatory review requires a `
369
+ + 'different-family reviewer or human sign-off, never a silent downgrade',
370
+ );
371
+ }
372
+
373
+ // Maintainer overrides bound the resolved depth last. A 'depth' pin wins;
374
+ // minDepth/maxDepth clamp within the enum. Invalid bound values are ignored
375
+ // with a named reason — schema validation in runtimeConfig.js rejects them
376
+ // at parse time; this guard covers programmatic callers.
377
+ const resolved = resolveOverrideBound(overrides, { role, model, project });
378
+ if (resolved && resolved.bound) {
379
+ const bound = resolved.bound;
380
+ let applied = null;
381
+ if (typeof bound.depth === 'string' && CROSS_CHECK_DEPTHS.includes(bound.depth)) {
382
+ depth = bound.depth;
383
+ applied = `depth=${bound.depth}`;
384
+ } else {
385
+ if (typeof bound.maxDepth === 'string'
386
+ && CROSS_CHECK_DEPTHS.includes(bound.maxDepth)
387
+ && depthIndex(depth) > depthIndex(bound.maxDepth)) {
388
+ depth = bound.maxDepth;
389
+ applied = `maxDepth=${bound.maxDepth}`;
390
+ }
391
+ if (typeof bound.minDepth === 'string'
392
+ && CROSS_CHECK_DEPTHS.includes(bound.minDepth)
393
+ && depthIndex(depth) < depthIndex(bound.minDepth)) {
394
+ depth = bound.minDepth;
395
+ applied = applied ? `${applied} + minDepth=${bound.minDepth}` : `minDepth=${bound.minDepth}`;
396
+ }
397
+ }
398
+ if (applied) {
399
+ reasons.push(`crossCheck override (${resolved.label}) applied: ${applied} → ${depth}`);
400
+ } else {
401
+ reasons.push(`crossCheck override (${resolved.label}) present but no enum bound applied`);
402
+ }
403
+ } else if (resolved) {
404
+ reasons.push(`crossCheck override (${resolved.label}) has no valid enum bound — ignored`);
405
+ } else if (isPlainObject(overrides) && Object.keys(overrides).some(
406
+ (key) => !CROSS_CHECK_SECTION_KEYS.has(key) && !BOUND_KEYS.has(key),
407
+ )) {
408
+ reasons.push('crossCheck override keys unrecognized — ignored');
409
+ }
410
+
411
+ return { depth, reasons };
412
+ }