@ngockhoale/ukit 3.0.2 → 3.0.4

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 (109) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/ukit +12 -4
  3. package/manifests/engineConformance.yaml +29 -0
  4. package/manifests/platform.full.yaml +13 -0
  5. package/package.json +2 -1
  6. package/scripts/audit/decision-coverage.mjs +295 -0
  7. package/scripts/bench/outline-savings.mjs +19 -4
  8. package/scripts/bench/parallel-agents.mjs +15 -4
  9. package/scripts/bench/runGold.mjs +22 -4
  10. package/scripts/bench/v3-ceremony.mjs +7 -1
  11. package/scripts/bug/triage.mjs +56 -17
  12. package/scripts/index/build-index.mjs +94 -28
  13. package/scripts/index/query-index.mjs +48 -14
  14. package/scripts/index/refresh-index.mjs +142 -62
  15. package/scripts/perf/audit-perf.mjs +8 -2
  16. package/scripts/skill/audit-skill.mjs +54 -25
  17. package/src/bug/triageBug.js +9 -6
  18. package/src/cli/adapters.js +6 -0
  19. package/src/cli/commands/code.js +7 -1
  20. package/src/cli/commands/indexArgs.js +4 -2
  21. package/src/cli/commands/indexTools.js +8 -1
  22. package/src/cli/commands/install.js +13 -0
  23. package/src/cli/commands/memory.js +10 -3
  24. package/src/cli/commands/status.js +17 -1
  25. package/src/cli/commands/update.js +7 -0
  26. package/src/context/detectProjectContext.js +3 -1
  27. package/src/core/codeintel/analogy.js +1 -1
  28. package/src/core/codeintel/diagnostics.js +9 -6
  29. package/src/core/codeintel/graph.js +14 -8
  30. package/src/core/codeintel/impact.js +0 -1
  31. package/src/core/codeintel/invalidation.js +5 -3
  32. package/src/core/codeintel/packet.js +11 -0
  33. package/src/core/codeintel/router.js +17 -3
  34. package/src/core/codeintel/semanticProvider.js +1 -1
  35. package/src/core/codeintel/summaries.js +11 -7
  36. package/src/core/compact/contextBudget.js +26 -12
  37. package/src/core/compact/index.js +15 -8
  38. package/src/core/docContracts.js +10 -2
  39. package/src/core/experiments/deliberation.js +321 -0
  40. package/src/core/experiments/dynamicWorkflow.js +492 -0
  41. package/src/core/fileOps.js +8 -1
  42. package/src/core/gatewayProbe.js +29 -1
  43. package/src/core/gatewayResilienceEnv.js +44 -3
  44. package/src/core/handoffDocValidator.js +3 -1
  45. package/src/core/hookChainDoctor.js +16 -1
  46. package/src/core/memory/deltaOverlays.js +448 -0
  47. package/src/core/memory/learningCandidates.js +302 -0
  48. package/src/core/memory/migrate.js +59 -32
  49. package/src/core/memory/recordStore.js +24 -1
  50. package/src/core/memory/store.js +44 -8
  51. package/src/core/memory/storeV2.js +48 -33
  52. package/src/core/memory/userMemory.js +11 -10
  53. package/src/core/output/index.js +15 -10
  54. package/src/core/permissionPolicy.js +8 -0
  55. package/src/core/runtimeConfig.js +224 -4
  56. package/src/core/sensitiveValueScanner.js +10 -2
  57. package/src/core/taskBudgetValidator.js +12 -17
  58. package/src/core/taskProgressGuard.js +59 -9
  59. package/src/core/unattendedDoctor.js +5 -2
  60. package/src/core/uninstall.js +37 -8
  61. package/src/decision/client.js +371 -0
  62. package/src/decision/lease.js +198 -0
  63. package/src/decision/preflight.js +492 -0
  64. package/src/decision/protocol.js +308 -0
  65. package/src/decision/registry.js +384 -0
  66. package/src/decision/shadow.js +281 -0
  67. package/src/decision/statePacket.js +165 -0
  68. package/src/diagnostics/failurePatterns.js +2 -1
  69. package/src/diagnostics/ledgerFiles.js +3 -1
  70. package/src/diagnostics/routeOutcomes.js +1 -29
  71. package/src/index/buildIndex.js +23 -11
  72. package/src/index/impactContext.js +21 -2
  73. package/src/index/importResolution.js +13 -7
  74. package/src/index/queryIndex.js +11 -5
  75. package/src/index/resolveContext.js +16 -6
  76. package/src/index/taskRouting.js +63 -2
  77. package/src/index/verificationPlan.js +12 -1
  78. package/src/learning/patternProposals.js +6 -0
  79. package/src/render/renderTemplate.js +1 -1
  80. package/src/skill/auditSkill.js +3 -1
  81. package/src/stack/detectStack.js +3 -1
  82. package/template_project/.claude/agents/handoff-planner.md +2 -5
  83. package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
  84. package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
  85. package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
  86. package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
  87. package/template_project/.claude/hooks/handoff-resume.sh +10 -1
  88. package/template_project/.claude/hooks/protect-files.sh +0 -1
  89. package/template_project/.claude/hooks/record-execution.mjs +13 -1
  90. package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
  91. package/template_project/.claude/hooks/session-episode.sh +9 -2
  92. package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
  93. package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
  94. package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
  95. package/template_project/.claude/ukit/index/route-task.mjs +444 -0
  96. package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
  97. package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
  98. package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
  99. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
  100. package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
  101. package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
  102. package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
  103. package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
  104. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
  105. package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
  106. package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
  107. package/template_project/ukit/storage/config.json +34 -1
  108. package/template_project/.claude/ukit/skill-router-state.json +0 -1
  109. package/template_project/.ukit/storage/cache/hook-latency/unknown.jsonl +0 -4
@@ -0,0 +1,492 @@
1
+ /**
2
+ * preflight.js (TASK-003 / M07)
3
+ *
4
+ * Atomic preflight bundle builder/validator/applier (CONTRACTS.md C15/C16,
5
+ * UNIC_DECISION_SPEC §4/§13). No execution lane may begin from an undecided
6
+ * AI policy state: the owner derives bounded candidates deterministically,
7
+ * sends ONE batch containing the whole preflight group, validates the answers
8
+ * cross-field, and applies the bundle atomically — never half old, half new.
9
+ *
10
+ * Apply order is decide → validate → apply → capture fresh execution context
11
+ * → acknowledge → receipt. A suggestion is never `applied` without host
12
+ * acknowledgement; advisory hosts record `confirmation:'adapter-applied'`.
13
+ */
14
+
15
+ import { createHash } from 'node:crypto';
16
+ import { resolveCheckpoint } from './client.js';
17
+ import { buildStatePacket, serializeStatePacket } from './statePacket.js';
18
+ import { createLease, createCacheEpoch, DEFAULT_LEASE_CHOICES } from './lease.js';
19
+ import { resolveModelTier } from '../core/executionContracts.js';
20
+
21
+ export const PREFLIGHT_DECISION_KEYS = Object.freeze([
22
+ 'preflight.execution-lane.v1',
23
+ 'preflight.model-role.v1',
24
+ 'preflight.reasoning-effort.v1',
25
+ 'preflight.rigor.v1',
26
+ 'preflight.capabilities.v1',
27
+ 'preflight.verification-depth.v1',
28
+ 'preflight.lease.v1',
29
+ ]);
30
+
31
+ const RIGOR_ORDER = ['r0', 'r1', 'r2', 'r3', 'r4'];
32
+ const EFFORT_ORDER = ['low', 'medium', 'high', 'max'];
33
+ const VERIFICATION_DEPTHS = ['sanity', 'targeted', 'impact', 'full'];
34
+
35
+ // Deterministic floors for a 'high-risk' route (ARCHITECTURE.md rigor matrix:
36
+ // R3+ → impact+ verification; decision-table-v2 forces effort 'high').
37
+ const HIGH_RISK_MIN_RIGOR = 'r3';
38
+ const HIGH_RISK_MIN_EFFORT = 'high';
39
+ const HIGH_RISK_MIN_DEPTH = 'impact';
40
+
41
+ function fingerprint(text) {
42
+ return createHash('sha256').update(text).digest('hex');
43
+ }
44
+
45
+ function decisionPlane(config) {
46
+ const dp = config?.decisionPlane ?? config ?? {};
47
+ return dp && typeof dp === 'object' ? dp : {};
48
+ }
49
+
50
+ function floorName(riskFloor) {
51
+ if (typeof riskFloor === 'string') return riskFloor;
52
+ return riskFloor?.floor ?? 'none';
53
+ }
54
+
55
+ function unique(values) {
56
+ return [...new Set(values.filter((v) => typeof v === 'string' && v.length > 0))];
57
+ }
58
+
59
+ // Owner-bounded lease choices must be positive integers — malformed config
60
+ // entries fall back to the spec defaults everywhere, not just in validation;
61
+ // otherwise Math.min(...) yields NaN and createLease throws on the fallback path.
62
+ function resolveLeaseChoices(dp) {
63
+ return Array.isArray(dp.leaseChoices) && dp.leaseChoices.length > 0
64
+ && dp.leaseChoices.every((v) => Number.isInteger(v) && v > 0)
65
+ ? dp.leaseChoices
66
+ : [...DEFAULT_LEASE_CHOICES];
67
+ }
68
+
69
+ /**
70
+ * Build the atomic preflight batch (C13): one boundary, one atomic question
71
+ * group covering lane, model role, reasoning effort, rigor, capabilities,
72
+ * verification depth, and lease. Candidates are derived deterministically
73
+ * from host capabilities, the route, and config — the model can only pick
74
+ * from hard-safe labels.
75
+ *
76
+ * @param {{route: object, hostCapabilities: object, config?: object,
77
+ * languageClass?: string, taskBoundary?: string}} args
78
+ * @returns {{batch: object, candidateMap: object}}
79
+ */
80
+ export function buildPreflightBatch({
81
+ route = {},
82
+ hostCapabilities = {},
83
+ config = {},
84
+ languageClass = 'unknown',
85
+ taskBoundary = 'task',
86
+ } = {}) {
87
+ const dp = decisionPlane(config);
88
+ const execution = route?.execution ?? {};
89
+ const capabilityPolicy = route?.capabilityPolicy ?? {};
90
+ const floor = floorName(execution.riskFloor);
91
+ const highRisk = floor === 'high-risk';
92
+
93
+ const leaseChoices = resolveLeaseChoices(dp);
94
+ const portableEfforts =
95
+ Array.isArray(dp.portableReasoningEfforts) && dp.portableReasoningEfforts.length > 0
96
+ ? dp.portableReasoningEfforts
97
+ : [...EFFORT_ORDER];
98
+
99
+ // --- deterministic candidate derivation (owner-side, never model-invented)
100
+ const laneCandidates = unique(
101
+ Array.isArray(hostCapabilities?.lanes) && hostCapabilities.lanes.length > 0
102
+ ? hostCapabilities.lanes
103
+ : [execution.mode],
104
+ );
105
+ const modelRoleCandidates = unique(
106
+ Array.isArray(hostCapabilities?.modelRoles) && hostCapabilities.modelRoles.length > 0
107
+ ? hostCapabilities.modelRoles
108
+ : [execution.modelTier],
109
+ );
110
+ const hostEfforts = hostCapabilities?.modelEfforts
111
+ ? unique(Object.values(hostCapabilities.modelEfforts).flat())
112
+ : null;
113
+ const effortCandidates = hostEfforts
114
+ ? portableEfforts.filter((e) => hostEfforts.includes(e))
115
+ : portableEfforts;
116
+ const rigorCandidates = highRisk
117
+ ? RIGOR_ORDER.slice(RIGOR_ORDER.indexOf(HIGH_RISK_MIN_RIGOR))
118
+ : [...RIGOR_ORDER];
119
+ const depthPool =
120
+ Array.isArray(hostCapabilities?.verificationDepths) && hostCapabilities.verificationDepths.length > 0
121
+ ? hostCapabilities.verificationDepths
122
+ : VERIFICATION_DEPTHS;
123
+ const depthCandidates = highRisk
124
+ ? depthPool.filter((d) => VERIFICATION_DEPTHS.indexOf(d) >= VERIFICATION_DEPTHS.indexOf(HIGH_RISK_MIN_DEPTH))
125
+ : depthPool;
126
+
127
+ const required = Array.isArray(capabilityPolicy.required) ? capabilityPolicy.required : [];
128
+ const recommended = Array.isArray(capabilityPolicy.recommended) ? capabilityPolicy.recommended : [];
129
+ const capabilitySets = {
130
+ ...(hostCapabilities?.capabilitySets && typeof hostCapabilities.capabilitySets === 'object'
131
+ ? hostCapabilities.capabilitySets
132
+ : {}),
133
+ };
134
+ if (required.length > 0) capabilitySets['required-only'] = [...required];
135
+ if (recommended.length > 0) capabilitySets['required+recommended'] = [...required, ...recommended];
136
+ if (Object.keys(capabilitySets).length === 0) capabilitySets.none = [];
137
+ const capabilityCandidates = Object.keys(capabilitySets);
138
+
139
+ // A choice question with no derivable candidates cannot be asked safely —
140
+ // encodeQuestion would reject it. Substitute the deterministic baseline
141
+ // singleton (the value the fallback bundle picks); when no deterministic
142
+ // label exists (e.g. no model role for an informational lane) the question
143
+ // is undecidable and is dropped from the batch and its atomic group.
144
+ const tierDecision = resolveModelTier({
145
+ executionMode: execution.mode ?? null,
146
+ riskFloor: { floor },
147
+ hostCapabilities,
148
+ });
149
+ const deterministicLabels = {
150
+ 'preflight.execution-lane.v1': execution.mode ?? null,
151
+ 'preflight.model-role.v1': tierDecision.tier ?? execution.modelTier ?? null,
152
+ 'preflight.reasoning-effort.v1': tierDecision.effort ?? 'medium',
153
+ 'preflight.capabilities.v1': 'none',
154
+ 'preflight.verification-depth.v1': highRisk ? HIGH_RISK_MIN_DEPTH : 'targeted',
155
+ 'preflight.lease.v1': String(Math.min(...leaseChoices)),
156
+ };
157
+
158
+ const candidateMap = {
159
+ rigor: rigorCandidates,
160
+ lease: leaseChoices,
161
+ capabilities: capabilitySets,
162
+ };
163
+
164
+ const questions = [
165
+ {
166
+ decisionKey: 'preflight.execution-lane.v1',
167
+ kind: 'choice',
168
+ instruction: 'Pick the execution lane for this task boundary.',
169
+ candidates: laneCandidates,
170
+ },
171
+ {
172
+ decisionKey: 'preflight.model-role.v1',
173
+ kind: 'choice',
174
+ instruction: 'Pick the execution model role from the host capability map.',
175
+ candidates: modelRoleCandidates,
176
+ },
177
+ {
178
+ decisionKey: 'preflight.reasoning-effort.v1',
179
+ kind: 'choice',
180
+ instruction: 'Pick the lowest reasoning effort sufficient for the next unresolved work.',
181
+ candidates: effortCandidates,
182
+ },
183
+ {
184
+ decisionKey: 'preflight.rigor.v1',
185
+ kind: 'score',
186
+ instruction: 'Pick the rigor level within the deterministic floor.',
187
+ candidates: rigorCandidates,
188
+ },
189
+ {
190
+ decisionKey: 'preflight.capabilities.v1',
191
+ kind: 'choice',
192
+ instruction: 'Pick the active capability set.',
193
+ candidates: capabilityCandidates,
194
+ },
195
+ {
196
+ decisionKey: 'preflight.verification-depth.v1',
197
+ kind: 'choice',
198
+ instruction: 'Pick the initial verification depth.',
199
+ candidates: depthCandidates,
200
+ },
201
+ {
202
+ decisionKey: 'preflight.lease.v1',
203
+ kind: 'choice',
204
+ instruction: 'Pick how many generations the bundle stays valid.',
205
+ candidates: leaseChoices.map(String),
206
+ },
207
+ ].map((q) => ({
208
+ schemaVersion: 1,
209
+ family: 'preflight',
210
+ owner: 'taskRouting',
211
+ hardConstraintRefs: [],
212
+ ...q,
213
+ candidates:
214
+ q.candidates.length === 0 && deterministicLabels[q.decisionKey] != null
215
+ ? [deterministicLabels[q.decisionKey]]
216
+ : q.candidates,
217
+ })).filter((q) => q.candidates.length > 0);
218
+
219
+ const statePacket = buildStatePacket({
220
+ taskClass: route?.intent?.kind,
221
+ execution: { mode: execution.mode, rigor: execution.rigor, phase: execution.phase },
222
+ riskSignals: route?.evidence?.riskSignals ?? [],
223
+ capabilityAvailability: Array.isArray(hostCapabilities?.capabilities)
224
+ ? hostCapabilities.capabilities
225
+ : [],
226
+ });
227
+ const serialized = serializeStatePacket(statePacket);
228
+ const stateFingerprint = fingerprint(serialized).slice(0, 16);
229
+ const hostFingerprint = hostCapabilities?.fingerprint ?? null;
230
+
231
+ const batch = {
232
+ batchVersion: 1,
233
+ batchId: `pf-${stateFingerprint}`,
234
+ boundary: 'preflight',
235
+ stateFingerprint,
236
+ cacheEpoch: createCacheEpoch({
237
+ id: `epoch-${fingerprint(hostFingerprint ?? 'none').slice(0, 8)}`,
238
+ prefixFingerprint: hostCapabilities?.prefixFingerprint ?? null,
239
+ }),
240
+ languageClass,
241
+ checkpoint: resolveCheckpoint(languageClass, config),
242
+ questions,
243
+ atomicQuestionGroups: [questions.map((q) => q.decisionKey)],
244
+ stateBudget: dp.maxStateTokens ?? { multilingual: 1024, english: 512 },
245
+ statePacket,
246
+ taskBoundary,
247
+ };
248
+
249
+ return { batch, candidateMap };
250
+ }
251
+
252
+ /**
253
+ * Validate a parsed answer set into an atomic preflight bundle (C15).
254
+ * Cross-field rules run over the WHOLE bundle: an unsupported model/effort
255
+ * pair, an out-of-floor rigor/effort/depth, or any invalid answer blocks the
256
+ * bundle — it is rejected, never coerced and never half-applied.
257
+ *
258
+ * @param {object[]} answers normalized DecisionAnswer list (C14)
259
+ * @param {{hostCapabilities?: object, riskFloor?: object|string,
260
+ * candidateMap?: object, config?: object, batch?: object,
261
+ * taskBoundary?: string}} options `batch` (from buildPreflightBatch)
262
+ * carries batchId/cacheEpoch onto the bundle for the receipt.
263
+ * @returns {{status: 'validated'|'blocked', errors: string[], bundle: object}}
264
+ */
265
+ export function validatePreflightAnswers(
266
+ answers,
267
+ { hostCapabilities = {}, riskFloor = null, candidateMap = null, config = {}, batch = null, taskBoundary = 'task' } = {},
268
+ ) {
269
+ const dp = decisionPlane(config);
270
+ const leaseChoices = resolveLeaseChoices(dp);
271
+ // Only keys actually asked in the batch are required — buildPreflightBatch
272
+ // drops undecidable questions (0 derivable candidates), and requiring their
273
+ // answers would block every such bundle at canary.
274
+ const askedKeys = new Set(
275
+ (Array.isArray(batch?.questions) ? batch.questions : [])
276
+ .map((q) => q?.decisionKey)
277
+ .filter((k) => typeof k === 'string'),
278
+ );
279
+ const isRequired = (key) => askedKeys.size === 0 || askedKeys.has(key);
280
+ const floor = floorName(riskFloor);
281
+ const highRisk = floor === 'high-risk';
282
+ const errors = [];
283
+
284
+ const byKey = new Map();
285
+ for (const answer of Array.isArray(answers) ? answers : []) {
286
+ if (answer && typeof answer.decisionKey === 'string') byKey.set(answer.decisionKey, answer);
287
+ }
288
+
289
+ const valueOf = (key) => {
290
+ const answer = byKey.get(key);
291
+ if (!answer) {
292
+ if (isRequired(key)) errors.push(`${key}: missing answer.`);
293
+ return undefined;
294
+ }
295
+ if (answer.validationStatus !== 'valid') {
296
+ errors.push(`${key}: invalid answer (${answer.fallbackCode ?? 'unknown'}).`);
297
+ return undefined;
298
+ }
299
+ return answer.value;
300
+ };
301
+
302
+ const lane = valueOf('preflight.execution-lane.v1');
303
+ const modelRole = valueOf('preflight.model-role.v1');
304
+ const effort = valueOf('preflight.reasoning-effort.v1');
305
+ const rigorScore = valueOf('preflight.rigor.v1');
306
+ const capabilityLabel = valueOf('preflight.capabilities.v1');
307
+ const depth = valueOf('preflight.verification-depth.v1');
308
+ const leaseLabel = valueOf('preflight.lease.v1');
309
+
310
+ // score → legend label; choice labels map through the candidate map.
311
+ const rigorCandidates = candidateMap?.rigor ?? RIGOR_ORDER;
312
+ let rigor = null;
313
+ if (rigorScore !== undefined) {
314
+ rigor = rigorCandidates[rigorScore - 1] ?? null;
315
+ if (rigor === null) errors.push(`preflight.rigor.v1: score ${rigorScore} is out of the candidate legend.`);
316
+ }
317
+
318
+ const capabilitySets = candidateMap?.capabilities ?? {};
319
+ let activeCapabilities = null;
320
+ if (capabilityLabel !== undefined) {
321
+ activeCapabilities = capabilitySets[capabilityLabel] ?? null;
322
+ if (activeCapabilities === null) {
323
+ errors.push(`preflight.capabilities.v1: unknown capability set '${capabilityLabel}'.`);
324
+ }
325
+ }
326
+
327
+ let leaseGranted = null;
328
+ if (leaseLabel !== undefined) {
329
+ const parsed = Number(leaseLabel);
330
+ if (!leaseChoices.includes(parsed)) {
331
+ errors.push(`preflight.lease.v1: '${leaseLabel}' is not an owner-bounded lease choice.`);
332
+ } else {
333
+ leaseGranted = parsed;
334
+ }
335
+ }
336
+
337
+ // --- cross-field validation over the whole bundle
338
+ if (lane !== undefined && Array.isArray(hostCapabilities?.lanes)
339
+ && hostCapabilities.lanes.length > 0 && !hostCapabilities.lanes.includes(lane)) {
340
+ errors.push(`execution lane '${lane}' is not host-eligible.`);
341
+ }
342
+ if (modelRole !== undefined && Array.isArray(hostCapabilities?.modelRoles)
343
+ && hostCapabilities.modelRoles.length > 0 && !hostCapabilities.modelRoles.includes(modelRole)) {
344
+ errors.push(`model role '${modelRole}' is not in the host capability map.`);
345
+ }
346
+ const supportedEfforts = modelRole != null ? hostCapabilities?.modelEfforts?.[modelRole] : null;
347
+ if (effort !== undefined && Array.isArray(supportedEfforts) && !supportedEfforts.includes(effort)) {
348
+ errors.push(`reasoning effort '${effort}' is not supported by model role '${modelRole}'.`);
349
+ }
350
+ if (highRisk) {
351
+ if (rigor !== null && RIGOR_ORDER.indexOf(rigor) < RIGOR_ORDER.indexOf(HIGH_RISK_MIN_RIGOR)) {
352
+ errors.push(`rigor '${rigor}' is below the high-risk floor '${HIGH_RISK_MIN_RIGOR}'.`);
353
+ }
354
+ if (effort !== undefined && EFFORT_ORDER.indexOf(effort) < EFFORT_ORDER.indexOf(HIGH_RISK_MIN_EFFORT)) {
355
+ errors.push(`reasoning effort '${effort}' is below the high-risk floor '${HIGH_RISK_MIN_EFFORT}'.`);
356
+ }
357
+ if (depth !== undefined && VERIFICATION_DEPTHS.indexOf(depth) < VERIFICATION_DEPTHS.indexOf(HIGH_RISK_MIN_DEPTH)) {
358
+ errors.push(`verification depth '${depth}' is below the high-risk floor '${HIGH_RISK_MIN_DEPTH}'.`);
359
+ }
360
+ }
361
+
362
+ const hostFingerprint = hostCapabilities?.fingerprint ?? null;
363
+ const bundle = {
364
+ preflightVersion: 1,
365
+ taskBoundary,
366
+ batchId: batch?.batchId ?? null,
367
+ executionLane: lane ?? null,
368
+ executionModelRole: modelRole ?? null,
369
+ executionModelId: modelRole != null ? (hostCapabilities?.modelIds?.[modelRole] ?? null) : null,
370
+ reasoningEffort: effort ?? null,
371
+ rigor: rigor !== null ? rigor?.toUpperCase() ?? null : null,
372
+ activeCapabilities: activeCapabilities ?? [],
373
+ verificationDepth: depth ?? null,
374
+ lease: leaseGranted !== null ? createLease(leaseGranted, 'generation') : null,
375
+ hostCapabilityFingerprint: hostFingerprint,
376
+ cacheEpoch: batch?.cacheEpoch ?? createCacheEpoch({
377
+ id: `epoch-${fingerprint(hostFingerprint ?? 'none').slice(0, 8)}`,
378
+ prefixFingerprint: hostCapabilities?.prefixFingerprint ?? null,
379
+ }),
380
+ status: errors.length === 0 ? 'validated' : 'blocked',
381
+ };
382
+
383
+ return { status: bundle.status, errors, bundle };
384
+ }
385
+
386
+ /**
387
+ * Apply a validated (or deterministic fallback) bundle:
388
+ * apply → capture fresh execution context → acknowledge → receipt (C16).
389
+ * A bundle that is not 'validated'/'fallback' is rejected — a suggestion is
390
+ * never applied, and a blocked bundle is never half-applied.
391
+ *
392
+ * @param {object} bundle C15 preflight record from validatePreflightAnswers
393
+ * or buildFallbackPreflight
394
+ * @param {{host?: {advisoryOnly?: boolean, apply?: Function}}} options
395
+ * @returns {{preflight: object, receipt: object}}
396
+ */
397
+ export function applyPreflight(bundle, { host = null } = {}) {
398
+ const selected = {
399
+ lane: bundle?.executionLane ?? null,
400
+ modelRole: bundle?.executionModelRole ?? null,
401
+ modelId: bundle?.executionModelId ?? null,
402
+ reasoningEffort: bundle?.reasoningEffort ?? null,
403
+ lease: bundle?.lease ?? null,
404
+ };
405
+ const baseReceipt = {
406
+ receiptVersion: 1,
407
+ batchId: bundle?.batchId ?? null,
408
+ decisionKeys: [...PREFLIGHT_DECISION_KEYS],
409
+ selected,
410
+ cacheEpoch: bundle?.cacheEpoch ?? null,
411
+ hostContextFingerprint: null,
412
+ confirmation: null,
413
+ status: 'rejected',
414
+ evidenceRefs: [],
415
+ };
416
+
417
+ if (!bundle || (bundle.status !== 'validated' && bundle.status !== 'fallback')) {
418
+ return { preflight: bundle, receipt: baseReceipt };
419
+ }
420
+
421
+ const applied = typeof host?.apply === 'function' ? host.apply(bundle) : null;
422
+ if (applied?.acknowledged !== true) {
423
+ // No host acknowledgement → not applied; the bundle keeps its status.
424
+ return { preflight: bundle, receipt: baseReceipt };
425
+ }
426
+
427
+ const confirmation =
428
+ bundle.status === 'fallback'
429
+ ? 'fallback-applied'
430
+ : host?.advisoryOnly === true
431
+ ? 'adapter-applied'
432
+ : 'native-context-captured';
433
+
434
+ const preflight = { ...bundle, status: 'applied' };
435
+ const receipt = {
436
+ ...baseReceipt,
437
+ hostContextFingerprint: applied.contextFingerprint ?? null,
438
+ confirmation,
439
+ status: 'applied',
440
+ evidenceRefs: Array.isArray(applied.evidenceRefs) ? applied.evidenceRefs : [],
441
+ };
442
+ return { preflight, receipt };
443
+ }
444
+
445
+ /**
446
+ * Deterministic baseline bundle for when the Decision Agent is unavailable
447
+ * (UNIC_DECISION_SPEC §4/§15): decision-table-v2 tier/effort, conservative
448
+ * rigor/depth floors, required capabilities only, minimal lease.
449
+ */
450
+ export function buildFallbackPreflight({
451
+ route = {},
452
+ hostCapabilities = {},
453
+ config = {},
454
+ taskBoundary = 'task',
455
+ } = {}) {
456
+ const dp = decisionPlane(config);
457
+ const execution = route?.execution ?? {};
458
+ const floor = floorName(execution.riskFloor);
459
+ const highRisk = floor === 'high-risk';
460
+ const leaseChoices = resolveLeaseChoices(dp);
461
+
462
+ const tierDecision = resolveModelTier({
463
+ executionMode: execution.mode ?? null,
464
+ riskFloor: typeof execution.riskFloor === 'object' ? execution.riskFloor : { floor },
465
+ hostCapabilities,
466
+ });
467
+ const modelRole = tierDecision.tier ?? execution.modelTier ?? null;
468
+ const required = Array.isArray(route?.capabilityPolicy?.required)
469
+ ? route.capabilityPolicy.required
470
+ : [];
471
+ const hostFingerprint = hostCapabilities?.fingerprint ?? null;
472
+
473
+ return {
474
+ preflightVersion: 1,
475
+ taskBoundary,
476
+ batchId: null,
477
+ executionLane: execution.mode ?? null,
478
+ executionModelRole: modelRole,
479
+ executionModelId: modelRole != null ? (hostCapabilities?.modelIds?.[modelRole] ?? null) : null,
480
+ reasoningEffort: tierDecision.effort ?? (highRisk ? 'high' : 'medium'),
481
+ rigor: execution.rigor ?? (highRisk ? 'R3' : 'R1'),
482
+ activeCapabilities: [...required],
483
+ verificationDepth: highRisk ? 'impact' : 'targeted',
484
+ lease: createLease(Math.min(...leaseChoices), 'generation'),
485
+ hostCapabilityFingerprint: hostFingerprint,
486
+ cacheEpoch: createCacheEpoch({
487
+ id: `epoch-${fingerprint(hostFingerprint ?? 'none').slice(0, 8)}`,
488
+ prefixFingerprint: hostCapabilities?.prefixFingerprint ?? null,
489
+ }),
490
+ status: 'fallback',
491
+ };
492
+ }