sneakoscope 0.8.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,19 @@ Research scouts now use named persona-inspired cognitive lenses: Einstein Scout,
59
59
 
60
60
  For existing 0.7.x users, the visible change is new report-only evidence, not a route personality rewrite. Team still feels like Team, DFix stays ultralight, DB remains conservative, QA-LOOP still dogfoods, PPT stays information-first, imagegen still requires real raster evidence, and Honest Mode remains the final truth pass. The original strong reminder idea became neutral RecallPulse so user-facing prompts stay short, professional, and non-repetitive; hook messages can point at status, but `mission-status-ledger.json` is the durable source when app-visible text disappears. The planning source is `docs/RECALLPULSE_0_8_0_TASKS.md`, and implementation is designed to land in safe task-sized slices before any enforcement promotion.
61
61
 
62
+ ## 0.9.0 Report-Only Decision Lattice
63
+
64
+ Sneakoscope 0.9.0 adds a report-only Decision Lattice planner that uses A* over proof-debt signals to explain which route or verification path the pipeline would prefer. It is an evidence and planning surface, not a runtime shortcut: SKS must not claim speedup, fast-lane accuracy, or reduced verification cost from the lattice until replay or scored eval evidence demonstrates those outcomes.
65
+
66
+ The lattice integrates with the existing proof-field and `sks pipeline plan` surfaces. Its reports are expected to show the explored frontier, the selected path, and rejected paths with their proof-debt reasons, so reviewers can audit why a route stayed on the full Team/Honest path or why a smaller verification plan was only proposed. Like RecallPulse, this is designed to land as report-only evidence first; route enforcement and performance claims remain gated by later validation.
67
+
68
+ Quick checks:
69
+
70
+ ```bash
71
+ sks proof-field scan --json --intent "small CLI change"
72
+ sks pipeline plan latest --proof-field --json
73
+ ```
74
+
62
75
  ## Requirements
63
76
 
64
77
  - Node.js `>=20.11`
@@ -239,7 +252,7 @@ sks code-structure scan --json
239
252
 
240
253
  `sks recallpulse` is the 0.8.0 report-only RecallPulse utility. It writes `recallpulse-decision.json`, `mission-status-ledger.json`, `route-proof-capsule.json`, `evidence-envelope.json`, `recallpulse-governance-report.json`, `recallpulse-task-goal-ledger.json`, and `recallpulse-eval-report.json` for the current mission. RecallPulse does not replace route gates, Honest Mode, DB safety, imagegen evidence, or TriWiki validation; it records cache hits, hydration needs, duplicate suppression, route-governance risks, and final-summary-ready durable status so later releases can promote only measured improvements. Checklist updates are sequential: every `Txxx` row is treated as a child `$Goal` checkpoint, and `sks recallpulse checklist ... --task T001 --apply` refuses out-of-order checks unless explicitly overridden.
241
254
 
242
- `sks pipeline plan` shows the active route lane, kept/skipped stages, verification commands, and no-unrequested-fallback invariant. `sks proof-field scan` is the lightweight rubric for small changes; risky or broad signals return to the full Team/Honest path.
255
+ `sks pipeline plan` shows the active route lane, kept/skipped stages, verification commands, and no-unrequested-fallback invariant. The 0.9.0 Decision Lattice augments this planning surface with report-only A*/proof-debt evidence: frontier paths considered, the selected path, and rejected paths with rejection reasons. `sks proof-field scan` remains the lightweight rubric for small changes; risky or broad signals return to the full Team/Honest path, and no speedup claim is valid without replay or eval evidence.
243
256
 
244
257
  ### Ambiguity Questions
245
258
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.8.6",
4
+ "version": "0.9.0",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -3828,20 +3828,20 @@ async function selftest() {
3828
3828
  if (!harnessReport.forgetting.fixture.passed || !harnessReport.tmux.views.includes('Harness Experiments View') || !harnessReport.reliability.tool_error_taxonomy.includes('Unknown')) throw new Error('selftest: harness growth fixture incomplete');
3829
3829
  const proofField = await proofFieldFixture();
3830
3830
  if (!proofField.validation.ok || !validateProofFieldReport(proofField.report).ok) throw new Error('selftest: proof field report invalid');
3831
- if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.route_economy_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest: proof field fixture checks incomplete');
3831
+ if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.route_economy_present || !proofField.checks.decision_lattice_present || !proofField.checks.decision_lattice_report_only || !proofField.checks.decision_lattice_selected_path || !proofField.checks.decision_lattice_frontier_present || !proofField.checks.decision_lattice_rejections_present || !proofField.checks.decision_lattice_scoring_formula_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest: proof field fixture checks incomplete');
3832
3832
  if (!speedLanePolicyText().includes('proof_field_fast_lane') || !proofField.report.execution_lane?.skip_when_fast?.includes('planning_debate')) throw new Error('selftest: Proof Field speed lane policy missing');
3833
3833
  const fastPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team small CLI help update'), task: 'small CLI help surface update', proofField: proofField.report });
3834
3834
  if (!validatePipelinePlan(fastPipelinePlan).ok || fastPipelinePlan.runtime_lane?.lane !== 'proof_field_fast_lane' || !fastPipelinePlan.skipped_stages.includes('planning_debate') || !fastPipelinePlan.invariants.includes('no_unrequested_fallback_code')) throw new Error('selftest: pipeline plan did not encode fast lane stage skips and fallback guard');
3835
3835
  const broadProofField = await buildProofField(tmp, { intent: 'database security route refactor', changedFiles: ['src/core/db-safety.mjs', 'src/core/routes.mjs', 'src/cli/main.mjs', 'README.md'] });
3836
3836
  const broadPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team database security route refactor'), task: 'database security route refactor', proofField: broadProofField });
3837
3837
  if (!validatePipelinePlan(broadPipelinePlan).ok || broadPipelinePlan.runtime_lane?.lane === 'proof_field_fast_lane' || broadPipelinePlan.skipped_stages.includes('planning_debate')) throw new Error('selftest: pipeline plan did not fail closed for broad/security work');
3838
- if (broadPipelinePlan.route_economy?.mode !== 'report_only' || !broadPipelinePlan.route_economy.active_team_triggers?.includes('broad_change_set') || !broadPipelinePlan.route_economy.verification_stage_cache_key) throw new Error('selftest: route economy projection missing from pipeline plan');
3838
+ if (broadPipelinePlan.route_economy?.mode !== 'report_only' || !broadPipelinePlan.route_economy.active_team_triggers?.includes('broad_change_set') || !broadPipelinePlan.route_economy.verification_stage_cache_key || !broadPipelinePlan.route_economy.decision_lattice?.report_only) throw new Error('selftest: route economy projection missing from pipeline plan');
3839
3839
  const workflowPerf = await runWorkflowPerfBench(tmp, {
3840
3840
  iterations: 2,
3841
3841
  intent: 'small CLI help surface update',
3842
3842
  changedFiles: ['src/cli/maintenance-commands.mjs', 'src/core/routes.mjs']
3843
3843
  });
3844
- if (!validateWorkflowPerfReport(workflowPerf).ok || workflowPerf.metrics.decision_mode !== 'fast_lane' || workflowPerf.metrics.execution_lane !== 'proof_field_fast_lane' || workflowPerf.metrics.pipeline_lane !== 'proof_field_fast_lane' || !workflowPerf.metrics.fast_lane_eligible || !workflowPerf.metrics.fast_lane_allowed || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest: workflow perf proof field did not produce a valid outcome-scored fast lane report');
3844
+ if (!validateWorkflowPerfReport(workflowPerf).ok || workflowPerf.metrics.decision_mode !== 'fast_lane' || workflowPerf.metrics.execution_lane !== 'proof_field_fast_lane' || workflowPerf.metrics.pipeline_lane !== 'proof_field_fast_lane' || !workflowPerf.metrics.fast_lane_eligible || !workflowPerf.metrics.fast_lane_allowed || !workflowPerf.metrics.decision_lattice_valid || Number(workflowPerf.metrics.decision_lattice_frontier_count) < 1 || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest: workflow perf proof field did not produce a valid outcome-scored fast lane report');
3845
3845
  if (classifyToolError({ message: 'operation timed out' }) !== 'Timeout' || classifyToolError({ message: 'unclassified weirdness' }) !== 'Unknown') throw new Error('selftest: tool error taxonomy classification');
3846
3846
  const coord = rgbaToWikiCoord({ r: 12, g: 34, b: 56, a: 255 });
3847
3847
  if (coord.schema !== 'sks.wiki-coordinate.v1' || coord.xyzw.length !== 4) throw new Error('selftest: RGBA wiki coordinate conversion');
@@ -969,6 +969,9 @@ export async function proofFieldCommand(sub, args = []) {
969
969
  console.log(`Workflow complexity: ${report.workflow_complexity.band} (${report.workflow_complexity.score})`);
970
970
  if (report.team_trigger_matrix.active_triggers.length) console.log(`Team triggers: ${report.team_trigger_matrix.active_triggers.join(', ')}`);
971
971
  console.log(`Proof cones: ${report.proof_cones.map((cone) => cone.id).join(', ')}`);
972
+ if (report.decision_lattice?.selected_path?.id) {
973
+ console.log(`Decision lattice: ${report.decision_lattice.selected_path.id} f=${report.decision_lattice.selected_path.cost?.f ?? 'n/a'} frontier=${report.decision_lattice.frontier?.expanded_order?.length || 0} rejected=${report.decision_lattice.rejected_alternatives?.length || 0}`);
974
+ }
972
975
  console.log(`Verification: ${report.fast_lane_decision.verification.join('; ')}`);
973
976
  console.log(`Report: ${path.relative(root, report.report_path)}`);
974
977
  }
@@ -0,0 +1,481 @@
1
+ export const DECISION_LATTICE_SCHEMA_VERSION = 1;
2
+
3
+ export const DEFAULT_LATTICE_WEIGHTS = Object.freeze({
4
+ step: 1,
5
+ proof_debt: 3,
6
+ risk: 1,
7
+ friction: 1,
8
+ info_gain: 1
9
+ });
10
+
11
+ const AXES = Object.freeze(['contract', 'context', 'implementation', 'verification', 'review']);
12
+
13
+ const DEFAULT_START = Object.freeze({
14
+ contract: 0,
15
+ context: 0,
16
+ implementation: 0,
17
+ verification: 0,
18
+ review: 0
19
+ });
20
+
21
+ const DEFAULT_GOAL = Object.freeze({
22
+ contract: 2,
23
+ context: 2,
24
+ implementation: 2,
25
+ verification: 2,
26
+ review: 1
27
+ });
28
+
29
+ const DEFAULT_ACTIONS = Object.freeze([
30
+ {
31
+ id: 'seal_contract',
32
+ label: 'Seal decision contract',
33
+ delta: { contract: 2 },
34
+ risk: 0.05,
35
+ friction: 0.25,
36
+ info_gain: 0.9,
37
+ notes: ['Removes ambiguity before route selection.']
38
+ },
39
+ {
40
+ id: 'read_triwiki',
41
+ label: 'Read bounded TriWiki context',
42
+ delta: { context: 1 },
43
+ risk: 0.05,
44
+ friction: 0.2,
45
+ info_gain: 0.7,
46
+ notes: ['Uses compact high-trust recall before editing.']
47
+ },
48
+ {
49
+ id: 'proof_field_scan',
50
+ label: 'Run proof-field scan',
51
+ delta: { context: 2, verification: 1 },
52
+ risk: 0.1,
53
+ friction: 0.35,
54
+ info_gain: 0.95,
55
+ notes: ['Scores route surface and escalation triggers.']
56
+ },
57
+ {
58
+ id: 'minimal_patch',
59
+ label: 'Implement smallest scoped change',
60
+ delta: { implementation: 2 },
61
+ risk: 0.35,
62
+ friction: 0.35,
63
+ info_gain: 0.4,
64
+ notes: ['Touches only the selected proof cone.']
65
+ },
66
+ {
67
+ id: 'focused_verification',
68
+ label: 'Run focused verification',
69
+ delta: { verification: 1 },
70
+ risk: 0.12,
71
+ friction: 0.45,
72
+ info_gain: 0.85,
73
+ notes: ['Checks syntax and behavior for the changed module.']
74
+ },
75
+ {
76
+ id: 'five_lane_review',
77
+ label: 'Collect five-lane Team review',
78
+ delta: { review: 1 },
79
+ risk: 0.2,
80
+ friction: 1.1,
81
+ info_gain: 1,
82
+ notes: ['Satisfies Team review gate for broad missions.']
83
+ },
84
+ {
85
+ id: 'honest_mode',
86
+ label: 'Run Honest Mode closeout',
87
+ delta: { verification: 1 },
88
+ risk: 0.05,
89
+ friction: 0.2,
90
+ info_gain: 0.65,
91
+ notes: ['Binds final claims to evidence and gaps.']
92
+ }
93
+ ]);
94
+
95
+ const DEFAULT_ROUTE_PATHS = Object.freeze([
96
+ {
97
+ id: 'proof_field_fast_lane',
98
+ label: 'Proof Field Fast Lane',
99
+ action_ids: ['seal_contract', 'read_triwiki', 'proof_field_scan', 'minimal_patch', 'focused_verification', 'honest_mode'],
100
+ notes: ['Lowest friction when scope is narrow and risk flags stay low.']
101
+ },
102
+ {
103
+ id: 'balanced_team_lane',
104
+ label: 'Balanced Team Lane',
105
+ action_ids: ['seal_contract', 'read_triwiki', 'proof_field_scan', 'minimal_patch', 'focused_verification', 'five_lane_review', 'honest_mode'],
106
+ notes: ['Adds review evidence while preserving a compact change surface.']
107
+ },
108
+ {
109
+ id: 'full_team_honest_path',
110
+ label: 'Full Team Honest Path',
111
+ action_ids: ['seal_contract', 'read_triwiki', 'proof_field_scan', 'five_lane_review', 'minimal_patch', 'focused_verification', 'five_lane_review', 'honest_mode'],
112
+ notes: ['Heaviest default for broad or release-sensitive missions.']
113
+ }
114
+ ]);
115
+
116
+ export function buildDecisionLatticeReport(input = {}) {
117
+ const weights = normalizeWeights(input.weights);
118
+ const start = normalizeState(input.start_state || input.start || DEFAULT_START);
119
+ const goal = normalizeState(input.goal_state || input.target_state || input.target || inferredGoal(input));
120
+ const actions = normalizeActions(input.actions || DEFAULT_ACTIONS);
121
+ const routePaths = normalizeRoutePaths(input.route_paths || input.candidate_route_paths || DEFAULT_ROUTE_PATHS, actions);
122
+ const grid = buildConceptualGrid(start, goal, actions);
123
+ const search = runAStar({ start, goal, actions, weights });
124
+ const routeCandidates = routePaths.map((routePath, index) => evaluateRoutePath(routePath, index, { start, goal, actions, weights }));
125
+ const candidates = routeCandidates.concat([{ ...search.selected_path, rank_hint: routeCandidates.length }]).sort(compareCandidates);
126
+ const selected = selectPath(candidates, search.selected_path);
127
+ const rejected = candidates
128
+ .filter((candidate) => candidate.id !== selected.id)
129
+ .map((candidate) => ({
130
+ id: candidate.id,
131
+ label: candidate.label,
132
+ f: candidate.cost.f,
133
+ delta_from_selected: round(candidate.cost.f - selected.cost.f),
134
+ rejection_reasons: rejectionReasons(candidate, selected)
135
+ }));
136
+ const report = {
137
+ schema_version: DECISION_LATTICE_SCHEMA_VERSION,
138
+ report_only: true,
139
+ deterministic: true,
140
+ module: 'decision-lattice',
141
+ scoring_formula: String(input.scoring_formula || 'f = g + h + risk + friction - info_gain'),
142
+ research_basis: {
143
+ model: 'Decision Lattice A* planner',
144
+ scoring_formula: 'f = g + h + risk + friction - info_gain',
145
+ proof_debt_heuristic: 'h is weighted remaining lattice debt across contract, context, implementation, verification, and review axes.'
146
+ },
147
+ input_summary: {
148
+ intent: String(input.intent || input.goal || '').trim() || null,
149
+ weights,
150
+ start_state: start,
151
+ goal_state: goal,
152
+ action_count: actions.length,
153
+ route_path_count: routePaths.length
154
+ },
155
+ heuristic: {
156
+ id: 'proof_debt',
157
+ h_start: proofDebt(start, goal, weights),
158
+ axes: AXES.map((axis) => ({
159
+ axis,
160
+ start: start[axis],
161
+ goal: goal[axis],
162
+ debt: debtForAxis(start, goal, axis),
163
+ weighted_debt: round(debtForAxis(start, goal, axis) * weights.proof_debt)
164
+ }))
165
+ },
166
+ conceptual_grid: grid,
167
+ frontier: search.frontier,
168
+ candidate_paths: candidates,
169
+ selected_path: selected,
170
+ rejected_alternatives: rejected,
171
+ validation: null
172
+ };
173
+ report.validation = validateDecisionLatticeReport(report);
174
+ return report;
175
+ }
176
+
177
+ function inferredGoal(input = {}) {
178
+ const goal = { ...DEFAULT_GOAL };
179
+ if (input.execution_lane?.fast_lane_allowed === true && !(input.team_trigger_matrix?.active_triggers || []).length) {
180
+ goal.review = 0;
181
+ }
182
+ return goal;
183
+ }
184
+
185
+ export function validateDecisionLatticeReport(report = {}) {
186
+ const issues = [];
187
+ if (report.schema_version !== DECISION_LATTICE_SCHEMA_VERSION) issues.push('schema_version');
188
+ if (report.report_only !== true) issues.push('report_only');
189
+ if (report.deterministic !== true) issues.push('deterministic');
190
+ if (report.research_basis?.scoring_formula !== 'f = g + h + risk + friction - info_gain') issues.push('scoring_formula');
191
+ if (!Array.isArray(report.heuristic?.axes) || report.heuristic.axes.length !== AXES.length) issues.push('heuristic_axes');
192
+ if (!Number.isFinite(Number(report.heuristic?.h_start))) issues.push('heuristic_h_start');
193
+ if (!Array.isArray(report.conceptual_grid?.cells) || report.conceptual_grid.cells.length < 1) issues.push('conceptual_grid');
194
+ if (!Array.isArray(report.frontier?.expanded_order) || report.frontier.expanded_order.length < 1) issues.push('frontier_expanded_order');
195
+ if (!Array.isArray(report.candidate_paths) || report.candidate_paths.length < 1) issues.push('candidate_paths');
196
+ if (!report.selected_path?.id || !Array.isArray(report.selected_path?.steps)) issues.push('selected_path');
197
+ if (!Array.isArray(report.rejected_alternatives)) issues.push('rejected_alternatives');
198
+ if (report.candidate_paths?.some((candidate) => !Number.isFinite(Number(candidate?.cost?.f)))) issues.push('candidate_costs');
199
+ if (report.selected_path?.cost?.f !== Math.min(...(report.candidate_paths || []).map((candidate) => candidate.cost.f))) issues.push('selected_path_not_min_f');
200
+ return { ok: issues.length === 0, issues };
201
+ }
202
+
203
+ function normalizeWeights(input = {}) {
204
+ return {
205
+ step: positiveNumber(input.step, DEFAULT_LATTICE_WEIGHTS.step),
206
+ proof_debt: positiveNumber(input.proof_debt, DEFAULT_LATTICE_WEIGHTS.proof_debt),
207
+ risk: positiveNumber(input.risk, DEFAULT_LATTICE_WEIGHTS.risk),
208
+ friction: positiveNumber(input.friction, DEFAULT_LATTICE_WEIGHTS.friction),
209
+ info_gain: positiveNumber(input.info_gain, DEFAULT_LATTICE_WEIGHTS.info_gain)
210
+ };
211
+ }
212
+
213
+ function normalizeState(input = {}) {
214
+ const state = {};
215
+ for (const axis of AXES) state[axis] = clampInt(input[axis], 0, 3);
216
+ return state;
217
+ }
218
+
219
+ function normalizeActions(input = []) {
220
+ return input
221
+ .map((action, index) => ({
222
+ id: safeId(action.id || `action_${index + 1}`),
223
+ label: String(action.label || action.id || `Action ${index + 1}`),
224
+ delta: normalizeDelta(action.delta || {}),
225
+ risk: nonNegativeNumber(action.risk, 0),
226
+ friction: nonNegativeNumber(action.friction, 0),
227
+ info_gain: nonNegativeNumber(action.info_gain, 0),
228
+ notes: arrayOfStrings(action.notes)
229
+ }))
230
+ .filter((action) => AXES.some((axis) => action.delta[axis] > 0))
231
+ .sort(compareById);
232
+ }
233
+
234
+ function normalizeRoutePaths(input = [], actions = []) {
235
+ const actionIds = new Set(actions.map((action) => action.id));
236
+ return input
237
+ .map((routePath, index) => ({
238
+ id: safeId(routePath.id || `route_path_${index + 1}`),
239
+ label: String(routePath.label || routePath.id || `Route Path ${index + 1}`),
240
+ action_ids: arrayOfStrings(routePath.action_ids || routePath.actions).map(safeId).filter((id) => actionIds.has(id)),
241
+ notes: arrayOfStrings(routePath.notes)
242
+ }))
243
+ .filter((routePath) => routePath.action_ids.length > 0)
244
+ .sort(compareById);
245
+ }
246
+
247
+ function normalizeDelta(delta = {}) {
248
+ const out = {};
249
+ for (const axis of AXES) out[axis] = clampInt(delta[axis], 0, 3);
250
+ return out;
251
+ }
252
+
253
+ function runAStar({ start, goal, actions, weights }) {
254
+ const open = [nodeForState(start, { g: 0, h: proofDebt(start, goal, weights), risk: 0, friction: 0, info_gain: 0, steps: [] })];
255
+ const best = new Map([[stateKey(start), 0]]);
256
+ const closed = [];
257
+ const snapshots = [];
258
+ let selected = open[0];
259
+
260
+ while (open.length > 0 && closed.length < 64) {
261
+ open.sort(compareNodes);
262
+ const current = open.shift();
263
+ closed.push(current);
264
+ snapshots.push({ step: closed.length, current: current.key, f: current.f, open: open.map((node) => node.key).sort() });
265
+ if (isGoal(current.state, goal)) {
266
+ selected = current;
267
+ break;
268
+ }
269
+ for (const action of actions) {
270
+ const nextState = applyAction(current.state, action, goal);
271
+ const key = stateKey(nextState);
272
+ const g = round(current.g + weights.step);
273
+ if (best.has(key) && best.get(key) <= g) continue;
274
+ best.set(key, g);
275
+ const risk = round(current.risk + action.risk * weights.risk);
276
+ const friction = round(current.friction + action.friction * weights.friction);
277
+ const infoGain = round(current.info_gain + action.info_gain * weights.info_gain);
278
+ const h = proofDebt(nextState, goal, weights);
279
+ open.push(nodeForState(nextState, {
280
+ g,
281
+ h,
282
+ risk,
283
+ friction,
284
+ info_gain: infoGain,
285
+ steps: current.steps.concat([stepFromAction(action, nextState)])
286
+ }));
287
+ }
288
+ }
289
+
290
+ return {
291
+ selected_path: pathFromNode('astar_frontier_path', 'A* Frontier Path', selected),
292
+ frontier: {
293
+ expanded_order: closed.map((node, index) => ({ index, key: node.key, f: node.f, h: node.h, steps: node.steps.map((step) => step.id) })),
294
+ open_nodes: open.sort(compareNodes).slice(0, 12).map((node) => ({ key: node.key, f: node.f, h: node.h })),
295
+ closed_nodes: closed.map((node) => node.key),
296
+ snapshots
297
+ }
298
+ };
299
+ }
300
+
301
+ function evaluateRoutePath(routePath, index, { start, goal, actions, weights }) {
302
+ const actionById = new Map(actions.map((action) => [action.id, action]));
303
+ let state = { ...start };
304
+ let g = 0;
305
+ let risk = 0;
306
+ let friction = 0;
307
+ let infoGain = 0;
308
+ const steps = [];
309
+ for (const id of routePath.action_ids) {
310
+ const action = actionById.get(id);
311
+ if (!action) continue;
312
+ g = round(g + weights.step);
313
+ risk = round(risk + action.risk * weights.risk);
314
+ friction = round(friction + action.friction * weights.friction);
315
+ infoGain = round(infoGain + action.info_gain * weights.info_gain);
316
+ state = applyAction(state, action, goal);
317
+ steps.push(stepFromAction(action, state));
318
+ }
319
+ const h = proofDebt(state, goal, weights);
320
+ const f = round(g + h + risk + friction - infoGain);
321
+ return {
322
+ id: routePath.id,
323
+ label: routePath.label,
324
+ rank_hint: index,
325
+ route: routePath.action_ids,
326
+ steps,
327
+ final_state: state,
328
+ proof_debt: h,
329
+ complete: isGoal(state, goal),
330
+ cost: { g, h, risk, friction, info_gain: infoGain, f },
331
+ notes: routePath.notes
332
+ };
333
+ }
334
+
335
+ function selectPath(candidates, astarPath) {
336
+ const complete = candidates.filter((candidate) => candidate.complete);
337
+ const pool = complete.length ? complete : candidates;
338
+ const selected = pool.slice().sort(compareCandidates)[0] || astarPath;
339
+ return selected.cost.f <= astarPath.cost.f ? selected : astarPath;
340
+ }
341
+
342
+ function pathFromNode(id, label, node) {
343
+ return {
344
+ id,
345
+ label,
346
+ route: node.steps.map((step) => step.id),
347
+ steps: node.steps,
348
+ final_state: node.state,
349
+ proof_debt: node.h,
350
+ complete: node.h === 0,
351
+ cost: {
352
+ g: node.g,
353
+ h: node.h,
354
+ risk: node.risk,
355
+ friction: node.friction,
356
+ info_gain: node.info_gain,
357
+ f: node.f
358
+ },
359
+ notes: ['Generated by A* frontier expansion over the conceptual lattice.']
360
+ };
361
+ }
362
+
363
+ function nodeForState(state, input) {
364
+ const f = round(input.g + input.h + input.risk + input.friction - input.info_gain);
365
+ return { ...input, state, key: stateKey(state), f };
366
+ }
367
+
368
+ function applyAction(state, action, goal) {
369
+ const next = {};
370
+ for (const axis of AXES) next[axis] = Math.min(goal[axis], state[axis] + action.delta[axis]);
371
+ return next;
372
+ }
373
+
374
+ function proofDebt(state, goal, weights) {
375
+ return round(AXES.reduce((sum, axis) => sum + debtForAxis(state, goal, axis), 0) * weights.proof_debt);
376
+ }
377
+
378
+ function debtForAxis(state, goal, axis) {
379
+ return Math.max(0, Number(goal[axis] || 0) - Number(state[axis] || 0));
380
+ }
381
+
382
+ function buildConceptualGrid(start, goal, actions) {
383
+ return {
384
+ axes: AXES.map((axis) => ({ axis, start: start[axis], goal: goal[axis], span: Math.max(0, goal[axis] - start[axis]) })),
385
+ cells: AXES.map((axis) => ({
386
+ id: `axis_${axis}`,
387
+ axis,
388
+ start: start[axis],
389
+ goal: goal[axis],
390
+ candidate_actions: actions.filter((action) => action.delta[axis] > 0).map((action) => action.id)
391
+ })),
392
+ legend: {
393
+ g: 'path steps already paid',
394
+ h: 'remaining proof debt',
395
+ risk: 'expected safety and integration exposure',
396
+ friction: 'coordination and verification drag',
397
+ info_gain: 'uncertainty removed by the step'
398
+ }
399
+ };
400
+ }
401
+
402
+ function rejectionReasons(candidate, selected) {
403
+ const reasons = [];
404
+ if (!candidate.complete) reasons.push('remaining_proof_debt');
405
+ if (candidate.cost.risk > selected.cost.risk) reasons.push('higher_risk');
406
+ if (candidate.cost.friction > selected.cost.friction) reasons.push('higher_friction');
407
+ if (candidate.cost.info_gain < selected.cost.info_gain) reasons.push('lower_info_gain');
408
+ if (candidate.cost.f > selected.cost.f) reasons.push('higher_total_f');
409
+ return reasons.length ? reasons : ['tie_broken_by_deterministic_order'];
410
+ }
411
+
412
+ function compareCandidates(a, b) {
413
+ return (a.cost.f - b.cost.f)
414
+ || (a.cost.h - b.cost.h)
415
+ || (a.cost.risk - b.cost.risk)
416
+ || (a.cost.friction - b.cost.friction)
417
+ || (b.cost.info_gain - a.cost.info_gain)
418
+ || a.id.localeCompare(b.id);
419
+ }
420
+
421
+ function compareNodes(a, b) {
422
+ return (a.f - b.f)
423
+ || (a.h - b.h)
424
+ || (a.risk - b.risk)
425
+ || (a.friction - b.friction)
426
+ || (b.info_gain - a.info_gain)
427
+ || a.key.localeCompare(b.key);
428
+ }
429
+
430
+ function compareById(a, b) {
431
+ return a.id.localeCompare(b.id);
432
+ }
433
+
434
+ function stateKey(state) {
435
+ return AXES.map((axis) => `${axis}:${state[axis]}`).join('|');
436
+ }
437
+
438
+ function isGoal(state, goal) {
439
+ return AXES.every((axis) => state[axis] >= goal[axis]);
440
+ }
441
+
442
+ function stepFromAction(action, state) {
443
+ return {
444
+ id: action.id,
445
+ label: action.label,
446
+ state_after: state,
447
+ risk: action.risk,
448
+ friction: action.friction,
449
+ info_gain: action.info_gain,
450
+ notes: action.notes
451
+ };
452
+ }
453
+
454
+ function arrayOfStrings(value) {
455
+ if (!Array.isArray(value)) return [];
456
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
457
+ }
458
+
459
+ function safeId(value) {
460
+ return String(value || 'item').trim().toLowerCase().replace(/[^a-z0-9_./-]+/g, '_').replace(/^_+|_+$/g, '') || 'item';
461
+ }
462
+
463
+ function clampInt(value, min, max) {
464
+ const number = Number(value);
465
+ if (!Number.isFinite(number)) return min;
466
+ return Math.max(min, Math.min(max, Math.floor(number)));
467
+ }
468
+
469
+ function positiveNumber(value, fallback) {
470
+ const number = Number(value);
471
+ return Number.isFinite(number) && number > 0 ? number : fallback;
472
+ }
473
+
474
+ function nonNegativeNumber(value, fallback) {
475
+ const number = Number(value);
476
+ return Number.isFinite(number) && number >= 0 ? number : fallback;
477
+ }
478
+
479
+ function round(value) {
480
+ return Math.round(Number(value || 0) * 1000) / 1000;
481
+ }
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.8.6';
8
+ export const PACKAGE_VERSION = '0.9.0';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -110,6 +110,11 @@ export async function runWorkflowPerfBench(root, opts = {}) {
110
110
  workflow_complexity_band: proofField?.workflow_complexity?.band || null,
111
111
  team_trigger_count: proofField?.team_trigger_matrix?.active_triggers?.length || 0,
112
112
  verification_stage_cache_key: proofField?.verification_stage_cache?.cache_key || null,
113
+ decision_lattice_selected_path: proofField?.decision_lattice?.selected_path?.id || null,
114
+ decision_lattice_frontier_count: proofField?.decision_lattice?.frontier?.expanded_order?.length || 0,
115
+ decision_lattice_rejected_alternative_count: proofField?.decision_lattice?.rejected_alternatives?.length || 0,
116
+ decision_lattice_scoring_formula: proofField?.decision_lattice?.scoring_formula || null,
117
+ decision_lattice_valid: Boolean(proofField?.decision_lattice?.report_only) && proofValidation.ok,
113
118
  outcome_criteria_passed: (proofField?.simplicity_scorecard?.criteria || []).filter((item) => item.passed).length,
114
119
  proof_field_valid: proofValidation.ok,
115
120
  pipeline_plan_valid: planValidation.ok
@@ -138,7 +143,19 @@ export function validateWorkflowPerfReport(report = {}) {
138
143
  if (!Number.isFinite(Number(report.metrics?.workflow_complexity_score))) issues.push('workflow_complexity_score');
139
144
  if (!report.metrics?.workflow_complexity_band) issues.push('workflow_complexity_band');
140
145
  if (!report.metrics?.verification_stage_cache_key) issues.push('verification_stage_cache_key');
146
+ if (!report.metrics?.decision_lattice_selected_path) issues.push('decision_lattice_selected_path');
147
+ if (!Number.isFinite(Number(report.metrics?.decision_lattice_frontier_count))) issues.push('decision_lattice_frontier_count');
148
+ if (!Number.isFinite(Number(report.metrics?.decision_lattice_rejected_alternative_count))) issues.push('decision_lattice_rejected_alternative_count');
149
+ if (!report.metrics?.decision_lattice_scoring_formula) issues.push('decision_lattice_scoring_formula');
150
+ if (report.metrics?.decision_lattice_valid !== true) issues.push('decision_lattice_valid');
141
151
  if (!report.proof_field || !validateProofFieldReport(report.proof_field).ok) issues.push('proof_field');
152
+ else {
153
+ const lattice = report.proof_field.decision_lattice;
154
+ if (report.metrics.decision_lattice_selected_path !== lattice?.selected_path?.id) issues.push('decision_lattice_selected_path_mismatch');
155
+ if (Number(report.metrics.decision_lattice_frontier_count) !== Number(lattice?.frontier?.expanded_order?.length || 0)) issues.push('decision_lattice_frontier_count_mismatch');
156
+ if (Number(report.metrics.decision_lattice_rejected_alternative_count) !== Number(lattice?.rejected_alternatives?.length || 0)) issues.push('decision_lattice_rejected_alternative_count_mismatch');
157
+ if (report.metrics.decision_lattice_scoring_formula !== lattice?.scoring_formula) issues.push('decision_lattice_scoring_formula_mismatch');
158
+ }
142
159
  if (!report.pipeline_plan || !validatePipelinePlan(report.pipeline_plan).ok) issues.push('pipeline_plan');
143
160
  if (!report.recommendation?.mode) issues.push('recommendation');
144
161
  return { ok: issues.length === 0, issues };
@@ -135,11 +135,32 @@ export function validatePipelinePlan(plan = {}) {
135
135
  if (!Array.isArray(plan.stages) || !plan.stages.length) issues.push('stages');
136
136
  if (!Array.isArray(plan.verification) || !plan.verification.length) issues.push('verification');
137
137
  if (!plan.route_economy?.mode) issues.push('route_economy');
138
+ const routeEconomyLatticeIssues = validateRouteEconomyDecisionLattice(plan.route_economy, plan.proof_field);
139
+ if (routeEconomyLatticeIssues.length) issues.push(...routeEconomyLatticeIssues.map((issue) => `route_economy.decision_lattice:${issue}`));
138
140
  if (plan.no_unrequested_fallback_code !== true || !plan.invariants?.includes('no_unrequested_fallback_code')) issues.push('fallback_guard');
139
141
  if (!plan.next_actions?.length) issues.push('next_actions');
140
142
  return { ok: issues.length === 0, issues };
141
143
  }
142
144
 
145
+ function validateRouteEconomyDecisionLattice(routeEconomy = {}, proof = {}) {
146
+ const lattice = routeEconomy.decision_lattice;
147
+ if (!lattice) return [];
148
+ const issues = [];
149
+ if (routeEconomy.report_only !== true || routeEconomy.mode !== 'report_only') issues.push('requires_report_only_route_economy');
150
+ if (lattice.report_only !== true) issues.push('report_only');
151
+ if (!lattice.selected_path) issues.push('selected_path');
152
+ if (!Number.isFinite(Number(lattice.selected_f_score))) issues.push('selected_f_score');
153
+ if (!Number.isFinite(Number(lattice.frontier_count)) || Number(lattice.frontier_count) < 1) issues.push('frontier_count');
154
+ if (!Number.isFinite(Number(lattice.rejected_alternatives_count))) issues.push('rejected_alternatives_count');
155
+ if (proof?.attached && proof.decision_lattice) {
156
+ const source = proof.decision_lattice;
157
+ if (lattice.selected_path !== source.selected_path?.id) issues.push('selected_path_mismatch');
158
+ if (Number(lattice.frontier_count) !== Number(source.frontier?.expanded_order?.length || 0)) issues.push('frontier_count_mismatch');
159
+ if (Number(lattice.rejected_alternatives_count) !== Number(source.rejected_alternatives?.length || 0)) issues.push('rejected_alternatives_count_mismatch');
160
+ }
161
+ return issues;
162
+ }
163
+
143
164
  function normalizeAmbiguity(value = {}, route) {
144
165
  const required = value.required ?? !CLARIFICATION_BYPASS_ROUTES.has(route?.id);
145
166
  const slots = Number(value.slots || 0);
@@ -173,7 +194,8 @@ function normalizeProofField(report) {
173
194
  contract_clarity: report.contract_clarity || null,
174
195
  workflow_complexity: report.workflow_complexity || null,
175
196
  team_trigger_matrix: report.team_trigger_matrix || null,
176
- verification_stage_cache: report.verification_stage_cache || null
197
+ verification_stage_cache: report.verification_stage_cache || null,
198
+ decision_lattice: report.decision_lattice || null
177
199
  };
178
200
  }
179
201
 
@@ -199,6 +221,13 @@ function routeEconomyPlan(proof = {}) {
199
221
  team_trigger_count: triggers.length,
200
222
  active_team_triggers: triggers,
201
223
  verification_stage_cache_key: proof.verification_stage_cache?.cache_key || null,
224
+ decision_lattice: proof.decision_lattice ? {
225
+ selected_path: proof.decision_lattice.selected_path?.id || null,
226
+ selected_f_score: proof.decision_lattice.selected_path?.cost?.f ?? null,
227
+ frontier_count: proof.decision_lattice.frontier?.expanded_order?.length || 0,
228
+ rejected_alternatives_count: proof.decision_lattice.rejected_alternatives?.length || 0,
229
+ report_only: proof.decision_lattice.report_only === true
230
+ } : null,
202
231
  deletion_policy: 'do_not_delete_or_skip_pipeline_stages_until_report_only_metrics_are_calibrated'
203
232
  };
204
233
  }
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { nowIso, readText, rel, runProcess, sha256, writeJsonAtomic } from './fsx.mjs';
3
+ import { buildDecisionLatticeReport, validateDecisionLatticeReport } from './decision-lattice.mjs';
3
4
 
4
5
  export const PROOF_FIELD_SCHEMA_VERSION = 1;
5
6
  export const FAST_LANE_MIN_SCORE = 0.75;
@@ -88,6 +89,20 @@ export async function buildProofField(root, opts = {}) {
88
89
  const verificationStageCache = verificationStageCachePlan({ sourceHash, changedFiles, verification: fastLane.verification });
89
90
  const simplicity = outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeWork, fastLane, workflowComplexity });
90
91
  const executionLane = executionLaneDecision({ fastLane, simplicity, workflowComplexity, teamTriggerMatrix });
92
+ const decisionLattice = normalizeDecisionLatticeReport(await buildDecisionLatticeReport({
93
+ intent,
94
+ changed_files: changedFiles,
95
+ proof_cones: selectedCones,
96
+ risk,
97
+ contract_clarity: contractClarity,
98
+ workflow_complexity: workflowComplexity,
99
+ team_trigger_matrix: teamTriggerMatrix,
100
+ verification_stage_cache: verificationStageCache,
101
+ simplicity_scorecard: simplicity,
102
+ fast_lane_decision: fastLane,
103
+ execution_lane: executionLane,
104
+ scoring_formula: 'simplicity_scorecard.score + contract_clarity.score - workflow_complexity.score - active_team_trigger_penalty'
105
+ }));
91
106
  return {
92
107
  schema_version: PROOF_FIELD_SCHEMA_VERSION,
93
108
  generated_at: nowIso(),
@@ -102,6 +117,7 @@ export async function buildProofField(root, opts = {}) {
102
117
  workflow_complexity: workflowComplexity,
103
118
  team_trigger_matrix: teamTriggerMatrix,
104
119
  verification_stage_cache: verificationStageCache,
120
+ decision_lattice: decisionLattice,
105
121
  simplicity_scorecard: simplicity,
106
122
  execution_lane: executionLane,
107
123
  proof_cones: selectedCones,
@@ -133,6 +149,8 @@ export function validateProofFieldReport(report = {}) {
133
149
  if (!Number.isFinite(Number(report.workflow_complexity?.score))) issues.push('workflow_complexity');
134
150
  if (!Array.isArray(report.team_trigger_matrix?.triggers)) issues.push('team_trigger_matrix');
135
151
  if (report.verification_stage_cache?.report_only !== true || !report.verification_stage_cache?.cache_key) issues.push('verification_stage_cache');
152
+ const latticeValidation = validateDecisionLatticeReport(report.decision_lattice);
153
+ if (!latticeValidation.ok) issues.push(`decision_lattice:${latticeValidation.issues.join('|')}`);
136
154
  if (!report.execution_lane?.lane) issues.push('execution_lane');
137
155
  if (report.execution_lane?.lane === SPEED_LANE_POLICY.fast_lane && report.execution_lane?.score < FAST_LANE_MIN_SCORE) issues.push('execution_lane_score');
138
156
  if (!Array.isArray(report.proof_cones)) issues.push('proof_cones');
@@ -158,12 +176,25 @@ export async function proofFieldFixture() {
158
176
  outcome_rubric_present: report.outcome_rubric.length === OUTCOME_RUBRIC.length,
159
177
  adversarial_lenses_present: report.outcome_rubric.every((item) => item.adversarial_lens) && report.simplicity_scorecard.criteria.every((item) => item.adversarial_lens),
160
178
  route_economy_present: report.contract_clarity?.report_only === true && report.workflow_complexity?.report_only === true && report.team_trigger_matrix?.report_only === true && report.verification_stage_cache?.report_only === true,
179
+ decision_lattice_present: validateDecisionLatticeReport(report.decision_lattice).ok,
180
+ decision_lattice_report_only: report.decision_lattice?.report_only === true,
181
+ decision_lattice_selected_path: Boolean(report.decision_lattice?.selected_path?.id),
182
+ decision_lattice_frontier_present: Array.isArray(report.decision_lattice?.frontier?.expanded_order) && report.decision_lattice.frontier.expanded_order.length > 0,
183
+ decision_lattice_rejections_present: Array.isArray(report.decision_lattice?.rejected_alternatives),
184
+ decision_lattice_scoring_formula_present: Boolean(report.decision_lattice?.scoring_formula),
161
185
  simplicity_score_usable: Number(report.simplicity_scorecard?.score) >= FAST_LANE_MIN_SCORE,
162
186
  execution_fast_lane_selected: report.execution_lane?.lane === SPEED_LANE_POLICY.fast_lane
163
187
  }
164
188
  };
165
189
  }
166
190
 
191
+ function normalizeDecisionLatticeReport(report = {}) {
192
+ return {
193
+ ...report,
194
+ scoring_formula: report.scoring_formula || report.research_basis?.scoring_formula || null
195
+ };
196
+ }
197
+
167
198
  async function gitChangedFiles(root) {
168
199
  const result = await runProcess('git', ['diff', '--name-only', 'HEAD', '--'], { cwd: root, timeoutMs: 10000, maxOutputBytes: 128 * 1024 });
169
200
  if (result.code !== 0) return [];