@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.
- package/CHANGELOG.md +21 -0
- package/bin/ukit +12 -4
- package/manifests/engineConformance.yaml +29 -0
- package/manifests/platform.full.yaml +13 -0
- package/package.json +2 -1
- package/scripts/audit/decision-coverage.mjs +295 -0
- package/scripts/bench/outline-savings.mjs +19 -4
- package/scripts/bench/parallel-agents.mjs +15 -4
- package/scripts/bench/runGold.mjs +22 -4
- package/scripts/bench/v3-ceremony.mjs +7 -1
- package/scripts/bug/triage.mjs +56 -17
- package/scripts/index/build-index.mjs +94 -28
- package/scripts/index/query-index.mjs +48 -14
- package/scripts/index/refresh-index.mjs +142 -62
- package/scripts/perf/audit-perf.mjs +8 -2
- package/scripts/skill/audit-skill.mjs +54 -25
- package/src/bug/triageBug.js +9 -6
- package/src/cli/adapters.js +6 -0
- package/src/cli/commands/code.js +7 -1
- package/src/cli/commands/indexArgs.js +4 -2
- package/src/cli/commands/indexTools.js +8 -1
- package/src/cli/commands/install.js +13 -0
- package/src/cli/commands/memory.js +10 -3
- package/src/cli/commands/status.js +17 -1
- package/src/cli/commands/update.js +7 -0
- package/src/context/detectProjectContext.js +3 -1
- package/src/core/codeintel/analogy.js +1 -1
- package/src/core/codeintel/diagnostics.js +9 -6
- package/src/core/codeintel/graph.js +14 -8
- package/src/core/codeintel/impact.js +0 -1
- package/src/core/codeintel/invalidation.js +5 -3
- package/src/core/codeintel/packet.js +11 -0
- package/src/core/codeintel/router.js +17 -3
- package/src/core/codeintel/semanticProvider.js +1 -1
- package/src/core/codeintel/summaries.js +11 -7
- package/src/core/compact/contextBudget.js +26 -12
- package/src/core/compact/index.js +15 -8
- package/src/core/docContracts.js +10 -2
- package/src/core/experiments/deliberation.js +321 -0
- package/src/core/experiments/dynamicWorkflow.js +492 -0
- package/src/core/fileOps.js +8 -1
- package/src/core/gatewayProbe.js +29 -1
- package/src/core/gatewayResilienceEnv.js +44 -3
- package/src/core/handoffDocValidator.js +3 -1
- package/src/core/hookChainDoctor.js +16 -1
- package/src/core/memory/deltaOverlays.js +448 -0
- package/src/core/memory/learningCandidates.js +302 -0
- package/src/core/memory/migrate.js +59 -32
- package/src/core/memory/recordStore.js +24 -1
- package/src/core/memory/store.js +44 -8
- package/src/core/memory/storeV2.js +48 -33
- package/src/core/memory/userMemory.js +11 -10
- package/src/core/output/index.js +15 -10
- package/src/core/permissionPolicy.js +8 -0
- package/src/core/runtimeConfig.js +224 -4
- package/src/core/sensitiveValueScanner.js +10 -2
- package/src/core/taskBudgetValidator.js +12 -17
- package/src/core/taskProgressGuard.js +59 -9
- package/src/core/unattendedDoctor.js +5 -2
- package/src/core/uninstall.js +37 -8
- package/src/decision/client.js +371 -0
- package/src/decision/lease.js +198 -0
- package/src/decision/preflight.js +492 -0
- package/src/decision/protocol.js +308 -0
- package/src/decision/registry.js +384 -0
- package/src/decision/shadow.js +281 -0
- package/src/decision/statePacket.js +165 -0
- package/src/diagnostics/failurePatterns.js +2 -1
- package/src/diagnostics/ledgerFiles.js +3 -1
- package/src/diagnostics/routeOutcomes.js +1 -29
- package/src/index/buildIndex.js +23 -11
- package/src/index/impactContext.js +21 -2
- package/src/index/importResolution.js +13 -7
- package/src/index/queryIndex.js +11 -5
- package/src/index/resolveContext.js +16 -6
- package/src/index/taskRouting.js +63 -2
- package/src/index/verificationPlan.js +12 -1
- package/src/learning/patternProposals.js +6 -0
- package/src/render/renderTemplate.js +1 -1
- package/src/skill/auditSkill.js +3 -1
- package/src/stack/detectStack.js +3 -1
- package/template_project/.claude/agents/handoff-planner.md +2 -5
- package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
- package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
- package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
- package/template_project/.claude/hooks/handoff-resume.sh +10 -1
- package/template_project/.claude/hooks/protect-files.sh +0 -1
- package/template_project/.claude/hooks/record-execution.mjs +13 -1
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
- package/template_project/.claude/hooks/session-episode.sh +9 -2
- package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
- package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
- package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
- package/template_project/.claude/ukit/index/route-task.mjs +444 -0
- package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
- package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
- package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
- package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
- package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
- package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
- package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
- package/template_project/ukit/storage/config.json +34 -1
- package/template_project/.claude/ukit/skill-router-state.json +0 -1
- 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
|
+
}
|