@meetless/mla 0.1.5 → 0.1.6

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 (41) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +31 -5
  3. package/dist/commands/activate.js +39 -18
  4. package/dist/commands/agent-memory.js +333 -0
  5. package/dist/commands/enrich.js +211 -2
  6. package/dist/commands/internal-auto-index.js +64 -1
  7. package/dist/commands/internal-pretool-observe.js +86 -1
  8. package/dist/commands/internal-redact-capture.js +130 -0
  9. package/dist/commands/pilot.js +385 -0
  10. package/dist/lib/agent-memory-capture/binding.js +115 -0
  11. package/dist/lib/agent-memory-capture/classify.js +68 -0
  12. package/dist/lib/agent-memory-capture/collector.js +69 -0
  13. package/dist/lib/agent-memory-capture/containment.js +74 -0
  14. package/dist/lib/agent-memory-capture/ledger.js +43 -0
  15. package/dist/lib/agent-memory-capture/live-collector.js +148 -0
  16. package/dist/lib/agent-memory-capture/live-ledger.js +45 -0
  17. package/dist/lib/agent-memory-capture/live-pipeline.js +344 -0
  18. package/dist/lib/agent-memory-capture/lock.js +98 -0
  19. package/dist/lib/agent-memory-capture/paths.js +47 -0
  20. package/dist/lib/agent-memory-capture/pipeline.js +222 -0
  21. package/dist/lib/agent-memory-capture/report.js +131 -0
  22. package/dist/lib/agent-memory-capture/types.js +14 -0
  23. package/dist/lib/agent-memory-capture/upsert-client.js +104 -0
  24. package/dist/lib/analytics/enforcement-classify.js +65 -0
  25. package/dist/lib/analytics/enforcement-incident.js +83 -0
  26. package/dist/lib/analytics/envelope.js +55 -1
  27. package/dist/lib/analytics/pilot.js +313 -0
  28. package/dist/lib/enrichment/ingest.js +98 -13
  29. package/dist/lib/enrichment/materialize-rules.js +81 -0
  30. package/dist/lib/enrichment/plan.js +72 -15
  31. package/dist/lib/enrichment/protocol.js +85 -5
  32. package/dist/lib/enrichment/scout-brief.js +35 -6
  33. package/dist/lib/redactor.js +104 -1
  34. package/dist/lib/scanner/agent-memory.js +55 -4
  35. package/dist/lib/scanner/managed-rules.js +0 -0
  36. package/dist/lib/scanner/scan.js +52 -1
  37. package/dist/lib/scanner/score.js +41 -3
  38. package/dist/lib/scanner/scout-mission.js +9 -7
  39. package/dist/lib/upgrade-apply.js +30 -0
  40. package/dist/lib/wire.js +2 -0
  41. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ // Pure, PII-safe classifiers for the enforcement-incident event (the deny tile,
3
+ // notes/20260627-mla-product-health-dashboard-posthog-metrics.md §5.1).
4
+ //
5
+ // Both functions read a tool name / file path ONLY to derive a closed enum; the
6
+ // raw string NEVER leaves the function (INV-POSTHOG-PII-1). Importing this module
7
+ // pulls in no recorder, config, store, or I/O, so the non-deny hot path of the
8
+ // PreToolUse hook can keep it at top level without weight.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.normalizeEnforcedTool = normalizeEnforcedTool;
11
+ exports.classifyTouchedSurface = classifyTouchedSurface;
12
+ /**
13
+ * Map a PreToolUse tool name to the closed ENFORCED_TOOLS enum. The notes-location
14
+ * pilot's admission gate guarantees the deny only fires on {Write, Edit}; anything
15
+ * else degrades to "unknown" rather than leaking the raw tool string.
16
+ */
17
+ function normalizeEnforcedTool(toolName) {
18
+ if (toolName === "Write" || toolName === "Edit")
19
+ return toolName;
20
+ return "unknown";
21
+ }
22
+ /**
23
+ * Classify a file path into the PII-safe touched-surface enum. The path is read only
24
+ * to derive the enum (extension + a few well-known path segments); the path itself
25
+ * never enters the returned value. Best-effort: an unrecognized shape degrades to
26
+ * "unknown" rather than guessing. Order is load-bearing -- a `.spec.ts` is a test
27
+ * (not code), a `.sql` under migrations is a migration (not config), etc.
28
+ */
29
+ function classifyTouchedSurface(filePath) {
30
+ if (!filePath || typeof filePath !== "string")
31
+ return "unknown";
32
+ const p = filePath.toLowerCase().replace(/\\/g, "/");
33
+ const base = p.split("/").pop() ?? p;
34
+ // tests win over code: a .spec.ts / __tests__/ path is a test surface.
35
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base) ||
36
+ /(^|\/)__tests__\//.test(p) ||
37
+ /(^|\/)tests?\//.test(p) ||
38
+ /_test\.(py|go|rb)$/.test(base)) {
39
+ return "tests";
40
+ }
41
+ // migrations win over config/code: a .sql or a migrations/ path is a migration.
42
+ if (/(^|\/)migrations?\//.test(p) || base.endsWith(".sql"))
43
+ return "migration";
44
+ // infra: container/IaC/CI/shell, before the generic config/code buckets.
45
+ if (base === "dockerfile" ||
46
+ base.endsWith(".dockerfile") ||
47
+ base.endsWith(".tf") ||
48
+ base.endsWith(".sh") ||
49
+ base.endsWith(".bash") ||
50
+ /(^|\/)(infra|deploy|\.github|terraform|helm|k8s)\//.test(p)) {
51
+ return "infra";
52
+ }
53
+ if (/\.(md|mdx|markdown|rst|txt|adoc)$/.test(base))
54
+ return "docs";
55
+ if (/\.(ya?ml|json|jsonc|toml|ini|cfg|conf|properties|lock)$/.test(base) ||
56
+ base.startsWith(".env") ||
57
+ base === ".gitignore" ||
58
+ base === ".npmrc") {
59
+ return "config";
60
+ }
61
+ if (/\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|c|h|cc|cpp|hpp|cs|swift|kt|kts|scala|m|mm|vue|svelte|ex|exs|clj|hs)$/.test(base)) {
62
+ return "code";
63
+ }
64
+ return "unknown";
65
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // Enforcement-incident emit seam (the deny tile,
3
+ // notes/20260627-mla-product-health-dashboard-posthog-metrics.md §5.1).
4
+ //
5
+ // The fail-soft, local-append-only bridge between the PreToolUse deny branch and the
6
+ // generic analytics spool. It mirrors ce0-emit.ts and upholds the same two invariants:
7
+ //
8
+ // - Local-append-only: the hook NEVER makes a synchronous network call. recordAnalyticsEvent
9
+ // appends to the local jsonl and buffers for the existing detached forward; remote
10
+ // delivery is that path's job.
11
+ // - Fail-soft: any fault (no config, a spool append fault, a build throw) is swallowed and
12
+ // never escalates into the blocked turn. The durable EnforcementAttempt row already
13
+ // recorded the deny; this telemetry is strictly best-effort on top of it.
14
+ //
15
+ // Difference from ce0-emit: where CE0 SKIPS when there is no ambient run/trace (a CE0 line
16
+ // that cannot join the enrichment is worse than none), a deny is rare and high-value and
17
+ // SELF-JOINS to its durable audit row via incident_id, so we MINT a run/trace when the fast
18
+ // path did not bootstrap one rather than drop the event.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.emitEnforcementIncident = emitEnforcementIncident;
21
+ const config_1 = require("../config");
22
+ const store_1 = require("./store");
23
+ const observability_1 = require("../observability");
24
+ const event_id_1 = require("./event-id");
25
+ const recorder_1 = require("./recorder");
26
+ /**
27
+ * Append one enforcement-incident event to the local analytics spool under a "hook"
28
+ * run-context envelope. Fail-soft and local-append-only: any fault is swallowed so a
29
+ * telemetry failure never disturbs the deny. The event_id is deterministic on the
30
+ * incident id so a re-fired hook dedups instead of double-counting the deny.
31
+ */
32
+ function emitEnforcementIncident(input, coords, deps = {}) {
33
+ try {
34
+ // A deny self-joins via incident_id, so mint a run/trace when absent rather than drop.
35
+ const traceId = deps.traceId ?? (0, observability_1.getRunTraceId)() ?? (0, observability_1.mintTraceId)();
36
+ const runId = deps.runId ?? (0, observability_1.getRunId)() ?? (0, observability_1.mintRunId)();
37
+ const readCfg = deps.readCfg ??
38
+ (() => {
39
+ try {
40
+ return (0, config_1.readConfig)();
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ });
46
+ const cfg = readCfg();
47
+ const mId = (deps.machineId ?? store_1.machineId)();
48
+ const ctx = {
49
+ workspaceId: coords.workspaceId,
50
+ sessionId: coords.sessionId,
51
+ // Prefer the configured actor; else the hashed machine id (workspace-scoped
52
+ // anonymous, never end-user PII).
53
+ distinctId: cfg?.actorUserId ?? mId,
54
+ runId,
55
+ traceId,
56
+ source: "hook",
57
+ actorWorkspaceUserId: cfg?.actorUserId ?? null,
58
+ repoFingerprint: deps.repoFingerprint ?? (0, observability_1.getRepoFingerprint)(),
59
+ now: new Date(coords.nowMs).toISOString(),
60
+ };
61
+ const payload = {
62
+ incident_id: input.incidentId,
63
+ decision: input.decision,
64
+ tool: input.tool,
65
+ touched_surface: input.touchedSurface,
66
+ rule_version_id: input.ruleVersionId,
67
+ // Born unreviewed; an offline labeler supersedes (deterministic id keyed at v0,
68
+ // a re-label emits v1+).
69
+ review_status: "unreviewed",
70
+ };
71
+ const record = deps.record ?? recorder_1.recordAnalyticsEvent;
72
+ record(ctx, {
73
+ eventType: "mla_enforcement_incident",
74
+ payload: payload,
75
+ eventId: (0, event_id_1.deterministicEventId)(input.incidentId, 0),
76
+ }, deps.env ?? process.env, () => {
77
+ /* fail-soft: an enforcement-telemetry append must never escalate into a blocking hook. */
78
+ });
79
+ }
80
+ catch {
81
+ // Fail-soft: enforcement telemetry must never disturb the turn it observed.
82
+ }
83
+ }
@@ -8,7 +8,7 @@
8
8
  // envelope fields and the payload fields sit at the same top level (matching
9
9
  // the local jsonl examples in section 7.4).
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.CE0_HOOKS = exports.OBLIGATION_OUTCOME_LABELS = exports.CONSULTATION_RESULTS = exports.CONSULTATION_EXECUTIONS = exports.MEMORY_REQUIREMENTS = exports.REVIEW_DECISIONS = exports.RELATION_EDGE_TYPES = exports.COMMAND_SCOPES = exports.RETRIEVAL_CONFIDENCES = exports.INJECT_OUTCOMES = exports.WINDOW_CLOSED_REASONS = exports.COVERAGE_GAP_TYPES = exports.QUERY_TOPIC_CATEGORIES = exports.GOVERNED_RELATION_TYPES = exports.TOUCHED_SURFACES = exports.COMMAND_OUTCOMES = exports.SOURCE_SURFACES = exports.EVENT_SOURCES = exports.EVENT_TYPES = exports.SCHEMA_VERSION = void 0;
11
+ exports.CE0_HOOKS = exports.OBLIGATION_OUTCOME_LABELS = exports.CONSULTATION_RESULTS = exports.CONSULTATION_EXECUTIONS = exports.MEMORY_REQUIREMENTS = exports.PILOT_FEEDBACK_VERDICTS = exports.PILOT_FEEDBACK_POINTS = exports.ENFORCEMENT_REVIEW_STATUSES = exports.ENFORCEMENT_DECISIONS = exports.ENFORCED_TOOLS = exports.REVIEW_DECISIONS = exports.RELATION_EDGE_TYPES = exports.COMMAND_SCOPES = exports.RETRIEVAL_CONFIDENCES = exports.INJECT_OUTCOMES = exports.WINDOW_CLOSED_REASONS = exports.COVERAGE_GAP_TYPES = exports.QUERY_TOPIC_CATEGORIES = exports.GOVERNED_RELATION_TYPES = exports.TOUCHED_SURFACES = exports.COMMAND_OUTCOMES = exports.SOURCE_SURFACES = exports.EVENT_SOURCES = exports.EVENT_TYPES = exports.SCHEMA_VERSION = void 0;
12
12
  exports.makeEnvelope = makeEnvelope;
13
13
  exports.buildAttribution = buildAttribution;
14
14
  exports.envelopeMissingKeys = envelopeMissingKeys;
@@ -27,6 +27,12 @@ exports.EVENT_TYPES = [
27
27
  "mla_contradiction",
28
28
  "mla_review_decision",
29
29
  "mla_stats_viewed",
30
+ // Pilot instrumentation (memo notes/20260624-mla-new-user-value-and-brownfield-proof.md
31
+ // §6 Phase 3): one explicit human verdict captured at a natural point (a reviewed
32
+ // candidate, a sampled injection, a fired alert, a blocked tool call). This is the
33
+ // pilot's primary value signal; the auto-derived inject heuristic band is explicitly
34
+ // NOT relied on for the pilot. Payload is enums/ids/booleans only (INV-POSTHOG-PII-1).
35
+ "mla_pilot_feedback",
30
36
  // CE0 evidence-consultation telemetry (§6.4). Named per the ratified proposal
31
37
  // contract (no `mla_` prefix): these four are the PostHog projection of the
32
38
  // obligation lifecycle and the dashboards in §6.4 query them by these names.
@@ -34,6 +40,14 @@ exports.EVENT_TYPES = [
34
40
  "evidence_consultation_completed",
35
41
  "evidence_obligation_finalized",
36
42
  "evidence_hook_health",
43
+ // Enforcement (PreToolUse deny) telemetry. The one append per fired deny that
44
+ // the product-health dashboard's deny tile reads
45
+ // (notes/20260627-mla-product-health-dashboard-posthog-metrics.md §5.1). Before
46
+ // this event the deny path produced ZERO analytics: the durable EnforcementAttempt
47
+ // row existed but no metric saw it, so "wrong actions blocked" was un-measurable.
48
+ // Payload is ids/enums only -- the blocked PATH never leaves the device, only its
49
+ // surface enum (INV-POSTHOG-PII-1).
50
+ "mla_enforcement_incident",
37
51
  ];
38
52
  exports.EVENT_SOURCES = ["cli", "hook", "mcp", "control", "intel"];
39
53
  // The emission-surface label carried in the attribution block (spec section 3.7
@@ -116,6 +130,46 @@ exports.RELATION_EDGE_TYPES = [
116
130
  "unknown",
117
131
  ];
118
132
  exports.REVIEW_DECISIONS = ["accept", "reject", "reclassify", "no_relation"];
133
+ // --- enforcement-incident enums (§5.1, the deny tile) -----------------------
134
+ // The closed wire forms for the PreToolUse enforcement event. Every value is a
135
+ // fixed enum so no open string (a tool name, a decision verb, a review verdict)
136
+ // reaches the privacy boundary.
137
+ // The tools the deny pilot is armed for. The notes-location admission gate is
138
+ // exactly {Write, Edit}; "unknown" is a defensive fallback the gate should make
139
+ // unreachable, kept so a future deny rule on another tool still classifies safely.
140
+ exports.ENFORCED_TOOLS = ["Write", "Edit", "unknown"];
141
+ // The enforcement verdict the hook emitted. Only "deny" fires today; "warn" is
142
+ // reserved for the soft-gate path so the tile can later split block vs warn.
143
+ exports.ENFORCEMENT_DECISIONS = ["deny", "warn"];
144
+ // The human-review label dimension the deny tile needs (§5.1: "confirmed /
145
+ // false-positive / unreviewed"). Born "unreviewed" at emit time; an offline
146
+ // labeler supersedes with "confirmed" or "false_positive" (e.g. the known
147
+ // notes-location-v1 vault-own-path false positive).
148
+ exports.ENFORCEMENT_REVIEW_STATUSES = ["unreviewed", "confirmed", "false_positive"];
149
+ // --- pilot feedback enums (memo §6 Phase 3) ---------------------------------
150
+ // The FOUR natural points at which the pilot samples an explicit human verdict
151
+ // (memo lines 551-552: "candidate accept/reject, alert true/false/uncertain, deny
152
+ // correct/incorrect, ordinary injection sampled occasionally"). A closed enum so no
153
+ // open string reaches the privacy boundary.
154
+ exports.PILOT_FEEDBACK_POINTS = ["candidate", "injection", "alert", "deny"];
155
+ // The union of every verdict any point can carry. The point->verdict pairing is
156
+ // validated in lib/analytics/pilot.ts (PILOT_FEEDBACK_POINT_VERDICTS); this tuple is
157
+ // only the closed membership set the boundary checks:
158
+ // candidate -> accept | reject
159
+ // injection -> useful | noise | uncertain
160
+ // alert -> confirmed | disposed | uncertain
161
+ // deny -> correct | incorrect
162
+ exports.PILOT_FEEDBACK_VERDICTS = [
163
+ "accept",
164
+ "reject",
165
+ "useful",
166
+ "noise",
167
+ "uncertain",
168
+ "confirmed",
169
+ "disposed",
170
+ "correct",
171
+ "incorrect",
172
+ ];
119
173
  // --- CE0 evidence-consultation telemetry enums (§6.4) -----------------------
120
174
  // The wire forms of the rules-layer CE0 enums. Re-declared here, in the analytics
121
175
  // layer, on purpose: the privacy boundary validates membership against THESE closed
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_INJECTION_SAMPLE_N = exports.PILOT_FEEDBACK_POINT_VERDICTS = void 0;
37
+ exports.validatePilotFeedback = validatePilotFeedback;
38
+ exports.buildPilotFeedbackEvent = buildPilotFeedbackEvent;
39
+ exports.injectionSampleN = injectionSampleN;
40
+ exports.shouldSampleInjection = shouldSampleInjection;
41
+ exports.aggregatePilot = aggregatePilot;
42
+ // Pilot instrumentation helpers (memo notes/20260624-mla-new-user-value-and-brownfield-proof.md
43
+ // §6 Phase 3). Pure + I/O-free so the four-hypothesis report and the sampling gate are
44
+ // directly testable. Three responsibilities:
45
+ //
46
+ // 1. validatePilotFeedback / buildPilotFeedbackEvent -- turn one human verdict at a
47
+ // natural point into a closed, PII-bounded mla_pilot_feedback RecordInput.
48
+ // 2. shouldSampleInjection -- the deterministic gate that makes injection feedback
49
+ // OCCASIONAL, never every-injection (memo lines 551-554: prompting on every
50
+ // injection makes mla itself annoying). Same inject -> same decision, so a hook
51
+ // never double-prompts and the test can pin the distribution.
52
+ // 3. aggregatePilot -- fold the local event jsonl into the memo's FOUR hypotheses.
53
+ // No dashboard: this is the "simple report" half of "store raw events and generate
54
+ // a simple report/export" (memo line 553).
55
+ //
56
+ // The auto-derived inject heuristic band (mla_evidence_outcome used/ignored) is
57
+ // deliberately NOT the value signal here; the memo forbids building it for the pilot.
58
+ // This module reads the EXPLICIT mla_pilot_feedback verdicts, and uses mla_command
59
+ // timestamps only for retention + time-to-first-useful.
60
+ const crypto = __importStar(require("crypto"));
61
+ const envelope_1 = require("./envelope");
62
+ const event_id_1 = require("./event-id");
63
+ // The point -> allowed-verdicts pairing (memo lines 551-552). A verdict outside its
64
+ // point's set is a programming/usage error, refused before anything is recorded.
65
+ exports.PILOT_FEEDBACK_POINT_VERDICTS = {
66
+ candidate: ["accept", "reject"],
67
+ injection: ["useful", "noise", "uncertain"],
68
+ alert: ["confirmed", "disposed", "uncertain"],
69
+ deny: ["correct", "incorrect"],
70
+ };
71
+ // Refuse a malformed verdict whole (the command maps a throw to exit 2). Three checks:
72
+ // the point is known, the verdict is allowed for that point, and prevented_mistake is
73
+ // only ever set on a CONFIRMED alert.
74
+ function validatePilotFeedback(input) {
75
+ if (!envelope_1.PILOT_FEEDBACK_POINTS.includes(input.point)) {
76
+ throw new Error(`unknown feedback point "${input.point}" (expected one of: ${envelope_1.PILOT_FEEDBACK_POINTS.join(", ")})`);
77
+ }
78
+ if (!envelope_1.PILOT_FEEDBACK_VERDICTS.includes(input.verdict)) {
79
+ throw new Error(`unknown verdict "${input.verdict}" (expected one of: ${envelope_1.PILOT_FEEDBACK_VERDICTS.join(", ")})`);
80
+ }
81
+ const allowed = exports.PILOT_FEEDBACK_POINT_VERDICTS[input.point];
82
+ if (!allowed.includes(input.verdict)) {
83
+ throw new Error(`verdict "${input.verdict}" is not valid for point "${input.point}" (allowed: ${allowed.join(", ")})`);
84
+ }
85
+ if (input.preventedMistake && !(input.point === "alert" && input.verdict === "confirmed")) {
86
+ throw new Error('prevented_mistake is a tag on a CONFIRMED alert only (point=alert, verdict=confirmed)');
87
+ }
88
+ }
89
+ // Build the RecordInput for one pilot verdict. Mints a fresh CLI-origin feedback_id
90
+ // (uuid, not a content hash: two identical verdicts seconds apart are distinct events).
91
+ function buildPilotFeedbackEvent(input) {
92
+ validatePilotFeedback(input);
93
+ const payload = {
94
+ feedback_id: (0, event_id_1.mintEventId)(),
95
+ feedback_point: input.point,
96
+ verdict: input.verdict,
97
+ prevented_mistake: input.preventedMistake === true,
98
+ sampled: input.sampled === true,
99
+ };
100
+ return {
101
+ eventType: "mla_pilot_feedback",
102
+ payload: payload,
103
+ };
104
+ }
105
+ // --- the sampling gate ------------------------------------------------------
106
+ // Default: sample ~1 in 5 injections for relevance feedback. Occasional, not every
107
+ // injection (the memo's explicit anti-annoyance constraint).
108
+ exports.DEFAULT_INJECTION_SAMPLE_N = 5;
109
+ // Resolve the 1-in-N sampling rate from env. 0 disables sampling (never prompt); 1
110
+ // means every injection (opt-in to blanket prompting); >=2 is the occasional band.
111
+ // A malformed/negative value falls back to the default rather than failing a hook.
112
+ function injectionSampleN(env = process.env) {
113
+ const raw = (env.MEETLESS_PILOT_INJECTION_SAMPLE_N || "").trim();
114
+ if (!raw)
115
+ return exports.DEFAULT_INJECTION_SAMPLE_N;
116
+ const n = Number(raw);
117
+ if (!Number.isFinite(n) || n < 0)
118
+ return exports.DEFAULT_INJECTION_SAMPLE_N;
119
+ return Math.floor(n);
120
+ }
121
+ // Map a stable key (an inject_id) to a 32-bit unsigned int via sha256. Deterministic
122
+ // across processes so the SAME inject yields the SAME sample decision -- a hook that
123
+ // re-evaluates a turn never flips from "prompt" to "don't" and double-asks.
124
+ function hashToUint(key) {
125
+ const hex = crypto.createHash("sha256").update(key).digest("hex").slice(0, 8);
126
+ return parseInt(hex, 16) >>> 0;
127
+ }
128
+ // Should this injection be sampled for explicit relevance feedback? Deterministic in
129
+ // `key`. n<=0 -> never; n===1 -> always; otherwise ~1-in-n by hash bucket. An empty
130
+ // key is never sampled (we cannot key a stable decision on nothing).
131
+ function shouldSampleInjection(key, n = exports.DEFAULT_INJECTION_SAMPLE_N) {
132
+ if (!key)
133
+ return false;
134
+ if (n <= 0)
135
+ return false;
136
+ if (n === 1)
137
+ return true;
138
+ return hashToUint(key) % n === 0;
139
+ }
140
+ // --- the four-hypothesis report --------------------------------------------
141
+ const DAY_MS = 24 * 60 * 60 * 1000;
142
+ function rate(numer, denom) {
143
+ return denom > 0 ? numer / denom : null;
144
+ }
145
+ function ms(iso) {
146
+ if (!iso)
147
+ return null;
148
+ const t = Date.parse(iso);
149
+ return Number.isFinite(t) ? t : null;
150
+ }
151
+ // Fold the raw local events into the four hypotheses. The verdict hypotheses (H1-H3)
152
+ // are scoped to the window; retention (H4) and time-to-first-useful intentionally span
153
+ // the FULL history (a 7/14-day retention question is meaningless inside a 7-day window).
154
+ function aggregatePilot(events, opts) {
155
+ const windowStart = opts.nowMs - opts.windowDays * DAY_MS;
156
+ const inWindow = (ev) => {
157
+ const t = ms(ev.created_at);
158
+ return t !== null && t >= windowStart;
159
+ };
160
+ const report = {
161
+ window_days: opts.windowDays,
162
+ generated_at: new Date(opts.nowMs).toISOString(),
163
+ onboarding: {
164
+ candidate_accept: 0,
165
+ candidate_reject: 0,
166
+ acceptance_rate: null,
167
+ review_accept: 0,
168
+ review_reject: 0,
169
+ time_to_first_useful_ms: null,
170
+ },
171
+ injection: {
172
+ useful: 0,
173
+ noise: 0,
174
+ uncertain: 0,
175
+ usefulness_rate: null,
176
+ sampled_feedback_count: 0,
177
+ total_injects: 0,
178
+ feedback_coverage_rate: null,
179
+ },
180
+ coordination: {
181
+ alert_confirmed: 0,
182
+ alert_disposed: 0,
183
+ alert_uncertain: 0,
184
+ confirmation_rate: null,
185
+ prevented_mistakes: 0,
186
+ deny_correct: 0,
187
+ deny_incorrect: 0,
188
+ deny_annoyance_rate: null,
189
+ contradictions_surfaced: 0,
190
+ contradictions_acted_on: 0,
191
+ },
192
+ retention: {
193
+ first_seen: null,
194
+ last_seen: null,
195
+ active_days: 0,
196
+ used_after_7d: false,
197
+ used_after_14d: false,
198
+ },
199
+ totals: { pilot_feedback_events: 0, events_scanned: events.length },
200
+ };
201
+ // Full-history scratch for retention + time-to-first-useful.
202
+ let onboardingStartMs = null; // first activate (fallback: first command)
203
+ let firstCommandMs = null;
204
+ let firstUsefulMs = null;
205
+ let lastCommandMs = null;
206
+ const activeDayKeys = new Set();
207
+ for (const ev of events) {
208
+ const t = ms(ev.created_at);
209
+ if (ev.event_type === "mla_command") {
210
+ const cmd = ev.command;
211
+ if (t !== null) {
212
+ if (firstCommandMs === null || t < firstCommandMs)
213
+ firstCommandMs = t;
214
+ if (lastCommandMs === null || t > lastCommandMs)
215
+ lastCommandMs = t;
216
+ activeDayKeys.add(new Date(t).toISOString().slice(0, 10));
217
+ if (cmd === "activate" && (onboardingStartMs === null || t < onboardingStartMs)) {
218
+ onboardingStartMs = t;
219
+ }
220
+ }
221
+ continue;
222
+ }
223
+ if (ev.event_type === "mla_pilot_feedback") {
224
+ const p = ev;
225
+ // time-to-first-useful spans full history (an early "useful" still counts).
226
+ if (t !== null &&
227
+ ((p.feedback_point === "candidate" && p.verdict === "accept") ||
228
+ (p.feedback_point === "injection" && p.verdict === "useful"))) {
229
+ if (firstUsefulMs === null || t < firstUsefulMs)
230
+ firstUsefulMs = t;
231
+ }
232
+ if (!inWindow(ev))
233
+ continue;
234
+ report.totals.pilot_feedback_events++;
235
+ switch (p.feedback_point) {
236
+ case "candidate":
237
+ if (p.verdict === "accept")
238
+ report.onboarding.candidate_accept++;
239
+ else if (p.verdict === "reject")
240
+ report.onboarding.candidate_reject++;
241
+ break;
242
+ case "injection":
243
+ if (p.verdict === "useful")
244
+ report.injection.useful++;
245
+ else if (p.verdict === "noise")
246
+ report.injection.noise++;
247
+ else if (p.verdict === "uncertain")
248
+ report.injection.uncertain++;
249
+ if (p.sampled)
250
+ report.injection.sampled_feedback_count++;
251
+ break;
252
+ case "alert":
253
+ if (p.verdict === "confirmed")
254
+ report.coordination.alert_confirmed++;
255
+ else if (p.verdict === "disposed")
256
+ report.coordination.alert_disposed++;
257
+ else if (p.verdict === "uncertain")
258
+ report.coordination.alert_uncertain++;
259
+ if (p.prevented_mistake)
260
+ report.coordination.prevented_mistakes++;
261
+ break;
262
+ case "deny":
263
+ if (p.verdict === "correct")
264
+ report.coordination.deny_correct++;
265
+ else if (p.verdict === "incorrect")
266
+ report.coordination.deny_incorrect++;
267
+ break;
268
+ }
269
+ continue;
270
+ }
271
+ if (!inWindow(ev))
272
+ continue;
273
+ if (ev.event_type === "mla_evidence_inject") {
274
+ report.injection.total_injects++;
275
+ }
276
+ else if (ev.event_type === "mla_review_decision") {
277
+ const d = ev.decision;
278
+ if (d === "accept")
279
+ report.onboarding.review_accept++;
280
+ else if (d === "reject")
281
+ report.onboarding.review_reject++;
282
+ }
283
+ else if (ev.event_type === "mla_contradiction") {
284
+ const c = ev;
285
+ if (c.contradiction_surfaced)
286
+ report.coordination.contradictions_surfaced++;
287
+ if (c.contradiction_acted_on)
288
+ report.coordination.contradictions_acted_on++;
289
+ }
290
+ }
291
+ // Derived rates.
292
+ const o = report.onboarding;
293
+ o.acceptance_rate = rate(o.candidate_accept, o.candidate_accept + o.candidate_reject);
294
+ const start = onboardingStartMs ?? firstCommandMs;
295
+ if (start !== null && firstUsefulMs !== null && firstUsefulMs >= start) {
296
+ o.time_to_first_useful_ms = firstUsefulMs - start;
297
+ }
298
+ const inj = report.injection;
299
+ inj.usefulness_rate = rate(inj.useful, inj.useful + inj.noise);
300
+ inj.feedback_coverage_rate = rate(inj.sampled_feedback_count, inj.total_injects);
301
+ const co = report.coordination;
302
+ co.confirmation_rate = rate(co.alert_confirmed, co.alert_confirmed + co.alert_disposed);
303
+ co.deny_annoyance_rate = rate(co.deny_incorrect, co.deny_correct + co.deny_incorrect);
304
+ const r = report.retention;
305
+ r.first_seen = firstCommandMs !== null ? new Date(firstCommandMs).toISOString() : null;
306
+ r.last_seen = lastCommandMs !== null ? new Date(lastCommandMs).toISOString() : null;
307
+ r.active_days = activeDayKeys.size;
308
+ if (firstCommandMs !== null && lastCommandMs !== null) {
309
+ r.used_after_7d = lastCommandMs - firstCommandMs >= 7 * DAY_MS;
310
+ r.used_after_14d = lastCommandMs - firstCommandMs >= 14 * DAY_MS;
311
+ }
312
+ return report;
313
+ }