@lorekit/cli 1.33.4 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -381,7 +381,10 @@ lorekit lint --json # { total, offline, remote } structured findings
381
381
 
382
382
  Rules: **empty-value** (blank/whitespace-only body), **short-value** (a non-empty
383
383
  body below a small length threshold), **untrimmed-value** (real content with
384
- surrounding whitespace), **empty-key** (blank key), and **malformed-scope** (e.g.
384
+ surrounding whitespace), **empty-key** (blank key), **volatile-key** (the key
385
+ carries a per-sighting identifier — a run of 6+ digits such as a GitHub comment
386
+ id, or a `pr<n>` / `issue<n>` segment — so it never collides, never dedups, and
387
+ freezes `seen_count` at 1), and **malformed-scope** (e.g.
385
388
  a single `:` where `::` is expected). `lint` **exits non-zero (1) when any issue
386
389
  is found**, so it is usable as a CI gate (`lorekit lint || exit 1`); a clean run —
387
390
  or one where only a store is unavailable — exits 0. The pure rule predicates live
package/bin/lorekit.mjs CHANGED
@@ -78,8 +78,9 @@ ${c.bold('Commands')}
78
78
  (resolve) hierarchy and mark, per key, which scope's memory WINS and which are
79
79
  shadowed — the real hook-resolution order. --json, --scope <s>.
80
80
  lint Flag low-quality memories (empty/short/untrimmed value, empty key,
81
- malformed scope) across the applicable scopes and both stores. Exits
82
- non-zero when issues are found (CI gate). --json, --scope <s>.
81
+ volatile key, malformed scope) across the applicable scopes and
82
+ both stores. Exits non-zero when issues are found (CI gate).
83
+ --json, --scope <s>.
83
84
  dedupe Find likely-duplicate memories via a zero-dep word-overlap HEURISTIC
84
85
  (Jaccard >= threshold, not semantic), grouped into clusters per
85
86
  store. --json, --scope <s>, --threshold <0..1>.
@@ -495,10 +496,12 @@ ${c.bold('Usage')}
495
496
 
496
497
  Checks every memory for the current directory's scopes (project/branch/repo/
497
498
  global), across both stores, against a small set of quality rules: empty or
498
- whitespace-only value, suspiciously short value, untrimmed value, empty key, and
499
- malformed scope (e.g. a single \`:\` where \`::\` is expected). Each finding names
500
- the rule it violated. Exits NON-ZERO when any issue is found, so it works as a CI
501
- gate; a clean run or one where only a store is unavailable — exits 0.
499
+ whitespace-only value, suspiciously short value, untrimmed value, empty key, a
500
+ volatile per-sighting identifier in the key (a run of 6+ digits, or a \`pr<n>\` /
501
+ \`issue<n>\` reference), and malformed scope (e.g. a single \`:\` where \`::\` is
502
+ expected). Each finding names the rule it violated. Exits NON-ZERO when any issue
503
+ is found, so it works as a CI gate; a clean run — or one where only a store is
504
+ unavailable — exits 0.
502
505
 
503
506
  ${c.bold('Options')}
504
507
  -d, --dir <path> Target project root (default: current directory)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.33.4",
3
+ "version": "1.34.0",
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": {
@@ -89,8 +89,8 @@ The short version is six phases; the first four are pure analysis.
89
89
  `lorekit scopes` (store-wide inventory: every scope with its lesson count)
90
90
  show where the mass is. Pick the noisiest scope to start.
91
91
  2. **Lint** — `lorekit lint --json` flags structurally bad lessons (empty /
92
- whitespace / suspiciously short / untrimmed values, empty keys, malformed
93
- scopes). These are the cheapest wins: fix or drop them first.
92
+ whitespace / suspiciously short / untrimmed values, empty keys, volatile
93
+ keys, malformed scopes). These are the cheapest wins: fix or drop them first.
94
94
  3. **Dedupe** — `lorekit dedupe --json` clusters near-duplicate lessons. Start at
95
95
  a high `--threshold` (e.g. `0.85`) for confident duplicates, then lower it to
96
96
  surface looser paraphrases. Use `lorekit show <scope::key>` to read each
@@ -38,6 +38,11 @@ Findings are structural, not semantic — each names its rule:
38
38
  - **empty-value / short-value / untrimmed-value** — the lesson carries little or
39
39
  no signal, or has stray leading/trailing whitespace.
40
40
  - **empty-key** — no key to address it by.
41
+ - **volatile-key** — the key carries a per-sighting identifier (a run of 6+
42
+ digits such as a GitHub comment id, or a `pr<n>` / `issue<n>` segment), so it
43
+ never collides with a later write, never dedups, and leaves `seen_count`
44
+ frozen at 1. Re-key it onto the structural pattern and move the identifier
45
+ into the body.
41
46
  - **malformed-scope** — the scope string is invalid.
42
47
 
43
48
  These are the cheapest wins and the least controversial, so clear them first.
@@ -271,6 +271,41 @@ export const LINT_RULES = {
271
271
  return v.trim() && v !== v.trim() ? 'value has leading/trailing whitespace' : null;
272
272
  },
273
273
  'empty-key': (e) => (String(e.key ?? '').trim() ? null : 'key is empty or whitespace-only'),
274
+ // A key carrying a per-sighting identifier (a comment id, a PR/issue number) is
275
+ // unique forever, so it never collides, so the upsert never dedups it, so
276
+ // `seen_count` stays frozen at 1 and the memory can never reach a recurrence
277
+ // threshold — a write-only record. Detection is deliberately conservative:
278
+ // • a run of 6+ digits (a GitHub comment id is ~10; `sha256`, `oauth2`,
279
+ // `wcag22`, and semantic versions are all shorter runs);
280
+ // • a `pr<n>` / `issue<n>` reference — the number joined by nothing, `-`, or
281
+ // `_` — delimited by `:`, `-`, `_`, `/`, or a string boundary, so mid-word
282
+ // digits (`oauth2`) never match.
283
+ // `volatileKeyAllow` is an embedder/test knob mirroring `short-value`'s
284
+ // `minValueLen` precedent — a list of substrings that exempt a key. There is
285
+ // no config key and no per-entry marker.
286
+ 'volatile-key': (e, { volatileKeyAllow = [] } = {}) => {
287
+ const key = String(e.key ?? '');
288
+ if (!key.trim()) return null; // an empty key is `empty-key`'s to report.
289
+ // Tolerate a bare string as well as a list, so a caller passing
290
+ // `{ volatileKeyAllow: 'lorekit-231' }` does not silently iterate characters.
291
+ const allowList = Array.isArray(volatileKeyAllow) ? volatileKeyAllow : [volatileKeyAllow];
292
+ for (const allow of allowList) {
293
+ if (allow && key.includes(String(allow))) return null;
294
+ }
295
+ const digitRun = key.match(/\d{6,}/);
296
+ if (digitRun) {
297
+ return `key contains a volatile per-sighting identifier: '${digitRun[0]}' (a run of ${digitRun[0].length} digits)`;
298
+ }
299
+ // Boundary-anchored rather than split-then-match: splitting on `-` would
300
+ // separate `pr` from `231` and `pr-231` would slip through. The reference
301
+ // must start at a boundary (`:`, `-`, `_`, `/`, or the string start) and end
302
+ // at one, so `oauth2`/`sha256`/`wcag22` still never match.
303
+ const reference = key.match(/(?:^|[:\-_/])((?:pr|issue)[-_]?\d+)(?=$|[:\-_/])/i);
304
+ if (reference) {
305
+ return `key contains a volatile per-sighting identifier: '${reference[1]}' (a pr/issue number segment)`;
306
+ }
307
+ return null;
308
+ },
274
309
  'malformed-scope': (e) => {
275
310
  const reason = scopeIssue(e.scope);
276
311
  return reason ? `malformed scope: ${reason}` : null;
package/src/lint.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  // `lorekit lint` — flag low-quality lessons across the applicable scopes and
2
2
  // both stores. Each finding names the rule it violated (empty/whitespace value,
3
- // suspiciously short value, untrimmed value, empty key, malformed scope). The
4
- // rules are pure predicates in `lessons-view.mjs` (`LINT_RULES` / `lintEntry`),
5
- // each independently unit-tested.
3
+ // suspiciously short value, untrimmed value, empty key, volatile key, malformed
4
+ // scope). The rules are pure predicates in `lessons-view.mjs` (`LINT_RULES` /
5
+ // `lintEntry`), each independently unit-tested.
6
6
  //
7
7
  // Exit convention: `lint` exits NON-ZERO (1) when any finding exists, so it is
8
8
  // usable as a CI gate (`lorekit lint || fail`); a clean run — or a run where the