@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,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The evidence-followthrough join: the ONE implementation shared by `mla
|
|
3
|
+
// adoption`, the evidence section of `mla stats`, and the Stop-hook local
|
|
4
|
+
// correlator (INV-ADOPTION-SOURCE-1). It lives here, in a lib module, precisely
|
|
5
|
+
// so every consumer references the same code path rather than reimplementing the
|
|
6
|
+
// math; the §10.5 "adoption parity" test asserts that shared reference.
|
|
7
|
+
//
|
|
8
|
+
// The product has two evidence channels, Push (context injected by the hook) and
|
|
9
|
+
// Pull (the agent's own meetless__* MCP calls). The naive "did the agent call any
|
|
10
|
+
// tool on an inject turn" metric lies both ways, so A1 splits it and joins three
|
|
11
|
+
// LOCAL trace files by (session_id, turn_index):
|
|
12
|
+
//
|
|
13
|
+
// ask-traces.jsonl the inject side (P0): enrichment.context_items[]
|
|
14
|
+
// with injected==true carry the source_ids we pushed.
|
|
15
|
+
// mcp-calls.jsonl the pull side (P1): one record per meetless__* call
|
|
16
|
+
// with evidence_tool + the source_ids it touched.
|
|
17
|
+
// report-citations.jsonl the push-reference side (P3): the source_ids the
|
|
18
|
+
// agent's final report CITED that turn.
|
|
19
|
+
//
|
|
20
|
+
// Per high-value inject turn (a turn that actually injected >=1 source_id):
|
|
21
|
+
// A1a pull_followthrough the agent PULLED an overlapping source_id
|
|
22
|
+
// via an EVIDENCE tool, same or immediate-child
|
|
23
|
+
// turn. relationship_verdict is an ACTION
|
|
24
|
+
// (evidence_tool=false) and never counts.
|
|
25
|
+
// A1b push_reference_followthrough the report CITED an injected source_id
|
|
26
|
+
// (Push adoption with no Pull).
|
|
27
|
+
// A1c evidence_followthrough_any A1a OR A1b -- the headline "are we useless"
|
|
28
|
+
// number.
|
|
29
|
+
//
|
|
30
|
+
// INV-EVIDENCE-OBSERVATION (§7.1): these are OBSERVATIONS of the agent's
|
|
31
|
+
// behavior, not a verification that the evidence was correct or well-used. A
|
|
32
|
+
// non-zero overlap is a strong positive signal; silent use undercounts.
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.normId = normId;
|
|
35
|
+
exports.overlap = overlap;
|
|
36
|
+
exports.computeFollowthrough = computeFollowthrough;
|
|
37
|
+
exports.buildAdoption = buildAdoption;
|
|
38
|
+
exports.parseInjectTurns = parseInjectTurns;
|
|
39
|
+
exports.parseMcpCalls = parseMcpCalls;
|
|
40
|
+
exports.parseReportCitations = parseReportCitations;
|
|
41
|
+
// --- the join ---------------------------------------------------------------
|
|
42
|
+
// Normalize a source_id for set-overlap: the inject side records the file id
|
|
43
|
+
// (e.g. NT:20260602-foo.md) while the agent typically cites the bare id
|
|
44
|
+
// (NT:20260602-foo). Strip a trailing .md and lowercase so both sides compare
|
|
45
|
+
// equal without ever collapsing two genuinely-different ids (they differ by far
|
|
46
|
+
// more than case or extension). Exported so the metric family and the correlator
|
|
47
|
+
// dedupe ids by the SAME rule the join uses.
|
|
48
|
+
function normId(s) {
|
|
49
|
+
return s.trim().replace(/\.md$/i, "").toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
// Original-form lookup so the row can report the injected id the agent matched,
|
|
52
|
+
// not its normalized form.
|
|
53
|
+
function overlap(injected, touched) {
|
|
54
|
+
const touchedSet = new Set(touched.map(normId));
|
|
55
|
+
return injected.filter((id) => touchedSet.has(normId(id)));
|
|
56
|
+
}
|
|
57
|
+
// computeFollowthrough scores each inject turn against the pulls and report
|
|
58
|
+
// citations in its window [N, N+window]. Default window=1 covers the turn and
|
|
59
|
+
// its immediate child (the A1a contract's "same turn_id OR its immediate child
|
|
60
|
+
// turn"); the same window applies to A1b since a multi-turn task's final report
|
|
61
|
+
// can land on N+1. The Stop-hook correlator passes the wider 3-turn window.
|
|
62
|
+
//
|
|
63
|
+
// A1a deliberately fires on source_id OVERLAP only, not the looser "query in the
|
|
64
|
+
// same evidence domain" alternative the prose also allows: domain matching
|
|
65
|
+
// cannot be made deterministic without a classifier and would reintroduce the
|
|
66
|
+
// exact false-positive (acceptance case 2) the split is designed to remove. The
|
|
67
|
+
// pull records keep the query string, so a future domain refinement has the raw
|
|
68
|
+
// material if we ever want it.
|
|
69
|
+
function computeFollowthrough(injects, calls, citations, window = 1) {
|
|
70
|
+
return injects.map((t) => {
|
|
71
|
+
const inWindow = (turn) => turn >= t.turn_index && turn <= t.turn_index + window;
|
|
72
|
+
// A1a: evidence-bearing pulls in the same session and window. The verdict
|
|
73
|
+
// tool (evidence_tool=false) is filtered out here -- an action is not a Pull.
|
|
74
|
+
const pulledIds = [];
|
|
75
|
+
for (const c of calls) {
|
|
76
|
+
if (c.session_id !== t.session_id || !c.evidence_tool || !inWindow(c.turn_index))
|
|
77
|
+
continue;
|
|
78
|
+
pulledIds.push(...c.source_ids);
|
|
79
|
+
}
|
|
80
|
+
const pulled_overlap = overlap(t.injected_source_ids, pulledIds);
|
|
81
|
+
// A1b: the report's cited source_ids in the same session and window.
|
|
82
|
+
const citedIds = [];
|
|
83
|
+
for (const r of citations) {
|
|
84
|
+
if (r.session_id !== t.session_id || !inWindow(r.turn_index))
|
|
85
|
+
continue;
|
|
86
|
+
citedIds.push(...r.source_ids);
|
|
87
|
+
}
|
|
88
|
+
const cited_overlap = overlap(t.injected_source_ids, citedIds);
|
|
89
|
+
const a1a_pull = pulled_overlap.length > 0;
|
|
90
|
+
const a1b_push_reference = cited_overlap.length > 0;
|
|
91
|
+
return {
|
|
92
|
+
session_id: t.session_id,
|
|
93
|
+
turn_index: t.turn_index,
|
|
94
|
+
injected_source_ids: t.injected_source_ids,
|
|
95
|
+
a1a_pull,
|
|
96
|
+
a1b_push_reference,
|
|
97
|
+
a1c_any: a1a_pull || a1b_push_reference,
|
|
98
|
+
pulled_overlap,
|
|
99
|
+
cited_overlap,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function buildAdoption(rows) {
|
|
104
|
+
const n = rows.length;
|
|
105
|
+
const a1a = rows.filter((r) => r.a1a_pull).length;
|
|
106
|
+
const a1b = rows.filter((r) => r.a1b_push_reference).length;
|
|
107
|
+
const a1c = rows.filter((r) => r.a1c_any).length;
|
|
108
|
+
const rate = (x) => (n ? x / n : 0);
|
|
109
|
+
return {
|
|
110
|
+
inject_turns: n,
|
|
111
|
+
a1a_pull: a1a,
|
|
112
|
+
a1b_push_reference: a1b,
|
|
113
|
+
a1c_any: a1c,
|
|
114
|
+
no_followthrough: n - a1c,
|
|
115
|
+
a1a_rate: rate(a1a),
|
|
116
|
+
a1b_rate: rate(a1b),
|
|
117
|
+
a1c_rate: rate(a1c),
|
|
118
|
+
rows,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// --- record parsers (best-effort; tolerate a partial line) ------------------
|
|
122
|
+
function asNum(v) {
|
|
123
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
124
|
+
}
|
|
125
|
+
function asStr(v) {
|
|
126
|
+
return typeof v === "string" ? v : "";
|
|
127
|
+
}
|
|
128
|
+
function asStrArray(v) {
|
|
129
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
130
|
+
}
|
|
131
|
+
// An inject turn is a trace line that actually PUSHED at least one source_id:
|
|
132
|
+
// enrichment.context_items[] with injected===true and a non-empty source_id.
|
|
133
|
+
// turn_index must be numeric to join (a null turn cannot be aligned).
|
|
134
|
+
function parseInjectTurns(traceLines) {
|
|
135
|
+
const byKey = new Map();
|
|
136
|
+
for (const t of traceLines) {
|
|
137
|
+
const session_id = asStr(t.session_id);
|
|
138
|
+
const turn_index = asNum(t.turn_index);
|
|
139
|
+
if (!session_id || turn_index === null)
|
|
140
|
+
continue;
|
|
141
|
+
const enrichment = t.enrichment ?? null;
|
|
142
|
+
const items = enrichment && Array.isArray(enrichment.context_items) ? enrichment.context_items : [];
|
|
143
|
+
const ids = [];
|
|
144
|
+
for (const raw of items) {
|
|
145
|
+
const item = raw;
|
|
146
|
+
if (item.injected !== true)
|
|
147
|
+
continue;
|
|
148
|
+
const sid = asStr(item.source_id);
|
|
149
|
+
if (sid)
|
|
150
|
+
ids.push(sid);
|
|
151
|
+
}
|
|
152
|
+
if (ids.length === 0)
|
|
153
|
+
continue;
|
|
154
|
+
const key = `${session_id} ${turn_index}`;
|
|
155
|
+
const existing = byKey.get(key);
|
|
156
|
+
if (existing) {
|
|
157
|
+
// Merge the rare duplicate (S,N) inject line.
|
|
158
|
+
existing.injected_source_ids = Array.from(new Set([...existing.injected_source_ids, ...ids]));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
byKey.set(key, { session_id, turn_index, injected_source_ids: Array.from(new Set(ids)) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return Array.from(byKey.values());
|
|
165
|
+
}
|
|
166
|
+
function parseMcpCalls(lines) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const c of lines) {
|
|
169
|
+
const session_id = asStr(c.session_id);
|
|
170
|
+
const turn_index = asNum(c.turn_index);
|
|
171
|
+
if (!session_id || turn_index === null)
|
|
172
|
+
continue;
|
|
173
|
+
out.push({
|
|
174
|
+
session_id,
|
|
175
|
+
turn_index,
|
|
176
|
+
evidence_tool: c.evidence_tool === true,
|
|
177
|
+
source_ids: asStrArray(c.source_ids),
|
|
178
|
+
query: asStr(c.query),
|
|
179
|
+
tool: asStr(c.tool),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
function parseReportCitations(lines) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const r of lines) {
|
|
187
|
+
const session_id = asStr(r.session_id);
|
|
188
|
+
const turn_index = asNum(r.turn_index);
|
|
189
|
+
if (!session_id || turn_index === null)
|
|
190
|
+
continue;
|
|
191
|
+
out.push({ session_id, turn_index, source_ids: asStrArray(r.source_ids) });
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Remote analytics forwarder (spec section 10.1, INV-JOIN-1, INV-CONSENT-1).
|
|
3
|
+
//
|
|
4
|
+
// Ships locally-recorded events to control, which dedupes by (workspace_id,
|
|
5
|
+
// event_id), lands them in control.analytics_events, rolls them up, and mirrors
|
|
6
|
+
// to PostHog server-side. The CLI never holds a PostHog key (INV-POSTHOG-PII-1
|
|
7
|
+
// is enforced server-side too, but we minimize here as well).
|
|
8
|
+
//
|
|
9
|
+
// Three gates, all of which must pass for a single event to leave the machine:
|
|
10
|
+
// 1. remoteAnalyticsEnabled(env) -> the opt-in posture (master kill wins)
|
|
11
|
+
// 2. isRemotelyEmittable(ev) -> has a real workspace_id + session_id to join
|
|
12
|
+
// 3. transport succeeds -> control POST. A failure is swallowed (never
|
|
13
|
+
// thrown) and counted in ForwardResult.failed;
|
|
14
|
+
// the optional onError hook is the operator
|
|
15
|
+
// seam. The event stays in the local jsonl
|
|
16
|
+
// (the durable record for `mla stats`), but
|
|
17
|
+
// the CLI does NOT re-forward it on a later
|
|
18
|
+
// run: a control outage simply means the
|
|
19
|
+
// global rollup never sees it, which the spec
|
|
20
|
+
// reports as unknown rather than zero
|
|
21
|
+
// (INV-GLOBAL-UNKNOWN-1). Forwarding is
|
|
22
|
+
// intentionally silent to the user (no-spam);
|
|
23
|
+
// wire onError to surface failures to an
|
|
24
|
+
// operator.
|
|
25
|
+
//
|
|
26
|
+
// X-Trace-ID is already stamped on every control POST by buildRequestHeaders, so
|
|
27
|
+
// the forward inherits the run's trace_id for free.
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.FORWARD_TIMEOUT_MS = exports.ANALYTICS_INGEST_PATH = void 0;
|
|
30
|
+
exports.partitionEmittable = partitionEmittable;
|
|
31
|
+
exports.forwardEvents = forwardEvents;
|
|
32
|
+
const http_1 = require("../http");
|
|
33
|
+
const consent_1 = require("./consent");
|
|
34
|
+
const envelope_1 = require("./envelope");
|
|
35
|
+
exports.ANALYTICS_INGEST_PATH = "/internal/v1/analytics/events";
|
|
36
|
+
// The forward runs at command finalize, so it must never add a long tail to a
|
|
37
|
+
// command's wall-clock. A short, hard bound: a slow or hung control costs at most
|
|
38
|
+
// this many ms before the run completes. On timeout the batch is dropped, not
|
|
39
|
+
// re-queued; it stays durable in the local jsonl and the global rollup tolerates
|
|
40
|
+
// the gap (INV-GLOBAL-UNKNOWN-1).
|
|
41
|
+
exports.FORWARD_TIMEOUT_MS = 3000;
|
|
42
|
+
// Partition events into those that may be shipped and those that can't (unbound
|
|
43
|
+
// runs). Exported for the test contract (INV-JOIN-1: an event with a null
|
|
44
|
+
// workspace never ships).
|
|
45
|
+
function partitionEmittable(events) {
|
|
46
|
+
const emittable = [];
|
|
47
|
+
const withheld = [];
|
|
48
|
+
for (const ev of events) {
|
|
49
|
+
if ((0, envelope_1.isRemotelyEmittable)(ev))
|
|
50
|
+
emittable.push(ev);
|
|
51
|
+
else
|
|
52
|
+
withheld.push(ev);
|
|
53
|
+
}
|
|
54
|
+
return { emittable, withheld };
|
|
55
|
+
}
|
|
56
|
+
// Forward a batch of events to control. Best-effort: a transport error never
|
|
57
|
+
// throws (analytics must not break a command), it is counted in `failed` so the
|
|
58
|
+
// caller can decide whether to log it. Returns the disposition counts.
|
|
59
|
+
async function forwardEvents(cfg, events, env = process.env, onError, timeoutMs = exports.FORWARD_TIMEOUT_MS) {
|
|
60
|
+
const result = {
|
|
61
|
+
attempted: events.length,
|
|
62
|
+
forwarded: 0,
|
|
63
|
+
skippedConsent: false,
|
|
64
|
+
skippedNotEmittable: 0,
|
|
65
|
+
failed: 0,
|
|
66
|
+
};
|
|
67
|
+
if (!(0, consent_1.remoteAnalyticsEnabled)(env)) {
|
|
68
|
+
result.skippedConsent = true;
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
const { emittable, withheld } = partitionEmittable(events);
|
|
72
|
+
result.skippedNotEmittable = withheld.length;
|
|
73
|
+
if (emittable.length === 0)
|
|
74
|
+
return result;
|
|
75
|
+
// Control's AgentReviewWorkspaceGuard authorizes ONE workspaceId per request,
|
|
76
|
+
// so a batch is grouped by workspace_id and posted once per group. In practice
|
|
77
|
+
// a single CLI run touches one workspace, but grouping keeps the contract honest
|
|
78
|
+
// if a flush ever spans more (and isolates a per-workspace transport failure).
|
|
79
|
+
for (const [workspaceId, group] of groupByWorkspace(emittable)) {
|
|
80
|
+
try {
|
|
81
|
+
// Control dedupes by (workspace_id, event_id), so the ingest is idempotent:
|
|
82
|
+
// re-POSTing an already-landed event never double-counts a rollup
|
|
83
|
+
// (INV-REMOTE-DEDUPE-1). The CLI does not auto-replay across runs; this is
|
|
84
|
+
// what would keep a future manual re-sync safe.
|
|
85
|
+
await (0, http_1.post)(cfg, exports.ANALYTICS_INGEST_PATH, { workspaceId, events: group }, timeoutMs);
|
|
86
|
+
result.forwarded += group.length;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
result.failed += group.length;
|
|
90
|
+
if (onError)
|
|
91
|
+
onError(err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
// Group emittable events by their workspace_id. Every event here passed
|
|
97
|
+
// isRemotelyEmittable, so workspace_id is a non-empty string; the cast is safe.
|
|
98
|
+
function groupByWorkspace(events) {
|
|
99
|
+
const groups = new Map();
|
|
100
|
+
for (const ev of events) {
|
|
101
|
+
const ws = ev.workspace_id;
|
|
102
|
+
const existing = groups.get(ws);
|
|
103
|
+
if (existing)
|
|
104
|
+
existing.push(ev);
|
|
105
|
+
else
|
|
106
|
+
groups.set(ws, [ev]);
|
|
107
|
+
}
|
|
108
|
+
return groups;
|
|
109
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Local trace-log directory + reader (spec section 7.4). The hook spool writes the
|
|
3
|
+
// three followthrough trace files here: ask-traces.jsonl (the inject side),
|
|
4
|
+
// mcp-calls.jsonl (the pull side), and report-citations.jsonl (the push-reference
|
|
5
|
+
// side). `mla adoption`, the evidence section of `mla stats`, and the Stop-hook
|
|
6
|
+
// correlator all read them through this ONE module so the path and the
|
|
7
|
+
// lenient-parse semantics match exactly (INV-ADOPTION-SOURCE-1).
|
|
8
|
+
//
|
|
9
|
+
// The path resolves from the LIVE MEETLESS_HOME on every call, NOT the module-load
|
|
10
|
+
// cached config.HOME, because the user-prompt-submit hook sets MEETLESS_HOME at
|
|
11
|
+
// spawn time and the detached correlator process must read the same logs directory
|
|
12
|
+
// the hook wrote to. (events.jsonl, by contrast, is read through store.ts on the
|
|
13
|
+
// cached HOME; in the live CLI the two agree because the env is fixed at startup.)
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.logsDir = logsDir;
|
|
49
|
+
exports.readLogJsonl = readLogJsonl;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const os = __importStar(require("os"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
// The trace-log directory: $MEETLESS_HOME/logs (live env, matching the hook spool).
|
|
54
|
+
function logsDir() {
|
|
55
|
+
return path.join(process.env.MEETLESS_HOME || path.join(os.homedir(), ".meetless"), "logs");
|
|
56
|
+
}
|
|
57
|
+
// Read one jsonl trace file under logsDir(). Lenient: a blank, partially-written,
|
|
58
|
+
// or corrupt line is skipped, never fatal, so a crash mid-append can never brick a
|
|
59
|
+
// reader. Returns [] when the file is absent.
|
|
60
|
+
function readLogJsonl(file) {
|
|
61
|
+
const p = path.join(logsDir(), file);
|
|
62
|
+
if (!fs.existsSync(p))
|
|
63
|
+
return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const line of fs.readFileSync(p, "utf8").split("\n")) {
|
|
66
|
+
if (!line.trim())
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const o = JSON.parse(line);
|
|
70
|
+
if (o && typeof o === "object")
|
|
71
|
+
out.push(o);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// tolerate a partially-written or corrupt line
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The North Star metric family (spec §5, INV-METRIC-DEFINITION-1). The review
|
|
3
|
+
// killed the single ambiguous "Evidence Utilization Rate" and replaced it with
|
|
4
|
+
// four metrics that are DEFINED SEPARATELY and never collapsed into one number:
|
|
5
|
+
//
|
|
6
|
+
// Injection Utilization Rate injects referenced / injects with offered>0
|
|
7
|
+
// the wall metric: did the injection land at all
|
|
8
|
+
// Evidence Item Utilization distinct referenced ids / distinct offered ids
|
|
9
|
+
// the drilldown: did the offered DOCS get used
|
|
10
|
+
// Reference Precision (v1) used / (used + ignored)
|
|
11
|
+
// v1 used:=referenced, so this is a reference-
|
|
12
|
+
// followthrough proxy, NOT material use. The
|
|
13
|
+
// dashboard MUST label it "Reference Precision
|
|
14
|
+
// (v1)", never "Inject Precision" (§4.2).
|
|
15
|
+
// Unknown Coverage unknown / closed inject windows
|
|
16
|
+
// the honesty term: how often we could not classify
|
|
17
|
+
//
|
|
18
|
+
// Injection Utilization can read 100% while Evidence Item Utilization reads 18%
|
|
19
|
+
// (a 10-doc inject where one doc was used is a utilized injection but a 1-in-10
|
|
20
|
+
// item rate). Both are true; they answer different questions; they render side by
|
|
21
|
+
// side, never merged.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.REFERENCE_PRECISION_V1_LABEL = void 0;
|
|
24
|
+
exports.computeMetrics = computeMetrics;
|
|
25
|
+
const followthrough_1 = require("./followthrough");
|
|
26
|
+
function computeMetrics(inputs) {
|
|
27
|
+
// Injection Utilization: only injects that actually offered evidence count in
|
|
28
|
+
// the denominator (a zero-result inject is a coverage gap, not a missed use).
|
|
29
|
+
const offeredInjects = inputs.filter((i) => i.evidence_offered > 0);
|
|
30
|
+
const referencedInjects = offeredInjects.filter((i) => i.referenced);
|
|
31
|
+
// Evidence Item Utilization: distinct source ids, normalized by the SAME rule
|
|
32
|
+
// the join uses so "NT:foo.md" and "NT:foo" collapse on both sides.
|
|
33
|
+
const distinctOffered = new Set();
|
|
34
|
+
const distinctReferenced = new Set();
|
|
35
|
+
for (const i of inputs) {
|
|
36
|
+
for (const id of i.offered_source_ids)
|
|
37
|
+
distinctOffered.add((0, followthrough_1.normId)(id));
|
|
38
|
+
for (const id of i.referenced_source_ids)
|
|
39
|
+
distinctReferenced.add((0, followthrough_1.normId)(id));
|
|
40
|
+
}
|
|
41
|
+
let used = 0;
|
|
42
|
+
let ignored = 0;
|
|
43
|
+
let unknown = 0;
|
|
44
|
+
let pending = 0;
|
|
45
|
+
for (const i of inputs) {
|
|
46
|
+
if (i.outcome === "used")
|
|
47
|
+
used++;
|
|
48
|
+
else if (i.outcome === "ignored")
|
|
49
|
+
ignored++;
|
|
50
|
+
else if (i.outcome === "unknown")
|
|
51
|
+
unknown++;
|
|
52
|
+
else if (i.outcome === "pending")
|
|
53
|
+
pending++;
|
|
54
|
+
}
|
|
55
|
+
const closedWindows = used + ignored + unknown;
|
|
56
|
+
return {
|
|
57
|
+
injection_utilization: offeredInjects.length
|
|
58
|
+
? referencedInjects.length / offeredInjects.length
|
|
59
|
+
: null,
|
|
60
|
+
evidence_item_utilization: distinctOffered.size
|
|
61
|
+
? distinctReferenced.size / distinctOffered.size
|
|
62
|
+
: null,
|
|
63
|
+
reference_precision_v1: used + ignored ? used / (used + ignored) : null,
|
|
64
|
+
unknown_coverage: closedWindows ? unknown / closedWindows : null,
|
|
65
|
+
injects_offered: offeredInjects.length,
|
|
66
|
+
injects_referenced: referencedInjects.length,
|
|
67
|
+
distinct_offered: distinctOffered.size,
|
|
68
|
+
distinct_referenced: distinctReferenced.size,
|
|
69
|
+
used,
|
|
70
|
+
ignored,
|
|
71
|
+
unknown,
|
|
72
|
+
pending,
|
|
73
|
+
closed_windows: closedWindows,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// The v1 dashboard label for reference_precision_v1. Centralized so every render
|
|
77
|
+
// path (mla stats, mla adoption) shows the same honest wording (§4.2).
|
|
78
|
+
exports.REFERENCE_PRECISION_V1_LABEL = "Reference Precision (v1)";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Analytics recorder: the single entry point a command uses to emit an event
|
|
3
|
+
// (spec section 10, the dual-sink contract). It does three things, in order:
|
|
4
|
+
//
|
|
5
|
+
// 1. Assemble the envelope from the run context (run_id, trace_id, session_id,
|
|
6
|
+
// workspace_id, distinct_id) + the caller's event_type/payload.
|
|
7
|
+
// 2. Append it to the local jsonl IMMEDIATELY (durable, offline-safe, the
|
|
8
|
+
// source of truth for `mla stats`). This happens even with remote analytics
|
|
9
|
+
// off (gated only by localStatsEnabled inside the store).
|
|
10
|
+
// 3. Buffer it for a best-effort remote forward at run finalize.
|
|
11
|
+
//
|
|
12
|
+
// The local append is synchronous and unconditional-by-consent; the remote
|
|
13
|
+
// forward is deferred to flushAnalyticsEvents() so a single run makes at most one
|
|
14
|
+
// control round-trip. This keeps analytics off the command's hot path: nothing
|
|
15
|
+
// here blocks or can throw into the command.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.buildEvent = buildEvent;
|
|
18
|
+
exports.recordAnalyticsEvent = recordAnalyticsEvent;
|
|
19
|
+
exports.flushAnalyticsEvents = flushAnalyticsEvents;
|
|
20
|
+
exports.resetRecorderForTesting = resetRecorderForTesting;
|
|
21
|
+
exports.peekBuffer = peekBuffer;
|
|
22
|
+
const observability_1 = require("../observability");
|
|
23
|
+
const envelope_1 = require("./envelope");
|
|
24
|
+
const event_id_1 = require("./event-id");
|
|
25
|
+
const store_1 = require("./store");
|
|
26
|
+
const store_2 = require("./store");
|
|
27
|
+
const forwarder_1 = require("./forwarder");
|
|
28
|
+
let buffer = [];
|
|
29
|
+
// Build the fully-formed, flat event (envelope + payload merged). Pure: no I/O,
|
|
30
|
+
// so the test contract can assert envelope completeness on the returned object.
|
|
31
|
+
function buildEvent(ctx, input) {
|
|
32
|
+
const runId = ctx.runId ?? (0, observability_1.getRunId)();
|
|
33
|
+
const traceId = ctx.traceId ?? (0, observability_1.getRunTraceId)();
|
|
34
|
+
if (!runId) {
|
|
35
|
+
throw new Error("buildEvent requires a run_id (call setRunId at bootstrap or pass ctx.runId)");
|
|
36
|
+
}
|
|
37
|
+
if (!traceId) {
|
|
38
|
+
throw new Error("buildEvent requires a trace_id (call setRunTraceId at bootstrap or pass ctx.traceId)");
|
|
39
|
+
}
|
|
40
|
+
const envelope = (0, envelope_1.makeEnvelope)({
|
|
41
|
+
event_id: input.eventId ?? (0, event_id_1.mintEventId)(),
|
|
42
|
+
event_type: input.eventType,
|
|
43
|
+
created_at: ctx.now,
|
|
44
|
+
emitted_at: ctx.now,
|
|
45
|
+
workspace_id: ctx.workspaceId,
|
|
46
|
+
distinct_id: ctx.distinctId ?? (ctx.workspaceId ? null : (0, store_2.machineId)()),
|
|
47
|
+
session_id: ctx.sessionId,
|
|
48
|
+
run_id: runId,
|
|
49
|
+
trace_id: traceId,
|
|
50
|
+
source: ctx.source ?? "cli",
|
|
51
|
+
actor_workspace_user_id: ctx.actorWorkspaceUserId ?? null,
|
|
52
|
+
repo_fingerprint: ctx.repoFingerprint ?? (0, observability_1.getRepoFingerprint)(),
|
|
53
|
+
});
|
|
54
|
+
// Flat merge: envelope first so a stray envelope-named payload key can't shadow
|
|
55
|
+
// the real join fields.
|
|
56
|
+
return { ...input.payload, ...envelope };
|
|
57
|
+
}
|
|
58
|
+
// Record an event: append locally now, buffer for remote forward later. Returns
|
|
59
|
+
// the built event so a caller (or test) can inspect it. Never throws on I/O.
|
|
60
|
+
function recordAnalyticsEvent(ctx, input, env = process.env, onError) {
|
|
61
|
+
const ev = buildEvent(ctx, input);
|
|
62
|
+
(0, store_1.appendEvent)(ev, env, onError);
|
|
63
|
+
buffer.push(ev);
|
|
64
|
+
return ev;
|
|
65
|
+
}
|
|
66
|
+
// Flush the buffered events to control (best-effort). Called once at run finalize.
|
|
67
|
+
// Drains the in-process buffer unconditionally: this is the only forward attempt
|
|
68
|
+
// for these events (there is no cross-run replay). On a forward failure they stay
|
|
69
|
+
// durable in the local jsonl (the source of truth for `mla stats`); only the remote
|
|
70
|
+
// rollup misses them, reported as unknown rather than zero (INV-GLOBAL-UNKNOWN-1).
|
|
71
|
+
async function flushAnalyticsEvents(cfg, env = process.env, onError, timeoutMs) {
|
|
72
|
+
const pending = buffer;
|
|
73
|
+
buffer = [];
|
|
74
|
+
if (pending.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
attempted: 0,
|
|
77
|
+
forwarded: 0,
|
|
78
|
+
skippedConsent: false,
|
|
79
|
+
skippedNotEmittable: 0,
|
|
80
|
+
failed: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return (0, forwarder_1.forwardEvents)(cfg, pending, env, onError, timeoutMs);
|
|
84
|
+
}
|
|
85
|
+
// Test-only: drop any buffered events so suites don't bleed state across cases.
|
|
86
|
+
function resetRecorderForTesting() {
|
|
87
|
+
buffer = [];
|
|
88
|
+
}
|
|
89
|
+
// Test/inspection: a copy of the current buffer.
|
|
90
|
+
function peekBuffer() {
|
|
91
|
+
return [...buffer];
|
|
92
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// mla_review_decision + mla_contradiction builders (spec §6.2, §7.2; T7.2).
|
|
3
|
+
//
|
|
4
|
+
// Pure, I/O-free: given the reviewed candidate's relation type and the verdict,
|
|
5
|
+
// produce the closed, PII-bounded analytics events to record at DECISION time.
|
|
6
|
+
// Both are recorded exactly ONCE per decision (the dashboard rollup is
|
|
7
|
+
// event-counting), so a single decision-time emit is the whole story and never
|
|
8
|
+
// double-counts.
|
|
9
|
+
//
|
|
10
|
+
// mla_review_decision -- every accept / reject / reclassify / no_relation. Tells
|
|
11
|
+
// us curation behavior and trust over time.
|
|
12
|
+
// mla_contradiction -- only the contradiction/supersession edge classes (the
|
|
13
|
+
// "did mla catch a stale assumption" signal). surfaced is
|
|
14
|
+
// always true at review time (the candidate WAS shown);
|
|
15
|
+
// acted_on is true only when the curator accepted it (made
|
|
16
|
+
// it a governed edge).
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.coerceRelationEdgeType = coerceRelationEdgeType;
|
|
19
|
+
exports.reviewDecisionLabel = reviewDecisionLabel;
|
|
20
|
+
exports.buildReviewAnalyticsEvents = buildReviewAnalyticsEvents;
|
|
21
|
+
const envelope_1 = require("./envelope");
|
|
22
|
+
const event_id_1 = require("./event-id");
|
|
23
|
+
// Map a raw relation-type key onto the closed edge-type enum. Anything not in the
|
|
24
|
+
// curated set (or null) becomes "unknown" -- no open string ever reaches PostHog
|
|
25
|
+
// (INV-POSTHOG-PII-1).
|
|
26
|
+
function coerceRelationEdgeType(raw) {
|
|
27
|
+
if (raw && envelope_1.RELATION_EDGE_TYPES.includes(raw)) {
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
// The verdict -> closed decision-label mapping. accept/reject pass through; a
|
|
33
|
+
// correction is "no_relation" when it asserts the edge should not exist, else
|
|
34
|
+
// "reclassify" (a re-type or a section-scope correction is still a reclassification).
|
|
35
|
+
function reviewDecisionLabel(verdict, correctionKind) {
|
|
36
|
+
if (verdict === "accept")
|
|
37
|
+
return "accept";
|
|
38
|
+
if (verdict === "reject")
|
|
39
|
+
return "reject";
|
|
40
|
+
if (correctionKind === "NO_RELATION")
|
|
41
|
+
return "no_relation";
|
|
42
|
+
return "reclassify";
|
|
43
|
+
}
|
|
44
|
+
function buildReviewAnalyticsEvents(input) {
|
|
45
|
+
const { candidateId, verdict, correctionKind, relationTypeId } = input;
|
|
46
|
+
const decision = reviewDecisionLabel(verdict, correctionKind);
|
|
47
|
+
const relationType = coerceRelationEdgeType(relationTypeId);
|
|
48
|
+
const events = [];
|
|
49
|
+
const reviewPayload = {
|
|
50
|
+
decision_id: candidateId,
|
|
51
|
+
decision_version: 1,
|
|
52
|
+
decision,
|
|
53
|
+
relation_type: relationType,
|
|
54
|
+
};
|
|
55
|
+
events.push({
|
|
56
|
+
eventType: "mla_review_decision",
|
|
57
|
+
eventId: (0, event_id_1.reviewDecisionEventId)(candidateId, 1),
|
|
58
|
+
payload: reviewPayload,
|
|
59
|
+
});
|
|
60
|
+
// Only contradiction/supersession edges are "stale assumption caught" signals.
|
|
61
|
+
if (relationType === "CONTRADICTS" || relationType === "SUPERSEDES") {
|
|
62
|
+
const contradictionPayload = {
|
|
63
|
+
contradiction_id: candidateId,
|
|
64
|
+
edge_type: relationType,
|
|
65
|
+
contradiction_surfaced: true,
|
|
66
|
+
contradiction_acted_on: verdict === "accept",
|
|
67
|
+
};
|
|
68
|
+
events.push({
|
|
69
|
+
eventType: "mla_contradiction",
|
|
70
|
+
eventId: (0, event_id_1.deterministicEventId)(`contradiction:${candidateId}`, 1),
|
|
71
|
+
payload: contradictionPayload,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return events;
|
|
75
|
+
}
|