ruvnet-brain 4.3.21 → 4.3.26
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 +5 -5
- package/bin/install.mjs +275 -60
- package/console/app.js +189 -9
- package/console/index.html +70 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/style.css +26 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +364 -28
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +47 -10
- package/scripts/release-transaction.mjs +40 -11
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -33,6 +33,7 @@ import { planFor } from './remedy-registry.mjs';
|
|
|
33
33
|
import { auditAll as capabilityAuditAll } from './capability-registry.mjs';
|
|
34
34
|
import { getVersion } from './version.mjs';
|
|
35
35
|
import { consoleRuntimeDigest } from './console-runtime-identity.mjs';
|
|
36
|
+
import { isCorpusReleaseTag } from '../kb/corpus-release-identity.mjs';
|
|
36
37
|
// L5 (ADR-028): the audit is the one place that observes live capability state, so it is where an
|
|
37
38
|
// OFFERED-then-now-`on` transition becomes an APPLIED — the numerator of the precision metric that
|
|
38
39
|
// tells the owner whether advocacy is landing or nagging. Both are pure reads/appends and never throw.
|
|
@@ -422,6 +423,27 @@ function wiringSurvey() {
|
|
|
422
423
|
if (mech) sites.push({ scope: 'project', project: projName, file: '.mcp.json', event: 'MCP', matcher: name, spec: full.slice(0, 160), mechanism: mech });
|
|
423
424
|
}
|
|
424
425
|
}
|
|
426
|
+
// THE MACHINE-WIDE FILE (console audit 2026-09-11). Plugins are switched on in ~/.claude/settings.json
|
|
427
|
+
// and nowhere else, and this survey never read that file — so the card said `plugin: 0` on a machine
|
|
428
|
+
// with 14 enabled, ruvnet-brain among them. Each enabled plugin is one global PLUGIN site. The file's
|
|
429
|
+
// own hooks go through the same classifier the project scan uses, so a machine-wide npx or
|
|
430
|
+
// global-binary hook is counted in the same words as a project one.
|
|
431
|
+
const machine = readJSON(path.join(CONSOLE_ROOT, '.claude', 'settings.json'));
|
|
432
|
+
if (machine) {
|
|
433
|
+
for (const [event, groups] of Object.entries(machine.hooks || {})) {
|
|
434
|
+
const list = Array.isArray(groups) ? groups : [groups];
|
|
435
|
+
for (const g of list) {
|
|
436
|
+
const hookArr = Array.isArray(g?.hooks) ? g.hooks : (g?.command ? [g] : []);
|
|
437
|
+
for (const h of hookArr) {
|
|
438
|
+
const mech = classifyCommand(h?.command);
|
|
439
|
+
if (mech) sites.push({ scope: 'global', file: '~/.claude/settings.json', event, matcher: g?.matcher ?? '*', spec: String(h.command).slice(0, 160), mechanism: mech });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const [name, on] of Object.entries(machine.enabledPlugins || {})) {
|
|
444
|
+
if (on === true) sites.push({ scope: 'global', file: '~/.claude/settings.json', event: 'plugin', matcher: name, spec: name, mechanism: 'PLUGIN' });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
425
447
|
return { sites, summary: summarizeWiring(sites) };
|
|
426
448
|
}
|
|
427
449
|
|
|
@@ -444,25 +466,41 @@ function sessionHookExists() {
|
|
|
444
466
|
//
|
|
445
467
|
// So the probe stops guessing and asks the filesystem: consider both known names, and use whichever
|
|
446
468
|
// actually holds rows. That is correct on their machine and on mine without knowing why they differ.
|
|
469
|
+
//
|
|
470
|
+
// THE SECOND HALF OF THAT LESSON (console audit 2026-09-11). "Whichever holds rows" was implemented
|
|
471
|
+
// as "whichever is LARGER", and on a machine where BOTH hold rows that picked the wrong one: the
|
|
472
|
+
// card scored ruvnet-brain on `.swarm/agentdb-memory.db` — 15,824 rows, the MCP coordination store
|
|
473
|
+
// the global CLAUDE.md warns is a DIFFERENT container — and rendered "15824 entries, 100% embedded"
|
|
474
|
+
// while the canonical `.swarm/memory.db`, the file `ruflo memory store` writes, held 5. The #127
|
|
475
|
+
// reporter's cases (canonical absent, or present but empty) still resolve to the coordination store;
|
|
476
|
+
// a canonical store that holds rows is now always the one scored, and BOTH files are reported so the
|
|
477
|
+
// page can say which it scored and which it did not.
|
|
447
478
|
const MEMORY_DB_NAMES = ['memory.db', 'agentdb-memory.db'];
|
|
479
|
+
// Row count that never throws and never hangs on a live store: null means "unreadable this instant".
|
|
480
|
+
function memoryRows(file) {
|
|
481
|
+
if (!fs.existsSync(file)) return null;
|
|
482
|
+
const r = robustRead(file, 'SELECT count(*) FROM memory_entries;');
|
|
483
|
+
return r.ok ? (r.value === null ? 0 : parseInt(r.value, 10)) : null;
|
|
484
|
+
}
|
|
485
|
+
export function memoryStores(projectDir) {
|
|
486
|
+
const [canonicalPath, coordinationPath] = MEMORY_DB_NAMES.map((name) => path.join(projectDir, '.swarm', name));
|
|
487
|
+
const canonical = { path: canonicalPath, exists: fs.existsSync(canonicalPath), rows: memoryRows(canonicalPath) };
|
|
488
|
+
const coordination = { path: coordinationPath, exists: fs.existsSync(coordinationPath), rows: memoryRows(coordinationPath) };
|
|
489
|
+
// A canonical store that holds rows — or is busy being written this instant (rows null) — is the
|
|
490
|
+
// one scored. Only an absent or EMPTY canonical store hands the score to the coordination file.
|
|
491
|
+
const scored = canonical.exists && (canonical.rows === null || canonical.rows > 0) ? canonical.path
|
|
492
|
+
: coordination.exists ? coordination.path
|
|
493
|
+
: canonical.path; // canonical name for the "absent" message
|
|
494
|
+
return { canonical, coordination, scored };
|
|
495
|
+
}
|
|
448
496
|
export function resolveMemoryDb(projectDir) {
|
|
449
|
-
|
|
450
|
-
.map((name) => path.join(projectDir, '.swarm', name))
|
|
451
|
-
.filter((file) => fs.existsSync(file));
|
|
452
|
-
if (!candidates.length) return path.join(projectDir, '.swarm/memory.db'); // canonical name for the "absent" message
|
|
453
|
-
if (candidates.length === 1) return candidates[0];
|
|
454
|
-
// Both present: prefer the one with real content. Size is a proxy for rows that costs no query and
|
|
455
|
-
// cannot throw on a locked or WAL-mode database — this runs inside a UI probe that must never hang.
|
|
456
|
-
return candidates.sort((a, b) => {
|
|
457
|
-
const sa = (() => { try { return fs.statSync(a).size; } catch { return 0; } })();
|
|
458
|
-
const sb = (() => { try { return fs.statSync(b).size; } catch { return 0; } })();
|
|
459
|
-
return sb - sa;
|
|
460
|
-
})[0];
|
|
497
|
+
return memoryStores(projectDir).scored;
|
|
461
498
|
}
|
|
462
499
|
|
|
463
500
|
function probeMemory(projectDir, { now = Date.now() } = {}) {
|
|
464
|
-
const
|
|
465
|
-
const
|
|
501
|
+
const stores = memoryStores(projectDir);
|
|
502
|
+
const db = stores.scored;
|
|
503
|
+
const probes = { stores };
|
|
466
504
|
// compaction survival + session surfacing are filesystem facts, always checkable
|
|
467
505
|
const snapshot = inspectSessionSnapshots(projectDir, { now });
|
|
468
506
|
const snapshotDetail = snapshot.kind === 'canonical'
|
|
@@ -503,8 +541,10 @@ function probeMemory(projectDir, { now = Date.now() } = {}) {
|
|
|
503
541
|
const liveNote = integ.mode === 'immutable=1' ? ' and in active use' : '';
|
|
504
542
|
if (integrity !== 'ok') probes.liveness = { status: 'fail', detail: `store is corrupt (integrity_check: ${integrity})` };
|
|
505
543
|
else if (total === null) probes.liveness = { status: 'notTested', detail: 'store opened but counts were unavailable this instant' };
|
|
506
|
-
|
|
507
|
-
|
|
544
|
+
// The file is NAMED in the sentence, so "15824 entries" can never again be read as the canonical
|
|
545
|
+
// store's count when it was the coordination store's.
|
|
546
|
+
else if (total > 0) probes.liveness = { status: 'ok', detail: `${path.basename(db)} is live${liveNote}, integrity ok, ${total} entries${embedded != null && total ? `, ${Math.round((embedded / total) * 100)}% embedded` : ''} (read-only)` };
|
|
547
|
+
else probes.liveness = { status: 'warn', detail: `${path.basename(db)} exists but is empty` };
|
|
508
548
|
|
|
509
549
|
const cp = robustRead(db, "SELECT max(updated_at) FROM memory_entries WHERE key LIKE 'project-state-current%';");
|
|
510
550
|
if (cp.ok && cp.value) {
|
|
@@ -542,12 +582,20 @@ function gatherMemory(cwd, { fleet = true } = {}) {
|
|
|
542
582
|
const scope = projectDirectory({ cwd });
|
|
543
583
|
const project = fs.existsSync(path.join(scope, '.swarm/memory.db')) ? scope : REPO;
|
|
544
584
|
const projName = project.replace(CONSOLE_ROOT + '/Code/', '').replace(CONSOLE_ROOT + '/', '~/');
|
|
545
|
-
const
|
|
585
|
+
const probes = probeMemory(project);
|
|
586
|
+
const health = scoreMemoryHealth({ project: projName, probes });
|
|
587
|
+
// Which file the score is about, and which file it deliberately is not — both with row counts.
|
|
588
|
+
const short = (p) => String(p).replace(CONSOLE_ROOT, '~');
|
|
589
|
+
health.stores = probes.stores ? {
|
|
590
|
+
scored: short(probes.stores.scored),
|
|
591
|
+
canonical: { ...probes.stores.canonical, path: short(probes.stores.canonical.path) },
|
|
592
|
+
coordination: { ...probes.stores.coordination, path: short(probes.stores.coordination.path) },
|
|
593
|
+
} : null;
|
|
546
594
|
return { fleet: fleet ? scanFleet() : null, health };
|
|
547
595
|
}
|
|
548
596
|
|
|
549
597
|
// ── Savings ledger (receipts only) ────────────────────────────────────────────────────────────────
|
|
550
|
-
function gatherSavings() {
|
|
598
|
+
function gatherSavings({ repo = REPO } = {}) {
|
|
551
599
|
// Primary source is the real routing-receipts ledger written by scripts/route-cheap.mjs.
|
|
552
600
|
const files = [
|
|
553
601
|
path.join(SYSTEM_HOME, '.claude/metaharness/routing-receipts.jsonl'),
|
|
@@ -555,7 +603,9 @@ function gatherSavings() {
|
|
|
555
603
|
// Canonical user-level ledger (issue #36 — the hooks no longer scatter per-CWD copies).
|
|
556
604
|
path.join(SYSTEM_HOME, '.cache/ruvnet-brain/token-ledger.jsonl'),
|
|
557
605
|
// Legacy location, still read so an existing user's history is not orphaned by the move.
|
|
558
|
-
|
|
606
|
+
// `repo` is injectable because on a developer checkout this file is REAL history (2 rows from
|
|
607
|
+
// 2026-07-14 on the owner's machine) and HOME isolation alone cannot keep it out of a fixture.
|
|
608
|
+
path.join(repo, 'plugin/scripts/.ruvnet-brain/token-ledger.jsonl'),
|
|
559
609
|
];
|
|
560
610
|
const receipts = [];
|
|
561
611
|
let baselineUsd = 0;
|
|
@@ -591,10 +641,16 @@ function gatherSavings() {
|
|
|
591
641
|
}
|
|
592
642
|
}
|
|
593
643
|
const usdSaved = +receipts.reduce((a, r) => a + (r.measuredUsd || 0), 0).toFixed(4);
|
|
644
|
+
const timed = receipts.filter((r) => r.measuredMs !== null);
|
|
594
645
|
const totals = receipts.length ? {
|
|
595
646
|
count: receipts.length,
|
|
596
647
|
usdSaved,
|
|
648
|
+
// SIGNED, and it stays signed. A net-slower ledger sums negative and is REPORTED negative — the
|
|
649
|
+
// page once turned that into a dash (console audit 2026-09-11: −46,737 ms across 25 timed
|
|
650
|
+
// routes, 23 of them slower than baseline, rendered as "—"). A loss is a finding about the
|
|
651
|
+
// router; `timedCount` says how many routes the number is even about.
|
|
597
652
|
msSaved: receipts.reduce((a, r) => a + (r.measuredMs || 0), 0),
|
|
653
|
+
timedCount: timed.length,
|
|
598
654
|
baselineUsd: +baselineUsd.toFixed(4),
|
|
599
655
|
pctSaved: baselineUsd > 0 ? Math.round((usdSaved / baselineUsd) * 100) : null,
|
|
600
656
|
} : null;
|
|
@@ -643,11 +699,86 @@ function unsupportedConfigControls() {
|
|
|
643
699
|
* off, and the Settings form shows the shipped default beside it as a recommendation rather than as
|
|
644
700
|
* a fact about their machine.
|
|
645
701
|
*/
|
|
702
|
+
/**
|
|
703
|
+
* NIGHTLY REFRESH IS THREE FACTS, NOT ONE BOOLEAN (console audit 2026-09-11). The toggle rendered
|
|
704
|
+
* `nightly: true` because the LaunchAgent was loaded, while ~/.claude/ruvnet-brain/config.json said
|
|
705
|
+
* `nightly: false` and the job had never completed a run. Three independently measured things —
|
|
706
|
+
* the CHOICE the user saved (config.json), the ENFORCEMENT the scheduler adapter reports (loaded,
|
|
707
|
+
* off, degraded), and the OUTCOME of the last completed run (its receipt) — had been collapsed into
|
|
708
|
+
* the one the user did not set. A single boolean is emitted only when choice and enforcement agree;
|
|
709
|
+
* otherwise the field is null (never chosen, or in disagreement) and `facts` carries all three so
|
|
710
|
+
* the page can print them side by side. Pure, so the rule is testable without a scheduler.
|
|
711
|
+
*/
|
|
712
|
+
export function reconcileNightly({ choice, schedule }) {
|
|
713
|
+
const enforcement = schedule?.state ?? 'unknown';
|
|
714
|
+
const run = schedule?.runHealth ?? null;
|
|
715
|
+
const enforced = enforcement === 'on' ? true : enforcement === 'off' ? false : null;
|
|
716
|
+
const chosen = choice === true || choice === false ? choice : null;
|
|
717
|
+
const agree = chosen === null ? null : (enforced !== null && chosen === enforced);
|
|
718
|
+
return {
|
|
719
|
+
value: agree === true ? chosen : null,
|
|
720
|
+
facts: {
|
|
721
|
+
choice: chosen,
|
|
722
|
+
enforcement,
|
|
723
|
+
enforcementEvidence: schedule?.evidence ?? null,
|
|
724
|
+
lastRun: run?.state ?? 'unknown',
|
|
725
|
+
lastRunEvidence: run?.evidence ?? null,
|
|
726
|
+
lastRunAt: run?.receipt?.finishedAt ?? run?.receipt?.startedAt ?? null,
|
|
727
|
+
agree,
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// The one thing the console never showed: WHICH repositories are in the brain, by name. It has
|
|
733
|
+
// always carried the count (206) and never the inventory — the owner asked for this list more than
|
|
734
|
+
// a dozen times. RVF-GENERATIONS.json is the manifest that matches disk exactly (verified 206 = 206);
|
|
735
|
+
// SOURCE.json is the provenance subset (104) that carries the upstream URL. Join on store name,
|
|
736
|
+
// never restate either. Ranked by build recency — never alphabetical (relevance-ordering rule).
|
|
737
|
+
function gatherInventory() {
|
|
738
|
+
const generations = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
|
|
739
|
+
const source = readJSON(path.join(INSTALLED_KB, 'SOURCE.json'));
|
|
740
|
+
const stores = generations?.stores && typeof generations.stores === 'object' ? generations.stores : null;
|
|
741
|
+
if (!stores) return { available: false, reason: 'RVF-GENERATIONS.json not readable in the installed brain', count: 0, stores: [] };
|
|
742
|
+
// SOURCE.json has carried `stores` as an array in some generations and a name-keyed object in
|
|
743
|
+
// others; accept both rather than silently joining nothing (measured 2026-09-16: object form,
|
|
744
|
+
// and the array-only read reported withProvenance=0 for a brain with 104 provenance rows).
|
|
745
|
+
const provenance = new Map();
|
|
746
|
+
const sourceRows = Array.isArray(source?.stores) ? source.stores
|
|
747
|
+
: source?.stores && typeof source.stores === 'object'
|
|
748
|
+
? Object.entries(source.stores).map(([name, row]) => ({ name, ...row })) : [];
|
|
749
|
+
for (const row of sourceRows) {
|
|
750
|
+
if (row?.name) provenance.set(String(row.name).toLowerCase(), row);
|
|
751
|
+
}
|
|
752
|
+
const rows = Object.entries(stores).map(([name, gen]) => {
|
|
753
|
+
const prov = provenance.get(name.toLowerCase());
|
|
754
|
+
return {
|
|
755
|
+
name,
|
|
756
|
+
sourceRepo: prov?.sourceRepo || null,
|
|
757
|
+
sourceCommit: gen?.sourceCommit ? String(gen.sourceCommit).slice(0, 7) : null,
|
|
758
|
+
builtUtc: gen?.builtUtc || null,
|
|
759
|
+
bytes: Number(gen?.bytes) || 0,
|
|
760
|
+
model: gen?.model || null,
|
|
761
|
+
private: prov?.updateManaged === false,
|
|
762
|
+
};
|
|
763
|
+
}).sort((a, b) => String(b.builtUtc || '').localeCompare(String(a.builtUtc || '')) || a.name.localeCompare(b.name));
|
|
764
|
+
return {
|
|
765
|
+
available: true,
|
|
766
|
+
count: rows.length,
|
|
767
|
+
withProvenance: rows.filter((r) => r.sourceRepo).length,
|
|
768
|
+
privateCount: rows.filter((r) => r.private).length,
|
|
769
|
+
brainVersion: generations.brainVersion || null,
|
|
770
|
+
releaseTag: generations.releaseTag || null,
|
|
771
|
+
totalBytes: rows.reduce((sum, r) => sum + r.bytes, 0),
|
|
772
|
+
stores: rows,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
646
776
|
function gatherConfig() {
|
|
647
777
|
const cfg = readJSON(CONFIG_PATH) || {};
|
|
648
778
|
const credential = openRouterCredentialStatus({ cwd: process.cwd() });
|
|
649
779
|
const schedule = nightlyStatus();
|
|
650
780
|
const bool = (v) => (v === true ? true : v === false ? false : null);
|
|
781
|
+
const nightly = reconcileNightly({ choice: bool(cfg.nightly), schedule });
|
|
651
782
|
const unavailable = unsupportedConfigControls();
|
|
652
783
|
if (!schedule.artifact.supported) {
|
|
653
784
|
unavailable.push({
|
|
@@ -662,7 +793,7 @@ function gatherConfig() {
|
|
|
662
793
|
values: {
|
|
663
794
|
openrouterKey: credential.configured, // boolean only — never the secret itself
|
|
664
795
|
provider: typeof cfg.provider === 'string' && cfg.provider ? cfg.provider : null,
|
|
665
|
-
nightly:
|
|
796
|
+
nightly: nightly.value,
|
|
666
797
|
routing: cfg.routing === 'off' ? 'off' : cfg.routing === 'auto' ? 'auto' : null,
|
|
667
798
|
qeFleet: bool(cfg.qeFleet),
|
|
668
799
|
},
|
|
@@ -675,7 +806,7 @@ function gatherConfig() {
|
|
|
675
806
|
unavailable,
|
|
676
807
|
runtime: {
|
|
677
808
|
openrouterKey: credential,
|
|
678
|
-
nightly: schedule,
|
|
809
|
+
nightly: { ...schedule, facts: nightly.facts },
|
|
679
810
|
},
|
|
680
811
|
};
|
|
681
812
|
}
|
|
@@ -791,8 +922,21 @@ function gatherBrainProfile() {
|
|
|
791
922
|
: installed.stores.length === 1
|
|
792
923
|
? PROFILE_RUVECTOR
|
|
793
924
|
: PROFILE_COMPLETE;
|
|
925
|
+
// THE "— stores · 0 MB" LIE (console audit 2026-09-11). `choices.complete` was rendered from
|
|
926
|
+
// `source` — the restore BUNDLE at COMPLETE_BRAIN_SOURCE, which on an installed host resolves to
|
|
927
|
+
// <runtime>/dist/ruvnet-brain and never exists after install — while `installed`, measured three
|
|
928
|
+
// lines up (184 stores, 979 MB on the owner's machine), went unused. discoverStoreFamilies()
|
|
929
|
+
// swallows ENOENT into [], so a complete brain rendered as zero and wore an `active` badge over it.
|
|
930
|
+
// Two different questions are now answered separately: what is INSTALLED here, and by which path
|
|
931
|
+
// Apply→complete could RESTORE it here. The path order mirrors saveBrainProfile()'s own branches
|
|
932
|
+
// (local bundle, then the signed forge-update download, then nothing), so the card describes the
|
|
933
|
+
// exact mechanism Apply would take — never a hopeful one, never a number nobody measured.
|
|
794
934
|
const source = measureBrainProfile(COMPLETE_BRAIN_SOURCE);
|
|
935
|
+
const bundlePresent = source.stores.includes(PROFILE_RUVECTOR) && source.storeCount > 1;
|
|
795
936
|
const updaterAvailable = fs.existsSync(path.join(INSTALLED_KB, 'forge-update.mjs'));
|
|
937
|
+
const restoreVia = bundlePresent ? 'local-bundle' : updaterAvailable ? 'signed-download' : null;
|
|
938
|
+
const completeInstalled = actual === PROFILE_COMPLETE;
|
|
939
|
+
const restorePath = COMPLETE_BRAIN_SOURCE.replace(CONSOLE_ROOT, '~');
|
|
796
940
|
return {
|
|
797
941
|
path: INSTALLED_KB.replace(CONSOLE_ROOT, '~'),
|
|
798
942
|
values: { brainProfile: actual },
|
|
@@ -803,10 +947,20 @@ function gatherBrainProfile() {
|
|
|
803
947
|
installed,
|
|
804
948
|
choices: {
|
|
805
949
|
complete: {
|
|
806
|
-
available:
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
950
|
+
available: restoreVia !== null,
|
|
951
|
+
// From the INSTALLED brain when complete is what is installed; from the local bundle when one
|
|
952
|
+
// exists to restore from; otherwise null — never a 0 that nothing measured.
|
|
953
|
+
storeCount: completeInstalled ? installed.storeCount : bundlePresent ? source.storeCount : null,
|
|
954
|
+
bytes: completeInstalled ? installed.bytes : bundlePresent ? source.bytes : null,
|
|
955
|
+
installed: completeInstalled ? { storeCount: installed.storeCount, bytes: installed.bytes } : null,
|
|
956
|
+
restoreBundle: {
|
|
957
|
+
present: bundlePresent,
|
|
958
|
+
path: restorePath,
|
|
959
|
+
storeCount: bundlePresent ? source.storeCount : null,
|
|
960
|
+
bytes: bundlePresent ? source.bytes : null,
|
|
961
|
+
},
|
|
962
|
+
updaterAvailable,
|
|
963
|
+
restoreVia,
|
|
810
964
|
},
|
|
811
965
|
ruvector: {
|
|
812
966
|
available: installed.stores.includes(PROFILE_RUVECTOR)
|
|
@@ -1960,6 +2114,34 @@ function readInstallChannel() {
|
|
|
1960
2114
|
* practice, because the check that did exist only ever triggered a blocking recompute rather than an
|
|
1961
2115
|
* honest stale-serve.
|
|
1962
2116
|
*/
|
|
2117
|
+
/**
|
|
2118
|
+
* THREE VERSIONS ON ONE PAGE (console audit 2026-09-11): the header chip said v4.3.21 (running
|
|
2119
|
+
* brain), the Install-channel row said v4.3.22 (plugin cache), the installed KB's RVF-GENERATIONS.json
|
|
2120
|
+
* said 4.3.10 — and nothing said they differed. Each is read from its own file and LABELED. `agree`
|
|
2121
|
+
* is true only when every measured one is identical, null when fewer than two could be read. A
|
|
2122
|
+
* version that cannot be read is null, never guessed.
|
|
2123
|
+
*/
|
|
2124
|
+
export function versionFacts({ release = null } = {}) {
|
|
2125
|
+
const gen = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
|
|
2126
|
+
const kbGeneration = typeof gen?.brainVersion === 'string' && gen.brainVersion ? gen.brainVersion.replace(/^v/, '') : null;
|
|
2127
|
+
const runningBrain = brainVersionOnDisk();
|
|
2128
|
+
let installedPlugin = null;
|
|
2129
|
+
try { installedPlugin = readInstallChannel().version || null; } catch { installedPlugin = null; }
|
|
2130
|
+
// A CORPUS RELEASE IS NOT A VERSION (ADR-086 step 16). `releases/latest` can now carry a
|
|
2131
|
+
// `corpus-sha256-<64 hex>` content address, and stripping a leading "v" off one yields a 78-char
|
|
2132
|
+
// string that this panel would have printed in the "latest release" slot beside `4.3.22`-shaped
|
|
2133
|
+
// numbers — in the one panel whose whole job is to make version disagreement visible. That is not
|
|
2134
|
+
// a version disagreement; it is a category error, and showing it as one would be the panel lying.
|
|
2135
|
+
// So the two identities are reported in two fields, each labeled for what it actually is.
|
|
2136
|
+
const rawTag = typeof release?.tag === 'string' && release.tag ? release.tag : null;
|
|
2137
|
+
const corpusTag = isCorpusReleaseTag(rawTag) ? rawTag : null;
|
|
2138
|
+
const latestRelease = rawTag && !corpusTag ? rawTag.replace(/^v/, '') : null;
|
|
2139
|
+
const known = [kbGeneration, runningBrain, installedPlugin].filter(Boolean);
|
|
2140
|
+
const agree = known.length < 2 ? null : known.every((x) => x === known[0]);
|
|
2141
|
+
return { kbGeneration, runningBrain, installedPlugin, latestRelease, latestCorpusRelease: corpusTag,
|
|
2142
|
+
agree, known: known.length };
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1963
2145
|
async function gatherTrust() {
|
|
1964
2146
|
// COLD ONLY: no successful release read has ever landed, so there is nothing to withhold or serve
|
|
1965
2147
|
// stale — the one exception serveCached() itself carves out for its own caches.
|
|
@@ -1971,7 +2153,7 @@ async function gatherTrust() {
|
|
|
1971
2153
|
const generatedAt = new Date().toISOString();
|
|
1972
2154
|
const data = { generatedAt, release };
|
|
1973
2155
|
if (release.ok) { TRUST_CACHE = { at: Date.parse(generatedAt), data }; saveConsoleCache(); }
|
|
1974
|
-
return { ...data, channel: readInstallChannel(), sbom: readSbom(), ...freshnessOf(generatedAt) };
|
|
2156
|
+
return { ...data, channel: readInstallChannel(), sbom: readSbom(), versions: versionFacts({ release }), ...freshnessOf(generatedAt) };
|
|
1975
2157
|
}
|
|
1976
2158
|
|
|
1977
2159
|
// WARM — including over-ceiling. Never await the network here; hand back what we measured, say
|
|
@@ -1980,7 +2162,7 @@ async function gatherTrust() {
|
|
|
1980
2162
|
if (fresh.stale) kickTrustRefresh();
|
|
1981
2163
|
// Disk facts stay live even when the release read is served from cache — the SBOM file and install
|
|
1982
2164
|
// channel can change (a fresh `npm run sbom`, a plugin update) between two calls inside the ceiling.
|
|
1983
|
-
return { ...TRUST_CACHE.data, channel: readInstallChannel(), sbom: readSbom(), ...fresh };
|
|
2165
|
+
return { ...TRUST_CACHE.data, channel: readInstallChannel(), sbom: readSbom(), versions: versionFacts({ release: TRUST_CACHE.data.release }), ...fresh };
|
|
1984
2166
|
}
|
|
1985
2167
|
|
|
1986
2168
|
// ── Assemble the read-models ─────────────────────────────────────────────────────────────────────
|
|
@@ -2130,7 +2312,7 @@ function gatherState(cwd, { fleet = true } = {}) {
|
|
|
2130
2312
|
generatedAt: new Date().toISOString(),
|
|
2131
2313
|
preStateHash,
|
|
2132
2314
|
host: { user: os.userInfo().username, platform: process.platform, node: process.version, npmPrefix: NPM_PREFIX.replace(SYSTEM_HOME, '~'), brainVersion: brainVersionOnDisk() },
|
|
2133
|
-
sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations },
|
|
2315
|
+
sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations, inventory: gatherInventory() },
|
|
2134
2316
|
};
|
|
2135
2317
|
// Cache the last good state so repeat page-loads paint instantly, same as the stack audit does.
|
|
2136
2318
|
// TOKEN is per-server-run and must never touch disk — ?fast=1 splices the live one back in.
|
|
@@ -2864,6 +3046,10 @@ function startServer({ port = Number(process.env.CONSOLE_PORT) || 7411, open = f
|
|
|
2864
3046
|
if (req.method === 'GET' && url === '/api/lessons') return sendJSON(res, 200, gatherLessons());
|
|
2865
3047
|
if (req.method === 'GET' && url === '/api/trust') return sendJSON(res, 200, await gatherTrust());
|
|
2866
3048
|
if (req.method === 'GET' && url === '/tips') { req.url = '/tips.html'; return serveStatic(req, res); }
|
|
3049
|
+
// What's in the brain: derived from the installed root's own receipts on every request (read-only,
|
|
3050
|
+
// no network, ~ms) — a coverage listing served from a stale cache would defeat its one purpose.
|
|
3051
|
+
if (req.method === 'GET' && url === '/api/scope') return sendJSON(res, 200, gatherScope());
|
|
3052
|
+
if (req.method === 'GET' && url === '/scope') { req.url = '/scope.html'; return serveStatic(req, res); }
|
|
2867
3053
|
if (req.method === 'POST') {
|
|
2868
3054
|
const body = await readBody(req);
|
|
2869
3055
|
if (body.token !== TOKEN) return sendJSON(res, 403, { error: 'bad or missing token' });
|
|
@@ -3046,7 +3232,155 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith('onboarding-consol
|
|
|
3046
3232
|
else { console.log(`\n onboarding-console — the RuvNet Brain configure page\n\n --serve [--open] start or safely replace the scoped local server\n --runtime-status print candidate, receipt, and live runtime status\n --print-state print the read-only state JSON and exit (for tests)\n --print-stack print the stack audit JSON and exit\n`); }
|
|
3047
3233
|
}
|
|
3048
3234
|
|
|
3235
|
+
// ── "What's in the brain" (the scope page) ─────────────────────────────────────────────────────────
|
|
3236
|
+
// Every verdict comes from the installed brain's OWN receipts, never from the shipped `status` word.
|
|
3237
|
+
// Measured 2026-09-11 (KB audit): release-projection.mjs:80 stamps every seeded row CURRENT — 63 real
|
|
3238
|
+
// rows had sourceCommit ≠ upstream.sha. GitHub `updatedAt` is metadata (222/227 repos moved on it in
|
|
3239
|
+
// one week) and `pushedAt` leads the default-branch HEAD on 39/227; only `committedAt` dates the commit
|
|
3240
|
+
// the brain ingests. Gists have no branches, so their `updatedAt` IS the content date — and the shipped
|
|
3241
|
+
// COVERAGE row carries no sourceCommit for them (479/479 null); the gist receipt set
|
|
3242
|
+
// (ruv-gists.sources.json → versionSha) is the brain's record of which revision it holds.
|
|
3243
|
+
//
|
|
3244
|
+
// The truth per row is `brainSha === upstreamSha`. A store with no recorded source commit is
|
|
3245
|
+
// "unverified" — never promoted to current on the strength of a date or a label. A store whose
|
|
3246
|
+
// .big.rvf is not at the installed root is "not in the brain", whatever the record says.
|
|
3247
|
+
const SCOPE_SENTENCE = "If rUv's last change to a repo is on or before the date the brain read it, everything in that repo is in the brain. If it's after, the brain is behind on that repo by the difference.";
|
|
3248
|
+
const SCOPE_DAY_MS = 86_400_000;
|
|
3249
|
+
const SCOPE_STALE_DAYS = 7;
|
|
3250
|
+
// A row's `desc` is what the thing DOES, so the page's search answers "is the stuff on Federation in
|
|
3251
|
+
// Ruflo?" — a description question — not only name lookups. Bounded so a README pasted into a card
|
|
3252
|
+
// cannot bloat 700 rows.
|
|
3253
|
+
const SCOPE_DESC_MAX = 200;
|
|
3254
|
+
const scopeBounded = (s) => (s.length > SCOPE_DESC_MAX ? `${s.slice(0, SCOPE_DESC_MAX - 1)}…` : s);
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* Pure: the installed brain's capability-cards.md → Map(lowercased repo/store name → first line of
|
|
3258
|
+
* its card, bounded). The card file is the ONLY per-repo description that ships to a customer
|
|
3259
|
+
* machine (data/ruvnet-registry.json is repo-side and never installs — measured 2026-09-12). Format,
|
|
3260
|
+
* measured on 182 sections: `## <name>`, one line saying what it does, one auto-derivation note.
|
|
3261
|
+
*/
|
|
3262
|
+
function parseCapabilityCards(text) {
|
|
3263
|
+
const cards = new Map();
|
|
3264
|
+
if (typeof text !== 'string') return cards;
|
|
3265
|
+
const lines = text.split('\n');
|
|
3266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3267
|
+
const h = /^##\s+(\S.*?)\s*$/.exec(lines[i]);
|
|
3268
|
+
if (!h) continue;
|
|
3269
|
+
let j = i + 1;
|
|
3270
|
+
while (j < lines.length && !lines[j].trim()) j += 1;
|
|
3271
|
+
const first = j < lines.length && !lines[j].startsWith('#') ? lines[j].trim() : '';
|
|
3272
|
+
if (first) cards.set(h[1].toLowerCase(), scopeBounded(first));
|
|
3273
|
+
}
|
|
3274
|
+
return cards;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
function scopeDesc(row, { isGist, store, gistId, gistSources, cards }) {
|
|
3278
|
+
if (isGist) {
|
|
3279
|
+
// A gist's only words are its file names (COVERAGE upstream.files; the receipt's files as fallback).
|
|
3280
|
+
const files = row.upstream?.files ?? gistSources?.gists?.[gistId]?.files ?? null;
|
|
3281
|
+
const names = Array.isArray(files) ? files.map((f) => (typeof f === 'string' ? f : f?.filename)).filter(Boolean) : [];
|
|
3282
|
+
return names.length ? scopeBounded(names.join(' · ')) : null;
|
|
3283
|
+
}
|
|
3284
|
+
const byName = cards?.get(String(row.name ?? '').toLowerCase());
|
|
3285
|
+
if (byName) return byName;
|
|
3286
|
+
return (store && cards?.get(String(store).toLowerCase())) || null;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
function scopeRow(row, { generations, gistSources, installedStores, cards }) {
|
|
3290
|
+
const isGist = row.kind === 'gist';
|
|
3291
|
+
const store = row.artifact?.store ?? null;
|
|
3292
|
+
const ledger = store ? (generations?.stores?.[store] ?? null) : null;
|
|
3293
|
+
const gistId = isGist ? String(row.key ?? '').replace(/^gist:/, '') : null;
|
|
3294
|
+
const upstreamSha = row.upstream?.sha ?? null;
|
|
3295
|
+
const desc = scopeDesc(row, { isGist, store, gistId, gistSources, cards });
|
|
3296
|
+
|
|
3297
|
+
let brainSha = row.artifact?.sourceCommit ?? null;
|
|
3298
|
+
if (brainSha == null) brainSha = isGist ? (gistSources?.gists?.[gistId]?.versionSha ?? null) : (ledger?.sourceCommit ?? null);
|
|
3299
|
+
|
|
3300
|
+
const ruvChangedAt = (isGist ? row.upstream?.updatedAt : row.upstream?.committedAt) ?? null;
|
|
3301
|
+
const brainReadAt = row.artifact?.ingestedAt ?? ledger?.builtUtc ?? null;
|
|
3302
|
+
|
|
3303
|
+
let installed = Boolean(store) && installedStores.has(store);
|
|
3304
|
+
// With a gist receipt set present, a gist the set does not enumerate is not in the store.
|
|
3305
|
+
if (installed && isGist && gistSources?.gists && !(gistId in gistSources.gists)) installed = false;
|
|
3306
|
+
|
|
3307
|
+
let bucket;
|
|
3308
|
+
if (!installed) bucket = 'not-in-brain';
|
|
3309
|
+
else if (brainSha && upstreamSha) bucket = brainSha === upstreamSha ? 'current' : 'behind';
|
|
3310
|
+
else bucket = 'unverified';
|
|
3311
|
+
|
|
3312
|
+
let behindDays = null;
|
|
3313
|
+
if (bucket === 'behind' && ruvChangedAt && brainReadAt) {
|
|
3314
|
+
const d = (Date.parse(ruvChangedAt) - Date.parse(brainReadAt)) / SCOPE_DAY_MS;
|
|
3315
|
+
behindDays = Number.isFinite(d) ? Math.max(0, Math.round(d)) : null;
|
|
3316
|
+
}
|
|
3317
|
+
return { kind: row.kind, name: row.name, url: row.url ?? null, store, desc, bucket, ruvChangedAt, brainReadAt, behindDays, brainSha, upstreamSha };
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
const scopeNewestFirst = (a, b) => {
|
|
3321
|
+
if (a.ruvChangedAt == null && b.ruvChangedAt == null) return String(a.name).localeCompare(String(b.name));
|
|
3322
|
+
if (a.ruvChangedAt == null) return 1;
|
|
3323
|
+
if (b.ruvChangedAt == null) return -1;
|
|
3324
|
+
return a.ruvChangedAt < b.ruvChangedAt ? 1 : a.ruvChangedAt > b.ruvChangedAt ? -1 : String(a.name).localeCompare(String(b.name));
|
|
3325
|
+
};
|
|
3326
|
+
|
|
3327
|
+
/** Pure: coverage + ledger + gist receipts + the set of installed store names → the page's payload. */
|
|
3328
|
+
function computeScope({ coverage, generations, gistSources, installedStores, root, cards = new Map(), now = Date.now() }) {
|
|
3329
|
+
const empty = () => ({ total: 0, current: 0, behind: 0, unverified: 0, notInBrain: 0, ineligible: 0 });
|
|
3330
|
+
const counts = { repos: empty(), gists: empty() };
|
|
3331
|
+
const out = { repos: [], gists: [] };
|
|
3332
|
+
const covered = new Set();
|
|
3333
|
+
for (const row of coverage.rows ?? []) {
|
|
3334
|
+
const kind = row.kind === 'gist' ? 'gists' : 'repos';
|
|
3335
|
+
if (row.artifact?.store) covered.add(row.artifact.store);
|
|
3336
|
+
counts[kind].total += 1;
|
|
3337
|
+
if (row.disposition !== 'eligible') { counts[kind].ineligible += 1; continue; }
|
|
3338
|
+
const r = scopeRow(row, { generations, gistSources, installedStores, cards });
|
|
3339
|
+
counts[kind][r.bucket === 'not-in-brain' ? 'notInBrain' : r.bucket] += 1;
|
|
3340
|
+
out[kind].push(r);
|
|
3341
|
+
}
|
|
3342
|
+
out.repos.sort(scopeNewestFirst);
|
|
3343
|
+
out.gists.sort(scopeNewestFirst);
|
|
3344
|
+
const observedAt = typeof coverage.observedAt === 'string' ? coverage.observedAt : null;
|
|
3345
|
+
const ageMs = observedAt ? now - Date.parse(observedAt) : NaN;
|
|
3346
|
+
const ageDays = Number.isFinite(ageMs) ? Math.round((ageMs / SCOPE_DAY_MS) * 10) / 10 : null;
|
|
3347
|
+
return {
|
|
3348
|
+
available: true,
|
|
3349
|
+
root,
|
|
3350
|
+
observedAt,
|
|
3351
|
+
ageDays,
|
|
3352
|
+
stale: ageDays == null || ageDays > SCOPE_STALE_DAYS,
|
|
3353
|
+
brainVersion: generations?.brainVersion ?? null,
|
|
3354
|
+
releaseTag: generations?.releaseTag ?? null,
|
|
3355
|
+
sentence: SCOPE_SENTENCE,
|
|
3356
|
+
counts,
|
|
3357
|
+
repos: out.repos,
|
|
3358
|
+
gists: out.gists,
|
|
3359
|
+
installedStoreCount: installedStores.size,
|
|
3360
|
+
installedOutsideCoverage: [...installedStores].filter((s) => !covered.has(s)).sort(),
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/** Read the installed root and compute. Read-only; no network; returns `available: false` rather than a guess. */
|
|
3365
|
+
function gatherScope() {
|
|
3366
|
+
const readJson = (rel) => { try { return JSON.parse(fs.readFileSync(path.join(INSTALLED_KB, rel), 'utf8')); } catch { return null; } };
|
|
3367
|
+
const coverage = readJson('COVERAGE.json');
|
|
3368
|
+
if (!coverage || !Array.isArray(coverage.rows)) {
|
|
3369
|
+
return { available: false, reason: `no readable COVERAGE.json at ${INSTALLED_KB}`, root: INSTALLED_KB, sentence: SCOPE_SENTENCE, repos: [], gists: [], counts: {}, installedOutsideCoverage: [], installedStoreCount: 0 };
|
|
3370
|
+
}
|
|
3371
|
+
let installedStores = new Set();
|
|
3372
|
+
try { installedStores = new Set(fs.readdirSync(INSTALLED_KB).filter((f) => f.endsWith('.big.rvf')).map((f) => f.slice(0, -'.big.rvf'.length))); }
|
|
3373
|
+
catch { /* unreadable root reads as nothing installed — every row then says "not in the brain" */ }
|
|
3374
|
+
let cardsText = null;
|
|
3375
|
+
try { cardsText = fs.readFileSync(path.join(INSTALLED_KB, 'capability-cards.md'), 'utf8'); } catch { /* no cards → every desc is null, never invented */ }
|
|
3376
|
+
return computeScope({ coverage, generations: readJson('RVF-GENERATIONS.json'), gistSources: readJson('ruv-gists.sources.json'),
|
|
3377
|
+
installedStores, root: INSTALLED_KB, cards: parseCapabilityCards(cardsText) });
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3049
3380
|
export {
|
|
3381
|
+
gatherScope,
|
|
3382
|
+
computeScope,
|
|
3383
|
+
parseCapabilityCards,
|
|
3050
3384
|
gatherState,
|
|
3051
3385
|
gatherStack,
|
|
3052
3386
|
gatherTrust,
|
|
@@ -3063,6 +3397,8 @@ export {
|
|
|
3063
3397
|
saveBrainProfile,
|
|
3064
3398
|
gatherRouterEngine,
|
|
3065
3399
|
autoEligibleIds,
|
|
3400
|
+
gatherConfig,
|
|
3401
|
+
gatherSavings,
|
|
3066
3402
|
};
|
|
3067
3403
|
// Exported for the cross-project cache-isolation test (console-cache-scope.test.mjs). serveCached's
|
|
3068
3404
|
// scopeKey is the guard that stops one project's cached state being served for another.
|