@lorekit/cli 1.39.1 → 1.39.2

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/telemetry.mjs +80 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.39.1",
3
+ "version": "1.39.2",
4
4
  "description": "Install the LoreKit shared-memory skill and run health checks for the LoreKit MCP server.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/telemetry.mjs CHANGED
@@ -39,6 +39,9 @@ const DEFAULT_DATASET = 'default';
39
39
  // Flags worth counting (e.g. how many installs are --global). Bounded on
40
40
  // purpose: only these booleans are ever attached, never free-form values.
41
41
  const FLAG_ATTRS = ['global', 'project', 'deep', 'yes', 'force', 'no-hooks', 'json', 'link'];
42
+ // One definition, used both to WRITE a flag attribute and to recognise one as
43
+ // reserved in `commandAttributes` — the two must not be able to drift.
44
+ const FLAG_ATTR_PREFIX = 'lorekit.cli.flag.';
42
45
 
43
46
  const OFF_VALUES = new Set(['0', 'off', 'false', 'no', 'disable', 'disabled']);
44
47
 
@@ -245,17 +248,87 @@ function resourceAttributes(version, env = process.env) {
245
248
 
246
249
  // ── Payload builders (pure — unit-tested) ─────────────────────────────────────
247
250
 
251
+ /**
252
+ * The CLOSED vocabulary of `lorekit.cli.outcome`, and the one place it is
253
+ * written down in code.
254
+ *
255
+ * The three values are not synonyms and the distinction is load-bearing:
256
+ *
257
+ * - `ok` — ran, exit 0.
258
+ * - `failure` — RAN TO COMPLETION and reported a negative VERDICT (a failing
259
+ * `doctor` check, a `lint` finding). The command did its job.
260
+ * - `error` — CRASHED. This is the only one that also sets the span status to
261
+ * `STATUS_CODE_ERROR`.
262
+ *
263
+ * That is what keeps the `cli` service's error rate a measure of the CLI being
264
+ * broken rather than of the user's environment being unhealthy (see the note
265
+ * above the non-zero-exit branch in {@link traceCommand}).
266
+ *
267
+ * WHY A FROZEN CONSTANT RATHER THAN THREE STRING LITERALS. The values were only
268
+ * ever written inline, so the vocabulary was discoverable from the emitted
269
+ * telemetry and nowhere else — and read from telemetry alone the distinction is
270
+ * genuinely easy to misread. A `doctor` that CRASHED in one release and FAILED
271
+ * GRACEFULLY in the next shows up as `error` then `failure` for the same
272
+ * user-visible symptom, which reads like the attribute drifting when it is
273
+ * actually the CLI getting better. Naming the set makes the difference legible
274
+ * at the call site and gives `telemetry.test.mjs` something to pin the docs to.
275
+ */
276
+ export const CLI_OUTCOMES = Object.freeze({
277
+ OK: 'ok',
278
+ FAILURE: 'failure',
279
+ ERROR: 'error',
280
+ });
281
+
282
+ /** The same vocabulary as a value list, for guards and exhaustiveness checks. */
283
+ export const CLI_OUTCOME_VALUES = Object.freeze(Object.values(CLI_OUTCOMES));
284
+
285
+ /** The attribute keys `commandAttributes` owns — see its docblock below. */
286
+ const isReservedAttr = (key) =>
287
+ key === 'lorekit.cli.command' ||
288
+ key === 'lorekit.cli.outcome' ||
289
+ key === 'lorekit.cli.exit_code' ||
290
+ key.startsWith(FLAG_ATTR_PREFIX);
291
+
248
292
  /**
249
293
  * Collect the bounded, non-PII attributes for a command invocation. Only the
250
294
  * command name, allow-listed boolean flags, the outcome and the exit code.
295
+ *
296
+ * Deliberately does NOT validate `outcome`: this runs inside the `finally` of
297
+ * every traced command, where throwing would turn a telemetry problem into a
298
+ * command failure. The vocabulary is enforced at the call sites (all of which
299
+ * are in this file) and pinned by `telemetry.test.mjs`.
300
+ *
301
+ * The keys this function owns — command, outcome, exit code, flags — are a
302
+ * RESERVED NAMESPACE: an `extraAttrs` entry under one of them is dropped, and
303
+ * the owned value (if any) is written afterwards. `extraAttrs` used to be
304
+ * merged over last, which meant a command returning
305
+ * `{ exitCode, 'lorekit.cli.outcome': … }` silently replaced the frozen value on
306
+ * its way out — a runtime path the source scan cannot see, because it proves
307
+ * the literal at the call site and not that the value reaches the wire.
308
+ *
309
+ * Reserving the NAMESPACE rather than just overwriting key by key matters
310
+ * because two of the owned keys are written conditionally: `exit_code` only
311
+ * when `exitCode` is a number, and each flag only when it is truthy. Overwriting
312
+ * alone therefore left the gap open in exactly the cases where the CLI emits
313
+ * nothing — an extras value would have been the only `lorekit.cli.exit_code` on
314
+ * the span, sourced from the command rather than from here.
315
+ *
316
+ * A collision is dropped, not rejected: this runs inside the `finally` of every
317
+ * traced command, where throwing would turn a telemetry problem into a command
318
+ * failure. Losing a datum a command should not have put there is the smaller
319
+ * harm than emitting an unowned value under an owned key.
251
320
  */
252
321
  export function commandAttributes({ command, args = {}, outcome, exitCode, extraAttrs = {} }) {
253
- const attrs = { 'lorekit.cli.command': command, 'lorekit.cli.outcome': outcome };
322
+ const attrs = {};
323
+ for (const [key, value] of Object.entries(extraAttrs)) {
324
+ if (!isReservedAttr(key)) attrs[key] = value;
325
+ }
326
+ attrs['lorekit.cli.command'] = command;
327
+ attrs['lorekit.cli.outcome'] = outcome;
254
328
  if (typeof exitCode === 'number') attrs['lorekit.cli.exit_code'] = exitCode;
255
329
  for (const flag of FLAG_ATTRS) {
256
- if (args[flag]) attrs[`lorekit.cli.flag.${flag}`] = true;
330
+ if (args[flag]) attrs[`${FLAG_ATTR_PREFIX}${flag}`] = true;
257
331
  }
258
- Object.assign(attrs, extraAttrs);
259
332
  return attrs;
260
333
  }
261
334
 
@@ -389,7 +462,7 @@ export async function probeTelemetryExport(config, { version = '0.0.0', timeoutM
389
462
  name: 'lorekit.cli.doctor.telemetry_probe',
390
463
  attributes: {
391
464
  'lorekit.cli.command': 'doctor',
392
- 'lorekit.cli.outcome': 'ok',
465
+ 'lorekit.cli.outcome': CLI_OUTCOMES.OK,
393
466
  'lorekit.telemetry.probe': true,
394
467
  },
395
468
  startMs: now,
@@ -533,7 +606,7 @@ export async function traceCommand(command, args, version, run) {
533
606
  // `outcome` is the command's VERDICT (ok | failure | error); `status` is the
534
607
  // SPAN status, and only a crash sets it to error. See the note above the
535
608
  // non-zero-exit branch below.
536
- let outcome = 'ok';
609
+ let outcome = CLI_OUTCOMES.OK;
537
610
  let status = 'ok';
538
611
  let statusMessage;
539
612
  let extraAttrs = {};
@@ -563,11 +636,11 @@ export async function traceCommand(command, args, version, run) {
563
636
  // the CLI being broken rather than of the user's environment being
564
637
  // unhealthy. Query the failure verdicts on those attributes, never on the
565
638
  // span status.
566
- outcome = 'failure';
639
+ outcome = CLI_OUTCOMES.FAILURE;
567
640
  }
568
641
  return exitCode;
569
642
  } catch (e) {
570
- outcome = 'error';
643
+ outcome = CLI_OUTCOMES.ERROR;
571
644
  status = 'error';
572
645
  // Record only a bounded, non-PII identifier — NEVER e.message. Node fs /
573
646
  // network error messages embed absolute paths (e.g. "ENOENT: ... open