onto-mcp 0.4.12 → 0.4.13
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/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +137 -0
- package/.onto/domains/accounting-kr/competency_qs.md +153 -153
- package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
- package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
- package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +169 -8
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
- package/.onto/processes/review/productized-live-path.md +9 -5
- package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
- package/.onto/processes/review/review-target-profile-contract.md +47 -4
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
- package/.onto/processes/shared/target-material-kind-contract.md +1 -1
- package/README.md +64 -1
- package/dist/cli.js +11 -1
- package/dist/core-api/reconstruct-api.js +575 -24
- package/dist/core-api/review-api.js +70 -5
- package/dist/core-api/review-progress.js +7 -0
- package/dist/core-api/runtime-observation.js +7 -0
- package/dist/core-runtime/artifact-io.js +75 -3
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
- package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
- package/dist/core-runtime/cli/prepare-review-session.js +34 -1
- package/dist/core-runtime/cli/render-review-final-output.js +2 -1
- package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
- package/dist/core-runtime/cli/run-seats.js +18 -0
- package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
- package/dist/core-runtime/cli/unit-resubmit.js +269 -0
- package/dist/core-runtime/cli/worker-structured-output.js +80 -2
- package/dist/core-runtime/discovery/host-detection.js +71 -22
- package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
- package/dist/core-runtime/discovery/review-cert-record.js +442 -0
- package/dist/core-runtime/discovery/review-execution-units.js +22 -0
- package/dist/core-runtime/discovery/seat-inventory.js +167 -0
- package/dist/core-runtime/discovery/settings-chain.js +311 -52
- package/dist/core-runtime/discovery/supported-models.js +282 -29
- package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
- package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
- package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
- package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
- package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
- package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
- package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
- package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
- package/dist/core-runtime/effort-calibration-ingest.js +135 -0
- package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
- package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
- package/dist/core-runtime/llm/llm-caller.js +188 -47
- package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
- package/dist/core-runtime/llm/model-switcher.js +1 -1
- package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
- package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
- package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
- package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
- package/dist/core-runtime/onboard/cli-host.js +5 -2
- package/dist/core-runtime/onboard/configure-provider.js +33 -1
- package/dist/core-runtime/onboard/host-target.js +28 -3
- package/dist/core-runtime/onboard/register.js +54 -12
- package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
- package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
- package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
- package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
- package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
- package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
- package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
- package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
- package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
- package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
- package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
- package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
- package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
- package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
- package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
- package/dist/core-runtime/reconstruct/record.js +42 -0
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
- package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
- package/dist/core-runtime/reconstruct/run.js +5198 -536
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
- package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
- package/dist/core-runtime/review/continuation-plan.js +12 -5
- package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
- package/dist/core-runtime/review/materializers.js +100 -11
- package/dist/core-runtime/review/nesting-batch.js +6 -2
- package/dist/core-runtime/review/obligation-shardability.js +138 -0
- package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
- package/dist/core-runtime/review/review-artifact-utils.js +217 -5
- package/dist/core-runtime/review/review-execution-steps.js +32 -8
- package/dist/core-runtime/review/review-progress-contract.js +8 -0
- package/dist/core-runtime/review/review-prompt-budget.js +97 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
- package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
- package/dist/core-runtime/route-identity.js +182 -0
- package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
- package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
- package/dist/core-runtime/target-material-kind.js +58 -0
- package/dist/mcp/server.js +12 -5
- package/dist/tui/app.js +167 -0
- package/dist/tui/data/event-follower.js +116 -0
- package/dist/tui/data/node-detail.js +42 -0
- package/dist/tui/data/projection-poll.js +26 -0
- package/dist/tui/data/session-discovery.js +65 -0
- package/dist/tui/index.js +108 -0
- package/dist/tui/screens/log.js +25 -0
- package/dist/tui/screens/node-detail.js +17 -0
- package/dist/tui/screens/session-selector.js +26 -0
- package/dist/tui/screens/workflow-tree.js +90 -0
- package/dist/tui/view-model/reconstruct-adapter.js +162 -0
- package/dist/tui/view-model/review-adapter.js +270 -0
- package/dist/tui/view-model/tree-view-model.js +19 -0
- package/package.json +18 -3
- package/settings.example.json +77 -16
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { REVIEW_EXECUTION_UNIT_IDS } from "../discovery/settings-chain.js";
|
|
2
|
+
import { reviewExecutionUnitActor } from "./review-execution-profile.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reference context window (tokens) that maps to multiplier 1 — the smallest
|
|
5
|
+
* window that still earns the DEFAULT (unchanged) budget. A window at or below
|
|
6
|
+
* this floors to multiplier 1. PRELIMINARY (INV-BENCH-1).
|
|
7
|
+
*/
|
|
8
|
+
export const BASELINE_WINDOW_TOKENS = 200_000;
|
|
9
|
+
/**
|
|
10
|
+
* The fixed embed line budget applied to a packet embed when no window-scaled
|
|
11
|
+
* value is resolved — the no-regression floor and the single source of this
|
|
12
|
+
* value, shared by the prepare-time budget resolution and the packet stage's
|
|
13
|
+
* fallback. (Unchanged from the historical packet-stage default of 300.)
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_MAX_EMBED_LINES = 300;
|
|
16
|
+
/**
|
|
17
|
+
* Hard ceiling on the window multiplier, so an arbitrarily large window cannot
|
|
18
|
+
* blow up the prompt budget without bound. PRELIMINARY (INV-BENCH-1).
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_WINDOW_MULTIPLIER = 4;
|
|
21
|
+
/**
|
|
22
|
+
* Convert a resolved context window (tokens) into a budget multiplier.
|
|
23
|
+
* undefined (unresolved model / no registered window) → 1 (no regression).
|
|
24
|
+
* Otherwise clamp(window / BASELINE_WINDOW_TOKENS, 1, MAX_WINDOW_MULTIPLIER) —
|
|
25
|
+
* floored at 1 so a small-window model is never penalised below DEFAULT, and
|
|
26
|
+
* ceilinged at MAX so a huge window cannot grow the budget without bound.
|
|
27
|
+
*/
|
|
28
|
+
export function reviewWindowMultiplier(contextWindowTokens) {
|
|
29
|
+
if (contextWindowTokens === undefined)
|
|
30
|
+
return 1;
|
|
31
|
+
return Math.min(Math.max(contextWindowTokens / BASELINE_WINDOW_TOKENS, 1), MAX_WINDOW_MULTIPLIER);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Window-proportional embed line budget — the true binding cut applied to the
|
|
35
|
+
* packet embed (`truncateForEmbedding`). Floored at `defaultMaxEmbedLines` so
|
|
36
|
+
* multiplier 1 returns the unchanged DEFAULT (no regression), and ceilinged at
|
|
37
|
+
* `defaultMaxEmbedLines * MAX_WINDOW_MULTIPLIER`.
|
|
38
|
+
*/
|
|
39
|
+
export function deriveReviewMaxEmbedLines(multiplier, defaultMaxEmbedLines) {
|
|
40
|
+
return Math.min(Math.max(Math.round(defaultMaxEmbedLines * multiplier), defaultMaxEmbedLines), defaultMaxEmbedLines * MAX_WINDOW_MULTIPLIER);
|
|
41
|
+
}
|
|
42
|
+
/** Merge an actor llm ref with an optional unit override (override wins per
|
|
43
|
+
* field), the same shallow merge the execution-profile resolver uses. Returns
|
|
44
|
+
* undefined only when both are absent. */
|
|
45
|
+
function mergeLensLlmRef(base, override) {
|
|
46
|
+
if (!base && !override)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { ...(base ?? {}), ...(override ?? {}) };
|
|
49
|
+
}
|
|
50
|
+
/** Look up the registered context window (tokens) for a resolved lens llm ref.
|
|
51
|
+
* The registry key is the MODEL provider + model id (e.g. openai/gpt-5.5), NOT
|
|
52
|
+
* the runtime execution adapter (codex_cli / claude_code). An unresolved
|
|
53
|
+
* provider or model, an unregistered pair, or an entry without a window all
|
|
54
|
+
* yield undefined. */
|
|
55
|
+
function resolveLlmRefWindowTokens(llm, registry) {
|
|
56
|
+
const provider = llm?.provider;
|
|
57
|
+
const modelId = llm?.model;
|
|
58
|
+
if (!provider || !modelId)
|
|
59
|
+
return undefined;
|
|
60
|
+
return registry.supported_models.find((entry) => entry.provider === provider && entry.model === modelId)?.context_window_tokens;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the lens model's context window (tokens) for the review prompt budget.
|
|
64
|
+
*
|
|
65
|
+
* All lenses share the single "lens" actor model, so the common path resolves
|
|
66
|
+
* that one representative (`execution.lens.llm`). When a lens-CLASS unit override
|
|
67
|
+
* (a unit whose actor is "lens": lens / issue_stance_response /
|
|
68
|
+
* deliberation_response) selects a different model, every such effective lens
|
|
69
|
+
* model is resolved and the MINIMUM window is taken — the min can never overflow
|
|
70
|
+
* any lens, so the budget stays safe for the smallest-window lens (the floor
|
|
71
|
+
* keeps it conservative).
|
|
72
|
+
*
|
|
73
|
+
* Returns undefined when no lens model resolves to a registered window, so the
|
|
74
|
+
* caller falls through to multiplier 1 → DEFAULT budgets (no regression). Never
|
|
75
|
+
* throws.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveReviewLensContextWindowTokens(execution, registry) {
|
|
78
|
+
const lensActorLlm = execution.lens.llm;
|
|
79
|
+
const windows = [];
|
|
80
|
+
const baseWindow = resolveLlmRefWindowTokens(lensActorLlm, registry);
|
|
81
|
+
if (baseWindow !== undefined)
|
|
82
|
+
windows.push(baseWindow);
|
|
83
|
+
for (const unitId of REVIEW_EXECUTION_UNIT_IDS) {
|
|
84
|
+
if (reviewExecutionUnitActor(unitId) !== "lens")
|
|
85
|
+
continue;
|
|
86
|
+
const override = execution.units[unitId]?.llm;
|
|
87
|
+
if (!override)
|
|
88
|
+
continue;
|
|
89
|
+
const effective = mergeLensLlmRef(lensActorLlm, override);
|
|
90
|
+
const window = resolveLlmRefWindowTokens(effective, registry);
|
|
91
|
+
if (window !== undefined)
|
|
92
|
+
windows.push(window);
|
|
93
|
+
}
|
|
94
|
+
if (windows.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
return Math.min(...windows);
|
|
97
|
+
}
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/** The complete check-id universe this gate can emit — the value-level single
|
|
2
|
+
* source (the check_id type derives from it). The review-cert/v2 evidence
|
|
3
|
+
* contract pins its per-run check universe against THIS list, so a harness
|
|
4
|
+
* that omits issue artifacts (the gate then emits a subset — see
|
|
5
|
+
* runSemanticQualityGate) cannot silently shrink the certified comparison. */
|
|
6
|
+
export const SEMANTIC_QUALITY_GATE_CHECK_IDS = [
|
|
7
|
+
"material_issue_recall",
|
|
8
|
+
"final_result_material_issue_recall",
|
|
9
|
+
"false_materiality_guard",
|
|
10
|
+
"boundary_uncertainty_preservation",
|
|
11
|
+
"non_material_finding_preservation",
|
|
12
|
+
"artifact_material_issue_recall",
|
|
13
|
+
"causal_materiality_shape",
|
|
14
|
+
"causal_relation_correctness",
|
|
15
|
+
"issue_dependency_preservation",
|
|
16
|
+
"actionability",
|
|
17
|
+
"count_list_consistency",
|
|
18
|
+
"grounding",
|
|
19
|
+
];
|
|
1
20
|
const DEFAULT_FIXTURE_ID = "review-pipeline-target-v1";
|
|
2
21
|
const FIXTURES = {
|
|
3
22
|
"review-pipeline-target-v1": {
|
|
@@ -458,14 +477,21 @@ export function evaluateReviewPipelineSemanticQualityGate(args) {
|
|
|
458
477
|
`non_material_finding_count=${nonMaterialFindings.length}`,
|
|
459
478
|
`boundary_notes_chars=${boundaryNotes?.length ?? 0}`,
|
|
460
479
|
]);
|
|
480
|
+
// Boundary uncertainty's preservation AUTHORITY is the finding-ledger
|
|
481
|
+
// (non-material findings), not the final Boundary Notes projection. A model
|
|
482
|
+
// preserves the decoy's uncertainty by recording it as a non-material,
|
|
483
|
+
// boundary-contextualized finding; whether it also echoes that into the final
|
|
484
|
+
// summary is a projection-style choice, not a quality signal — one model may
|
|
485
|
+
// prioritize the MATERIAL issue's own confidence boundaries in the final note
|
|
486
|
+
// and keep the non-material decoy in the ledger for audit, which is at least as
|
|
487
|
+
// sound. So this reads nonMaterialText (the authority) and stays independent of
|
|
488
|
+
// false_materiality_guard, which checks the ORTHOGONAL axis: the decoy was not
|
|
489
|
+
// mis-promoted to a material issue. (A decoy surfaced only in the final note but
|
|
490
|
+
// absent from the authority still fails here — authority is where it must live.)
|
|
461
491
|
const boundaryUncertainty = check("boundary_uncertainty_preservation", !falseMaterialityCandidateObserved ||
|
|
462
|
-
(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
boundaryNotes === null
|
|
466
|
-
? "Boundary Notes section missing"
|
|
467
|
-
: `Boundary Notes chars=${boundaryNotes.length}`,
|
|
468
|
-
`expected: compact note preserving ${fixture.boundaryUncertaintyTerms.join(", ")} uncertainty`,
|
|
492
|
+
containsBoundarySensitiveUncertainty(nonMaterialText, fixture), [
|
|
493
|
+
`non_material_boundary_chars=${nonMaterialText.length}`,
|
|
494
|
+
`expected boundary uncertainty (${fixture.boundaryUncertaintyTerms.join(", ")}) preserved in the finding-ledger authority (non-material findings)`,
|
|
469
495
|
]);
|
|
470
496
|
const materialActionRefs = new Set(materialIssues.flatMap((issue) => strings(issue.action_candidates)));
|
|
471
497
|
for (const item of actionCandidates) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { inventoryHasRenderableStructure, } from "../spreadsheet-structure-observer.js";
|
|
3
|
+
/**
|
|
4
|
+
* risk_signal kinds that do NOT count as `structural_risk_signals` backing:
|
|
5
|
+
* - `unreadable_sheet_part`: an observation-failure marker, not a structural risk (so a
|
|
6
|
+
* risk-only corrupt shell backs nothing → not inspectable; R4 held).
|
|
7
|
+
* - `macro_present`: already owned by `access_and_protection_hygiene` (the observer pushes
|
|
8
|
+
* BOTH a `macro_present` boolean and a `macro_present` risk_signal — counting the signal
|
|
9
|
+
* here would double-back and re-open the #3/R4 asymmetry).
|
|
10
|
+
* - `external_links_present`: already owned by the explicit `external_links.length > 0`
|
|
11
|
+
* clause (same double-back hazard).
|
|
12
|
+
* The genuine residual structural-risk kinds are `ragged_row`, `oversized_zip_entry`,
|
|
13
|
+
* `pivot_table_cap`. (Error cells are NOT risk_signals — they live in `error_cells[]`,
|
|
14
|
+
* ORed in separately below.)
|
|
15
|
+
*/
|
|
16
|
+
const NON_STRUCTURAL_RISK_KINDS = new Set([
|
|
17
|
+
"unreadable_sheet_part",
|
|
18
|
+
"macro_present",
|
|
19
|
+
"external_links_present",
|
|
20
|
+
]);
|
|
21
|
+
/** True when a risk_signal kind is a genuine structural risk (not an observation-failure
|
|
22
|
+
* marker or a signal already owned by another obligation). */
|
|
23
|
+
export function isStructuralRiskSignal(kind) {
|
|
24
|
+
return !NON_STRUCTURAL_RISK_KINDS.has(kind);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The positive backing rule: each spreadsheet review obligation is listed only when its
|
|
28
|
+
* specific evidence exists in this ref's inventory. Mirrors the catalog in
|
|
29
|
+
* `reviewMaterialGoals("spreadsheet")` (target-material-kind.ts) one-to-one.
|
|
30
|
+
*/
|
|
31
|
+
function computeBackedGoals(inv) {
|
|
32
|
+
const goals = [];
|
|
33
|
+
if (inv.formula_cells_total > 0) {
|
|
34
|
+
goals.push("formula_integrity");
|
|
35
|
+
}
|
|
36
|
+
if (inv.formula_patterns.some((pattern) => pattern.cross_sheet_refs.length > 0) ||
|
|
37
|
+
inv.cross_sheet_key_overlap.length > 0) {
|
|
38
|
+
goals.push("cross_sheet_reference_integrity");
|
|
39
|
+
}
|
|
40
|
+
if (inv.named_ranges.length > 0) {
|
|
41
|
+
goals.push("named_range_hygiene");
|
|
42
|
+
}
|
|
43
|
+
if (inv.data_validations.length > 0) {
|
|
44
|
+
goals.push("data_validation_coverage");
|
|
45
|
+
}
|
|
46
|
+
if (inv.macro_present || inv.sheets.some((sheet) => sheet.hidden || sheet.protected)) {
|
|
47
|
+
goals.push("access_and_protection_hygiene");
|
|
48
|
+
}
|
|
49
|
+
if (inv.risk_signals.some((signal) => isStructuralRiskSignal(signal.kind)) ||
|
|
50
|
+
inv.external_links.length > 0 ||
|
|
51
|
+
inv.error_cells.length > 0) {
|
|
52
|
+
goals.push("structural_risk_signals");
|
|
53
|
+
}
|
|
54
|
+
return goals;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute the single per-ref disposition from the shared workbook inventory (or its
|
|
58
|
+
* absence). Pure, total: never throws. `inventory` is undefined when the ref was never
|
|
59
|
+
* observed (e.g. a directory path that did not pass isSpreadsheetRef).
|
|
60
|
+
*/
|
|
61
|
+
export function computeSpreadsheetDisposition(inventory, ref) {
|
|
62
|
+
const resolved = path.resolve(ref);
|
|
63
|
+
if (inventory === undefined) {
|
|
64
|
+
return {
|
|
65
|
+
ref: resolved,
|
|
66
|
+
inspectable: false,
|
|
67
|
+
reason: `${resolved}: workbook not observed`,
|
|
68
|
+
sha256: null,
|
|
69
|
+
backed_goals: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// The observer declined/failed to read it (unsupported format, unreadable, oversized,
|
|
73
|
+
// or a guarded decode crash) — content_sha256 is "" on a true skip, the real hash when
|
|
74
|
+
// the bytes were read but extraction is unimplemented (.xls etc.).
|
|
75
|
+
if (inventory.unsupported_reason !== null) {
|
|
76
|
+
return {
|
|
77
|
+
ref: resolved,
|
|
78
|
+
inspectable: false,
|
|
79
|
+
reason: `${resolved}: ${inventory.unsupported_reason}`,
|
|
80
|
+
sha256: inventory.content_sha256 || null,
|
|
81
|
+
backed_goals: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const renderable = inventoryHasRenderableStructure(inventory);
|
|
85
|
+
return {
|
|
86
|
+
ref: resolved,
|
|
87
|
+
inspectable: renderable,
|
|
88
|
+
reason: renderable
|
|
89
|
+
? null
|
|
90
|
+
: `${resolved}: no renderable structure (empty or unreadable workbook)`,
|
|
91
|
+
sha256: inventory.content_sha256 || null,
|
|
92
|
+
backed_goals: renderable ? computeBackedGoals(inventory) : [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified route identity (effort-calibration simplification, design
|
|
3
|
+
* 20260617-effort-calibration-simplification-telemetry-derived-design.md §5).
|
|
4
|
+
*
|
|
5
|
+
* A RouteIdentity is a projection of the model-switcher's already-resolved
|
|
6
|
+
* selection — it REUSES LlmExecutionAdapter / LlmProviderName / LlmBillingMode
|
|
7
|
+
* rather than minting a parallel vocabulary. The only genuinely new fields are
|
|
8
|
+
* `route_provenance` (witnessed vs profile-derived) and `route_completeness`
|
|
9
|
+
* (whether the route resolved past model_provider).
|
|
10
|
+
*
|
|
11
|
+
* Provenance honesty (design §3, §8):
|
|
12
|
+
* - reconstruct derives execution_adapter / model_provider from the resolved
|
|
13
|
+
* selection carried on the call config (witnessed at the call boundary), NOT
|
|
14
|
+
* by reverse-mapping effective_base_url (which cannot recover model_provider
|
|
15
|
+
* for codex-cli://oauth or a custom openai-compatible base).
|
|
16
|
+
* - billing_mode is a code-path constant (`declared_billing_mode`), so it stays
|
|
17
|
+
* declared-provenance even on the reconstruct (otherwise witnessed) side.
|
|
18
|
+
* - effective_base_url is corroboration only, and identifies custom proxy bases
|
|
19
|
+
* that downgrade route_completeness.
|
|
20
|
+
*/
|
|
21
|
+
import { DEFAULT_GROK_BASE_URL, DEFAULT_LMSTUDIO_BASE_URL, } from "./llm/model-switcher.js";
|
|
22
|
+
/**
|
|
23
|
+
* Map the model-switcher's RuntimeLlmProvider brand to the canonical model
|
|
24
|
+
* vendor. The only brand that differs from its model_provider is `codex` (the
|
|
25
|
+
* openai OAuth brand). Returns null for an unknown brand (route_completeness
|
|
26
|
+
* then degrades to under_determined).
|
|
27
|
+
*/
|
|
28
|
+
export function modelProviderFromRuntimeProvider(provider) {
|
|
29
|
+
switch (provider) {
|
|
30
|
+
case "codex":
|
|
31
|
+
case "openai":
|
|
32
|
+
return "openai";
|
|
33
|
+
case "anthropic":
|
|
34
|
+
return "anthropic";
|
|
35
|
+
case "grok":
|
|
36
|
+
return "grok";
|
|
37
|
+
case "lmstudio":
|
|
38
|
+
return "lmstudio";
|
|
39
|
+
default:
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The non-default openai-compatible bases that ARE normal provider routes. */
|
|
44
|
+
const KNOWN_OPENAI_COMPATIBLE_BASE_URLS = new Set([
|
|
45
|
+
DEFAULT_GROK_BASE_URL,
|
|
46
|
+
DEFAULT_LMSTUDIO_BASE_URL,
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* Whether an `openai_compatible_http` route runs against a CUSTOM proxy base —
|
|
50
|
+
* a base_url that is neither a mock sentinel nor one of the known provider
|
|
51
|
+
* defaults. The selection still resolves an adapter + model_provider, but a
|
|
52
|
+
* custom endpoint cannot be treated as the normal provider route (design §6 MF2):
|
|
53
|
+
* effective_base_url is corroboration only, so a custom base downgrades
|
|
54
|
+
* route_completeness rather than passing as fully witnessed.
|
|
55
|
+
*/
|
|
56
|
+
function isCustomOpenAiCompatibleBase(adapter, baseUrl) {
|
|
57
|
+
if (adapter !== "openai_compatible_http")
|
|
58
|
+
return false;
|
|
59
|
+
if (!baseUrl)
|
|
60
|
+
return false;
|
|
61
|
+
return !KNOWN_OPENAI_COMPATIBLE_BASE_URLS.has(baseUrl);
|
|
62
|
+
}
|
|
63
|
+
function completeness(adapter, modelProvider, baseUrl) {
|
|
64
|
+
if (!modelProvider)
|
|
65
|
+
return "under_determined";
|
|
66
|
+
if (!adapter)
|
|
67
|
+
return "provider_only";
|
|
68
|
+
if (isCustomOpenAiCompatibleBase(adapter, baseUrl))
|
|
69
|
+
return "provider_only";
|
|
70
|
+
return "complete";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the witnessed reconstruct RouteIdentity from the fields available at the
|
|
74
|
+
* call-record boundary: the resolved provider brand and execution_adapter
|
|
75
|
+
* carried on the call config (witnessed), the call result's declared billing
|
|
76
|
+
* mode and effective_base_url. A `mock://` base short-circuits to the mock
|
|
77
|
+
* adapter (coarse; reconstruct contributes no realization).
|
|
78
|
+
*/
|
|
79
|
+
export function witnessedReconstructRouteIdentity(input) {
|
|
80
|
+
const effectiveBaseUrl = input.effectiveBaseUrl ?? null;
|
|
81
|
+
if (effectiveBaseUrl?.startsWith("mock://")) {
|
|
82
|
+
return {
|
|
83
|
+
execution_adapter: "mock",
|
|
84
|
+
model_provider: null,
|
|
85
|
+
billing_mode: input.declaredBillingMode ?? "local",
|
|
86
|
+
effective_base_url: effectiveBaseUrl,
|
|
87
|
+
route_provenance: "witnessed",
|
|
88
|
+
route_completeness: "complete",
|
|
89
|
+
realization: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const adapter = input.executionAdapter ?? null;
|
|
93
|
+
const modelProvider = modelProviderFromRuntimeProvider(input.provider);
|
|
94
|
+
return {
|
|
95
|
+
execution_adapter: adapter,
|
|
96
|
+
model_provider: modelProvider,
|
|
97
|
+
billing_mode: input.declaredBillingMode ?? null,
|
|
98
|
+
effective_base_url: effectiveBaseUrl,
|
|
99
|
+
route_provenance: "witnessed",
|
|
100
|
+
route_completeness: completeness(adapter, modelProvider, effectiveBaseUrl),
|
|
101
|
+
realization: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build a profile-derived RouteIdentity from a review route projection (already
|
|
106
|
+
* structurally rich: adapter / model_provider / billing / base_url). Review has
|
|
107
|
+
* no result-level base_url witness, so provenance is profile_derived (design
|
|
108
|
+
* §4, §11.1). `realization` (semantic_mock | boundary_stub | fixture) is
|
|
109
|
+
* review-only.
|
|
110
|
+
*/
|
|
111
|
+
export function profileDerivedRouteIdentity(input) {
|
|
112
|
+
const adapter = input.executionAdapter ?? null;
|
|
113
|
+
const modelProvider = input.modelProvider ?? null;
|
|
114
|
+
const effectiveBaseUrl = input.effectiveBaseUrl ?? null;
|
|
115
|
+
return {
|
|
116
|
+
execution_adapter: adapter,
|
|
117
|
+
model_provider: modelProvider,
|
|
118
|
+
billing_mode: input.billingMode ?? null,
|
|
119
|
+
effective_base_url: effectiveBaseUrl,
|
|
120
|
+
route_provenance: "profile_derived",
|
|
121
|
+
route_completeness: completeness(adapter, modelProvider, effectiveBaseUrl),
|
|
122
|
+
realization: input.realization ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Derived single-string projection of a RouteIdentity for CLI `--route`
|
|
127
|
+
* comparison and grouping keys. The structured RouteIdentity stays canonical;
|
|
128
|
+
* this is a lossy projection (design §11.2). Shape:
|
|
129
|
+
* `<adapter|provider_only>:<billing>:<model_provider>`.
|
|
130
|
+
*/
|
|
131
|
+
export function routeToken(identity) {
|
|
132
|
+
const adapter = identity.execution_adapter ?? "provider_only";
|
|
133
|
+
const billing = identity.billing_mode ?? "unknown";
|
|
134
|
+
const provider = identity.model_provider ?? "unknown";
|
|
135
|
+
return `${adapter}:${billing}:${provider}`;
|
|
136
|
+
}
|
|
137
|
+
const ROUTE_COMPLETENESS_ORDER = {
|
|
138
|
+
complete: 0,
|
|
139
|
+
provider_only: 1,
|
|
140
|
+
under_determined: 2,
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The most degraded completeness across a set of identities. The route axis of
|
|
144
|
+
* the decision-grade gate (design §7 Q3) uses the worst — one legacy
|
|
145
|
+
* provider-only source taints the merged profile's route. `complete` is the
|
|
146
|
+
* fold identity (an empty list returns `complete`); a caller that treats "no
|
|
147
|
+
* identities at all" as a degradation must handle the empty case itself.
|
|
148
|
+
*/
|
|
149
|
+
export function worstRouteCompleteness(values) {
|
|
150
|
+
let worst = "complete";
|
|
151
|
+
for (const value of values) {
|
|
152
|
+
if (ROUTE_COMPLETENESS_ORDER[value] > ROUTE_COMPLETENESS_ORDER[worst]) {
|
|
153
|
+
worst = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return worst;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Whether a declared `--route` hint corroborates a derived identity. The hint is
|
|
160
|
+
* a human label — historically provider-level (`"anthropic"`) or the slash form
|
|
161
|
+
* (`"anthropic/claude-cli"`) — while the canonical route is the structured
|
|
162
|
+
* RouteIdentity. The hint is demoted to a non-fatal cross-check (design §5,
|
|
163
|
+
* §11.2): an EXACT match against the full routeToken, the model_provider, or the
|
|
164
|
+
* execution_adapter always corroborates. The lenient provider-head fallback
|
|
165
|
+
* (first slash segment → model_provider) applies ONLY when the adapter is
|
|
166
|
+
* unknown (provider_only / under_determined): for a `complete` identity the
|
|
167
|
+
* adapter IS known, so a slash hint naming a different adapter (e.g.
|
|
168
|
+
* `anthropic/claude-cli` against an `anthropic_sdk` identity) must NOT be
|
|
169
|
+
* silently corroborated — that would hide the SDK-vs-OAuth split this refactor
|
|
170
|
+
* exists to surface.
|
|
171
|
+
*/
|
|
172
|
+
export function routeHintMatches(hint, identity) {
|
|
173
|
+
if (hint === routeToken(identity) ||
|
|
174
|
+
hint === identity.model_provider ||
|
|
175
|
+
hint === identity.execution_adapter) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (identity.route_completeness === "complete")
|
|
179
|
+
return false;
|
|
180
|
+
const head = hint.split("/")[0] ?? hint;
|
|
181
|
+
return head === identity.model_provider;
|
|
182
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0.5 header escalation (design §10 C′ / §11 ESC-1) — a SEPARATE named step that
|
|
3
|
+
* runs AFTER the deterministic observer, never inside it. The observer flags a
|
|
4
|
+
* sheet whose header is ambiguous (e.g. a headerless all-text sheet whose first
|
|
5
|
+
* DATA row scored as labels) with `header_confidence: "low"`. For those sheets
|
|
6
|
+
* this step asks an LLM to resolve the header row under a strict bounded-submit
|
|
7
|
+
* contract, with a deterministic replay cache and a fail-soft downgrade to the
|
|
8
|
+
* heuristic when the model is unavailable or answers out of contract.
|
|
9
|
+
*
|
|
10
|
+
* LLM authority is narrow (capability-boundary): the model returns ONLY a
|
|
11
|
+
* header-row index (or null); code owns the trigger gate, the submission
|
|
12
|
+
* validation, the cache key, the downgrade policy, and the recorded provenance.
|
|
13
|
+
* The model is injected so this module stays deterministic under test; production
|
|
14
|
+
* wiring adapts the in-process llm caller to {@link HeaderEscalationLlm}. This
|
|
15
|
+
* module is intentionally unwired — consumers (reconstruct seed-stage / review)
|
|
16
|
+
* call it as a post-observation step.
|
|
17
|
+
*/
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import { SPREADSHEET_OBSERVER_ADAPTER_VERSION } from "./spreadsheet-structure-observer.js";
|
|
20
|
+
/** Bumped when the escalation prompt/contract changes — folds into the cache key
|
|
21
|
+
* so a contract change invalidates prior decisions (design §11 CACHE-1). */
|
|
22
|
+
export const HEADER_ESCALATION_TRIGGER_VERSION = 1;
|
|
23
|
+
/** Rows shown to the model — the leading window the observer also scans. */
|
|
24
|
+
export const ESCALATION_ROW_WINDOW = 15;
|
|
25
|
+
/** Columns shown to the model — bounds the prompt for very wide sheets. */
|
|
26
|
+
export const ESCALATION_COL_WINDOW = 40;
|
|
27
|
+
/** Per-cell char cap in the rendered prompt, to keep the payload bounded. */
|
|
28
|
+
const ESCALATION_CELL_CHAR_CAP = 40;
|
|
29
|
+
/** Races the injected model call against an abort budget so a never-settling
|
|
30
|
+
* adapter rejects (→ `llm_unavailable`) instead of hanging the await. The timer
|
|
31
|
+
* is cleared when the model settles first; a late settle after timeout is
|
|
32
|
+
* ignored (no unhandled rejection). */
|
|
33
|
+
function callLlmWithTimeout(llm, args, timeoutMs) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
reject(new Error("header_escalation_timeout"));
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
// Defer through a microtask so a synchronous throw from the adapter routes
|
|
43
|
+
// to the rejection handler (clearing the timer) rather than escaping the
|
|
44
|
+
// executor with the timer still pending.
|
|
45
|
+
Promise.resolve()
|
|
46
|
+
.then(() => llm(args))
|
|
47
|
+
.then((value) => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
resolve(value);
|
|
53
|
+
}, (error) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function renderRowsTable(rows, colCount) {
|
|
63
|
+
const cols = Math.min(colCount, ESCALATION_COL_WINDOW);
|
|
64
|
+
const colSuffix = colCount > cols ? ` | … (+${colCount - cols} cols)` : "";
|
|
65
|
+
return rows
|
|
66
|
+
.slice(0, ESCALATION_ROW_WINDOW)
|
|
67
|
+
.map((row, index) => {
|
|
68
|
+
const cells = Array.from({ length: cols }, (_, col) => (row[col] ?? "")
|
|
69
|
+
.replace(/\s+/g, " ")
|
|
70
|
+
.trim()
|
|
71
|
+
.slice(0, ESCALATION_CELL_CHAR_CAP));
|
|
72
|
+
return `row ${index}: ${cells.map((cell) => cell || "∅").join(" | ")}${colSuffix}`;
|
|
73
|
+
})
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
/** Bounded prompt: the leading rows + the exact JSON contract. Deterministic in
|
|
77
|
+
* the candidate, so its hash anchors the replay cache. */
|
|
78
|
+
export function renderHeaderPrompt(candidate) {
|
|
79
|
+
const shown = Math.min(candidate.rows.length, ESCALATION_ROW_WINDOW);
|
|
80
|
+
return [
|
|
81
|
+
`Sheet "${candidate.sheetName}" has an ambiguous header. Its first ${shown} rows (0-indexed) are below.`,
|
|
82
|
+
`Identify the single row that is the column HEADER (labels, not data). If the sheet has no header row, answer null.`,
|
|
83
|
+
``,
|
|
84
|
+
renderRowsTable(candidate.rows, candidate.colCount),
|
|
85
|
+
``,
|
|
86
|
+
`Reply with ONLY this JSON and no other fields: {"header_row_index": <0-based row index, or null>}.`,
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
89
|
+
function shortHash(value) {
|
|
90
|
+
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
91
|
+
}
|
|
92
|
+
/** Deterministic replay key (design §11 CACHE-1): any drift in content,
|
|
93
|
+
* extractor/trigger version, prompt, route provider, model id/effort, or
|
|
94
|
+
* data-layer caps yields a fresh key, invalidating a stale decision. */
|
|
95
|
+
export function headerEscalationCacheKey(parts) {
|
|
96
|
+
return crypto
|
|
97
|
+
.createHash("sha256")
|
|
98
|
+
.update(JSON.stringify([
|
|
99
|
+
parts.contentSha256,
|
|
100
|
+
parts.extractorVersion,
|
|
101
|
+
parts.triggerVersion,
|
|
102
|
+
parts.promptHash,
|
|
103
|
+
parts.provider,
|
|
104
|
+
parts.modelId,
|
|
105
|
+
parts.effort,
|
|
106
|
+
parts.dataLayerCapsHash,
|
|
107
|
+
parts.sheetName,
|
|
108
|
+
]))
|
|
109
|
+
.digest("hex");
|
|
110
|
+
}
|
|
111
|
+
/** Strict bounded-submit validation: accept ONLY `{ header_row_index }` with an
|
|
112
|
+
* in-range integer or null; reject extra/unknown fields, wrong types, and
|
|
113
|
+
* out-of-range indices (the model owns no other field). */
|
|
114
|
+
export function parseHeaderSubmission(raw, rowCount) {
|
|
115
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
116
|
+
return null;
|
|
117
|
+
const keys = Object.keys(raw);
|
|
118
|
+
if (keys.length !== 1 || keys[0] !== "header_row_index")
|
|
119
|
+
return null;
|
|
120
|
+
const value = raw.header_row_index;
|
|
121
|
+
if (value === null)
|
|
122
|
+
return { header_row_index: null };
|
|
123
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
124
|
+
return null;
|
|
125
|
+
if (value < 0 || value >= rowCount)
|
|
126
|
+
return null;
|
|
127
|
+
return { header_row_index: value };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve a sheet's header row, escalating to the model only when the heuristic
|
|
131
|
+
* was ambiguous. A high-confidence heuristic is trusted as-is. Returns the
|
|
132
|
+
* heuristic (with a `downgradeReason`) whenever the model is unavailable or
|
|
133
|
+
* answers out of contract, so escalation never blocks observation. Only
|
|
134
|
+
* successful LLM resolutions are cached (a transient downgrade re-attempts next
|
|
135
|
+
* run).
|
|
136
|
+
*/
|
|
137
|
+
export async function escalateHeader(candidate, options) {
|
|
138
|
+
const heuristic = {
|
|
139
|
+
sheetName: candidate.sheetName,
|
|
140
|
+
headerRowIndex: candidate.heuristic.headerRowIndex,
|
|
141
|
+
confidence: candidate.heuristic.confidence,
|
|
142
|
+
source: "heuristic",
|
|
143
|
+
};
|
|
144
|
+
if (candidate.heuristic.confidence === "high")
|
|
145
|
+
return heuristic;
|
|
146
|
+
const prompt = renderHeaderPrompt(candidate);
|
|
147
|
+
const promptHash = shortHash(prompt);
|
|
148
|
+
const cacheKey = headerEscalationCacheKey({
|
|
149
|
+
contentSha256: options.contentSha256,
|
|
150
|
+
extractorVersion: SPREADSHEET_OBSERVER_ADAPTER_VERSION,
|
|
151
|
+
triggerVersion: HEADER_ESCALATION_TRIGGER_VERSION,
|
|
152
|
+
promptHash,
|
|
153
|
+
provider: options.model.provider,
|
|
154
|
+
modelId: options.model.id,
|
|
155
|
+
effort: options.model.effort,
|
|
156
|
+
dataLayerCapsHash: options.dataLayerCapsHash,
|
|
157
|
+
sheetName: candidate.sheetName,
|
|
158
|
+
});
|
|
159
|
+
// The cache is an optimization, never a gate: a durable backing store that
|
|
160
|
+
// throws on read must not block resolution.
|
|
161
|
+
const cached = cacheGet(options.cache, cacheKey);
|
|
162
|
+
if (cached)
|
|
163
|
+
return cached;
|
|
164
|
+
let raw;
|
|
165
|
+
try {
|
|
166
|
+
raw = await callLlmWithTimeout(options.llm, { prompt, promptHash }, options.timeoutMs);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { ...heuristic, downgradeReason: "llm_unavailable" };
|
|
170
|
+
}
|
|
171
|
+
// Validate against the rendered window, not the full row count: the model can
|
|
172
|
+
// only legitimately pick a row it was shown.
|
|
173
|
+
const windowRowCount = Math.min(candidate.rows.length, ESCALATION_ROW_WINDOW);
|
|
174
|
+
const submission = parseHeaderSubmission(raw, windowRowCount);
|
|
175
|
+
if (!submission) {
|
|
176
|
+
return { ...heuristic, downgradeReason: "invalid_submission" };
|
|
177
|
+
}
|
|
178
|
+
const resolved = {
|
|
179
|
+
sheetName: candidate.sheetName,
|
|
180
|
+
headerRowIndex: submission.header_row_index,
|
|
181
|
+
confidence: "high",
|
|
182
|
+
source: "llm",
|
|
183
|
+
};
|
|
184
|
+
// A cache write failure (full disk, permissions) must not reject a valid
|
|
185
|
+
// resolution — replay support cannot change resolution availability.
|
|
186
|
+
cacheSet(options.cache, cacheKey, resolved);
|
|
187
|
+
return resolved;
|
|
188
|
+
}
|
|
189
|
+
function cacheGet(cache, key) {
|
|
190
|
+
try {
|
|
191
|
+
return cache?.get(key);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function cacheSet(cache, key, value) {
|
|
198
|
+
try {
|
|
199
|
+
cache?.set(key, value);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// non-fatal: the resolution is still returned.
|
|
203
|
+
}
|
|
204
|
+
}
|