@gcunharodrigues/wrxn 0.7.0 → 0.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.7.0",
3
+ "version": "0.7.3",
4
4
  "description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
5
5
  "bin": {
6
6
  "wrxn": "bin/wrxn.cjs"
@@ -16,11 +16,19 @@
16
16
  "test": "node --test --require ./test/setup.cjs"
17
17
  },
18
18
  "dependencies": {
19
- "recon-wrxn": "6.0.0-wrxn.3"
19
+ "recon-wrxn": "6.0.0-wrxn.6"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/gcunharodrigues/wrxn-kernel.git"
27
+ },
28
+ "publishConfig": {
29
+ "provenance": true,
30
+ "access": "public"
31
+ },
24
32
  "license": "MIT",
25
33
  "private": false
26
34
  }
@@ -35,14 +35,17 @@ This skill **queries that set and reports it**. It is the third maintenance loop
35
35
  node .wrxn/sync.cjs report
36
36
  ```
37
37
 
38
- 2. **Read the JSON** it prints — `{ status, stale[], unwatermarked[] }`:
38
+ 2. **Read the JSON** it prints — `{ status, stale[], unwatermarked[], orphaned[] }`:
39
39
 
40
- - **`status: "synced"`** — the stale set is empty. **Say so briefly ("all synced") and stop.** Do not
41
- manufacture findings. A clean tree is a successful no-op.
40
+ - **`status: "synced"`** — both the stale AND orphaned sets are empty. **Say so briefly ("all synced") and
41
+ stop.** Do not manufacture findings. A clean tree is a successful no-op.
42
42
  - **`status: "drift"`** — present each `stale[]` entry to the operator: the **doc** page, the **symbol**
43
43
  that moved, and **`synced_to` → `current`** (the watermark vs the source's current fingerprint). If
44
- `unwatermarked[]` is non-empty, note those separatelydocs that declare `derived_from` but were
45
- never watermarked (so drift can't yet be computed for them).
44
+ `orphaned[]` is non-empty, flag those DISTINCTLYeach is a **dangling** doc (`doc` + `synced_to`) whose
45
+ `derived_from:` source symbol was **renamed or deleted**, so its provenance is gone and drift can no
46
+ longer be computed. The reconcile loop below cannot fix an orphan (there is no live source to re-stamp
47
+ against); surface it so the operator can re-anchor or retire the page. If `unwatermarked[]` is non-empty,
48
+ note those separately — docs that declare `derived_from` but were never watermarked.
46
49
  - **`status: "unavailable"`** — recon's serve door is not warm (no `recon-wrxn serve` running, or it was
47
50
  unreachable). Report "drift unavailable — start `recon-wrxn serve` and retry." Never treat this as
48
51
  "all synced": unknown is not clean.
package/payload/.mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "recon-wrxn": {
4
4
  "command": "npx",
5
- "args": ["-y", "recon-wrxn@6.0.0-wrxn.3", "serve"]
5
+ "args": ["-y", "recon-wrxn@6.0.0-wrxn.6", "serve"]
6
6
  }
7
7
  }
8
8
  }
@@ -50,6 +50,14 @@ const HARVEST_DIR = ['.wrxn', 'harvest'];
50
50
  const STAGED_FILE = 'staged.jsonl'; // the proposed-but-unconfirmed merges (survivor body + absorbed, by-reference).
51
51
  const AUDIT_FILE = 'audit.jsonl'; // append-only outcome log (stage + commit + decay events).
52
52
  const DECAY_STAGED_FILE = 'decay-staged.jsonl'; // harvest-04: proposed-but-unconfirmed decay annotations (by-reference). Distinct fixed name from merge's staged.jsonl; both non-.md so recon never recalls a staged-but-unconfirmed op.
53
+ // Bounded retention for the timestamped <ts>.jsonl check reports (phase-4.5-04). `check` writes a fresh
54
+ // report every run; without a cap the dir grows without bound on a long-lived install. Keep the N most-recent
55
+ // (ISO timestamps sort lexically = chronologically — no clock read). 20 is a generous trailing window of
56
+ // recent checks for trend/diff while strictly bounding growth; env override (clamped >= 1 so the just-written
57
+ // report is never pruned) mirrors synapse's WRXN_HANDOFF_PCT precedent. REPORT_RE matches ONLY a timestamped
58
+ // report — the fixed-name state files (staged/audit/decay-staged.jsonl) + .gitkeep never match, never prune.
59
+ const REPORT_RETAIN_DEFAULT = 20;
60
+ const REPORT_RE = /^\d{4}-\d{2}-\d{2}T.*\.jsonl$/;
53
61
  const BODY_MAX = 32000; // survivor body cap (chars) — a durable merged page, not a dump (dream/sync parity).
54
62
  const WIKI_REL = ['.wrxn', 'wiki']; // all merge targets confine under <root>/.wrxn/wiki/<knowledge-tier>/.
55
63
  const WIKI_PREFIX = '.wrxn/wiki/'; // stripped to form the reinforce.json wiki-rel join key (recall-surface parity).
@@ -296,6 +304,22 @@ function nearDupQualifies(hit) {
296
304
  return Number.isFinite(sem) && hasSemantic && sem >= NEAR_DUP_THRESHOLD;
297
305
  }
298
306
 
307
+ // Order clusters by a STABLE total order so an unchanged tree yields a byte-identical report every run
308
+ // (phase-4.5-04). Primary key = the lexically-first member. The old `(a,b) => a.members[0] < b.members[0]
309
+ // ? -1 : 1` returned 1 for BOTH compare(a,b) AND compare(b,a) on an equal leading member — non-antisymmetric,
310
+ // so V8's sort could resolve tied clusters by input permutation → non-reproducible reports. Clusters are
311
+ // disjoint connected components, so leading members never collide TODAY; the secondary keys make this a
312
+ // proper total order regardless (defense in depth): larger cluster first, then the stronger edge score, then
313
+ // the full member list — and two truly-identical clusters compare EQUAL (return 0).
314
+ function compareClusters(a, b) {
315
+ if (a.members[0] !== b.members[0]) return a.members[0] < b.members[0] ? -1 : 1;
316
+ if (a.members.length !== b.members.length) return b.members.length - a.members.length; // larger cluster first
317
+ if (a.score !== b.score) return b.score - a.score; // stronger dup signal first
318
+ const ja = a.members.join('');
319
+ const jb = b.members.join('');
320
+ return ja < jb ? -1 : ja > jb ? 1 : 0; // full member list; identical clusters → 0
321
+ }
322
+
299
323
  // Collapse pairwise near-dup edges into connected-component CLUSTERS (union-find), so a symmetric A↔B
300
324
  // match is reported once and a transitive A-B-C chain is one cluster of three. Each cluster carries its
301
325
  // sorted members + the STRONGEST edge similarity (the clearest dup signal). Singletons are not clusters.
@@ -333,7 +357,7 @@ function clusterNearDups(edges) {
333
357
  if (members.size < 2) continue;
334
358
  clusters.push({ members: [...members].sort(), score: Math.round((groupScore.get(r) || 0) * 1e4) / 1e4 });
335
359
  }
336
- return clusters.sort((a, b) => (a.members[0] < b.members[0] ? -1 : 1));
360
+ return clusters.sort(compareClusters);
337
361
  }
338
362
 
339
363
  // ── the door (IO shell, injectable transport) — the recall-surface.cjs contract ──
@@ -504,6 +528,41 @@ function reportPath(dir, ts) {
504
528
  return file;
505
529
  }
506
530
 
531
+ // The effective report-retention bound: the WRXN_HARVEST_RETAIN env override (clamped to a whole number >= 1
532
+ // so a bogus/zero value can never prune away the just-written report), else the sane default (phase-4.5-04).
533
+ function reportRetention() {
534
+ const env = Number(process.env.WRXN_HARVEST_RETAIN);
535
+ return Number.isFinite(env) && env >= 1 ? Math.floor(env) : REPORT_RETAIN_DEFAULT;
536
+ }
537
+
538
+ // Prune the timestamped check reports under .wrxn/harvest/ to the retention bound, keeping the `keep` most-
539
+ // recent (phase-4.5-04). ISO timestamps sort lexically = chronologically, so the oldest are the lexical
540
+ // prefix — no clock is read. ONLY <ts>.jsonl reports are eligible (REPORT_RE); the fixed-name state files
541
+ // (staged/audit/decay-staged.jsonl) + .gitkeep never match, so curation/merge/decay trails are never touched.
542
+ // Fail-soft: an unreadable dir or a failed unlink is swallowed — retention is hygiene, never the point of check.
543
+ function pruneReports(dir, keep, protect) {
544
+ let names;
545
+ try {
546
+ names = fs.readdirSync(dir);
547
+ } catch {
548
+ return; // no report dir yet → nothing to prune
549
+ }
550
+ const reports = names.filter((n) => REPORT_RE.test(n)).sort(); // lexical = chronological (oldest first)
551
+ // Delete the oldest down to the `keep` bound, but NEVER the just-written report (`protect`): a same-
552
+ // millisecond collision names it `<base>-N.jsonl`, which collates BEFORE its `<base>.jsonl` sibling, so a
553
+ // blind oldest-prefix prune could delete the fresh report `check` is about to return (phase-4.5-04 review).
554
+ let toDelete = reports.length - keep;
555
+ for (let i = 0; i < reports.length && toDelete > 0; i++) {
556
+ if (reports[i] === protect) continue; // the fresh report is retained and counts toward `keep`
557
+ try {
558
+ fs.unlinkSync(path.join(dir, reports[i]));
559
+ } catch {
560
+ /* fail-soft — a vanished/locked report never breaks the check run */
561
+ }
562
+ toDelete--;
563
+ }
564
+ }
565
+
507
566
  // ── check: the IO orchestrator ───────────────────────────────────────────────────
508
567
  // scanLocal (always) + nearDupFromDoor (degrades to unavailable when cold) → assemble → write the jsonl.
509
568
  // REPORT-ONLY: the ONLY write is the report under .wrxn/harvest/. `transport` is injected in tests.
@@ -520,6 +579,7 @@ async function check(root, { transport, timeoutMs } = {}) {
520
579
  const dir = harvestDir(root);
521
580
  const file = reportPath(dir, ts);
522
581
  fs.writeFileSync(file, records.length ? records.map((r) => JSON.stringify(r)).join('\n') + '\n' : '');
582
+ pruneReports(dir, reportRetention(), path.basename(file)); // phase-4.5-04: bound the report dir, never pruning the fresh report
523
583
  const nearDupCount = near.status === 'unavailable' ? 0 : near.clusters.length;
524
584
  return {
525
585
  report: path.relative(root, file),
@@ -1154,7 +1214,10 @@ module.exports = {
1154
1214
  scanLocal,
1155
1215
  isProse,
1156
1216
  nearDupQualifies,
1217
+ compareClusters,
1157
1218
  clusterNearDups,
1219
+ pruneReports,
1220
+ reportRetention,
1158
1221
  assembleRecords,
1159
1222
  tierOfPath,
1160
1223
  isHarvestPath,
@@ -1187,4 +1250,5 @@ module.exports = {
1187
1250
  NEAR_DUP_THRESHOLD,
1188
1251
  FIND_PATH,
1189
1252
  REINFORCE_WINDOW_DAYS,
1253
+ REPORT_RETAIN_DEFAULT,
1190
1254
  };
@@ -15,10 +15,12 @@
15
15
  //
16
16
  // Subcommand:
17
17
  // report query recon_drift over the serve door and print the drift summary JSON:
18
- // { status, stale[], unwatermarked[] }
19
- // · status "drift" — at least one doc is stale (stale[] names doc/symbol/synced_to/current).
20
- // · status "synced" — the warm door computed an EMPTY stale set ("all synced"; AC3 no-op,
21
- // never manufactures stale rows).
18
+ // { status, stale[], unwatermarked[], orphaned[] }
19
+ // · status "drift" — at least one doc is stale OR orphaned. stale[] names doc/symbol/
20
+ // synced_to/current; orphaned[] (phase-4.5-02) names a doc whose
21
+ // derived_from source symbol is GONE (doc/synced_to, no symbol/current).
22
+ // · status "synced" — the warm door computed an EMPTY stale AND orphaned set ("all synced";
23
+ // AC3 no-op, never manufactures rows).
22
24
  // · status "unavailable" — recon is unreachable OR answered without the structured drift sidecar
23
25
  // (no warm door, a timeout, a non-200, a malformed body, or a 200 whose
24
26
  // body lacks an affirmative `drift.stale` array — unknown is not clean,
@@ -117,11 +119,18 @@ function summarizeDrift(parsed) {
117
119
  if (!isEntry(d) || !Array.isArray(d.stale)) return unavailable();
118
120
  const stale = d.stale.filter(isEntry).map(normEntry);
119
121
  const unwatermarked = (Array.isArray(d.unwatermarked) ? d.unwatermarked : []).filter(isEntry).map(normEntry);
120
- return { status: stale.length ? 'drift' : 'synced', stale, unwatermarked };
122
+ // phase-4.5-02: the third drift class a watermarked page whose derived_from source symbol is GONE from
123
+ // recon's graph (renamed/deleted). Each entry normalizes to { doc, synced_to } (no symbol/current — the
124
+ // source is absent). It is DISTINCT from stale (source moved) and unwatermarked (never reconciled), but
125
+ // like stale it MUST elevate the status off a clean "synced": a dangling page is the exact case sync used
126
+ // to hide (the operator never learned the page was un-reconcilable). An older recon with no drift.orphaned
127
+ // degrades to [] (back-compatible, never throws).
128
+ const orphaned = (Array.isArray(d.orphaned) ? d.orphaned : []).filter(isEntry).map(normEntry);
129
+ return { status: stale.length || orphaned.length ? 'drift' : 'synced', stale, unwatermarked, orphaned };
121
130
  }
122
131
 
123
132
  function unavailable() {
124
- return { status: 'unavailable', stale: [], unwatermarked: [] };
133
+ return { status: 'unavailable', stale: [], unwatermarked: [], orphaned: [] };
125
134
  }
126
135
 
127
136
  // ── the door (IO shell, injectable transport) — the recall-surface.cjs contract ─