ruvnet-brain 4.0.90-dev β 4.2.2-dev
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 +26 -3
- package/bin/install.mjs +142 -20
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/scripts/brain-score.mjs +17 -8
- package/scripts/build-bundle.mjs +13 -3
- package/scripts/card-from-source.mjs +114 -0
- package/scripts/corpus-candidate.mjs +294 -0
- package/scripts/corpus-reconcile.mjs +273 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/ingest-new-repos.mjs +122 -0
- package/scripts/nightly-wrapper.sh +43 -8
- package/scripts/rebuild-gists-from-receipts.mjs +246 -0
- package/scripts/release-transaction.mjs +30 -2
- package/scripts/release.mjs +147 -0
- package/scripts/rvf-generation.mjs +44 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# π§ RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### π§ RuvNet Brain β [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
8
8
|
|
|
9
9
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack β delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
10
10
|
|
|
@@ -56,7 +56,30 @@
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
## What's new in 4.
|
|
59
|
+
## What's new in 4.2 β it loads what rUv ships, without being asked
|
|
60
|
+
|
|
61
|
+
**The corpus stopped drifting behind the org.** Until 4.2 nothing ever ingested a new repo: the
|
|
62
|
+
nightly refreshed lessons, health and proofs and contained *zero* ingestion, so a repo entered the
|
|
63
|
+
brain only when a human typed the command. `brain-stamp.mjs` had been measuring that gap every
|
|
64
|
+
night, but nothing consumed it until the ingestion loop shipped.
|
|
65
|
+
|
|
66
|
+
- **187 stores, up from 69.** Everything rUv ships that has content, pulled in and kept level by
|
|
67
|
+
`scripts/ingest-new-repos.mjs` running nightly, newest-first. Empty repos (`size=0KB`) are skipped
|
|
68
|
+
rather than retried forever β a permanent nightly failure that is actually correct behaviour
|
|
69
|
+
trains you to ignore the failure line, which is how a real one would hide inside it.
|
|
70
|
+
- **174 capability cards, up from 39.** Ingesting a repo is not the same as making it reachable: a
|
|
71
|
+
store with no card is *dark* β valid bytes no by-description query can find. Cards are written
|
|
72
|
+
from each repo's **own** description and README, never from its name, and where there is no
|
|
73
|
+
grounded source text the store is **left dark and reported** rather than given an invented
|
|
74
|
+
sentence. 13 remain dark for exactly that reason.
|
|
75
|
+
- **The download barely grew** for 2.4x the corpus β most of rUv's repos are small, so the bulk
|
|
76
|
+
of the bundle was always the large ones. (No size is hand-typed here: a number typed onto a
|
|
77
|
+
public page rots the moment the next bundle is built, and this repo gates against exactly that.)
|
|
78
|
+
- **The nightly stopped discarding its own writes.** It hardcoded a node whose ABI did not match
|
|
79
|
+
agentdb's binding, so `lesson-bridge --apply` and `learning-replay` wrote into a silent
|
|
80
|
+
non-persistent fallback. It now resolves an ABI-matched interpreter and fails loudly instead.
|
|
81
|
+
|
|
82
|
+
## What's new in 4.2 β it anticipates, and it learns whether it was right
|
|
60
83
|
|
|
61
84
|
**Building toward L4/L5 (3.9.x, dev).** The mechanisms for the top two rungs of the proactivity
|
|
62
85
|
ladder are built and wired β but they are **not yet verified to 4.0's bar**, which requires all five
|
|
@@ -522,7 +545,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
522
545
|
|
|
523
546
|
This project versions in the open (see the live badge up top for the exact plugin version; the downloadable knowledge bundle is a separate track) β we don't claim βdone,β βcomplete,β or βzero hallucinations.β Where it stands:
|
|
524
547
|
|
|
525
|
-
- β
**The grounding brain is real and proven** β 70 public stores Β· 140,
|
|
548
|
+
- β
**The grounding brain is real and proven** β 70 public stores Β· 140,520 public source chunks (77 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
|
|
526
549
|
- β
**Code-level depth** β the code-rich repos are indexed to full function bodies; βhow is it implemented?β returns the implementation. Verified in the shipped bundle (clean-room 3/3).
|
|
527
550
|
- β
**Routing holds** β named 47/48, described 26/28, scenario 7/8; behavioral L1βL3 all pass (**L4 downgraded β it measures that the brain spoke, not that anything listened**); private stores fenced out of the public bundle (zero-leak verified).
|
|
528
551
|
- β οΈ **Two routing residuals** (above) β surfaced, not hidden.
|
package/bin/install.mjs
CHANGED
|
@@ -547,7 +547,7 @@ async function unzipInto(zipPath, cacheDir, sourceDir = null) {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
/**
|
|
550
|
-
* ISSUE #128 β STALE PLUGIN GENERATIONS
|
|
550
|
+
* ISSUE #128 / #153 β STALE PLUGIN GENERATIONS MAY STILL BACK LIVE SESSIONS.
|
|
551
551
|
*
|
|
552
552
|
* `pruneUnlistedStores` below states the rule this repo already believes: "an update is a
|
|
553
553
|
* replacement, not an overlay." That rule was applied to KB stores and never to the plugin cache.
|
|
@@ -563,6 +563,12 @@ async function unzipInto(zipPath, cacheDir, sourceDir = null) {
|
|
|
563
563
|
* restart, one says an update is landing, one mandates a third version), so whichever the model
|
|
564
564
|
* obeys, the user is misinformed by a source that sounds definitive.
|
|
565
565
|
*
|
|
566
|
+
* Registry reachability identifies candidates; it is not deletion proof. Claude freezes
|
|
567
|
+
* CLAUDE_PLUGIN_ROOT at session start, so an unregistered generation can remain live until that
|
|
568
|
+
* session exits. Modern roots expose `.in_use` PID-incarnation leases. Live or ambiguous leases
|
|
569
|
+
* retain the complete root; dead leases are collected. Legacy roots without the protocol receive
|
|
570
|
+
* a 14-day compatibility grace.
|
|
571
|
+
*
|
|
566
572
|
* WHAT MAKES DELETION SAFE HERE, since this removes directories from someone's machine:
|
|
567
573
|
* β’ The registry is the ONLY authority. Unreadable, missing, or naming no ruvnet-brain install β
|
|
568
574
|
* nothing is removed and a reason is returned. Never guess what to delete β the same discipline
|
|
@@ -574,17 +580,26 @@ async function unzipInto(zipPath, cacheDir, sourceDir = null) {
|
|
|
574
580
|
* here, and deleting through that link would delete the working tree.
|
|
575
581
|
* β’ EVERY registered installPath is protected, not just the first β the plugin may legitimately be
|
|
576
582
|
* installed at both user and project scope, and treating one as stale would delete a live install.
|
|
577
|
-
* β’
|
|
583
|
+
* β’ Registered generations are excluded by RESOLVED PATH, never by version string.
|
|
584
|
+
* β’ The registry is re-read immediately before deletion; any byte change retains the candidate.
|
|
585
|
+
* β’ Malformed, unreadable, symlinked, live, or unverifiable lease state fails closed.
|
|
578
586
|
* β’ Report-only unless `apply` is set, and every removal is returned by name, so a silent sweep is
|
|
579
587
|
* impossible.
|
|
580
588
|
*/
|
|
581
589
|
export function prunePluginGenerations({
|
|
582
590
|
registryPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json'),
|
|
583
591
|
apply = false,
|
|
592
|
+
now = () => Date.now(),
|
|
593
|
+
graceMs = 14 * 24 * 60 * 60 * 1000,
|
|
594
|
+
processIdentity = pluginProcessIdentity,
|
|
584
595
|
} = {}) {
|
|
585
|
-
const nothing = (why) => ({ active: [], stale: [], removed: [], bytes: 0, why });
|
|
596
|
+
const nothing = (why) => ({ active: [], stale: [], marked: [], removed: [], bytes: 0, staleBytes: 0, cleanupBlocked: [], why });
|
|
597
|
+
let registryRaw;
|
|
586
598
|
let registry;
|
|
587
|
-
try {
|
|
599
|
+
try {
|
|
600
|
+
registryRaw = fs.readFileSync(registryPath, 'utf8');
|
|
601
|
+
registry = JSON.parse(registryRaw);
|
|
602
|
+
} catch (e) {
|
|
588
603
|
return nothing(`plugin registry unreadable (${e.code || e.message}) β refusing to remove anything`);
|
|
589
604
|
}
|
|
590
605
|
const active = Object.entries(registry?.plugins || {})
|
|
@@ -609,17 +624,126 @@ export function prunePluginGenerations({
|
|
|
609
624
|
stale.push({ path: full, version: name, bytes: dirBytes(full) });
|
|
610
625
|
}
|
|
611
626
|
}
|
|
612
|
-
const
|
|
613
|
-
if (!apply) return { active, stale, removed: [], bytes, why: null };
|
|
627
|
+
const staleBytes = stale.reduce((n, s) => n + s.bytes, 0);
|
|
628
|
+
if (!apply) return { active, stale, marked: [], removed: [], bytes: 0, staleBytes, cleanupBlocked: [], why: null };
|
|
614
629
|
|
|
630
|
+
const marked = [];
|
|
615
631
|
const removed = [];
|
|
632
|
+
const cleanupBlocked = [];
|
|
616
633
|
let freed = 0;
|
|
617
|
-
for (const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
634
|
+
for (const candidate of stale) {
|
|
635
|
+
const marker = path.join(candidate.path, '.orphaned_at');
|
|
636
|
+
if (!fs.existsSync(marker)) {
|
|
637
|
+
const temporary = `${marker}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
|
|
638
|
+
try {
|
|
639
|
+
fs.writeFileSync(temporary, String(now()), { flag: 'wx', mode: 0o600 });
|
|
640
|
+
fs.renameSync(temporary, marker);
|
|
641
|
+
marked.push(candidate.version);
|
|
642
|
+
} catch {
|
|
643
|
+
try { fs.rmSync(temporary, { force: true }); } catch { /* retain on ambiguity */ }
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
let orphanedAt;
|
|
648
|
+
try {
|
|
649
|
+
const markerStat = fs.lstatSync(marker);
|
|
650
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink()) {
|
|
651
|
+
cleanupBlocked.push({ version: candidate.version, reason: '.orphaned_at is not a trusted file' });
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
orphanedAt = Number(fs.readFileSync(marker, 'utf8').trim());
|
|
655
|
+
if (!Number.isFinite(orphanedAt) || orphanedAt <= 0) {
|
|
656
|
+
cleanupBlocked.push({ version: candidate.version, reason: '.orphaned_at is malformed' });
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
cleanupBlocked.push({ version: candidate.version, reason: '.orphaned_at could not be inspected' });
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const leaseDir = path.join(candidate.path, '.in_use');
|
|
665
|
+
let leaseCapable = false;
|
|
666
|
+
let leaseNames = [];
|
|
667
|
+
try {
|
|
668
|
+
if (fs.existsSync(leaseDir)) {
|
|
669
|
+
const leaseDirStat = fs.lstatSync(leaseDir);
|
|
670
|
+
if (leaseDirStat.isSymbolicLink() || !leaseDirStat.isDirectory()) {
|
|
671
|
+
cleanupBlocked.push({ version: candidate.version, reason: '.in_use is not a trusted directory' });
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
leaseCapable = true;
|
|
675
|
+
leaseNames = fs.readdirSync(leaseDir);
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
cleanupBlocked.push({ version: candidate.version, reason: '.in_use could not be inspected' });
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (!leaseCapable && now() - orphanedAt < graceMs) continue;
|
|
683
|
+
let safeToReap = true;
|
|
684
|
+
for (const leaseName of leaseNames) {
|
|
685
|
+
const leasePath = path.join(leaseDir, leaseName);
|
|
686
|
+
let lease;
|
|
687
|
+
try {
|
|
688
|
+
const leaseStat = fs.lstatSync(leasePath);
|
|
689
|
+
if (!leaseStat.isFile() || leaseStat.isSymbolicLink()) throw new Error('not a trusted file');
|
|
690
|
+
lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
691
|
+
if (!Number.isSafeInteger(lease?.pid) || lease.pid <= 0 || typeof lease?.procStart !== 'string' || !lease.procStart.trim()) {
|
|
692
|
+
throw new Error('malformed');
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
cleanupBlocked.push({ version: candidate.version, reason: `lease ${leaseName} is malformed or unreadable` });
|
|
696
|
+
safeToReap = false;
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
const state = processIdentity(lease);
|
|
700
|
+
if (state !== 'dead') {
|
|
701
|
+
cleanupBlocked.push({ version: candidate.version, reason: `lease ${leaseName} is ${state === 'live' ? 'live' : 'ambiguous'}` });
|
|
702
|
+
safeToReap = false;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
try { fs.rmSync(leasePath); } catch {
|
|
706
|
+
cleanupBlocked.push({ version: candidate.version, reason: `dead lease ${leaseName} could not be removed` });
|
|
707
|
+
safeToReap = false;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!safeToReap) continue;
|
|
712
|
+
|
|
713
|
+
// The host registry can advance while the collector scans. Any byte change invalidates the
|
|
714
|
+
// original authority snapshot; retain every candidate rather than deleting on stale evidence.
|
|
715
|
+
let currentRaw;
|
|
716
|
+
try { currentRaw = fs.readFileSync(registryPath, 'utf8'); } catch { continue; }
|
|
717
|
+
if (currentRaw !== registryRaw) continue;
|
|
718
|
+
try {
|
|
719
|
+
fs.rmSync(candidate.path, { recursive: true });
|
|
720
|
+
removed.push(candidate.version);
|
|
721
|
+
freed += candidate.bytes;
|
|
722
|
+
} catch { /* retained for a later pass */ }
|
|
621
723
|
}
|
|
622
|
-
return {
|
|
724
|
+
return {
|
|
725
|
+
active, stale, marked, removed, bytes: freed, staleBytes, cleanupBlocked,
|
|
726
|
+
why: stale.length
|
|
727
|
+
? 'lease-proven orphan reconciliation; legacy generations receive a 14-day grace and live or ambiguous generations are retained'
|
|
728
|
+
: null,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Verify the exact PID incarnation recorded by a plugin generation lease. */
|
|
733
|
+
function pluginProcessIdentity({ pid, procStart }) {
|
|
734
|
+
try { process.kill(pid, 0); } catch (error) {
|
|
735
|
+
return error?.code === 'ESRCH' ? 'dead' : 'unknown';
|
|
736
|
+
}
|
|
737
|
+
const result = process.platform === 'win32'
|
|
738
|
+
? spawnSync('powershell.exe', [
|
|
739
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
740
|
+
`$p=Get-Process -Id ${pid} -ErrorAction Stop; $p.StartTime.ToUniversalTime().ToString('ddd MMM dd HH:mm:ss yyyy',[Globalization.CultureInfo]::InvariantCulture)`,
|
|
741
|
+
], { encoding: 'utf8' })
|
|
742
|
+
: spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
743
|
+
encoding: 'utf8', env: { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' },
|
|
744
|
+
});
|
|
745
|
+
if (result.status !== 0 || !String(result.stdout || '').trim()) return 'unknown';
|
|
746
|
+
return String(result.stdout).trim() === procStart.trim() ? 'live' : 'dead';
|
|
623
747
|
}
|
|
624
748
|
|
|
625
749
|
/** Size of a directory tree, best-effort β used only to report what was or would be freed. */
|
|
@@ -2563,18 +2687,16 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
2563
2687
|
const applied = runStableSpine(apply);
|
|
2564
2688
|
const okApplied = !applied.error && applied.status === 0;
|
|
2565
2689
|
if (okApplied) {
|
|
2566
|
-
// ISSUE #
|
|
2567
|
-
//
|
|
2568
|
-
// has applied, so every OTHER generation in that cache is unreachable by design and can only
|
|
2569
|
-
// mislead β its skills still load, its hooks still fire, and its version banner still speaks.
|
|
2570
|
-
// Making it a consequence of updating, rather than a command someone has to remember, is the
|
|
2571
|
-
// whole point; a cleanup that must be invoked is a cleanup that does not happen.
|
|
2572
|
-
// Best-effort and reported by name β it must never turn a good update into a failed one.
|
|
2690
|
+
// ISSUE #153 β running Claude sessions freeze their plugin root. Collect only generations whose
|
|
2691
|
+
// leases prove that exact PID incarnation dead, or legacy roots past the compatibility grace.
|
|
2573
2692
|
try {
|
|
2574
2693
|
const gens = prunePluginGenerations({ apply: true });
|
|
2575
2694
|
if (gens.removed.length) {
|
|
2576
|
-
ok(`pruned ${gens.removed.length} stale plugin generation(s), freed ${(gens.bytes / 1048576).toFixed(1)} MB: ${gens.removed.
|
|
2577
|
-
}
|
|
2695
|
+
ok(`pruned ${gens.removed.length} stale plugin generation(s), freed ${(gens.bytes / 1048576).toFixed(1)} MB: ${gens.removed.join(', ')}`);
|
|
2696
|
+
}
|
|
2697
|
+
if (gens.cleanupBlocked?.length) {
|
|
2698
|
+
warn(`plugin cleanup retained ${gens.cleanupBlocked.length} generation(s): ${gens.cleanupBlocked.map((item) => `${item.version} (${item.reason})`).join(', ')}`);
|
|
2699
|
+
} else if (!gens.removed.length && gens.why) {
|
|
2578
2700
|
warn(`stale plugin generations were not pruned: ${gens.why}`);
|
|
2579
2701
|
}
|
|
2580
2702
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.2-dev",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain β a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code β grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.2.2-dev",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
Binary file
|
package/scripts/brain-score.mjs
CHANGED
|
@@ -132,7 +132,7 @@ function readPanel() {
|
|
|
132
132
|
* than a guess when the denominator is unknown β a wrong denominator still renders, which is worse
|
|
133
133
|
* than an absent one.
|
|
134
134
|
*/
|
|
135
|
-
async function readCoverage() {
|
|
135
|
+
export async function readCoverage(rootOverride) {
|
|
136
136
|
const { storeRoot, storesAt, cardsAt } = await import('../kb/store-root.mjs');
|
|
137
137
|
// ROUTABILITY IS ALIAS-AWARE, BECAUSE THE ROUTER IS.
|
|
138
138
|
//
|
|
@@ -148,25 +148,34 @@ async function readCoverage() {
|
|
|
148
148
|
// broke alias resolution outright. A metric that cannot see aliases manufactures work that
|
|
149
149
|
// damages the thing it measures.
|
|
150
150
|
const { repositoryNames } = await import('../kb/card-lane.mjs');
|
|
151
|
-
const root = storeRoot();
|
|
151
|
+
const root = rootOverride ?? storeRoot();
|
|
152
|
+
// NEVER-MATERIALIZED, THE SAME AMBIGUITY `restore-local-ingests.mjs`'s `classify()` WAS FIXED
|
|
153
|
+
// FOR (PR #143, Night 1) β but never carried here. `storesAt()`/`cardsAt()` silently return
|
|
154
|
+
// `[]` on ENOENT, so a host whose store root was never restored (a fresh checkout, this very
|
|
155
|
+
// nightly agent's own ephemeral container) reads IDENTICALLY to a materialized root that
|
|
156
|
+
// genuinely has zero coverage. Checked once, here, before either derived number is computed β
|
|
157
|
+
// a wrong `0` still renders, which is worse than an honestly unmeasured one.
|
|
158
|
+
const neverMaterialized = !fs.existsSync(root);
|
|
152
159
|
const stores = storesAt(root);
|
|
153
160
|
const cards = new Set(cardsAt(root));
|
|
154
161
|
const org = readJson('data/org-repo-count.json');
|
|
155
162
|
const total = Number.isFinite(org?.count) && org.count > 0 ? org.count : null;
|
|
156
163
|
const carded = stores.filter((s) => isRoutable(s, cards, root, repositoryNames)).length;
|
|
164
|
+
const absentDetail = 'store root does not exist on this host (never materialized) β not evidence of live coverage';
|
|
157
165
|
return {
|
|
158
166
|
catalogue: {
|
|
159
|
-
value: total ? Math.round((stores.length / total) * 1000) / 10 : null,
|
|
160
|
-
detail:
|
|
161
|
-
|
|
167
|
+
value: neverMaterialized ? null : (total ? Math.round((stores.length / total) * 1000) / 10 : null),
|
|
168
|
+
detail: neverMaterialized ? absentDetail
|
|
169
|
+
: (total ? `${stores.length}/${total} live repos` : `${stores.length} stores, org total UNKNOWN`),
|
|
170
|
+
at: neverMaterialized ? null : (org?.at ?? null),
|
|
162
171
|
},
|
|
163
172
|
routable: {
|
|
164
|
-
value: stores.length ? Math.round((carded / stores.length) * 1000) / 10
|
|
165
|
-
detail: `${carded}/${stores.length} built stores have a card`,
|
|
173
|
+
value: neverMaterialized || !stores.length ? null : Math.round((carded / stores.length) * 1000) / 10,
|
|
174
|
+
detail: neverMaterialized ? absentDetail : `${carded}/${stores.length} built stores have a card`,
|
|
166
175
|
// Derived from the live store root at this instant, so it is current BY CONSTRUCTION. A
|
|
167
176
|
// read-now value reported as stale would train the reader to ignore the stale flag, which
|
|
168
177
|
// costs more than the flag is worth.
|
|
169
|
-
at: new Date().toISOString(),
|
|
178
|
+
at: neverMaterialized ? null : new Date().toISOString(),
|
|
170
179
|
},
|
|
171
180
|
};
|
|
172
181
|
}
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { getVersion, getVersionTag, stripTag } from './version.mjs';
|
|
21
21
|
import { auditRvfIndexes } from './rvf-index-audit.mjs';
|
|
22
|
+
import { validateSelectedRvfGenerations } from './rvf-generation.mjs';
|
|
22
23
|
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
23
24
|
// sibling while the account actually had 200 β one stale fact, restated twice (2026-08-12).
|
|
24
25
|
import { orgRepoCount } from './org-repo-count.mjs';
|
|
@@ -162,6 +163,15 @@ if (built.length === 0) {
|
|
|
162
163
|
console.error('[build-bundle] FATAL: zero public RVF stores are eligible for release. Refusing to publish an empty brain bundle.');
|
|
163
164
|
process.exit(1);
|
|
164
165
|
}
|
|
166
|
+
const generationValidation = validateSelectedRvfGenerations(ASSETS, {
|
|
167
|
+
selectedStores: built,
|
|
168
|
+
privateStores: [...PRIVATE_STORES],
|
|
169
|
+
});
|
|
170
|
+
if (generationValidation.failures.length) {
|
|
171
|
+
console.error('[build-bundle] FATAL: RVF generation ledger does not exactly bind selected roots:');
|
|
172
|
+
for (const failure of generationValidation.failures) console.error(` ${failure}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
165
175
|
const rvfIndexAudit = await auditRvfIndexes(
|
|
166
176
|
built.map((name) => path.join(ASSETS, `${name}.big.rvf`)),
|
|
167
177
|
);
|
|
@@ -208,7 +218,7 @@ for (const name of built) {
|
|
|
208
218
|
if (!fs.existsSync(generationsFile)) {
|
|
209
219
|
missing.push('RVF-GENERATIONS.json');
|
|
210
220
|
} else {
|
|
211
|
-
const source =
|
|
221
|
+
const source = generationValidation.manifest;
|
|
212
222
|
const stores = {};
|
|
213
223
|
for (const { name } of builtRepos) {
|
|
214
224
|
if (!source.stores?.[name]) missing.push(`RVF-GENERATIONS.json:${name}`);
|
|
@@ -216,8 +226,8 @@ for (const name of built) {
|
|
|
216
226
|
}
|
|
217
227
|
fs.writeFileSync(path.join(OUT, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
218
228
|
schemaVersion: source.schemaVersion,
|
|
219
|
-
brainVersion:
|
|
220
|
-
releaseTag:
|
|
229
|
+
brainVersion: stripTag(BRAIN_VERSION),
|
|
230
|
+
releaseTag: BRAIN_VERSION,
|
|
221
231
|
stores,
|
|
222
232
|
}, null, 2)}\n`);
|
|
223
233
|
copied++;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* card-from-source.mjs β give a DARK store a card written from the repo's own words.
|
|
4
|
+
*
|
|
5
|
+
* A store with no capability card is DARK: the bytes are valid and no by-description query can
|
|
6
|
+
* reach them. `ingest-repo.mjs` says so on every run. After the 2026-08-20 bulk ingest the brain
|
|
7
|
+
* held ~111 stores and ~71 of them were dark β loaded, and unusable for anything except a query
|
|
8
|
+
* that already knew the repo's name, which is precisely the query a knowledge base is least needed
|
|
9
|
+
* for.
|
|
10
|
+
*
|
|
11
|
+
* THE LINE THIS DOES NOT CROSS. `ingest-new-repos.mjs` deliberately refuses to write cards, because
|
|
12
|
+
* a card invented from a repo NAME is a confident claim in the routing layer that nobody grounded β
|
|
13
|
+
* it routes real questions to a corpus that cannot answer them, which is worse than an honest gap.
|
|
14
|
+
* That refusal stands. This is a different act: it copies the repo's OWN description and the
|
|
15
|
+
* opening of its OWN README. Every content word in the card came from the repository. Nothing here
|
|
16
|
+
* infers what a project is "probably" for.
|
|
17
|
+
*
|
|
18
|
+
* AND IT SAYS WHAT IT IS. Each generated card carries a marker so a reader can tell it apart from
|
|
19
|
+
* the hand-written ones, which are richer β they say when to REACH for a tool and what it is NOT.
|
|
20
|
+
* An auto-derived card is a floor, not a substitute: it makes a store reachable and invites a
|
|
21
|
+
* better card later. Claiming otherwise would be the overselling this file exists to avoid.
|
|
22
|
+
*
|
|
23
|
+
* node scripts/card-from-source.mjs # report which stores are dark
|
|
24
|
+
* node scripts/card-from-source.mjs --apply # write cards for them
|
|
25
|
+
* node scripts/card-from-source.mjs --apply --max 20
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { storeRoot, darkStores } from '../kb/store-root.mjs';
|
|
32
|
+
|
|
33
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
34
|
+
const OWNER = process.env.RUVNET_ORG_OWNER || 'ruvnet';
|
|
35
|
+
const APPLY = process.argv.includes('--apply');
|
|
36
|
+
const arg = (n, d) => { const i = process.argv.indexOf(n); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
37
|
+
const MAX = Number(arg('--max', '500'));
|
|
38
|
+
export const MARKER = 'Auto-derived from the repository\'s own description and README';
|
|
39
|
+
|
|
40
|
+
/** The repo's own description + README opening. Never a guess about what the name implies. */
|
|
41
|
+
export function sourceFacts(repo) {
|
|
42
|
+
let description = '';
|
|
43
|
+
let readme = '';
|
|
44
|
+
try {
|
|
45
|
+
const meta = JSON.parse(execFileSync('gh', ['api', `repos/${OWNER}/${repo}`], { encoding: 'utf8', maxBuffer: 1 << 24 }));
|
|
46
|
+
description = String(meta?.description || '').trim();
|
|
47
|
+
} catch { /* a repo can legitimately have no description */ }
|
|
48
|
+
try {
|
|
49
|
+
const raw = execFileSync('gh', ['api', `repos/${OWNER}/${repo}/readme`, '-H', 'Accept: application/vnd.github.raw'],
|
|
50
|
+
{ encoding: 'utf8', maxBuffer: 1 << 24 });
|
|
51
|
+
readme = raw;
|
|
52
|
+
} catch { /* and no README */ }
|
|
53
|
+
return { description, readme };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** First real prose of a README: skip badges, HTML, headings, and link-only lines. */
|
|
57
|
+
export function firstProse(readme, limit = 600) {
|
|
58
|
+
const lines = String(readme || '').split('\n');
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const t = line.trim();
|
|
62
|
+
if (!t) { if (out.length) break; continue; }
|
|
63
|
+
if (t.startsWith('#') || t.startsWith('<') || t.startsWith('|') || t.startsWith('---')) continue;
|
|
64
|
+
if (/^!?\[[^\]]*\]\([^)]*\)$/.test(t)) continue; // a lone badge or image
|
|
65
|
+
if (/^\[!\[/.test(t)) continue; // badge-wrapped link
|
|
66
|
+
out.push(t);
|
|
67
|
+
if (out.join(' ').length > limit) break;
|
|
68
|
+
}
|
|
69
|
+
return out.join(' ').replace(/\s+/g, ' ').slice(0, limit).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildCard(repo, { description, readme }) {
|
|
73
|
+
const prose = firstProse(readme);
|
|
74
|
+
if (!description && !prose) return null; // nothing grounded to say β leave it dark, honestly
|
|
75
|
+
const body = [description, prose].filter(Boolean).join(' β ');
|
|
76
|
+
return `## ${repo.toLowerCase()}\n${body}\n(${MARKER}; a hand-written card saying when to reach for it, and when not to, would be better.)\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function insertSorted(file, card, name) {
|
|
80
|
+
const s = fs.readFileSync(file, 'utf8');
|
|
81
|
+
if (new RegExp(`^## ${name}$`, 'mi').test(s)) return false;
|
|
82
|
+
const heads = [...s.matchAll(/^## (.+)$/gm)].map((m) => [m.index, m[1].trim().toLowerCase()]);
|
|
83
|
+
const pos = heads.find(([, n]) => n > name)?.[0];
|
|
84
|
+
fs.writeFileSync(file, pos === undefined ? `${s.replace(/\n+$/, '')}\n\n${card}` : s.slice(0, pos) + card + '\n' + s.slice(pos));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isMain = (() => {
|
|
89
|
+
try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
|
|
90
|
+
catch { return false; }
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
if (isMain) {
|
|
94
|
+
const root = storeRoot();
|
|
95
|
+
const dark = darkStores(root);
|
|
96
|
+
console.log(`[card] ${dark.length} dark store(s) β valid bytes no by-description query can reach`);
|
|
97
|
+
if (!dark.length) process.exit(0);
|
|
98
|
+
if (!APPLY) {
|
|
99
|
+
console.log(`[card] ${dark.slice(0, 20).join(', ')}${dark.length > 20 ? ' β¦' : ''}`);
|
|
100
|
+
console.log('[card] report only. Re-run with --apply to write cards from each repo\'s own source.');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
const repoCards = path.join(ROOT, 'kb', 'capability-cards.md');
|
|
104
|
+
const liveCards = path.join(root, 'capability-cards.md');
|
|
105
|
+
let wrote = 0; let skipped = 0;
|
|
106
|
+
for (const name of dark.slice(0, MAX)) {
|
|
107
|
+
const card = buildCard(name, sourceFacts(name));
|
|
108
|
+
if (!card) { skipped += 1; console.log(`[card] ${name}: no description and no README prose β left dark rather than invented`); continue; }
|
|
109
|
+
for (const f of [repoCards, liveCards]) { try { insertSorted(f, card, name.toLowerCase()); } catch { /* keep going */ } }
|
|
110
|
+
wrote += 1;
|
|
111
|
+
}
|
|
112
|
+
const remaining = darkStores(root).length;
|
|
113
|
+
console.log(`\n[card] wrote ${wrote}, left ${skipped} dark for lack of grounded source text, ${remaining} still dark.`);
|
|
114
|
+
}
|