mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
orphan_ratio's denominator is swamped on auto_chronicle brains by the machine-generated chronicle events (life/events/<day>-<hash>, written per eligible event) — no inbound links by design. The shipped policy already excludes raw/atoms/skills/dreaming/daily and extracts/, but life/events/ was still counted; on a 1,657-page auto_chronicle brain it was ~72% of the orphan mass, enough to pin the ratio red. Add 'life/events/' to DENY_PREFIXES — a scoped prefix, NOT the whole `life/` first-segment, so human-authored life/diary/ (gbrain capture --type diary) stays IN the denominator. Same shipped hardcoded-class mechanism as the existing entries; not the #2215 user-config route (closed not_planned). Knowledge classes (concepts/people/notes/projects) also stay in, so genuine graph decay still trips. Regression in test/orphans-pure-fn.test.ts: life/events/ now excluded (fails before, passes after); life/diary/ and concepts//notes//projects/ pinned as still-counted. doctor's orphan_ratio uses the same shouldExclude path (getOrphansData; local + doctor-remote MCP), covered transitively.
131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
/**
|
|
* Shared orphan-reporting exclusion policy.
|
|
*
|
|
* These are pages where "no inbound links" is expected and should not count
|
|
* against health. Keep this in core so the CLI orphan report and engine health
|
|
* dashboard cannot drift.
|
|
*
|
|
* Defaults are GBrain-wide conventions only. Brain-specific exclusions
|
|
* (private folder names, one-off fixture slugs) belong in the brain's own
|
|
* config, not here:
|
|
*
|
|
* gbrain config set orphans.exclude_prefixes "my-private-folder/,archive/"
|
|
* gbrain config set orphans.exclude_slugs "some-one-off-page"
|
|
*/
|
|
|
|
// '/readme' — a README is a folder descriptor, not a knowledge node;
|
|
// nothing is expected to wikilink to it.
|
|
const AUTO_SUFFIX_PATTERNS = ['/_index', '/log', '/readme'];
|
|
|
|
// 'readme' / 'index' — root-level folder descriptors, same rationale as the
|
|
// '/readme' suffix. 'schema' — written by the schema pack on init; 'log' —
|
|
// the root brain log.
|
|
const PSEUDO_SLUGS = new Set(['_atlas', '_index', '_stats', '_orphans', '_scratch', 'claude', 'readme', 'index', 'schema', 'log']);
|
|
|
|
const RAW_SEGMENT = '/raw/';
|
|
|
|
const DENY_PREFIXES = [
|
|
'output/',
|
|
'outputs/',
|
|
'dashboards/',
|
|
'scripts/',
|
|
'templates/',
|
|
'_templates/',
|
|
'openclaw/config/',
|
|
'extracts/',
|
|
// auto_chronicle event volume (life/events/<day>-<hash>) — machine leaf, no
|
|
// inbound links by design. Deny-prefix (not whole `life/` first-segment) so
|
|
// human-authored life/diary/ stays IN the orphan denominator. (#2264)
|
|
'life/events/',
|
|
];
|
|
|
|
const FIRST_SEGMENT_EXCLUSIONS = new Set([
|
|
'scratch',
|
|
'thoughts',
|
|
'catalog',
|
|
'entities',
|
|
'raw',
|
|
'atoms',
|
|
'skills',
|
|
'dreaming',
|
|
'daily',
|
|
// 'inbox' — GTD-style intake tray: dated collector records in transit
|
|
// (email digests, alerts) awaiting triage; nothing links INTO an inbox
|
|
// item, same rationale as 'daily'.
|
|
'inbox',
|
|
]);
|
|
|
|
const ROOT_DATE_SLUG = /^\d{4}-\d{2}-\d{2}(?:-.+)?$/;
|
|
|
|
function isAgentWorkspaceConvention(slug: string): boolean {
|
|
if (!slug.startsWith('agents/')) return false;
|
|
if (slug.includes('/memory/dreaming/')) return true;
|
|
return /^agents\/[^/]+\/(?:agents|identity|soul|tools|user|heartbeat|dreams|dormant)$/.test(slug);
|
|
}
|
|
|
|
/** Per-brain additions to the convention defaults (from config). */
|
|
export interface OrphanPolicyOverrides {
|
|
excludePrefixes?: string[];
|
|
excludeSlugs?: string[];
|
|
}
|
|
|
|
/** Config keys for per-brain orphan exclusions (comma-separated values). */
|
|
export const ORPHAN_EXCLUDE_PREFIXES_KEY = 'orphans.exclude_prefixes';
|
|
export const ORPHAN_EXCLUDE_SLUGS_KEY = 'orphans.exclude_slugs';
|
|
|
|
function parseList(value: string | null): string[] {
|
|
if (!value) return [];
|
|
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Load per-brain orphan exclusions from the brain config table. Callers with
|
|
* an engine in hand (getHealth, `gbrain orphans`) pass the result as the
|
|
* second argument to shouldExcludeFromOrphanReporting.
|
|
*/
|
|
export async function loadOrphanPolicyOverrides(
|
|
engine: { getConfig(key: string): Promise<string | null> },
|
|
): Promise<OrphanPolicyOverrides> {
|
|
const [prefixes, slugs] = await Promise.all([
|
|
engine.getConfig(ORPHAN_EXCLUDE_PREFIXES_KEY),
|
|
engine.getConfig(ORPHAN_EXCLUDE_SLUGS_KEY),
|
|
]);
|
|
return { excludePrefixes: parseList(prefixes), excludeSlugs: parseList(slugs) };
|
|
}
|
|
|
|
export function shouldExcludeFromOrphanReporting(
|
|
slug: string,
|
|
overrides?: OrphanPolicyOverrides,
|
|
): boolean {
|
|
if (PSEUDO_SLUGS.has(slug)) return true;
|
|
|
|
for (const suffix of AUTO_SUFFIX_PATTERNS) {
|
|
if (slug.endsWith(suffix)) return true;
|
|
}
|
|
|
|
if (slug.includes(RAW_SEGMENT)) return true;
|
|
if (slug.includes('/daily/')) return true;
|
|
|
|
for (const prefix of DENY_PREFIXES) {
|
|
if (slug.startsWith(prefix)) return true;
|
|
}
|
|
|
|
const firstSegment = slug.split('/')[0];
|
|
if (FIRST_SEGMENT_EXCLUSIONS.has(firstSegment)) return true;
|
|
|
|
if (ROOT_DATE_SLUG.test(slug)) return true;
|
|
|
|
if (slug.startsWith('_brain-')) return true;
|
|
|
|
if (isAgentWorkspaceConvention(slug)) return true;
|
|
|
|
if (overrides) {
|
|
if (overrides.excludeSlugs?.includes(slug)) return true;
|
|
for (const prefix of overrides.excludePrefixes ?? []) {
|
|
if (slug.startsWith(prefix)) return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|