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 +14 -1
- package/package.json +1 -1
- package/src/cli/main.mjs +3 -3
- package/src/cli/maintenance-commands.mjs +3 -0
- package/src/core/decision-lattice.mjs +481 -0
- package/src/core/fsx.mjs +1 -1
- package/src/core/perf-bench.mjs +17 -0
- package/src/core/pipeline.mjs +30 -1
- package/src/core/proof-field.mjs +31 -0
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`
|
|
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.
|
|
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
|
+
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
|
|
package/src/core/perf-bench.mjs
CHANGED
|
@@ -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 };
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -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
|
}
|
package/src/core/proof-field.mjs
CHANGED
|
@@ -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 [];
|