fraim 2.0.339 → 2.0.341

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.
@@ -21,15 +21,10 @@ const learning_usage_attestation_1 = require("./learning-usage-attestation");
21
21
  const learning_usage_verify_1 = require("./learning-usage-verify");
22
22
  const pack_home_1 = require("../cli/utils/pack-home");
23
23
  /**
24
- * The repair for a usage record that has gone wrong.
25
- *
26
- * The record is derived (#1103 R17), so the fix is to rebuild it rather than to
27
- * reconcile it in place. Measured on a 733-file retrospective corpus: a rebuild
28
- * lands with zero split identities and one model bucket, because the identity
29
- * resolution and the model normalization fix the causes and a rebuilt store
30
- * carries no legacy shape.
24
+ * Preserve checkpointed history and distinguish backup recovery from attestation backfill.
25
+ * Backfill supplements older firings; it cannot reconstruct missing offers.
31
26
  */
32
- const REBUILD_NOTE = 'Delete the `usage/` directory and run `learning-usage backfill` to rebuild the record from the retrospectives. The record is derived: every firing is also written in the retrospective that attested it, so a rebuild restores one identity per lesson and one bucket per model. It costs the offer history, which rebuilds itself from live traffic.';
27
+ const REBUILD_NOTE = "Keep `learning-usage.json`, its `.checkpoint` backup, `events/` and any conflict copies. After consolidation, the snapshot and checkpoint backup preserve lifetime counters and already-counted source markers; the remaining journal cannot rebuild that history. A missing or torn snapshot is read from its checkpoint backup automatically. Restore matching checkpoint and journal files from backup if both copies are damaged. Inspect split identities and conflict copies before repairing them; do not delete the snapshot to clear a warning. Separately, `learning-usage backfill` can recover older firings from retrospectives; raw learnings and retrospectives cannot restore runtime offers.";
33
28
  function resolveWorkspaceRoot(options) {
34
29
  return node_path_1.default.resolve(options.workspaceRoot || options.root || process.cwd());
35
30
  }
@@ -282,7 +277,8 @@ const reportCommand = addModelOption(addCommonOptions(new commander_1.Command('r
282
277
  });
283
278
  emit(report, options.json, () => {
284
279
  const lines = [
285
- `${report.totals.entriesOffered} entries were offered. ${report.totals.entriesWithFirings} fired. ${report.totals.entriesNeverFired} have never fired.`,
280
+ report.totals.entriesOffered === null ? 'Corpus totals are unavailable; listed entries have independent usable evidence.'
281
+ : `${report.totals.entriesOffered} entries were offered. ${report.totals.entriesWithFirings} fired. ${report.totals.entriesNeverFired} have never fired.`,
286
282
  ];
287
283
  if (report.workingHardest.length) {
288
284
  lines.push('', 'Working hardest');
@@ -300,7 +296,7 @@ const reportCommand = addModelOption(addCommonOptions(new commander_1.Command('r
300
296
  report.ignoredWhileInContext.forEach((e, i) => lines.push(`${i + 1}. ${e.title}. Attested ignored ${e.ignored} time${e.ignored === 1 ? '' : 's'} (fired ${e.fired}). In context and the work went against it, so the entry needs work rather than another recurrence.`));
301
297
  }
302
298
  if (report.integrityFlagged > 0) {
303
- lines.push('', `${report.integrityFlagged} entr${report.integrityFlagged === 1 ? 'y has' : 'ies have'} an integrity flag: more attested firings than recorded offers. Run \`learning-usage verify\` before acting on anything below.`);
299
+ lines.push('', `${report.integrityFlagged} entries were excluded from usage conclusions. Run \`learning-usage verify\` for diagnostic details.`);
304
300
  }
305
301
  lines.push('', 'Nothing is retired from this view. Run sleep-on-learnings to decide.');
306
302
  return lines.join('\n');
@@ -455,7 +451,8 @@ const pruneCommand = addCommonOptions(new commander_1.Command('prune'))
455
451
  const workspaceRoot = resolveWorkspaceRoot(options);
456
452
  const limits = (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
457
453
  const result = (0, learning_usage_store_1.pruneUsageStore)(limits);
458
- emit({ ...result, storePath: (0, learning_usage_store_1.resolveUsageStorePath)(), limits }, options.json, () => `Pruned ${result.prunedOfferRecords} offer records and ${result.prunedFiringRecords} firing records across ${result.entries} entries. Aggregates unchanged.`);
454
+ emit({ ...result, storePath: (0, learning_usage_store_1.resolveUsageStorePath)(), limits }, options.json, () => result.maintenanceStatus === 'deferred' ? `Maintenance deferred; history unchanged. ${result.evidence?.observations.join(', ') ?? ''}`
455
+ : `Pruned ${result.prunedOfferRecords} offer records and ${result.prunedFiringRecords} firing records across ${result.entries} entries. Aggregates unchanged.`);
459
456
  });
460
457
  function mb(bytes) {
461
458
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
@@ -477,9 +474,11 @@ const verifyCommand = addModelOption(addCommonOptions(new commander_1.Command('v
477
474
  '',
478
475
  ];
479
476
  const finding = (count, singular, plural, remedy) => count === 0 ? ` none: ${singular}` : ` ${count} ${count === 1 ? singular : plural}. ${remedy}`;
477
+ if (report.evidence)
478
+ lines.push(`Evidence: ${report.evidence.status}. ${report.evidence.observations.join(', ')}.`, ...(report.evidence.recovery ? [`Recovered from ${report.evidence.recovery}; primary replacement was not persisted by this read.`] : []), '');
480
479
  lines.push('Identity', finding(report.twinPairs.length, 'lesson tracked under two keys', 'lessons tracked under two keys', 'Its offers and its firings are on different records, so every count about it is partial.'), report.unmatchedTitleKeys.length
481
480
  ? ` ${report.unmatchedTitleKeys.length} title-keyed record${report.unmatchedTitleKeys.length === 1 ? ' has' : 's have'} no id twin. Expected for entries no tier has stamped; run \`stamp-ids\` to reduce it.`
482
- : ' every title-keyed record has been matched or has no twin', '', 'Counters', finding(report.invariantViolations.length, 'entry holds more attested firings than recorded offers', 'entries hold more attested firings than recorded offers', 'An entry cannot fire without being offered, so one of the two counters is wrong. History stranded in a store copy nothing reads is the usual cause.'), finding(report.caseVariantModelBuckets.length, 'model bucket differs only by case', 'model buckets differ only by case', 'One model spelled two ways splits its own denominator, so every per-model conclusion about it is computed on a fraction of its traffic.'), finding(report.unknownModelAttribution.length, 'entry fired with no model recorded', 'entries fired with no model recorded', 'Per-model retirement cannot be answered for these, and they are held back rather than counted as zero. These were attested before record-firings carried --agent and --model, so the model was never recorded against them and a rebuild cannot invent it; attestations recorded from now on carry both.'), '', 'Storage');
481
+ : ' every title-keyed record has been matched or has no twin', '', 'Counters', finding(report.invariantViolations.length, 'entry holds more attested firings than recorded offers', 'entries hold more attested firings than recorded offers', 'The counts are inconsistent. This observation does not establish their cause.'), finding(report.caseVariantModelBuckets.length, 'model bucket differs only by case', 'model buckets differ only by case', 'One model spelled two ways splits its own denominator, so every per-model conclusion about it is computed on a fraction of its traffic.'), finding(report.unknownModelAttribution.length, 'entry has incomplete model attribution', 'entries have incomplete model attribution', 'Affected model conclusions are withheld. The cause is not established; unknown historical attribution cannot be inferred from the current model.'), '', 'Storage');
483
482
  if (report.storeCopies.length === 0) {
484
483
  lines.push(' one store file, no unmerged copies');
485
484
  }
@@ -488,14 +487,14 @@ const verifyCommand = addModelOption(addCommonOptions(new commander_1.Command('v
488
487
  for (const copy of report.storeCopies) {
489
488
  lines.push(` ${copy.path}: ${copy.readable ? `${copy.entries} entries, ${copy.offers} offers, ${copy.firings} firings` : `unreadable (${copy.error ?? 'unknown error'})`}`);
490
489
  }
491
- lines.push(' Nothing reads these. Their history is recoverable only through a rebuild.');
490
+ lines.push(' Nothing reads these. Preserve them and the checkpoint backup for inspection; do not delete snapshots to clear this warning.');
492
491
  }
493
492
  lines.push(` event logs folded: ${report.eventLogs.length === 0 ? 'none' : report.eventLogs.map((l) => `${l.machineId} (${l.bytes} bytes)`).join(', ')}`);
494
493
  for (const [machineId, lag] of Object.entries(report.checkpointLag)) {
495
494
  lines.push(` checkpoint lag for ${machineId}: ${lag} bytes${lag > 0 ? ' behind the log head' : ''}`);
496
495
  }
497
496
  lines.push(report.compactionAdvised
498
- ? ` log size ${mb(report.totalLogBytes)} is past the ${mb(report.compactionAdvisoryBytes)} point where a cold fold stops fitting the context-injection budget. Run \`learning-usage prune\` to compact it to the configured retention window.`
497
+ ? ` log size ${mb(report.totalLogBytes)} is past the ${mb(report.compactionAdvisoryBytes)} point where a cold fold stops fitting the context-injection budget. Run \`learning-usage prune\` to consolidate processed events into the snapshot and checkpoint backup, then clear them independently of retention.`
499
498
  : ` log size ${mb(report.totalLogBytes)} of ${mb(report.compactionAdvisoryBytes)} before compaction is worth running.`);
500
499
  if (report.rebuildAdvised)
501
500
  lines.push('', REBUILD_NOTE);
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.modelAttributionIncomplete = modelAttributionIncomplete;
4
+ exports.countEvidenceProblem = countEvidenceProblem;
5
+ exports.usageEvidenceExclusions = usageEvidenceExclusions;
6
+ const learning_usage_store_1 = require("./learning-usage-store");
7
+ const validCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
8
+ /** Missing attribution is unknown, including a known bucket beside unattributed history. */
9
+ function modelAttributionIncomplete(record) {
10
+ const buckets = [record.offered.byModel, record.fired.byModel];
11
+ if (buckets.some(map => !map || Object.entries(map).some(([name, n]) => !validCount(n) || !(0, learning_usage_store_1.normalizeModelName)(name) || (0, learning_usage_store_1.normalizeModelName)(name) !== name)))
12
+ return true;
13
+ return Object.values(record.offered.byModel).reduce((a, b) => a + b, 0) !== record.offered.total
14
+ || Object.values(record.fired.byModel).reduce((a, b) => a + b, 0)
15
+ !== record.fired.byOutcome.applied + record.fired.byOutcome.prevented;
16
+ }
17
+ /** Validate counts independently of a cached integrity flag. Never alter the record. */
18
+ function countEvidenceProblem(record, offersEraStart) {
19
+ const counts = [record.offered.total, record.offered.aboveThreshold, record.fired.total,
20
+ ...Object.values(record.offered.byModel), ...Object.values(record.fired.byModel),
21
+ ...Object.values(record.fired.byOutcome)];
22
+ if (counts.some(n => !validCount(n)) || learning_usage_store_1.FIRING_OUTCOMES.some(outcome => !validCount(record.fired.byOutcome[outcome]))
23
+ || record.offered.aboveThreshold > record.offered.total
24
+ || Object.values(record.fired.byOutcome).reduce((a, b) => a + b, 0) !== record.fired.total) {
25
+ return 'Usage counters are inconsistent.';
26
+ }
27
+ const violation = record.integrity ?? (0, learning_usage_store_1.integrityViolation)(record, offersEraStart);
28
+ if (!offersEraStart && record.offered.total > 0 && record.fired.total > record.offered.total) {
29
+ return 'Excess uses lack provenance establishing the offer-recording era.';
30
+ }
31
+ return violation ? violation.detail : null;
32
+ }
33
+ /** Read-time exclusions follow the actual record, never ambient UI or model state. */
34
+ function usageEvidenceExclusions(store) {
35
+ const excluded = new Map();
36
+ for (const key of store.evidence?.excludedKeys ?? [])
37
+ excluded.set(key, 'Unreadable usage record.');
38
+ const byTitle = new Map();
39
+ for (const record of Object.values(store.entries)) {
40
+ const problem = countEvidenceProblem(record, store.offersEraStart);
41
+ if (store.evidence?.status === 'unavailable')
42
+ excluded.set(record.key, 'Usage history is unavailable.');
43
+ else if (problem)
44
+ excluded.set(record.key, problem);
45
+ if (!record.fileType)
46
+ continue;
47
+ for (const title of record.titles) {
48
+ const titleKey = (0, learning_usage_store_1.titleUsageKey)(record.fileType, title);
49
+ byTitle.set(titleKey, [...new Set([...(byTitle.get(titleKey) ?? []), record.key])]);
50
+ }
51
+ }
52
+ for (const keys of byTitle.values()) {
53
+ if (keys.length > 1 && keys.some(key => key.startsWith('T:'))) {
54
+ for (const key of keys)
55
+ excluded.set(key, 'Ambiguous duplicate usage identity.');
56
+ }
57
+ }
58
+ return excluded;
59
+ }
@@ -7,43 +7,17 @@ exports.listEventLogs = listEventLogs;
7
7
  exports.readEventsSince = readEventsSince;
8
8
  exports.compactEventLogs = compactEventLogs;
9
9
  /**
10
- * Issue #1752 — the durable half of the usage record.
10
+ * Per-machine usage journals: job runs append complete JSON lines; sleep folds
11
+ * them into a recoverable snapshot and removes processed records.
11
12
  *
12
- * The store used to be one mutable JSON document rewritten by every writer with a
13
- * plain `writeFileSync`. Measured (spike S1b): eight concurrent processes doing
14
- * that on a *plain local disk*, no sync folder involved, lost 1589 of 1600
15
- * writes. The four OneDrive conflict copies in issue #1752 are a symptom of the
16
- * same defect, not its cause.
13
+ * A cleared journal contains only {_usageJournalThroughBytes:N}. This framing
14
+ * marker preserves its logical position across atomic replacement, so a crash
15
+ * between checkpoint publication and journal cleanup cannot reset offsets or
16
+ * count an event twice. It contains no activity details. New appends continue
17
+ * from N; an older/missing snapshot cannot silently cold-fold compacted history.
17
18
  *
18
- * So the durable artifact is an append-only log, one file per machine:
19
- *
20
- * OFFER/FIRING event → appendFileSync of one whole JSON line
21
- * usage/learning-usage.json → a derived snapshot with a byte checkpoint
22
- *
23
- * Three measurements decided the shape, and each closed off an alternative:
24
- *
25
- * S2 16 processes x 1000 lines on the real OneDrive-synced path produced
26
- * 16000/16000 complete parseable lines, 0 interleaved, 0 conflict copies.
27
- * That is what removes the need for a same-machine lock. The per-machine
28
- * filename is what removes the cross-machine question, because two
29
- * machines then never write one file.
30
- * S3 `renameSync` over a target any process holds open fails EPERM 100 times
31
- * in 100 on Windows, on a synced path *and* a local one, because Node's
32
- * `openSync(path,'r')` does not request FILE_SHARE_DELETE. The usage store
33
- * is read by concurrent MCP processes by design, so rename-publish is not
34
- * available here. Recorded so it is not re-proposed. See also #820.
35
- * S4b A full fold is linear: 84 ms at today's volume, 794 ms at ten times it.
36
- * A snapshot plus a tail fold is 2.6 ms at both 9.9 MB and 79 MB. Flat.
37
- *
38
- * This module owns the log files and nothing else: framing, appending, reading a
39
- * tail from a byte offset, and compaction. It knows a line is a JSON object and
40
- * knows nothing about what a usage record looks like. That is deliberate — the
41
- * record shape lives in `learning-usage-store.ts`, which imports this, so there
42
- * is no cycle between them.
43
- *
44
- * The same primitive is already used by `src/ai-hub/raw-event-log-store.ts`
45
- * (append one JSON line, track byte offsets); this is the learning-usage
46
- * equivalent rather than a second general mechanism.
19
+ * Maintenance requires no concurrent writers or sync activity. Ordinary writers
20
+ * append one payload; different machines use different filenames.
47
21
  */
48
22
  const fs_1 = require("fs");
49
23
  const os_1 = require("os");
@@ -51,6 +25,7 @@ const path_1 = require("path");
51
25
  /** Subdirectory of the usage folder holding the per-machine logs. */
52
26
  const EVENT_DIR_NAME = 'events';
53
27
  const LOG_SUFFIX = '.jsonl';
28
+ const CHECKPOINT_MARKER = '_usageJournalThroughBytes';
54
29
  /** The log directory that belongs to a snapshot: `<usage>/events/`. */
55
30
  function resolveEventDir(snapshotPath) {
56
31
  return (0, path_1.join)((0, path_1.dirname)(snapshotPath), EVENT_DIR_NAME);
@@ -136,10 +111,25 @@ function listEventLogs(eventDir) {
136
111
  continue;
137
112
  const path = (0, path_1.join)(eventDir, name);
138
113
  let size = 0;
114
+ let baseOffset = 0;
115
+ let headerBytes = 0;
139
116
  try {
140
117
  const fd = (0, fs_1.openSync)(path, 'r');
141
118
  try {
142
119
  size = (0, fs_1.fstatSync)(fd).size;
120
+ const prefix = Buffer.alloc(Math.min(size, 128));
121
+ (0, fs_1.readSync)(fd, prefix, 0, prefix.length, 0);
122
+ const newline = prefix.indexOf(0x0a);
123
+ if (newline >= 0) {
124
+ try {
125
+ const header = JSON.parse(prefix.subarray(0, newline).toString('utf8'));
126
+ if (Object.keys(header).length === 1 && Number.isSafeInteger(header[CHECKPOINT_MARKER]) && header[CHECKPOINT_MARKER] >= 0) {
127
+ baseOffset = header[CHECKPOINT_MARKER];
128
+ headerBytes = newline + 1;
129
+ }
130
+ }
131
+ catch { /* An ordinary first event, not a checkpoint marker. */ }
132
+ }
143
133
  }
144
134
  finally {
145
135
  (0, fs_1.closeSync)(fd);
@@ -148,14 +138,20 @@ function listEventLogs(eventDir) {
148
138
  catch {
149
139
  continue;
150
140
  }
151
- logs.push({ machineId: name.slice(0, -LOG_SUFFIX.length), path, size });
141
+ logs.push({ machineId: name.slice(0, -LOG_SUFFIX.length), path, size, baseOffset, headerBytes });
152
142
  }
153
143
  return logs.sort((a, b) => a.machineId.localeCompare(b.machineId));
154
144
  }
155
145
  function readTail(log, from) {
156
- const restarted = from > log.size;
146
+ const end = log.baseOffset + log.size - log.headerBytes;
147
+ if (from < log.baseOffset)
148
+ throw new Error('Usage checkpoint is missing or older than the consolidated journal. Restore learning-usage.json or its .checkpoint backup; raw retrospectives cannot restore runtime offers.');
149
+ const restarted = from > end;
150
+ if (restarted && log.baseOffset > 0)
151
+ throw new Error('Usage journal is behind its checkpoint; restore matching files before continuing.');
157
152
  const start = restarted ? 0 : Math.max(0, from);
158
- const length = log.size - start;
153
+ const physicalStart = start - log.baseOffset + log.headerBytes;
154
+ const length = log.size - physicalStart;
159
155
  if (length <= 0) {
160
156
  return { ...log, records: [], skippedLines: 0, through: start, restarted };
161
157
  }
@@ -164,7 +160,7 @@ function readTail(log, from) {
164
160
  const fd = (0, fs_1.openSync)(log.path, 'r');
165
161
  try {
166
162
  const buffer = Buffer.alloc(length);
167
- const read = (0, fs_1.readSync)(fd, buffer, 0, length, start);
163
+ const read = (0, fs_1.readSync)(fd, buffer, 0, length, physicalStart);
168
164
  text = buffer.subarray(0, read).toString('utf8');
169
165
  }
170
166
  finally {
@@ -202,7 +198,7 @@ function readTail(log, from) {
202
198
  skippedLines += 1;
203
199
  }
204
200
  }
205
- return { ...log, records, skippedLines, through: log.size - partialBytes, restarted };
201
+ return { ...log, records, skippedLines, through: end - partialBytes, restarted };
206
202
  }
207
203
  /**
208
204
  * Read every log's tail past its checkpoint.
@@ -220,32 +216,42 @@ function readEventsSince(eventDir, checkpoints) {
220
216
  };
221
217
  }
222
218
  /**
223
- * Rewrite every log keeping only the records `keep` accepts.
224
- *
225
- * Required rather than an optimization: S4 measured a cold full fold growing
226
- * linearly to 794 ms at ten times today's volume, and the checkpoint only makes a
227
- * cold fold rare, not bounded.
228
- *
229
- * Published by writing a temp file and renaming it. That is safe here and not on
230
- * the snapshot (S3), because nothing holds a compacted log open for reading in
231
- * the way concurrent MCP processes hold the snapshot: this runs from the CLI, and
232
- * a failed rename leaves the original log untouched.
219
+ * Clear fully checkpointed logs whose records are all understood by this reader.
220
+ * Preserve unknown, malformed, partial or newly appended logs byte-for-byte.
221
+ * Flush then atomically replace each log with its logical-position header; the
222
+ * checkpoint saved before this call remains correct on either side of a crash.
233
223
  */
234
- function compactEventLogs(eventDir, keep) {
235
- const result = { scanned: 0, kept: 0, dropped: 0, skippedLines: 0, logs: [] };
224
+ function compactEventLogs(eventDir, keep, checkpoints) {
225
+ const result = { scanned: 0, kept: 0, dropped: 0, skippedLines: 0, logs: [], checkpoints: { ...checkpoints } };
236
226
  for (const log of listEventLogs(eventDir)) {
237
- const read = readTail(log, 0);
227
+ const read = readTail(log, log.baseOffset);
238
228
  result.scanned += read.records.length;
239
229
  result.skippedLines += read.skippedLines;
230
+ result.kept += read.records.length;
231
+ // The snapshot knows nothing about a later append or torn
232
+ // tail. Leave that log and its old checkpoint alone for the next fold.
233
+ if (read.through !== log.baseOffset + log.size - log.headerBytes
234
+ || read.through !== (checkpoints[log.machineId] ?? 0) || read.skippedLines > 0)
235
+ continue;
240
236
  const retained = read.records.filter(keep);
241
- result.kept += retained.length;
242
- result.dropped += read.records.length - retained.length;
243
- if (retained.length === read.records.length && read.skippedLines === 0)
237
+ // Unknown records may belong to a newer reader. Leave their whole log
238
+ // intact so their original positions remain meaningful.
239
+ if (retained.length > 0 || read.records.length === 0)
244
240
  continue;
245
241
  const temp = `${log.path}.compacting`;
246
242
  try {
247
- (0, fs_1.writeFileSync)(temp, retained.length ? `${retained.map((r) => JSON.stringify(r)).join('\n')}\n` : '', 'utf8');
243
+ const payload = `${JSON.stringify({ [CHECKPOINT_MARKER]: read.through })}\n`;
244
+ (0, fs_1.writeFileSync)(temp, payload, { encoding: 'utf8', flush: true });
245
+ if ((0, fs_1.statSync)(log.path).size !== log.size) {
246
+ (0, fs_1.rmSync)(temp, { force: true });
247
+ continue;
248
+ }
248
249
  (0, fs_1.renameSync)(temp, log.path);
250
+ const dropped = read.records.length - retained.length;
251
+ result.kept -= dropped;
252
+ result.dropped += dropped;
253
+ // The logical checkpoint remains unchanged across the atomic replacement.
254
+ result.checkpoints[log.machineId] = read.through;
249
255
  }
250
256
  catch {
251
257
  // The log is authoritative and the original is still intact, so a failed
@@ -15,6 +15,7 @@ exports.resolveCurrentModel = resolveCurrentModel;
15
15
  * Nothing here writes.
16
16
  */
17
17
  const learning_usage_store_1 = require("./learning-usage-store");
18
+ const learning_usage_eligibility_1 = require("./learning-usage-eligibility");
18
19
  function toLookupValue(record) {
19
20
  return {
20
21
  key: record.key,
@@ -24,7 +25,7 @@ function toLookupValue(record) {
24
25
  firedTotalAttestations: record.fired.total,
25
26
  lastFired: record.fired.lastFiredAt,
26
27
  lastOffered: record.offered.lastOfferedAt,
27
- firedUnder: Object.keys(record.fired.byModel),
28
+ firedUnder: (0, learning_usage_eligibility_1.modelAttributionIncomplete)(record) ? undefined : Object.keys(record.fired.byModel),
28
29
  byOutcome: record.fired.byOutcome,
29
30
  log: record.fired.log,
30
31
  };
@@ -36,7 +37,10 @@ function toLookupValue(record) {
36
37
  */
37
38
  function buildUsageLookup(store) {
38
39
  const lookup = new Map();
40
+ const excluded = (0, learning_usage_eligibility_1.usageEvidenceExclusions)(store);
39
41
  for (const record of Object.values(store.entries)) {
42
+ if (excluded.has(record.key))
43
+ continue;
40
44
  const value = toLookupValue(record);
41
45
  lookup.set(record.key, value);
42
46
  if (record.fileType) {