@meetless/mla 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The CE0 durable store: the local SQLite WAL database that backs the evidence-
|
|
3
|
+
// consultation forcing function's three records
|
|
4
|
+
// (notes/20260617-evidence-consultation-forcing-function-proposal.md Part VII,
|
|
5
|
+
// P0.1-P0.6). It is the single CE0 runtime authority for "did this turn's
|
|
6
|
+
// obligation get satisfied?", read with one local SQLite lookup off the PreToolUse
|
|
7
|
+
// hot path. The doc mandates SQLite (with WAL) precisely so the obligation lifecycle
|
|
8
|
+
// can be a transactional read/write rather than an append-log scan.
|
|
9
|
+
//
|
|
10
|
+
// SCOPE (Commit 4): the three record TABLES and their typed insert/read only. The
|
|
11
|
+
// LocalTurnIdentity PARENT table and its BEGIN IMMEDIATE sequence allocation are the
|
|
12
|
+
// next slice; deny, recovery, correction, checkpoint, and any rollout-mode column are
|
|
13
|
+
// held seams and are deliberately absent. The offline per-subject CoverageAuditLabel
|
|
14
|
+
// is a JSONL artifact (the `mla evidence` workflow), never a table here.
|
|
15
|
+
//
|
|
16
|
+
// The CLI intentionally does not depend on @meetless/utils (see kb-candidate.ts), so
|
|
17
|
+
// the value shapes below are vendored with field names byte-identical to the utils CE0
|
|
18
|
+
// types; the array fields persist as JSON text columns and round-trip across that
|
|
19
|
+
// boundary without a translation layer.
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
54
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.Ce0StoreSchemaVersionError = exports.CE0_SCHEMA_VERSION = void 0;
|
|
58
|
+
exports.defaultCe0StorePath = defaultCe0StorePath;
|
|
59
|
+
exports.consultationRecordToReducerInput = consultationRecordToReducerInput;
|
|
60
|
+
exports.openCe0Store = openCe0Store;
|
|
61
|
+
exports.closeCe0Store = closeCe0Store;
|
|
62
|
+
exports.insertTurnMemoryAssessment = insertTurnMemoryAssessment;
|
|
63
|
+
exports.getTurnMemoryAssessment = getTurnMemoryAssessment;
|
|
64
|
+
exports.listTurnMemoryAssessments = listTurnMemoryAssessments;
|
|
65
|
+
exports.allocateTurnIdentity = allocateTurnIdentity;
|
|
66
|
+
exports.openTurnAtomically = openTurnAtomically;
|
|
67
|
+
exports.resolveLatestTurnIdentity = resolveLatestTurnIdentity;
|
|
68
|
+
exports.insertTurnRuleObligation = insertTurnRuleObligation;
|
|
69
|
+
exports.getTurnRuleObligation = getTurnRuleObligation;
|
|
70
|
+
exports.listDeadlineClaimedObligations = listDeadlineClaimedObligations;
|
|
71
|
+
exports.insertConsultationAttempt = insertConsultationAttempt;
|
|
72
|
+
exports.appendConsultationAttempt = appendConsultationAttempt;
|
|
73
|
+
exports.getConsultationAttempt = getConsultationAttempt;
|
|
74
|
+
exports.listConsultationsForTurn = listConsultationsForTurn;
|
|
75
|
+
exports.claimFirstStop = claimFirstStop;
|
|
76
|
+
exports.recordStopResponseSnapshot = recordStopResponseSnapshot;
|
|
77
|
+
exports.finalizeObligation = finalizeObligation;
|
|
78
|
+
const path = __importStar(require("path"));
|
|
79
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
80
|
+
const config_1 = require("../config");
|
|
81
|
+
const ce0_sampling_bucket_1 = require("./ce0-sampling-bucket");
|
|
82
|
+
const interception_schema_1 = require("./interception-schema");
|
|
83
|
+
/** The CE0 local store path: one SQLite file per machine under the Meetless home (the
|
|
84
|
+
* obligation rows carry their own workspace_id, and every read/finalize filters by the
|
|
85
|
+
* resolved workspace). It lives HERE, on the lean store module, rather than on the heavy
|
|
86
|
+
* `mla evidence` command module, so the PreToolUse deny hot path can resolve the store
|
|
87
|
+
* path without pulling evidence.ts's analytics/observability graph (latency lever A,
|
|
88
|
+
* notes/20260615-...-consolidated-proposal.md). The `mla evidence` command re-exports it
|
|
89
|
+
* for backward compatibility. */
|
|
90
|
+
function defaultCe0StorePath() {
|
|
91
|
+
return path.join(config_1.HOME, "ce0", "evidence.db");
|
|
92
|
+
}
|
|
93
|
+
/** Project a persisted consultation record onto the deterministic reducer's read subset. The
|
|
94
|
+
* reducer (requirement-subject) treats `result` as optional and never reads `source`, so the
|
|
95
|
+
* stored null becomes undefined and source is dropped. This is the SINGLE home of the
|
|
96
|
+
* record -> reducer-input mapping: both the offline export (`ce0-evidence`) and the finalization
|
|
97
|
+
* projector (`ce0-telemetry-project`) recompute the proof set over the same shape, so neither
|
|
98
|
+
* may carry its own copy and drift. */
|
|
99
|
+
function consultationRecordToReducerInput(rec) {
|
|
100
|
+
return {
|
|
101
|
+
consultationId: rec.consultationId,
|
|
102
|
+
consultationSubjects: rec.consultationSubjects,
|
|
103
|
+
execution: rec.execution,
|
|
104
|
+
result: rec.result ?? undefined,
|
|
105
|
+
deliveredToAnsweringContext: rec.deliveredToAnsweringContext,
|
|
106
|
+
orderingToken: rec.orderingToken,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Schema. Exactly three tables. Array fields are JSON text; booleans are 0/1
|
|
111
|
+
// integers; the obligation is unique per (turn, rule version) per the doc.
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
const SCHEMA = `
|
|
114
|
+
CREATE TABLE IF NOT EXISTS turn_memory_assessment (
|
|
115
|
+
assessment_id TEXT PRIMARY KEY,
|
|
116
|
+
workspace_id TEXT NOT NULL,
|
|
117
|
+
session_id TEXT NOT NULL,
|
|
118
|
+
local_turn_sequence INTEGER NOT NULL,
|
|
119
|
+
requirement TEXT NOT NULL,
|
|
120
|
+
markers_matched TEXT NOT NULL,
|
|
121
|
+
exclusions_matched TEXT NOT NULL,
|
|
122
|
+
classifier_version TEXT NOT NULL,
|
|
123
|
+
marker_set_version TEXT NOT NULL,
|
|
124
|
+
exclusion_set_version TEXT NOT NULL,
|
|
125
|
+
created_at INTEGER NOT NULL,
|
|
126
|
+
sampling_bucket TEXT NOT NULL,
|
|
127
|
+
prompt_hash TEXT NOT NULL,
|
|
128
|
+
-- §2.3 asserted-answer half of the recall snapshot, all nullable: filled by a later UPDATE
|
|
129
|
+
-- (Stage A stamps stop_observed_at; best-effort Stage B fills response_hash + response_source_ref),
|
|
130
|
+
-- absent at the UserPromptSubmit insert and forever when Stage B cannot label the answer.
|
|
131
|
+
stop_observed_at INTEGER,
|
|
132
|
+
response_hash TEXT,
|
|
133
|
+
response_source_ref TEXT,
|
|
134
|
+
UNIQUE (workspace_id, session_id, local_turn_sequence)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
CREATE TABLE IF NOT EXISTS turn_rule_obligation (
|
|
138
|
+
obligation_id TEXT PRIMARY KEY,
|
|
139
|
+
workspace_id TEXT NOT NULL,
|
|
140
|
+
session_id TEXT NOT NULL,
|
|
141
|
+
local_turn_sequence INTEGER NOT NULL,
|
|
142
|
+
rule_id TEXT NOT NULL,
|
|
143
|
+
rule_version_id TEXT NOT NULL,
|
|
144
|
+
required_subjects TEXT NOT NULL,
|
|
145
|
+
subject_satisfaction TEXT NOT NULL,
|
|
146
|
+
status TEXT NOT NULL,
|
|
147
|
+
state_version INTEGER NOT NULL,
|
|
148
|
+
deadline_claimed_at INTEGER,
|
|
149
|
+
deadline_claimed_version INTEGER,
|
|
150
|
+
response_hash TEXT,
|
|
151
|
+
outcome TEXT,
|
|
152
|
+
canonical_payload_hash TEXT NOT NULL,
|
|
153
|
+
UNIQUE (workspace_id, session_id, local_turn_sequence, rule_version_id),
|
|
154
|
+
-- §2.3: a finalized obligation carries exactly one terminal outcome, and only a finalized
|
|
155
|
+
-- one does. status = FINALIZED IFF outcome IS NOT NULL, enforced at the DB so no slice can
|
|
156
|
+
-- leave a half-finalized row (FINALIZED with no outcome, or an outcome on a live status).
|
|
157
|
+
CHECK ((status = 'FINALIZED') = (outcome IS NOT NULL))
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE IF NOT EXISTS consultation_attempt (
|
|
161
|
+
consultation_id TEXT PRIMARY KEY,
|
|
162
|
+
workspace_id TEXT NOT NULL,
|
|
163
|
+
session_id TEXT NOT NULL,
|
|
164
|
+
local_turn_sequence INTEGER NOT NULL,
|
|
165
|
+
source TEXT NOT NULL,
|
|
166
|
+
consultation_subjects TEXT NOT NULL,
|
|
167
|
+
execution TEXT NOT NULL,
|
|
168
|
+
result TEXT,
|
|
169
|
+
delivered_to_answering_context INTEGER NOT NULL,
|
|
170
|
+
ordering_token INTEGER NOT NULL,
|
|
171
|
+
created_at INTEGER NOT NULL
|
|
172
|
+
);
|
|
173
|
+
`;
|
|
174
|
+
/** The schema generation this code writes. Bump this whenever SCHEMA or INTERCEPTION_SCHEMA changes
|
|
175
|
+
* shape (a column added, dropped, or retyped). A store stamped with an older generation cannot be
|
|
176
|
+
* written by this code, so the opener refuses it instead of silently tolerating the drift. Because
|
|
177
|
+
* CE0 is an unshipped local harness ("local unshipped schema may be changed directly"), the resolution
|
|
178
|
+
* is a deliberate operator rebuild of the dev store, never an in-code migration / compatibility path. */
|
|
179
|
+
exports.CE0_SCHEMA_VERSION = 1;
|
|
180
|
+
/** Raised when `openCe0Store` is handed a populated store whose stamped schema generation is not the
|
|
181
|
+
* one this code writes. The opener refuses it loudly (and non-destructively) rather than returning a
|
|
182
|
+
* store whose writes would fail silently inside a fail-soft hook, the exact failure that let the live
|
|
183
|
+
* dogfood store drop 100% of its consultation captures undetected. */
|
|
184
|
+
class Ce0StoreSchemaVersionError extends Error {
|
|
185
|
+
dbPath;
|
|
186
|
+
found;
|
|
187
|
+
expected;
|
|
188
|
+
constructor(dbPath, found, expected) {
|
|
189
|
+
super(`CE0 store at ${dbPath} is schema version ${found}, but this mla writes version ${expected}. ` +
|
|
190
|
+
`The store predates a schema change and its writes would fail silently; rebuild it ` +
|
|
191
|
+
`(delete the file and let it recreate, or restore from a current build).`);
|
|
192
|
+
this.dbPath = dbPath;
|
|
193
|
+
this.found = found;
|
|
194
|
+
this.expected = expected;
|
|
195
|
+
this.name = "Ce0StoreSchemaVersionError";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.Ce0StoreSchemaVersionError = Ce0StoreSchemaVersionError;
|
|
199
|
+
/** Open (creating if needed) the CE0 store at `dbPath`, in WAL mode, with the schema
|
|
200
|
+
* applied. WAL keeps the PreToolUse reader off the writer's lock; a bounded busy_timeout
|
|
201
|
+
* keeps the hook from ever blocking behind a concurrent writer. The single bootstrap
|
|
202
|
+
* applies both the CE0 forcing-function schema and the rules interception schema
|
|
203
|
+
* (one database, one opener, no second migration framework).
|
|
204
|
+
*
|
|
205
|
+
* Before applying the schema, the opener reconciles the store's stamped generation: a brand-new
|
|
206
|
+
* (table-less) database is created at the current version, a store already at the current version is
|
|
207
|
+
* accepted, and a populated store at any OTHER version is REFUSED (it predates a schema change and the
|
|
208
|
+
* CREATE TABLE IF NOT EXISTS bootstrap cannot reshape its existing tables). Refusing loudly here turns a
|
|
209
|
+
* silent, fail-soft-swallowed write failure into a detectable one. */
|
|
210
|
+
function openCe0Store(dbPath) {
|
|
211
|
+
const db = new better_sqlite3_1.default(dbPath);
|
|
212
|
+
db.pragma("journal_mode = WAL");
|
|
213
|
+
db.pragma("foreign_keys = ON");
|
|
214
|
+
db.pragma("busy_timeout = 50");
|
|
215
|
+
const found = db.pragma("user_version", { simple: true });
|
|
216
|
+
const tableCount = db
|
|
217
|
+
.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
218
|
+
.get().n;
|
|
219
|
+
// A populated store carries its own generation; if it is not the one we write, its tables cannot be
|
|
220
|
+
// reshaped by CREATE TABLE IF NOT EXISTS and its writes would fail. A fresh (table-less) store has no
|
|
221
|
+
// generation yet and is simply stamped below.
|
|
222
|
+
if (tableCount > 0 && found !== exports.CE0_SCHEMA_VERSION) {
|
|
223
|
+
db.close();
|
|
224
|
+
throw new Ce0StoreSchemaVersionError(dbPath, found, exports.CE0_SCHEMA_VERSION);
|
|
225
|
+
}
|
|
226
|
+
db.exec(SCHEMA);
|
|
227
|
+
db.exec(interception_schema_1.INTERCEPTION_SCHEMA);
|
|
228
|
+
db.pragma(`user_version = ${exports.CE0_SCHEMA_VERSION}`);
|
|
229
|
+
return { db };
|
|
230
|
+
}
|
|
231
|
+
function closeCe0Store(store) {
|
|
232
|
+
store.db.close();
|
|
233
|
+
}
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// turn_memory_assessment
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
function insertTurnMemoryAssessment(store, rec) {
|
|
238
|
+
store.db
|
|
239
|
+
.prepare(`INSERT INTO turn_memory_assessment
|
|
240
|
+
(assessment_id, workspace_id, session_id, local_turn_sequence, requirement,
|
|
241
|
+
markers_matched, exclusions_matched, classifier_version, marker_set_version,
|
|
242
|
+
exclusion_set_version, created_at, sampling_bucket, prompt_hash,
|
|
243
|
+
stop_observed_at, response_hash, response_source_ref)
|
|
244
|
+
VALUES
|
|
245
|
+
(@assessment_id, @workspace_id, @session_id, @local_turn_sequence, @requirement,
|
|
246
|
+
@markers_matched, @exclusions_matched, @classifier_version, @marker_set_version,
|
|
247
|
+
@exclusion_set_version, @created_at, @sampling_bucket, @prompt_hash,
|
|
248
|
+
@stop_observed_at, @response_hash, @response_source_ref)`)
|
|
249
|
+
.run({
|
|
250
|
+
assessment_id: rec.assessmentId,
|
|
251
|
+
workspace_id: rec.workspaceId,
|
|
252
|
+
session_id: rec.sessionId,
|
|
253
|
+
local_turn_sequence: rec.localTurnSequence,
|
|
254
|
+
requirement: rec.requirement,
|
|
255
|
+
markers_matched: JSON.stringify(rec.markersMatched),
|
|
256
|
+
exclusions_matched: JSON.stringify(rec.exclusionsMatched),
|
|
257
|
+
classifier_version: rec.classifierVersion,
|
|
258
|
+
marker_set_version: rec.markerSetVersion,
|
|
259
|
+
exclusion_set_version: rec.exclusionSetVersion,
|
|
260
|
+
created_at: rec.createdAt,
|
|
261
|
+
sampling_bucket: rec.samplingBucket,
|
|
262
|
+
prompt_hash: rec.promptHash,
|
|
263
|
+
stop_observed_at: rec.stopObservedAt ?? null,
|
|
264
|
+
response_hash: rec.responseHash ?? null,
|
|
265
|
+
response_source_ref: rec.responseSourceRef ? JSON.stringify(rec.responseSourceRef) : null,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function mapAssessmentRow(row) {
|
|
269
|
+
const rec = {
|
|
270
|
+
assessmentId: row.assessment_id,
|
|
271
|
+
workspaceId: row.workspace_id,
|
|
272
|
+
sessionId: row.session_id,
|
|
273
|
+
localTurnSequence: row.local_turn_sequence,
|
|
274
|
+
requirement: row.requirement,
|
|
275
|
+
markersMatched: JSON.parse(row.markers_matched),
|
|
276
|
+
exclusionsMatched: JSON.parse(row.exclusions_matched),
|
|
277
|
+
classifierVersion: row.classifier_version,
|
|
278
|
+
markerSetVersion: row.marker_set_version,
|
|
279
|
+
exclusionSetVersion: row.exclusion_set_version,
|
|
280
|
+
createdAt: row.created_at,
|
|
281
|
+
samplingBucket: row.sampling_bucket,
|
|
282
|
+
promptHash: row.prompt_hash,
|
|
283
|
+
};
|
|
284
|
+
// §2.3 snapshot fields are OPTIONAL: a NULL column maps to an ABSENT key (not an `undefined` value),
|
|
285
|
+
// so a never-snapshotted assessment deep-equals the UserPromptSubmit insert shape that omitted them.
|
|
286
|
+
if (row.stop_observed_at != null)
|
|
287
|
+
rec.stopObservedAt = row.stop_observed_at;
|
|
288
|
+
if (row.response_hash != null)
|
|
289
|
+
rec.responseHash = row.response_hash;
|
|
290
|
+
if (row.response_source_ref != null) {
|
|
291
|
+
rec.responseSourceRef = JSON.parse(row.response_source_ref);
|
|
292
|
+
}
|
|
293
|
+
return rec;
|
|
294
|
+
}
|
|
295
|
+
function getTurnMemoryAssessment(store, assessmentId) {
|
|
296
|
+
const row = store.db
|
|
297
|
+
.prepare(`SELECT * FROM turn_memory_assessment WHERE assessment_id = ?`)
|
|
298
|
+
.get(assessmentId);
|
|
299
|
+
if (!row)
|
|
300
|
+
return null;
|
|
301
|
+
return mapAssessmentRow(row);
|
|
302
|
+
}
|
|
303
|
+
/** List every memory assessment in the workspace. The offline telemetry sweep
|
|
304
|
+
* (`mla evidence ce0-emit-telemetry`) projects one memory_requirement_assessed event per
|
|
305
|
+
* row (proposal §6.4: the precision/recall denominator). Unlike the obligation export,
|
|
306
|
+
* EVERY assessment is a telemetry fact regardless of requirement or any deadline claim, so
|
|
307
|
+
* there is no filter here. Deterministically ordered by (session, sequence, assessmentId) so
|
|
308
|
+
* a re-run projects the same events in the same order. */
|
|
309
|
+
function listTurnMemoryAssessments(store, workspaceId) {
|
|
310
|
+
const rows = store.db
|
|
311
|
+
.prepare(`SELECT * FROM turn_memory_assessment
|
|
312
|
+
WHERE workspace_id = ?
|
|
313
|
+
ORDER BY session_id, local_turn_sequence, assessment_id`)
|
|
314
|
+
.all(workspaceId);
|
|
315
|
+
return rows.map(mapAssessmentRow);
|
|
316
|
+
}
|
|
317
|
+
/** Mint this turn's localTurnSequence (MAX+1 per workspace+session) and sampling bucket, then insert
|
|
318
|
+
* the assessment row. This is the side-effecting CORE shared by the two turn-opening entry points:
|
|
319
|
+
* `allocateTurnIdentity` wraps it in its own BEGIN IMMEDIATE, and `openTurnAtomically` runs it inside
|
|
320
|
+
* the same transaction as the obligation insert. It is deliberately NOT exported: every caller must go
|
|
321
|
+
* through one of those transaction boundaries so the sequence allocation stays serialized (the unique
|
|
322
|
+
* (workspace, session, sequence) index is the backstop). */
|
|
323
|
+
function mintAndInsertAssessment(store, d) {
|
|
324
|
+
const { maxSeq } = store.db
|
|
325
|
+
.prepare(`SELECT MAX(local_turn_sequence) AS maxSeq
|
|
326
|
+
FROM turn_memory_assessment
|
|
327
|
+
WHERE workspace_id = ? AND session_id = ?`)
|
|
328
|
+
.get(d.workspaceId, d.sessionId);
|
|
329
|
+
const localTurnSequence = (maxSeq ?? 0) + 1;
|
|
330
|
+
const rec = {
|
|
331
|
+
...d,
|
|
332
|
+
localTurnSequence,
|
|
333
|
+
// The bucket binds to the same natural key as the row's UNIQUE constraint, so it must be
|
|
334
|
+
// minted here, after the sequence, never on the draft (R3 P0.9; ce0-sampling-bucket.ts).
|
|
335
|
+
samplingBucket: (0, ce0_sampling_bucket_1.samplingBucketFor)({
|
|
336
|
+
workspaceId: d.workspaceId,
|
|
337
|
+
sessionId: d.sessionId,
|
|
338
|
+
localTurnSequence,
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
insertTurnMemoryAssessment(store, rec);
|
|
342
|
+
return rec;
|
|
343
|
+
}
|
|
344
|
+
/** Mint this turn's LocalTurnIdentity and persist its assessment in one serialized
|
|
345
|
+
* transaction: `BEGIN IMMEDIATE; nextSequence = MAX(localTurnSequence) + 1; insert`.
|
|
346
|
+
* BEGIN IMMEDIATE takes the write lock up front so two UserPromptSubmit processes for
|
|
347
|
+
* the same (workspace, session) cannot read the same MAX and collide; the unique
|
|
348
|
+
* (workspace, session, sequence) index is the backstop. Returns the persisted record
|
|
349
|
+
* carrying the minted sequence. */
|
|
350
|
+
function allocateTurnIdentity(store, draft) {
|
|
351
|
+
const mint = store.db.transaction((d) => mintAndInsertAssessment(store, d));
|
|
352
|
+
return mint.immediate(draft);
|
|
353
|
+
}
|
|
354
|
+
/** Open a turn ATOMICALLY (proposal §1.3 req 1 physical resolution, R4 P0.4): in ONE BEGIN IMMEDIATE
|
|
355
|
+
* transaction, mint the LocalTurnIdentity, insert the assessment, and (when the turn is REQUIRED)
|
|
356
|
+
* insert its obligation. `buildObligation` runs INSIDE the transaction so the obligation it returns can
|
|
357
|
+
* carry the localTurnSequence just minted; returning null is a non-REQUIRED turn whose only row is the
|
|
358
|
+
* assessment. If the obligation insert throws, the assessment insert and the sequence allocation roll
|
|
359
|
+
* back with it, so a REQUIRED turn is never left half-open (an assessment with no obligation to grade,
|
|
360
|
+
* which would silently undercount the graded obligation set against the assessed-REQUIRED set). */
|
|
361
|
+
function openTurnAtomically(store, draft, buildObligation) {
|
|
362
|
+
const run = store.db.transaction((d) => {
|
|
363
|
+
const assessment = mintAndInsertAssessment(store, d);
|
|
364
|
+
const obligation = buildObligation(assessment);
|
|
365
|
+
if (obligation)
|
|
366
|
+
insertTurnRuleObligation(store, obligation);
|
|
367
|
+
return { assessment, obligation };
|
|
368
|
+
});
|
|
369
|
+
return run.immediate(draft);
|
|
370
|
+
}
|
|
371
|
+
/** Resolve the LocalTurnIdentity a later hook should inherit: the highest-sequence
|
|
372
|
+
* assessment for the (workspace, session), or null if the turn has none yet. */
|
|
373
|
+
function resolveLatestTurnIdentity(store, coord) {
|
|
374
|
+
const row = store.db
|
|
375
|
+
.prepare(`SELECT workspace_id, session_id, local_turn_sequence
|
|
376
|
+
FROM turn_memory_assessment
|
|
377
|
+
WHERE workspace_id = ? AND session_id = ?
|
|
378
|
+
ORDER BY local_turn_sequence DESC
|
|
379
|
+
LIMIT 1`)
|
|
380
|
+
.get(coord.workspaceId, coord.sessionId);
|
|
381
|
+
if (!row)
|
|
382
|
+
return null;
|
|
383
|
+
return {
|
|
384
|
+
workspaceId: row.workspace_id,
|
|
385
|
+
sessionId: row.session_id,
|
|
386
|
+
localTurnSequence: row.local_turn_sequence,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// turn_rule_obligation
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
function insertTurnRuleObligation(store, rec) {
|
|
393
|
+
store.db
|
|
394
|
+
.prepare(`INSERT INTO turn_rule_obligation
|
|
395
|
+
(obligation_id, workspace_id, session_id, local_turn_sequence, rule_id,
|
|
396
|
+
rule_version_id, required_subjects, subject_satisfaction, status, state_version,
|
|
397
|
+
deadline_claimed_at, deadline_claimed_version, response_hash, outcome,
|
|
398
|
+
canonical_payload_hash)
|
|
399
|
+
VALUES
|
|
400
|
+
(@obligation_id, @workspace_id, @session_id, @local_turn_sequence, @rule_id,
|
|
401
|
+
@rule_version_id, @required_subjects, @subject_satisfaction, @status, @state_version,
|
|
402
|
+
@deadline_claimed_at, @deadline_claimed_version, @response_hash, @outcome,
|
|
403
|
+
@canonical_payload_hash)`)
|
|
404
|
+
.run({
|
|
405
|
+
obligation_id: rec.obligationId,
|
|
406
|
+
workspace_id: rec.workspaceId,
|
|
407
|
+
session_id: rec.sessionId,
|
|
408
|
+
local_turn_sequence: rec.localTurnSequence,
|
|
409
|
+
rule_id: rec.ruleId,
|
|
410
|
+
rule_version_id: rec.ruleVersionId,
|
|
411
|
+
required_subjects: JSON.stringify(rec.requiredSubjects),
|
|
412
|
+
subject_satisfaction: JSON.stringify(rec.subjectSatisfaction),
|
|
413
|
+
status: rec.status,
|
|
414
|
+
state_version: rec.stateVersion,
|
|
415
|
+
deadline_claimed_at: rec.deadlineClaimedAt,
|
|
416
|
+
deadline_claimed_version: rec.deadlineClaimedVersion,
|
|
417
|
+
response_hash: rec.responseHash,
|
|
418
|
+
outcome: rec.outcome,
|
|
419
|
+
canonical_payload_hash: rec.canonicalPayloadHash,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function mapObligationRow(row) {
|
|
423
|
+
return {
|
|
424
|
+
obligationId: row.obligation_id,
|
|
425
|
+
workspaceId: row.workspace_id,
|
|
426
|
+
sessionId: row.session_id,
|
|
427
|
+
localTurnSequence: row.local_turn_sequence,
|
|
428
|
+
ruleId: row.rule_id,
|
|
429
|
+
ruleVersionId: row.rule_version_id,
|
|
430
|
+
requiredSubjects: JSON.parse(row.required_subjects),
|
|
431
|
+
subjectSatisfaction: JSON.parse(row.subject_satisfaction),
|
|
432
|
+
status: row.status,
|
|
433
|
+
stateVersion: row.state_version,
|
|
434
|
+
deadlineClaimedAt: row.deadline_claimed_at ?? null,
|
|
435
|
+
deadlineClaimedVersion: row.deadline_claimed_version ?? null,
|
|
436
|
+
responseHash: row.response_hash ?? null,
|
|
437
|
+
outcome: row.outcome ?? null,
|
|
438
|
+
canonicalPayloadHash: row.canonical_payload_hash,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function getTurnRuleObligation(store, obligationId) {
|
|
442
|
+
const row = store.db
|
|
443
|
+
.prepare(`SELECT * FROM turn_rule_obligation WHERE obligation_id = ?`)
|
|
444
|
+
.get(obligationId);
|
|
445
|
+
if (!row)
|
|
446
|
+
return null;
|
|
447
|
+
return mapObligationRow(row);
|
|
448
|
+
}
|
|
449
|
+
/** List every obligation in the workspace whose first-Stop deadline has been claimed (the
|
|
450
|
+
* frozen, due-resolved set the `mla evidence` export labels). A live obligation whose deadline
|
|
451
|
+
* is still null is NOT exportable: its eligibility boundary is not yet fixed. FINALIZED rows are
|
|
452
|
+
* INCLUDED here (this is a mechanical read of the claimed set); the export workflow, not the
|
|
453
|
+
* store, decides to skip already-labeled ones. Deterministically ordered by (session, sequence,
|
|
454
|
+
* obligationId) so the JSONL artifact is stable across runs. */
|
|
455
|
+
function listDeadlineClaimedObligations(store, workspaceId) {
|
|
456
|
+
const rows = store.db
|
|
457
|
+
.prepare(`SELECT * FROM turn_rule_obligation
|
|
458
|
+
WHERE workspace_id = ? AND deadline_claimed_at IS NOT NULL
|
|
459
|
+
ORDER BY session_id, local_turn_sequence, obligation_id`)
|
|
460
|
+
.all(workspaceId);
|
|
461
|
+
return rows.map(mapObligationRow);
|
|
462
|
+
}
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// consultation_attempt
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
function insertConsultationAttempt(store, rec) {
|
|
467
|
+
store.db
|
|
468
|
+
.prepare(`INSERT INTO consultation_attempt
|
|
469
|
+
(consultation_id, workspace_id, session_id, local_turn_sequence,
|
|
470
|
+
source, consultation_subjects, execution, result, delivered_to_answering_context,
|
|
471
|
+
ordering_token, created_at)
|
|
472
|
+
VALUES
|
|
473
|
+
(@consultation_id, @workspace_id, @session_id, @local_turn_sequence,
|
|
474
|
+
@source, @consultation_subjects, @execution, @result, @delivered_to_answering_context,
|
|
475
|
+
@ordering_token, @created_at)`)
|
|
476
|
+
.run({
|
|
477
|
+
consultation_id: rec.consultationId,
|
|
478
|
+
workspace_id: rec.workspaceId,
|
|
479
|
+
session_id: rec.sessionId,
|
|
480
|
+
local_turn_sequence: rec.localTurnSequence,
|
|
481
|
+
source: rec.source,
|
|
482
|
+
consultation_subjects: JSON.stringify(rec.consultationSubjects),
|
|
483
|
+
execution: rec.execution,
|
|
484
|
+
result: rec.result,
|
|
485
|
+
delivered_to_answering_context: rec.deliveredToAnsweringContext ? 1 : 0,
|
|
486
|
+
ordering_token: rec.orderingToken,
|
|
487
|
+
created_at: rec.createdAt,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/** Record one governed-memory consultation, minting its orderingToken in the same serialized
|
|
491
|
+
* transaction: `BEGIN IMMEDIATE; nextToken = MAX(ordering_token) + 1; insert`. The token is a
|
|
492
|
+
* per-turn monotonic position (scoped to the (workspace, session, sequence) the consultation
|
|
493
|
+
* belongs to), NOT a wall clock: the first-Stop deadline claim reads the high-water token as
|
|
494
|
+
* the eligibility boundary, and the reducer breaks proof ties on it. BEGIN IMMEDIATE takes the
|
|
495
|
+
* write lock up front so two concurrent appends in one turn cannot read the same MAX. Returns
|
|
496
|
+
* the persisted record carrying the minted token. */
|
|
497
|
+
function appendConsultationAttempt(store, draft) {
|
|
498
|
+
const mint = store.db.transaction((d) => {
|
|
499
|
+
const { maxTok } = store.db
|
|
500
|
+
.prepare(`SELECT MAX(ordering_token) AS maxTok
|
|
501
|
+
FROM consultation_attempt
|
|
502
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?`)
|
|
503
|
+
.get(d.workspaceId, d.sessionId, d.localTurnSequence);
|
|
504
|
+
const rec = { ...d, orderingToken: (maxTok ?? 0) + 1 };
|
|
505
|
+
insertConsultationAttempt(store, rec);
|
|
506
|
+
return rec;
|
|
507
|
+
});
|
|
508
|
+
return mint.immediate(draft);
|
|
509
|
+
}
|
|
510
|
+
function mapConsultationRow(row) {
|
|
511
|
+
return {
|
|
512
|
+
consultationId: row.consultation_id,
|
|
513
|
+
workspaceId: row.workspace_id,
|
|
514
|
+
sessionId: row.session_id,
|
|
515
|
+
localTurnSequence: row.local_turn_sequence,
|
|
516
|
+
source: row.source,
|
|
517
|
+
consultationSubjects: JSON.parse(row.consultation_subjects),
|
|
518
|
+
execution: row.execution,
|
|
519
|
+
result: row.result ?? null,
|
|
520
|
+
deliveredToAnsweringContext: row.delivered_to_answering_context === 1,
|
|
521
|
+
orderingToken: row.ordering_token,
|
|
522
|
+
createdAt: row.created_at,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function getConsultationAttempt(store, consultationId) {
|
|
526
|
+
const row = store.db
|
|
527
|
+
.prepare(`SELECT * FROM consultation_attempt WHERE consultation_id = ?`)
|
|
528
|
+
.get(consultationId);
|
|
529
|
+
if (!row)
|
|
530
|
+
return null;
|
|
531
|
+
return mapConsultationRow(row);
|
|
532
|
+
}
|
|
533
|
+
/** List one turn's consultations, ordered by (orderingToken, consultationId): the same total
|
|
534
|
+
* order the satisfaction reducer imposes, so the export's raw facts read in eligibility order.
|
|
535
|
+
* Scoped to the exact (workspace, session, localTurnSequence) coordinate. */
|
|
536
|
+
function listConsultationsForTurn(store, coord) {
|
|
537
|
+
const rows = store.db
|
|
538
|
+
.prepare(`SELECT * FROM consultation_attempt
|
|
539
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?
|
|
540
|
+
ORDER BY ordering_token, consultation_id`)
|
|
541
|
+
.all(coord.workspaceId, coord.sessionId, coord.localTurnSequence);
|
|
542
|
+
return rows.map(mapConsultationRow);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* §2.3 Stage A: the first Stop's immediate, I/O-free observation of a turn. In one serialized
|
|
546
|
+
* `BEGIN IMMEDIATE` transaction it does two things:
|
|
547
|
+
* 1. Stamps `stopObservedAt` on the turn's assessment for EVERY classified turn (REQUIRED or
|
|
548
|
+
* not), if-null so a later Stop never overwrites the first observation. A NOT_REQUIRED /
|
|
549
|
+
* UNKNOWN turn has an assessment but no obligation; it is still stamped, so the offline
|
|
550
|
+
* false-negative recall sample carries the same answer evidence as a flagged turn.
|
|
551
|
+
* 2. When an obligation is present, freezes the eligibility boundary: it finds the obligation by
|
|
552
|
+
* its unique turn key, reads the turn's high-water orderingToken (the boundary), and
|
|
553
|
+
* CAS-advances stateVersion while writing the deadline fields. The status column is
|
|
554
|
+
* deliberately untouched: the claim fixes the boundary, it does not declare the obligation
|
|
555
|
+
* satisfied (satisfaction is recomputed offline over the frozen eligible set).
|
|
556
|
+
* The deadline claim is idempotent: once a deadline is set, a later Stop returns ALREADY_CLAIMED
|
|
557
|
+
* and never moves the boundary, so a consultation that arrives after the first Stop (a higher
|
|
558
|
+
* token) cannot retroactively become eligible. No filesystem read happens here; the response
|
|
559
|
+
* snapshot is the best-effort Stage B, outside this transaction. BEGIN IMMEDIATE serializes
|
|
560
|
+
* concurrent Stops so exactly one wins the claim and the first stamp.
|
|
561
|
+
*/
|
|
562
|
+
function claimFirstStop(store, coord, ruleVersionId, now) {
|
|
563
|
+
const run = store.db.transaction(() => {
|
|
564
|
+
// Stage A step 1: stamp the assessment if-null. A pure write, no I/O; a harmless no-op when no
|
|
565
|
+
// assessment row exists for the coord (e.g. a Stop for a turn CE0 never assessed).
|
|
566
|
+
store.db
|
|
567
|
+
.prepare(`UPDATE turn_memory_assessment
|
|
568
|
+
SET stop_observed_at = ?
|
|
569
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?
|
|
570
|
+
AND stop_observed_at IS NULL`)
|
|
571
|
+
.run(now(), coord.workspaceId, coord.sessionId, coord.localTurnSequence);
|
|
572
|
+
// Stage A step 2: claim the obligation deadline, when an obligation is present.
|
|
573
|
+
const key = store.db
|
|
574
|
+
.prepare(`SELECT obligation_id FROM turn_rule_obligation
|
|
575
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?
|
|
576
|
+
AND rule_version_id = ?`)
|
|
577
|
+
.get(coord.workspaceId, coord.sessionId, coord.localTurnSequence, ruleVersionId);
|
|
578
|
+
if (!key)
|
|
579
|
+
return { status: "NO_OBLIGATION" };
|
|
580
|
+
const obligation = getTurnRuleObligation(store, key.obligation_id);
|
|
581
|
+
if (obligation.deadlineClaimedAt !== null) {
|
|
582
|
+
return {
|
|
583
|
+
status: "ALREADY_CLAIMED",
|
|
584
|
+
claim: {
|
|
585
|
+
obligationId: obligation.obligationId,
|
|
586
|
+
deadlineClaimedAt: obligation.deadlineClaimedAt,
|
|
587
|
+
deadlineClaimedVersion: obligation.deadlineClaimedVersion,
|
|
588
|
+
stateVersion: obligation.stateVersion,
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const { maxTok } = store.db
|
|
593
|
+
.prepare(`SELECT MAX(ordering_token) AS maxTok FROM consultation_attempt
|
|
594
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?`)
|
|
595
|
+
.get(coord.workspaceId, coord.sessionId, coord.localTurnSequence);
|
|
596
|
+
const boundary = maxTok ?? 0;
|
|
597
|
+
const observed = obligation.stateVersion;
|
|
598
|
+
store.db
|
|
599
|
+
.prepare(`UPDATE turn_rule_obligation
|
|
600
|
+
SET deadline_claimed_at = ?, deadline_claimed_version = ?, state_version = ?
|
|
601
|
+
WHERE obligation_id = ? AND state_version = ? AND deadline_claimed_at IS NULL`)
|
|
602
|
+
.run(boundary, observed, observed + 1, obligation.obligationId, observed);
|
|
603
|
+
return {
|
|
604
|
+
status: "CLAIMED",
|
|
605
|
+
claim: {
|
|
606
|
+
obligationId: obligation.obligationId,
|
|
607
|
+
deadlineClaimedAt: boundary,
|
|
608
|
+
deadlineClaimedVersion: observed,
|
|
609
|
+
stateVersion: observed + 1,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
return run.immediate();
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* §2.3 Stage B: record the best-effort response snapshot (`responseHash` + `responseSourceRef`) onto
|
|
617
|
+
* an already-stamped assessment, OUTSIDE and AFTER the Stage A deadline transaction. The two fields
|
|
618
|
+
* move together as one pair, never with `stopObservedAt` (proposal lines 1096-1098): Stage A stamped
|
|
619
|
+
* the observation, this fills the response evidence the offline exporter rehydrates from the pointer.
|
|
620
|
+
*
|
|
621
|
+
* Idempotent under repeated Stop continuations (P0.6): a later Stop may fill a snapshot that is still
|
|
622
|
+
* missing, but it may NEVER overwrite one that already completed. The guard is `response_hash IS NULL`
|
|
623
|
+
* checked inside one serialized `BEGIN IMMEDIATE` transaction, so concurrent Stops cannot both write.
|
|
624
|
+
* `stop_observed_at` is deliberately untouched here; only the response pair is written.
|
|
625
|
+
*/
|
|
626
|
+
function recordStopResponseSnapshot(store, coord, snapshot) {
|
|
627
|
+
const run = store.db.transaction(() => {
|
|
628
|
+
const existing = store.db
|
|
629
|
+
.prepare(`SELECT response_hash FROM turn_memory_assessment
|
|
630
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?`)
|
|
631
|
+
.get(coord.workspaceId, coord.sessionId, coord.localTurnSequence);
|
|
632
|
+
if (!existing)
|
|
633
|
+
return { status: "NO_ASSESSMENT" };
|
|
634
|
+
if (existing.response_hash != null)
|
|
635
|
+
return { status: "ALREADY_RECORDED" };
|
|
636
|
+
store.db
|
|
637
|
+
.prepare(`UPDATE turn_memory_assessment
|
|
638
|
+
SET response_hash = ?, response_source_ref = ?
|
|
639
|
+
WHERE workspace_id = ? AND session_id = ? AND local_turn_sequence = ?
|
|
640
|
+
AND response_hash IS NULL`)
|
|
641
|
+
.run(snapshot.responseHash, JSON.stringify(snapshot.responseSourceRef), coord.workspaceId, coord.sessionId, coord.localTurnSequence);
|
|
642
|
+
return { status: "RECORDED" };
|
|
643
|
+
});
|
|
644
|
+
return run.immediate();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Write a turn obligation's terminal outcome and move it to FINALIZED, guarded by a
|
|
648
|
+
* compare-and-swap on stateVersion (the same token the deadline claim advances). The human
|
|
649
|
+
* labeler chooses the outcome offline (§2.3); this is the only thing that finalizes a due turn.
|
|
650
|
+
* In one serialized `BEGIN IMMEDIATE` transaction: read the stored stateVersion, fail closed as
|
|
651
|
+
* CAS_CONFLICT if it does not match the caller's expectation (a concurrent finalize or a stale
|
|
652
|
+
* label), else set status='FINALIZED', write the outcome, and advance stateVersion. The CAS makes
|
|
653
|
+
* a re-import of an already-finalized obligation a clean conflict, never a double-write.
|
|
654
|
+
*/
|
|
655
|
+
function finalizeObligation(store, cmd) {
|
|
656
|
+
const run = store.db.transaction(() => {
|
|
657
|
+
const obligation = getTurnRuleObligation(store, cmd.obligationId);
|
|
658
|
+
if (!obligation) {
|
|
659
|
+
return { status: "NO_OBLIGATION", obligationId: cmd.obligationId };
|
|
660
|
+
}
|
|
661
|
+
if (obligation.stateVersion !== cmd.expectedStateVersion) {
|
|
662
|
+
return {
|
|
663
|
+
status: "CAS_CONFLICT",
|
|
664
|
+
obligationId: cmd.obligationId,
|
|
665
|
+
expectedStateVersion: cmd.expectedStateVersion,
|
|
666
|
+
actualStateVersion: obligation.stateVersion,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const next = cmd.expectedStateVersion + 1;
|
|
670
|
+
store.db
|
|
671
|
+
.prepare(`UPDATE turn_rule_obligation
|
|
672
|
+
SET status = 'FINALIZED', outcome = ?, state_version = ?
|
|
673
|
+
WHERE obligation_id = ? AND state_version = ?`)
|
|
674
|
+
.run(cmd.outcome, next, cmd.obligationId, cmd.expectedStateVersion);
|
|
675
|
+
return {
|
|
676
|
+
status: "FINALIZED",
|
|
677
|
+
obligationId: cmd.obligationId,
|
|
678
|
+
outcome: cmd.outcome,
|
|
679
|
+
stateVersion: next,
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
return run.immediate();
|
|
683
|
+
}
|