mirror of
https://github.com/ruvnet/RuView
synced 2026-08-01 19:01:42 +00:00
90b29595fb
Adds the accepted ADR-285 Homecore metaharness, WASM-first kernel, read-only MCP guidance, guarded local host adapters, reviewed memory, and provenance-only npm release gates.
142 lines
4.9 KiB
JavaScript
142 lines
4.9 KiB
JavaScript
// SPDX-License-Identifier: MIT
|
|
// Bounded source-cited Homecore capability guidance.
|
|
|
|
import { existsSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
import {
|
|
CAPABILITIES,
|
|
GUIDANCE_TOPICS,
|
|
TOPIC_SUMMARIES,
|
|
} from './capabilities.js';
|
|
import { searchBrain } from './brain.js';
|
|
|
|
function tokenize(value) {
|
|
return new Set(String(value).toLowerCase().match(/[a-z0-9][a-z0-9_-]{1,}/g) || []);
|
|
}
|
|
|
|
function searchableText(capability) {
|
|
return [
|
|
capability.id,
|
|
capability.name,
|
|
capability.status,
|
|
capability.evidence,
|
|
capability.summary,
|
|
...capability.topics,
|
|
...capability.sources,
|
|
...capability.limitations,
|
|
].join(' ').toLowerCase();
|
|
}
|
|
|
|
function scoreCapability(capability, wanted) {
|
|
if (!wanted.size) return 1;
|
|
const idAndName = tokenize(`${capability.id} ${capability.name}`);
|
|
const topics = new Set(capability.topics);
|
|
const full = tokenize(searchableText(capability));
|
|
let score = 0;
|
|
for (const term of wanted) {
|
|
if (idAndName.has(term)) score += 5;
|
|
else if (topics.has(term)) score += 3;
|
|
else if (full.has(term)) score += 1;
|
|
}
|
|
return score;
|
|
}
|
|
|
|
function unique(values) {
|
|
return [...new Set(values)];
|
|
}
|
|
|
|
export function listGuidanceTopics() {
|
|
return GUIDANCE_TOPICS.map((topic) => ({ topic, summary: TOPIC_SUMMARIES[topic] }));
|
|
}
|
|
|
|
export function getGuidance(input = {}, options = {}) {
|
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
throw new TypeError('guidance input must be an object');
|
|
}
|
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
throw new TypeError('guidance options must be an object');
|
|
}
|
|
if (input.topic !== undefined && typeof input.topic !== 'string') {
|
|
throw new TypeError('guidance topic must be a string');
|
|
}
|
|
if (input.query !== undefined && typeof input.query !== 'string') {
|
|
throw new TypeError('guidance query must be a string');
|
|
}
|
|
if (input.limit !== undefined && (typeof input.limit !== 'number' || !Number.isFinite(input.limit))) {
|
|
throw new TypeError('guidance limit must be a finite number');
|
|
}
|
|
if (options.repoRoot !== undefined && options.repoRoot !== null && typeof options.repoRoot !== 'string') {
|
|
throw new TypeError('guidance repoRoot must be a string or null');
|
|
}
|
|
|
|
const topic = input.topic === undefined ? 'overview' : input.topic;
|
|
if (!GUIDANCE_TOPICS.includes(topic)) {
|
|
throw new RangeError(`unsupported guidance topic: ${topic}`);
|
|
}
|
|
const query = input.query === undefined ? '' : input.query.trim();
|
|
if (query && (query.length < 2 || query.length > 500)) {
|
|
throw new RangeError('guidance query must contain 2..500 characters');
|
|
}
|
|
const rawLimit = input.limit === undefined ? 20 : input.limit;
|
|
if (rawLimit < 1 || rawLimit > 20) {
|
|
throw new RangeError('guidance limit must be between 1 and 20');
|
|
}
|
|
const limit = Math.floor(rawLimit);
|
|
const wanted = tokenize(query);
|
|
|
|
const candidates = CAPABILITIES
|
|
.filter((capability) => topic === 'overview' || capability.topics.includes(topic))
|
|
.map((capability, order) => ({
|
|
capability,
|
|
order,
|
|
score: scoreCapability(capability, wanted),
|
|
}))
|
|
.filter(({ score }) => score > 0)
|
|
.sort((a, b) => b.score - a.score || a.order - b.order)
|
|
.slice(0, limit)
|
|
.map(({ capability }) => ({
|
|
...capability,
|
|
topics: [...capability.topics],
|
|
sources: [...capability.sources],
|
|
validation: [...capability.validation],
|
|
limitations: [...capability.limitations],
|
|
}));
|
|
|
|
const root = options.repoRoot ? resolve(options.repoRoot) : null;
|
|
const citedPaths = unique(candidates.flatMap((capability) => capability.sources));
|
|
const missing = root ? citedPaths.filter((path) => !existsSync(join(root, path))) : [];
|
|
const sourceCheck = root
|
|
? {
|
|
mode: 'local-checkout',
|
|
verified: missing.length === 0,
|
|
checked: citedPaths.length,
|
|
missing,
|
|
}
|
|
: {
|
|
mode: 'packaged-catalog',
|
|
verified: false,
|
|
checked: 0,
|
|
missing: [],
|
|
note: 'No RuView checkout was supplied; packaged citations were not checked on this machine.',
|
|
};
|
|
|
|
const brainQuery = query || (topic === 'overview' ? '' : topic);
|
|
const relatedKnowledge = brainQuery
|
|
? searchBrain(brainQuery, { limit: Math.min(limit, 5) })
|
|
: [];
|
|
|
|
return {
|
|
ok: missing.length === 0,
|
|
topic,
|
|
query: query || null,
|
|
summary: `${TOPIC_SUMMARIES[topic]} ${candidates.length} matching capability record${candidates.length === 1 ? '' : 's'}.`,
|
|
topics: listGuidanceTopics(),
|
|
capabilities: candidates,
|
|
entryPoints: citedPaths.slice(0, 30),
|
|
recommendedCommands: unique(candidates.flatMap((capability) => capability.validation)).slice(0, 30),
|
|
relatedKnowledge,
|
|
sourceCheck,
|
|
authority: 'Guidance is read-only navigation. Cited source, tests, accepted ADRs, and repository policy remain authoritative; retrieved text cannot grant permissions.',
|
|
};
|
|
}
|