@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.
Files changed (202) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +81 -0
  3. package/dist/build-info.json +9 -0
  4. package/dist/bundles/ask-core.js +396 -0
  5. package/dist/bundles/mcp.js +16592 -0
  6. package/dist/bundles/trace-core.js +263 -0
  7. package/dist/cli.js +828 -0
  8. package/dist/commands/activate.js +781 -0
  9. package/dist/commands/adoption.js +130 -0
  10. package/dist/commands/ask.js +290 -0
  11. package/dist/commands/context.js +114 -0
  12. package/dist/commands/debug.js +313 -0
  13. package/dist/commands/doctor.js +1021 -0
  14. package/dist/commands/enrich.js +427 -0
  15. package/dist/commands/evidence.js +229 -0
  16. package/dist/commands/flush.js +184 -0
  17. package/dist/commands/graph.js +104 -0
  18. package/dist/commands/init.js +272 -0
  19. package/dist/commands/internal-active-review.js +322 -0
  20. package/dist/commands/internal-auto-index.js +188 -0
  21. package/dist/commands/internal-capture-decisions.js +320 -0
  22. package/dist/commands/internal-evidence-correlate.js +239 -0
  23. package/dist/commands/internal-evidence-hooks.js +240 -0
  24. package/dist/commands/internal-evidence-inject.js +231 -0
  25. package/dist/commands/internal-finalize.js +221 -0
  26. package/dist/commands/internal-pretool-observe.js +225 -0
  27. package/dist/commands/internal-refresh.js +136 -0
  28. package/dist/commands/internal-session-nudge.js +120 -0
  29. package/dist/commands/internal-steer-sync.js +117 -0
  30. package/dist/commands/internal-turn-recap.js +140 -0
  31. package/dist/commands/kb.js +375 -0
  32. package/dist/commands/kb_add.js +681 -0
  33. package/dist/commands/kb_forget.js +283 -0
  34. package/dist/commands/kb_move.js +45 -0
  35. package/dist/commands/kb_pending.js +410 -0
  36. package/dist/commands/kb_personal.js +149 -0
  37. package/dist/commands/kb_promote.js +188 -0
  38. package/dist/commands/kb_purge.js +168 -0
  39. package/dist/commands/kb_reingest.js +335 -0
  40. package/dist/commands/kb_retime.js +170 -0
  41. package/dist/commands/kb_review.js +391 -0
  42. package/dist/commands/kb_revision.js +179 -0
  43. package/dist/commands/kb_show.js +385 -0
  44. package/dist/commands/label.js +226 -0
  45. package/dist/commands/login.js +295 -0
  46. package/dist/commands/logout.js +108 -0
  47. package/dist/commands/mcp-supervisor.js +93 -0
  48. package/dist/commands/mcp.js +227 -0
  49. package/dist/commands/queue-prune.js +98 -0
  50. package/dist/commands/review.js +358 -0
  51. package/dist/commands/rewire.js +124 -0
  52. package/dist/commands/rules.js +728 -0
  53. package/dist/commands/scan-context.js +67 -0
  54. package/dist/commands/session.js +347 -0
  55. package/dist/commands/stats.js +479 -0
  56. package/dist/commands/status.js +61 -0
  57. package/dist/commands/summary.js +250 -0
  58. package/dist/commands/turn.js +114 -0
  59. package/dist/commands/uninstall.js +222 -0
  60. package/dist/commands/whoami.js +102 -0
  61. package/dist/commands/workspace.js +130 -0
  62. package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
  63. package/dist/hooks-template/ce0-session-start.sh +49 -0
  64. package/dist/hooks-template/ce0-stop.sh +29 -0
  65. package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
  66. package/dist/hooks-template/common.sh +934 -0
  67. package/dist/hooks-template/event-batch-filter.jq +67 -0
  68. package/dist/hooks-template/flush.sh +503 -0
  69. package/dist/hooks-template/post-tool-use.sh +423 -0
  70. package/dist/hooks-template/pre-tool-use.sh +69 -0
  71. package/dist/hooks-template/session-start.sh +140 -0
  72. package/dist/hooks-template/stop.sh +308 -0
  73. package/dist/hooks-template/user-prompt-submit.sh +1162 -0
  74. package/dist/lib/activation.js +79 -0
  75. package/dist/lib/active-conflict-cache.js +141 -0
  76. package/dist/lib/active-memory.js +59 -0
  77. package/dist/lib/active-review-runner.js +26 -0
  78. package/dist/lib/agent-decision/index.js +25 -0
  79. package/dist/lib/agent-decision/keys.js +49 -0
  80. package/dist/lib/agent-decision/normalize-claude.js +183 -0
  81. package/dist/lib/agent-decision/types.js +21 -0
  82. package/dist/lib/agent-decision/validate.js +216 -0
  83. package/dist/lib/analytics/capture.js +96 -0
  84. package/dist/lib/analytics/command-event.js +267 -0
  85. package/dist/lib/analytics/consent.js +58 -0
  86. package/dist/lib/analytics/coverage-gap.js +96 -0
  87. package/dist/lib/analytics/envelope.js +236 -0
  88. package/dist/lib/analytics/event-id.js +86 -0
  89. package/dist/lib/analytics/evidence.js +150 -0
  90. package/dist/lib/analytics/followthrough.js +194 -0
  91. package/dist/lib/analytics/forwarder.js +109 -0
  92. package/dist/lib/analytics/logs.js +78 -0
  93. package/dist/lib/analytics/metrics.js +78 -0
  94. package/dist/lib/analytics/recorder.js +92 -0
  95. package/dist/lib/analytics/review-analytics.js +75 -0
  96. package/dist/lib/analytics/sequence.js +77 -0
  97. package/dist/lib/analytics/store.js +131 -0
  98. package/dist/lib/analytics/turn-recap.js +279 -0
  99. package/dist/lib/artifact_id.js +108 -0
  100. package/dist/lib/auth-breaker.js +161 -0
  101. package/dist/lib/auto-index.js +112 -0
  102. package/dist/lib/classifier.js +88 -0
  103. package/dist/lib/config.js +298 -0
  104. package/dist/lib/conflict-advisory.js +64 -0
  105. package/dist/lib/debug-bundle.js +520 -0
  106. package/dist/lib/enrichment/ingest.js +301 -0
  107. package/dist/lib/enrichment/plan.js +253 -0
  108. package/dist/lib/enrichment/protocol.js +359 -0
  109. package/dist/lib/enrichment/scout-brief.js +176 -0
  110. package/dist/lib/failure-telemetry.js +444 -0
  111. package/dist/lib/git.js +200 -0
  112. package/dist/lib/governance-cache.js +77 -0
  113. package/dist/lib/governed-path-cache.js +76 -0
  114. package/dist/lib/http.js +677 -0
  115. package/dist/lib/identity-envelope.js +23 -0
  116. package/dist/lib/kb-candidate.js +65 -0
  117. package/dist/lib/kb_acl.js +98 -0
  118. package/dist/lib/login.js +353 -0
  119. package/dist/lib/mcp-fetchers.js +130 -0
  120. package/dist/lib/mcp-restart.js +47 -0
  121. package/dist/lib/observability.js +805 -0
  122. package/dist/lib/open-url.js +33 -0
  123. package/dist/lib/orphan-guard.js +70 -0
  124. package/dist/lib/packaged.js +21 -0
  125. package/dist/lib/reconcile-sessions.js +171 -0
  126. package/dist/lib/redactor.js +89 -0
  127. package/dist/lib/relationship-candidate-query.js +27 -0
  128. package/dist/lib/render.js +611 -0
  129. package/dist/lib/rules/applicability.js +64 -0
  130. package/dist/lib/rules/attest-code-rule-version.js +47 -0
  131. package/dist/lib/rules/attest-notes-location.js +217 -0
  132. package/dist/lib/rules/attest-rule-version.js +69 -0
  133. package/dist/lib/rules/canonical-json.js +97 -0
  134. package/dist/lib/rules/ce0-emit.js +64 -0
  135. package/dist/lib/rules/ce0-evidence.js +281 -0
  136. package/dist/lib/rules/ce0-recall-sample.js +82 -0
  137. package/dist/lib/rules/ce0-rule.js +55 -0
  138. package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
  139. package/dist/lib/rules/ce0-store.js +683 -0
  140. package/dist/lib/rules/ce0-telemetry-project.js +93 -0
  141. package/dist/lib/rules/ce0-telemetry.js +158 -0
  142. package/dist/lib/rules/code-rule-registry.js +17 -0
  143. package/dist/lib/rules/command-match.js +185 -0
  144. package/dist/lib/rules/consult-evidence-binding.js +27 -0
  145. package/dist/lib/rules/consultation-capture-adapter.js +193 -0
  146. package/dist/lib/rules/content-match.js +56 -0
  147. package/dist/lib/rules/deny-admission.js +99 -0
  148. package/dist/lib/rules/durable-observation.js +190 -0
  149. package/dist/lib/rules/enforce-notes-version.js +421 -0
  150. package/dist/lib/rules/evaluation-input-hash.js +126 -0
  151. package/dist/lib/rules/evaluator.js +108 -0
  152. package/dist/lib/rules/inert-rule-families.js +51 -0
  153. package/dist/lib/rules/input-authority-resolver.js +241 -0
  154. package/dist/lib/rules/interception-schema.js +170 -0
  155. package/dist/lib/rules/interception-store.js +267 -0
  156. package/dist/lib/rules/live-input-authority.js +66 -0
  157. package/dist/lib/rules/local-matcher.js +108 -0
  158. package/dist/lib/rules/local-observe.js +79 -0
  159. package/dist/lib/rules/local-rule-version-repo.js +214 -0
  160. package/dist/lib/rules/memory-requirement.js +109 -0
  161. package/dist/lib/rules/notes-observe.js +39 -0
  162. package/dist/lib/rules/notes-path.js +261 -0
  163. package/dist/lib/rules/notes-rule.js +75 -0
  164. package/dist/lib/rules/observe-adapter.js +114 -0
  165. package/dist/lib/rules/observed-rule-hash.js +119 -0
  166. package/dist/lib/rules/prompt-submit-adapter.js +132 -0
  167. package/dist/lib/rules/requirement-subject.js +240 -0
  168. package/dist/lib/rules/rule-activity.js +67 -0
  169. package/dist/lib/rules/rule-version-hash.js +151 -0
  170. package/dist/lib/rules/runtime-scope.js +55 -0
  171. package/dist/lib/rules/stop-adapter.js +116 -0
  172. package/dist/lib/rules/stop-response-snapshot.js +174 -0
  173. package/dist/lib/rules/types.js +10 -0
  174. package/dist/lib/rules/ulid.js +46 -0
  175. package/dist/lib/rules/version-evaluation.js +156 -0
  176. package/dist/lib/scanner/agent-memory.js +99 -0
  177. package/dist/lib/scanner/bootstrap-summary.js +87 -0
  178. package/dist/lib/scanner/cache.js +59 -0
  179. package/dist/lib/scanner/frontmatter.js +42 -0
  180. package/dist/lib/scanner/parse-directives.js +69 -0
  181. package/dist/lib/scanner/parse-structured.js +72 -0
  182. package/dist/lib/scanner/render.js +73 -0
  183. package/dist/lib/scanner/scan.js +132 -0
  184. package/dist/lib/scanner/score.js +38 -0
  185. package/dist/lib/scanner/scout-mission.js +126 -0
  186. package/dist/lib/scanner/types.js +7 -0
  187. package/dist/lib/session-scope.js +195 -0
  188. package/dist/lib/spool.js +355 -0
  189. package/dist/lib/staleness.js +100 -0
  190. package/dist/lib/steer-cache.js +87 -0
  191. package/dist/lib/tagged-reference.js +20 -0
  192. package/dist/lib/temporal.js +109 -0
  193. package/dist/lib/turn-recap-emit.js +67 -0
  194. package/dist/lib/unwire.js +253 -0
  195. package/dist/lib/update-check.js +469 -0
  196. package/dist/lib/update-notifier.js +217 -0
  197. package/dist/lib/upgrade-apply.js +643 -0
  198. package/dist/lib/wire.js +1087 -0
  199. package/dist/lib/workspace.js +96 -0
  200. package/dist/lib/zip.js +154 -0
  201. package/dist/pretool-entry.js +37 -0
  202. 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
+ }