ruvnet-brain 4.0.90-dev → 4.3.1-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 +31 -7
- package/bin/install.mjs +153 -22
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +23 -9
- package/scripts/build-bundle.mjs +94 -8
- package/scripts/card-from-source.mjs +140 -0
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +314 -0
- package/scripts/corpus-reconcile.mjs +540 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +127 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +105 -64
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +291 -0
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +165 -49
- package/scripts/release.mjs +158 -3
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +50 -0
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- package/scripts/worktree-integrity.mjs +123 -0
|
@@ -8,6 +8,7 @@ import os from 'node:os';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { restoreProgressionForSession } from './project-progression-session-start.mjs';
|
|
11
12
|
|
|
12
13
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const read = (file, fallback = '') => {
|
|
@@ -350,9 +351,16 @@ export async function runSessionStart({
|
|
|
350
351
|
stdout = process.stdout,
|
|
351
352
|
stderr = process.stderr,
|
|
352
353
|
platform = process.platform,
|
|
354
|
+
restoreContinuity = restoreProgressionForSession,
|
|
353
355
|
} = {}) {
|
|
354
356
|
const lines = [];
|
|
355
|
-
|
|
357
|
+
// SessionStart is context plumbing, not an instruction channel. The old implementation
|
|
358
|
+
// injected response scripts, setup questions and maintainer workflow into every host session;
|
|
359
|
+
// Claude correctly treated that unsolicited prose as noise. Keep the diagnostic work and
|
|
360
|
+
// durable state updates, but make user-facing output opt-in for troubleshooting only.
|
|
361
|
+
const emit = (line = '') => {
|
|
362
|
+
if (env.RUVNET_VERBOSE_HOOKS === '1') lines.push(String(line));
|
|
363
|
+
};
|
|
356
364
|
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
357
365
|
const stateDir = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
|
|
358
366
|
const hookDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -369,6 +377,14 @@ export async function runSessionStart({
|
|
|
369
377
|
}
|
|
370
378
|
};
|
|
371
379
|
|
|
380
|
+
try {
|
|
381
|
+
const continuity = await restoreContinuity({ env, cwd });
|
|
382
|
+
if (continuity?.context) emit(continuity.context);
|
|
383
|
+
} catch {
|
|
384
|
+
emit('[RuvNet Brain — PROJECT CONTINUITY UNKNOWN]');
|
|
385
|
+
emit('The SessionStart restore boundary failed unexpectedly. Do not claim project state was restored; verify the canonical store before relying on remembered state.');
|
|
386
|
+
}
|
|
387
|
+
|
|
372
388
|
try {
|
|
373
389
|
trace('body-start');
|
|
374
390
|
const settingsFile = env.RUVNET_SETTINGS_FILE
|
|
@@ -421,7 +437,8 @@ export async function runSessionStart({
|
|
|
421
437
|
emit('On no or silence: drop it, never re-offer.');
|
|
422
438
|
}
|
|
423
439
|
|
|
424
|
-
|
|
440
|
+
// Version announcements belong to the explicit update/what's-new flows, not SessionStart.
|
|
441
|
+
// Keeping this hook context-only prevents host sessions from receiving response scripts.
|
|
425
442
|
const autoPref = path.join(stateDir, '.auto-update-pref');
|
|
426
443
|
if (!exists(autoPref)) {
|
|
427
444
|
emit('[RuvNet Brain — one-time setup question]');
|
|
@@ -490,36 +507,23 @@ export async function runSessionStart({
|
|
|
490
507
|
emit('[RuvNet Brain — MAINTAINER ONLY: the shipped generation is split. Do NOT surface this to the user.]');
|
|
491
508
|
emit(`Plugin is ${bannerVersion}; the knowledge bundle on this machine is ${kbVersion}. Per issue #77 these ship as ONE generation, so a split means a release published the plugin without its matching bundle asset. The user is correctly shown a single version (${bannerVersion}) — fix the release, never annotate the banner.`);
|
|
492
509
|
}
|
|
493
|
-
let confidenceInstruction;
|
|
494
510
|
if (readiness.state === 'ready') {
|
|
495
511
|
emit('USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is ready and live; the grounding hooks are active.');
|
|
496
|
-
confidenceInstruction = `Open your FIRST response with ONE short, warm confirmation in your own words (2-3 lines, then move on; never repeat it this session). It must say "🧠 RuvNet Brain active (v${bannerVersion})" — ONE version, in parentheses, always; never a second number, never a bundle tag beside it — and convey: it grounds rUv's stack (RVF, Ruflo, AgentDB, SPARC, agentic-flow…) in his real source rather than guessing; npx github:stuinfla/ruvnet-brain --doctor checks it; ${consoleInvoke} opens a visual settings page.`;
|
|
497
512
|
} else if (readiness.state === 'degraded') {
|
|
498
513
|
const receipt = readiness.receipt || {};
|
|
499
514
|
emit(`USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered but degraded (${receipt.phase || 'startup'}: ${receipt.error || 'readiness failed'}); the grounding hooks remain active.`);
|
|
500
|
-
confidenceInstruction = `Open your FIRST response with ONE short line: "🧠 RuvNet Brain active (v${bannerVersion}) — search is degraded right now." Do not claim source grounding until a real search succeeds. npx github:stuinfla/ruvnet-brain --doctor shows the current verdict; ${consoleInvoke} opens the Console.`;
|
|
501
515
|
} else {
|
|
502
516
|
emit('USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered; live readiness is not yet proven. The grounding hooks are active.');
|
|
503
|
-
confidenceInstruction = `Open your FIRST response with ONE short line: "🧠 RuvNet Brain active (v${bannerVersion}) — search is registered and will prove readiness on first use." Do not claim source grounding until a real search returns a citation. npx github:stuinfla/ruvnet-brain --doctor shows the current verdict; ${consoleInvoke} opens the Console.`;
|
|
504
517
|
}
|
|
505
|
-
emit(confidenceInstruction);
|
|
506
518
|
}
|
|
507
|
-
|
|
508
|
-
const claudeJson = read(path.join(home, '.claude.json'));
|
|
509
|
-
if (commandExists('ruflo', env, platform) || /"ruflo"/.test(claudeJson)
|
|
510
|
-
|| exists(path.join(cwd, '.claude-flow')) || exists(path.join(cwd, '.swarm'))) {
|
|
511
|
-
emit('[RuvNet Brain — token intelligence + QE, mention once]');
|
|
512
|
-
emit('Right after the confidence line, ONE line, never repeated this session: token-smart routing, a QE test fleet and a 0-100 repo scorecard are available — say "do this cheaper", "QE this", "score my harness", "score this repo", /brain-build or /brain-prompt. Scoring is free; the self-improvement loop and cheap-model routing need an OPENROUTER_API_KEY.');
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const playbook = `${hookDir}${path.sep}..${path.sep}skills${path.sep}ruvnet-brain${path.sep}PLAYBOOK.md`;
|
|
516
|
-
emit('[RuvNet Brain — standing build playbook for this session (referenced by later turns as THE PLAYBOOK)]');
|
|
517
|
-
emit(`Read ${playbook} before the first build response. It requires source inspection, search_ruvnet grounding, project-memory recall, and the real owning rUv tool—never a silent hand-roll or generic substitute.`);
|
|
518
519
|
}
|
|
519
520
|
} catch (error) {
|
|
520
521
|
if (env.RUVNET_SESSION_TRACE === '1') stderr.write(`SESSION_TRACE native-fail-open ${error?.message || error}\n`);
|
|
521
522
|
}
|
|
522
523
|
|
|
524
|
+
if (env.RUVNET_VERBOSE_HOOKS !== '1') {
|
|
525
|
+
lines.push(`[RuvNet Brain v${running || 'unknown'} — active this session]`);
|
|
526
|
+
}
|
|
523
527
|
const output = lines.length ? `${lines.join('\n')}\n` : '';
|
|
524
528
|
meter({ env, cwd, stateDir, output });
|
|
525
529
|
stdout.write(output);
|
|
@@ -65,7 +65,12 @@ export const METERED_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];
|
|
|
65
65
|
export const FLEET_RUNNERS = [
|
|
66
66
|
{ name: 'the QE fleet', match: /(^|[|;&\s])(aqe|agentic-qe)\b/ },
|
|
67
67
|
{ name: 'the agent-flow orchestrator', match: /(^|[|;&\s])agentic-flow\b/ },
|
|
68
|
-
|
|
68
|
+
// Match Ruflo's COMMAND position, not an arbitrary later token. The former `[^|;&]*swarm`
|
|
69
|
+
// search classified every mandatory `ruflo memory ... --path .swarm/memory.db` checkpoint as a
|
|
70
|
+
// fleet and blocked it whenever the hook process had a metered key. Ruflo's live help permits
|
|
71
|
+
// global flags before the command, so consume only those known flags and then require a genuine
|
|
72
|
+
// fleet command. An absolute global-binary path is invocation-equivalent to bare `ruflo`.
|
|
73
|
+
{ name: 'a ruflo swarm', match: /(^|[|;&\s])(?:[^\s|;&]*\/)?ruflo\b(?:\s+(?:(?:-v|--verbose|-Q|--quiet|--no-color|-i|--interactive)|(?:-c|--config|--format)(?:=[^\s|;&]+|\s+[^\s|;&]+)))*\s+(?:swarm|hive-mind|agent\s+spawn|task\s+orchestrate)\b/ },
|
|
69
74
|
{ name: 'flow-nexus', match: /(^|[|;&\s])flow-nexus\b/ },
|
|
70
75
|
];
|
|
71
76
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { verifyCapabilityClaimAggregate } from '../plugin/scripts/capability-claim-evidence.mjs';
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const REQUIRED_REVIEWERS = Object.freeze(['claude-fable-5', 'gpt-5.6-sol']);
|
|
10
|
+
|
|
11
|
+
const command = (cwd, bin, args) => {
|
|
12
|
+
const result = spawnSync(bin, args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
13
|
+
if (result.status !== 0) throw new Error(`${bin} ${args.join(' ')} failed: ${String(result.stderr || result.stdout).trim()}`);
|
|
14
|
+
return String(result.stdout || '').trim();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function evaluateCompletion({ root = ROOT, run = command,
|
|
18
|
+
receiptFile = path.join(root, 'release-evidence', 'product-integrity-receipt.json'),
|
|
19
|
+
capabilityClaimPublicKey = null } = {}) {
|
|
20
|
+
const failures = [];
|
|
21
|
+
let head = null;
|
|
22
|
+
let version = null;
|
|
23
|
+
const check = (label, fn) => { try { fn(); } catch (error) { failures.push(`${label}: ${error.message}`); } };
|
|
24
|
+
|
|
25
|
+
check('worktree', () => {
|
|
26
|
+
const dirty = run(root, 'git', ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
27
|
+
if (dirty) throw new Error('working tree is not clean');
|
|
28
|
+
if (run(root, 'git', ['branch', '--show-current']) !== 'main') throw new Error('release checkout is not main');
|
|
29
|
+
head = run(root, 'git', ['rev-parse', 'HEAD']);
|
|
30
|
+
const remote = run(root, 'git', ['rev-parse', 'origin/main']);
|
|
31
|
+
if (head !== remote) throw new Error(`HEAD ${head} differs from origin/main ${remote}`);
|
|
32
|
+
});
|
|
33
|
+
check('version', () => {
|
|
34
|
+
version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
35
|
+
const npmVersion = JSON.parse(run(root, 'npm', ['view', 'ruvnet-brain@latest', 'version', '--json']));
|
|
36
|
+
if (npmVersion !== version) throw new Error(`npm latest ${npmVersion} differs from ${version}`);
|
|
37
|
+
const release = JSON.parse(run(root, 'gh', ['release', 'view', `v${version}`, '--repo', 'stuinfla/ruvnet-brain',
|
|
38
|
+
'--json', 'tagName,targetCommitish,isDraft,isPrerelease']));
|
|
39
|
+
if (release.tagName !== `v${version}` || release.targetCommitish !== head || release.isDraft || release.isPrerelease) {
|
|
40
|
+
throw new Error('GitHub release does not expose the exact main candidate');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
check('public proof', () => {
|
|
44
|
+
if (!fs.existsSync(receiptFile)) throw new Error(`missing ${receiptFile}`);
|
|
45
|
+
const receipt = JSON.parse(fs.readFileSync(receiptFile, 'utf8'));
|
|
46
|
+
if (receipt.schemaVersion !== 1 || receipt.kind !== 'ruvnet-brain-product-integrity'
|
|
47
|
+
|| receipt.verdict !== 'PASS' || receipt.sourceSha !== head || receipt.version !== version) {
|
|
48
|
+
throw new Error('product integrity receipt identity or verdict is invalid');
|
|
49
|
+
}
|
|
50
|
+
if (receipt.release?.state !== 'install-verified' || receipt.release?.publicAggregateVerified !== true) {
|
|
51
|
+
throw new Error('release is not cryptographically install-verified');
|
|
52
|
+
}
|
|
53
|
+
if (receipt.coverage?.eligibleCurrent !== receipt.coverage?.eligibleTotal
|
|
54
|
+
|| receipt.coverage?.gistCurrent !== receipt.coverage?.gistTotal) {
|
|
55
|
+
throw new Error('installed public corpus is incomplete');
|
|
56
|
+
}
|
|
57
|
+
if (receipt.retrieval?.deltaCitationRate !== 1 || receipt.retrieval?.recallAt10 < 0.98
|
|
58
|
+
|| receipt.retrieval?.skipped !== 0 || receipt.retrieval?.unknown !== 0) {
|
|
59
|
+
throw new Error('retrieval acceptance is incomplete');
|
|
60
|
+
}
|
|
61
|
+
if (receipt.hosts?.passed !== 9 || receipt.hosts?.required !== 9) throw new Error('public 3x3 host matrix is incomplete');
|
|
62
|
+
if (receipt.lifecycle?.nativeRuns !== 2 || receipt.lifecycle?.secondVerdict !== 'noop'
|
|
63
|
+
|| receipt.lifecycle?.redundantCopyCount !== 0 || receipt.lifecycle?.withinRetentionBudget !== true) {
|
|
64
|
+
throw new Error('native two-run lifecycle proof is incomplete');
|
|
65
|
+
}
|
|
66
|
+
if (receipt.continuity?.verdict !== 'PASS'
|
|
67
|
+
|| receipt.continuity?.crashResumeVerified !== true
|
|
68
|
+
|| receipt.continuity?.concurrentWritersVerified !== true
|
|
69
|
+
|| receipt.continuity?.hosts?.join(',') !== 'claude,codex') {
|
|
70
|
+
throw new Error('cross-host AgentDB continuity proof is incomplete');
|
|
71
|
+
}
|
|
72
|
+
const capabilityKey = capabilityClaimPublicKey
|
|
73
|
+
|| fs.readFileSync(path.join(root, 'keys', 'ruvnet-brain-signing.pub.pem'));
|
|
74
|
+
verifyCapabilityClaimAggregate(receipt.capabilityClaims, capabilityKey);
|
|
75
|
+
if (receipt.capabilityClaims.verdict !== 'PASS'
|
|
76
|
+
|| receipt.capabilityClaims.identity.sourceSha !== head
|
|
77
|
+
|| receipt.capabilityClaims.os?.join(',') !== 'linux,macos,windows'
|
|
78
|
+
|| receipt.capabilityClaims.hosts?.join(',') !== 'claude,codex'
|
|
79
|
+
|| receipt.capabilityClaims.untested?.length) {
|
|
80
|
+
throw new Error('RuvNet capability-claim integrity proof is incomplete');
|
|
81
|
+
}
|
|
82
|
+
for (const reviewer of REQUIRED_REVIEWERS) {
|
|
83
|
+
const review = receipt.reviews?.find((row) => row.id === reviewer);
|
|
84
|
+
if (!review || review.score < 95 || review.sourceSha !== head || review.untested?.length) {
|
|
85
|
+
throw new Error(`${reviewer} review is missing, below 95, stale, or has untested scope`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (receipt.mechanicalScore < 98) throw new Error('mechanical whole-product score is below 98');
|
|
89
|
+
});
|
|
90
|
+
return { ok: failures.length === 0, head, version, receiptFile, failures };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
94
|
+
const receiptIndex = process.argv.indexOf('--receipt');
|
|
95
|
+
const result = evaluateCompletion({ receiptFile: receiptIndex >= 0 ? path.resolve(process.argv[receiptIndex + 1]) : undefined });
|
|
96
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
97
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
98
|
+
}
|
|
@@ -71,31 +71,34 @@ const L3 = [
|
|
|
71
71
|
{ q: 'implement a nearest-neighbour query against an .rvf store', expect: 'ruvector', apiRx: /query|search|knn|nearest/i },
|
|
72
72
|
{ q: 'store and recall an agent memory entry with AgentDB', expect: 'agentdb', apiRx: /store|recall|memory|insert|search/i },
|
|
73
73
|
];
|
|
74
|
-
// ── L4 ORCHESTRATE: run the real hook; assert
|
|
74
|
+
// ── L4 ORCHESTRATE: run the real hook; assert quiet build behavior and useful grounding ──────────
|
|
75
75
|
const L4 = [
|
|
76
76
|
{
|
|
77
77
|
name: 'newbie "go make magic"',
|
|
78
78
|
prompt: 'go make magic and build me an app that helps people learn',
|
|
79
|
-
//
|
|
80
|
-
must: [
|
|
79
|
+
// Build prompts must not inject a response script into the model context.
|
|
80
|
+
must: [],
|
|
81
|
+
mustNot: ['SPARC', 'DDD', 'QA gate', 'frontend-design', 'image generation', 'API key', 'PARALLEL', 'end your response'],
|
|
81
82
|
},
|
|
82
83
|
{
|
|
83
84
|
name: 'spec → build (RuvNet-named)',
|
|
84
85
|
prompt: 'here is a product spec, build a knowledge base feature using ruvector',
|
|
85
|
-
//
|
|
86
|
-
must: ['search_ruvnet', 'ground'
|
|
86
|
+
// RuvNet facts still receive a grounding reminder; build workflow prose stays silent.
|
|
87
|
+
must: ['search_ruvnet', 'ground'],
|
|
88
|
+
mustNot: ['take the wheel', 'SPARC', 'DDD', 'QA gate', 'frontend-design', 'image generation', 'API key', 'PARALLEL', 'end your response'],
|
|
87
89
|
},
|
|
88
90
|
{
|
|
89
91
|
name: 'classical-default drift',
|
|
90
92
|
prompt: 'set up pinecone and langchain to do rag over my docs',
|
|
91
|
-
//
|
|
92
|
-
must: ['classical default', 'RuVector', 'Ruflo', 'AgentDB'
|
|
93
|
+
// Drift guidance remains actionable, without the unrelated build playbook.
|
|
94
|
+
must: ['classical default', 'RuVector', 'Ruflo', 'AgentDB'],
|
|
95
|
+
mustNot: ['SPARC', 'DDD', 'QA gate', 'frontend-design', 'image generation', 'API key', 'PARALLEL', 'end your response'],
|
|
93
96
|
},
|
|
94
97
|
{
|
|
95
98
|
name: 'pure recall (no build)',
|
|
96
99
|
prompt: 'what can ruflo actually do',
|
|
97
100
|
must: ['search_ruvnet', 'ground'],
|
|
98
|
-
mustNot: ['take the wheel'
|
|
101
|
+
mustNot: ['take the wheel', 'SPARC', 'DDD', 'QA gate', 'frontend-design', 'image generation', 'API key', 'PARALLEL', 'end your response'], // no build script
|
|
99
102
|
},
|
|
100
103
|
];
|
|
101
104
|
|
package/scripts/brain-score.mjs
CHANGED
|
@@ -132,8 +132,8 @@ 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() {
|
|
136
|
-
const { storeRoot, storesAt, cardsAt } = await import('../kb/store-root.mjs');
|
|
135
|
+
export async function readCoverage(rootOverride) {
|
|
136
|
+
const { storeRoot, storesAt, cardsAt, rootNeverMaterialized } = await import('../kb/store-root.mjs');
|
|
137
137
|
// ROUTABILITY IS ALIAS-AWARE, BECAUSE THE ROUTER IS.
|
|
138
138
|
//
|
|
139
139
|
// `carded` was `stores.filter((s) => cards.has(s))` — a direct name match against card HEADINGS,
|
|
@@ -148,25 +148,39 @@ 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
|
+
//
|
|
159
|
+
// `rootNeverMaterialized()` (kb/store-root.mjs), not `fs.existsSync(root)`: the latter returns
|
|
160
|
+
// `true` for a stray FILE at `root` (ENOTDIR) or an unreadable directory (EACCES), so either
|
|
161
|
+
// rendered "materialized" here and let the exact same false-current-`0` back in for a different
|
|
162
|
+
// errno class than ENOENT — flagged as an accepted, unfixed gap by the ENOENT-only fix itself.
|
|
163
|
+
const neverMaterialized = rootNeverMaterialized(root);
|
|
152
164
|
const stores = storesAt(root);
|
|
153
165
|
const cards = new Set(cardsAt(root));
|
|
154
166
|
const org = readJson('data/org-repo-count.json');
|
|
155
167
|
const total = Number.isFinite(org?.count) && org.count > 0 ? org.count : null;
|
|
156
168
|
const carded = stores.filter((s) => isRoutable(s, cards, root, repositoryNames)).length;
|
|
169
|
+
const absentDetail = 'store root does not exist on this host (never materialized) — not evidence of live coverage';
|
|
157
170
|
return {
|
|
158
171
|
catalogue: {
|
|
159
|
-
value: total ? Math.round((stores.length / total) * 1000) / 10 : null,
|
|
160
|
-
detail:
|
|
161
|
-
|
|
172
|
+
value: neverMaterialized ? null : (total ? Math.round((stores.length / total) * 1000) / 10 : null),
|
|
173
|
+
detail: neverMaterialized ? absentDetail
|
|
174
|
+
: (total ? `${stores.length}/${total} live repos` : `${stores.length} stores, org total UNKNOWN`),
|
|
175
|
+
at: neverMaterialized ? null : (org?.at ?? null),
|
|
162
176
|
},
|
|
163
177
|
routable: {
|
|
164
|
-
value: stores.length ? Math.round((carded / stores.length) * 1000) / 10
|
|
165
|
-
detail: `${carded}/${stores.length} built stores have a card`,
|
|
178
|
+
value: neverMaterialized || !stores.length ? null : Math.round((carded / stores.length) * 1000) / 10,
|
|
179
|
+
detail: neverMaterialized ? absentDetail : `${carded}/${stores.length} built stores have a card`,
|
|
166
180
|
// Derived from the live store root at this instant, so it is current BY CONSTRUCTION. A
|
|
167
181
|
// read-now value reported as stale would train the reader to ignore the stale flag, which
|
|
168
182
|
// costs more than the flag is worth.
|
|
169
|
-
at: new Date().toISOString(),
|
|
183
|
+
at: neverMaterialized ? null : new Date().toISOString(),
|
|
170
184
|
},
|
|
171
185
|
};
|
|
172
186
|
}
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// node scripts/build-bundle.mjs [--assets /path/to/release-assets]
|
|
14
14
|
// [--out dist/ruvnet-brain] [--version v0.2.0-dev]
|
|
15
|
+
import crypto from 'node:crypto';
|
|
15
16
|
import fs from 'node:fs';
|
|
16
17
|
import os from 'node:os';
|
|
17
18
|
import path from 'node:path';
|
|
@@ -19,6 +20,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { getVersion, getVersionTag, stripTag } from './version.mjs';
|
|
21
22
|
import { auditRvfIndexes } from './rvf-index-audit.mjs';
|
|
23
|
+
import { readRvfGenerations, validateSelectedRvfGenerations } from './rvf-generation.mjs';
|
|
24
|
+
import { validatePublicInventory } from './public-inventory.mjs';
|
|
22
25
|
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
23
26
|
// sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
|
|
24
27
|
import { orgRepoCount } from './org-repo-count.mjs';
|
|
@@ -32,6 +35,8 @@ const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && proc
|
|
|
32
35
|
// source/runtime files rooted in this exact checkout; only generated RVF families come from ASSETS.
|
|
33
36
|
const ASSETS = path.resolve(ROOT, arg('--assets', 'kb'));
|
|
34
37
|
const OUT = path.resolve(ROOT, arg('--out', 'dist/ruvnet-brain'));
|
|
38
|
+
const COVERAGE = arg('--coverage', null);
|
|
39
|
+
const PROJECTION = arg('--projection', null);
|
|
35
40
|
const BRAIN_VERSION = arg('--version', getVersionTag()); // inherits the single source of truth
|
|
36
41
|
|
|
37
42
|
// ---- registry: tier + the full 169-repo pending list -------------------------------------------
|
|
@@ -156,12 +161,58 @@ fs.rmSync(OUT, { recursive: true, force: true });
|
|
|
156
161
|
// A failed rebuild must not leave an older archive looking like this invocation's output.
|
|
157
162
|
fs.rmSync(ZIP, { force: true });
|
|
158
163
|
fs.mkdirSync(OUT, { recursive: true });
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
// The shipped bundle must carry the exact policy boundary used during assembly. The public
|
|
165
|
+
// inventory validator consumes this file from the candidate output, so reading it only from the
|
|
166
|
+
// source checkout would leave the release projection unable to prove its private-store exclusion.
|
|
167
|
+
cp(path.join(KB, 'PRIVATE-STORES.json'), OUT, { required: true });
|
|
168
|
+
|
|
169
|
+
let built = discoverBuilt();
|
|
170
|
+
let excludedPublicStores = [];
|
|
171
|
+
if (COVERAGE) {
|
|
172
|
+
const coverageFile = path.resolve(COVERAGE);
|
|
173
|
+
if (!fs.existsSync(coverageFile)) {
|
|
174
|
+
console.error(`[build-bundle] FATAL: coverage policy missing (${coverageFile})`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
let coverage;
|
|
178
|
+
try { coverage = JSON.parse(fs.readFileSync(coverageFile, 'utf8')); }
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error(`[build-bundle] FATAL: coverage policy unreadable (${error.message})`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
let inventory;
|
|
184
|
+
try {
|
|
185
|
+
inventory = validatePublicInventory({ assetsDir: ASSETS, coverage, ledger: readRvfGenerations(ASSETS) });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`[build-bundle] FATAL: public inventory is not exhaustive (${error.message})`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const selected = new Set(inventory.publicStores);
|
|
191
|
+
built = built.filter((store) => selected.has(store.toLowerCase()));
|
|
192
|
+
excludedPublicStores = inventory.excludedRepositories;
|
|
193
|
+
const missing = inventory.publicStores.filter((store) => !built.some((candidate) => candidate.toLowerCase() === store));
|
|
194
|
+
if (missing.length) {
|
|
195
|
+
console.error(`[build-bundle] FATAL: classified public store(s) missing: ${missing.join(', ')}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (excludedPublicStores.length) {
|
|
199
|
+
console.log(`[build-bundle] EXCLUDED ${excludedPublicStores.length} policy-ineligible public store(s): ${excludedPublicStores.join(', ')}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
161
202
|
if (built.length === 0) {
|
|
162
203
|
console.error('[build-bundle] FATAL: zero public RVF stores are eligible for release. Refusing to publish an empty brain bundle.');
|
|
163
204
|
process.exit(1);
|
|
164
205
|
}
|
|
206
|
+
const generationValidation = validateSelectedRvfGenerations(ASSETS, {
|
|
207
|
+
selectedStores: built,
|
|
208
|
+
privateStores: [...PRIVATE_STORES],
|
|
209
|
+
excludedStores: excludedPublicStores,
|
|
210
|
+
});
|
|
211
|
+
if (generationValidation.failures.length) {
|
|
212
|
+
console.error('[build-bundle] FATAL: RVF generation ledger does not exactly bind selected roots:');
|
|
213
|
+
for (const failure of generationValidation.failures) console.error(` ${failure}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
165
216
|
const rvfIndexAudit = await auditRvfIndexes(
|
|
166
217
|
built.map((name) => path.join(ASSETS, `${name}.big.rvf`)),
|
|
167
218
|
);
|
|
@@ -208,7 +259,7 @@ for (const name of built) {
|
|
|
208
259
|
if (!fs.existsSync(generationsFile)) {
|
|
209
260
|
missing.push('RVF-GENERATIONS.json');
|
|
210
261
|
} else {
|
|
211
|
-
const source =
|
|
262
|
+
const source = generationValidation.manifest;
|
|
212
263
|
const stores = {};
|
|
213
264
|
for (const { name } of builtRepos) {
|
|
214
265
|
if (!source.stores?.[name]) missing.push(`RVF-GENERATIONS.json:${name}`);
|
|
@@ -216,8 +267,8 @@ for (const name of built) {
|
|
|
216
267
|
}
|
|
217
268
|
fs.writeFileSync(path.join(OUT, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
218
269
|
schemaVersion: source.schemaVersion,
|
|
219
|
-
brainVersion:
|
|
220
|
-
releaseTag:
|
|
270
|
+
brainVersion: stripTag(BRAIN_VERSION),
|
|
271
|
+
releaseTag: BRAIN_VERSION,
|
|
221
272
|
stores,
|
|
222
273
|
}, null, 2)}\n`);
|
|
223
274
|
copied++;
|
|
@@ -233,14 +284,16 @@ cpDir(path.join(ROOT, 'primer'), path.join(OUT, 'primer'));
|
|
|
233
284
|
// time so code-implemented capabilities are retrievable as high-confidence prose. discoverRepos in the
|
|
234
285
|
// bundle finds concepts.big.rvf automatically, so search_ruvnet searches it with no extra config.
|
|
235
286
|
const hasConcepts = fs.existsSync(path.join(ASSETS, 'concepts.big.rvf'));
|
|
236
|
-
if (hasConcepts) for (const suf of ['concepts.big.rvf', 'concepts.big.rvf.idmap.json', 'concepts.big.rvf.embed.json', 'concepts.big.passages.jsonl', 'concepts.big.meta.json', 'concepts.passages.jsonl', 'concepts.meta.json']) cp(suf, OUT, { asset: true });
|
|
287
|
+
if (hasConcepts) for (const suf of ['concepts.big.rvf', 'concepts.big.rvf.idmap.json', 'concepts.big.rvf.embed.json', 'concepts.big.passages.jsonl', 'concepts.big.meta.json', 'concepts.passages.jsonl', 'concepts.meta.json', 'concepts.sources.json']) cp(suf, OUT, { asset: true });
|
|
237
288
|
|
|
238
289
|
// RUV-GISTS store (rUv's public gists — release notes / integration dossiers; big-only, same shape as
|
|
239
290
|
// concepts, unioned by search_ruvnet at query time). BUG FIX: only `concepts` was special-cased above,
|
|
240
291
|
// so ruv-gists — a PUBLIC store, not private-fenced — was silently omitted from EVERY bundle ever
|
|
241
292
|
// shipped. Include it, WITH .big.passages.jsonl (the big variant opens it by name, same as concepts).
|
|
242
293
|
const hasGists = fs.existsSync(path.join(ASSETS, 'ruv-gists.big.rvf'));
|
|
243
|
-
if (hasGists) for (const suf of ['ruv-gists.big.rvf', 'ruv-gists.big.rvf.idmap.json', 'ruv-gists.big.rvf.embed.json', 'ruv-gists.big.passages.jsonl', 'ruv-gists.big.meta.json', 'ruv-gists.passages.jsonl', 'ruv-gists.meta.json']) cp(suf, OUT, { asset: true });
|
|
294
|
+
if (hasGists) for (const suf of ['ruv-gists.big.rvf', 'ruv-gists.big.rvf.idmap.json', 'ruv-gists.big.rvf.embed.json', 'ruv-gists.big.passages.jsonl', 'ruv-gists.big.meta.json', 'ruv-gists.passages.jsonl', 'ruv-gists.meta.json', 'ruv-gists.sources.json']) cp(suf, OUT, { asset: true });
|
|
295
|
+
// The inventory projection consumes this registry from the assembled bundle, not the checkout.
|
|
296
|
+
cp('public-store-classes.json', OUT, { asset: true });
|
|
244
297
|
|
|
245
298
|
// capability-cards.md — the FAST LANE's zero-ML answer source (kb/card-lane.mjs, the first
|
|
246
299
|
// responder search_ruvnet consults before the heavy cross-repo search). Ships as its own small
|
|
@@ -414,6 +467,14 @@ cp(path.join(ROOT, 'scripts', 'verify-bundle.mjs'), OUT, { required: true });
|
|
|
414
467
|
fs.mkdirSync(path.join(OUT, 'keys'), { recursive: true });
|
|
415
468
|
cp(path.join(ROOT, 'keys', 'ruvnet-brain-signing.pub.pem'), path.join(OUT, 'keys'), { required: true });
|
|
416
469
|
|
|
470
|
+
// ReleaseProjection is generated from the exact candidate asset tree immediately before this
|
|
471
|
+
// assembly. Copy all three linked ledgers together; a partial projection is never publishable.
|
|
472
|
+
if (PROJECTION) {
|
|
473
|
+
for (const file of ['COVERAGE.json', 'CORPUS-COVERAGE.json', 'PUBLIC-RVF-GENERATIONS.json']) {
|
|
474
|
+
cp(path.join(path.resolve(PROJECTION), file), OUT, { required: true });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
417
478
|
// ---- manifest ----------------------------------------------------------------------------------
|
|
418
479
|
const builtLower = new Set(built.map((b) => b.toLowerCase()));
|
|
419
480
|
const pendingRepos = regFlat.filter((r) => !builtLower.has(r.name.toLowerCase())).map((r) => ({ name: r.name, tier: r.tier }));
|
|
@@ -506,7 +567,32 @@ function collectArchiveFiles(dir, prefix = '') {
|
|
|
506
567
|
}
|
|
507
568
|
}
|
|
508
569
|
collectArchiveFiles(OUT);
|
|
509
|
-
archiveFiles.sort();
|
|
570
|
+
archiveFiles.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
571
|
+
// Bind the exact archive payload before release verification. The manifest excludes itself;
|
|
572
|
+
// verifiers recompute the same payload set and then validate this file's identities.
|
|
573
|
+
function archiveIdentity(relative) {
|
|
574
|
+
const file = path.join(OUT, relative);
|
|
575
|
+
const hash = crypto.createHash('sha256');
|
|
576
|
+
const fd = fs.openSync(file, 'r');
|
|
577
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
578
|
+
try {
|
|
579
|
+
let bytes;
|
|
580
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
581
|
+
} finally { fs.closeSync(fd); }
|
|
582
|
+
return { path: relative.split(path.sep).join('/'), sha256: hash.digest('hex'), bytes: fs.statSync(file).size };
|
|
583
|
+
}
|
|
584
|
+
const archiveManifest = {
|
|
585
|
+
schemaVersion: 1,
|
|
586
|
+
kind: 'ruvnet-brain-archive-manifest',
|
|
587
|
+
version: stripTag(BRAIN_VERSION),
|
|
588
|
+
releaseTag: BRAIN_VERSION,
|
|
589
|
+
fileCount: archiveFiles.length,
|
|
590
|
+
totalBytes: archiveFiles.reduce((total, file) => total + fs.statSync(path.join(OUT, file)).size, 0),
|
|
591
|
+
files: archiveFiles.map(archiveIdentity),
|
|
592
|
+
};
|
|
593
|
+
fs.writeFileSync(path.join(OUT, 'ARCHIVE-MANIFEST.json'), `${JSON.stringify(archiveManifest, null, 2)}\n`);
|
|
594
|
+
archiveFiles.push('ARCHIVE-MANIFEST.json');
|
|
595
|
+
archiveFiles.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
510
596
|
const zipped = process.platform === 'win32'
|
|
511
597
|
? spawnSync('powershell.exe', [
|
|
512
598
|
'-NoProfile',
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
// Repository names may contain RegExp metacharacters (`ruv.io` is already in this corpus). The
|
|
80
|
+
// heading lookup is literal identity, so escape the name before compiling it.
|
|
81
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
82
|
+
|
|
83
|
+
export function insertSorted(file, card, name) {
|
|
84
|
+
const s = fs.readFileSync(file, 'utf8');
|
|
85
|
+
if (new RegExp(`^## ${escapeRegExp(name)}$`, 'mi').test(s)) return false;
|
|
86
|
+
const heads = [...s.matchAll(/^## (.+)$/gm)].map((m) => [m.index, m[1].trim().toLowerCase()]);
|
|
87
|
+
const pos = heads.find(([, n]) => n > name)?.[0];
|
|
88
|
+
fs.writeFileSync(file, pos === undefined ? `${s.replace(/\n+$/, '')}\n\n${card}` : s.slice(0, pos) + card + '\n' + s.slice(pos));
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Apply one grounded card to every required surface without turning a failed write into success. */
|
|
93
|
+
export function applyCardToFiles(files, card, name, insert = insertSorted) {
|
|
94
|
+
let changed = 0;
|
|
95
|
+
const failures = [];
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
try {
|
|
98
|
+
if (insert(file, card, name)) changed += 1;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
failures.push({ file, error: String(error?.message || error) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { changed, failures };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isMain = (() => {
|
|
107
|
+
try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
|
|
108
|
+
catch { return false; }
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
if (isMain) {
|
|
112
|
+
const root = storeRoot();
|
|
113
|
+
const dark = darkStores(root);
|
|
114
|
+
console.log(`[card] ${dark.length} dark store(s) — valid bytes no by-description query can reach`);
|
|
115
|
+
if (!dark.length) process.exit(0);
|
|
116
|
+
if (!APPLY) {
|
|
117
|
+
console.log(`[card] ${dark.slice(0, 20).join(', ')}${dark.length > 20 ? ' …' : ''}`);
|
|
118
|
+
console.log('[card] report only. Re-run with --apply to write cards from each repo\'s own source.');
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
const repoCards = path.join(ROOT, 'kb', 'capability-cards.md');
|
|
122
|
+
const liveCards = path.join(root, 'capability-cards.md');
|
|
123
|
+
let wrote = 0; let skipped = 0; let failed = 0;
|
|
124
|
+
for (const name of dark.slice(0, MAX)) {
|
|
125
|
+
const card = buildCard(name, sourceFacts(name));
|
|
126
|
+
if (!card) { skipped += 1; console.log(`[card] ${name}: no description and no README prose — left dark rather than invented`); continue; }
|
|
127
|
+
const outcome = applyCardToFiles([repoCards, liveCards], card, name.toLowerCase());
|
|
128
|
+
if (outcome.failures.length) {
|
|
129
|
+
failed += 1;
|
|
130
|
+
console.error(`[card] ${name}: FAILED/PARTIAL (${outcome.changed}/2 surfaces changed) — `
|
|
131
|
+
+ outcome.failures.map(({ file, error }) => `${file}: ${error}`).join('; '));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (outcome.changed > 0) wrote += 1;
|
|
135
|
+
else skipped += 1;
|
|
136
|
+
}
|
|
137
|
+
const remaining = darkStores(root).length;
|
|
138
|
+
console.log(`\n[card] wrote ${wrote}, skipped ${skipped}, failed ${failed}, ${remaining} still dark.`);
|
|
139
|
+
if (failed) process.exitCode = 1;
|
|
140
|
+
}
|