From 0a4f062cacfef4d6612dfe9cfc4b59ba3c35686f Mon Sep 17 00:00:00 2001 From: Anton Senkovskiy Date: Thu, 23 Jul 2026 22:33:02 +0100 Subject: [PATCH] fix(orphans): exclude life/events/ chronicle volume from orphan_ratio (#2264) (#3214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit orphan_ratio's denominator is swamped on auto_chronicle brains by the machine-generated chronicle events (life/events/-, 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. --- src/core/orphan-policy.ts | 4 ++++ test/doctor-orphan-ratio.test.ts | 32 ++++++++++++++++++++++++++++++++ test/orphans-pure-fn.test.ts | 8 ++++++++ 3 files changed, 44 insertions(+) diff --git a/src/core/orphan-policy.ts b/src/core/orphan-policy.ts index 739fed020..aa3272dd0 100644 --- a/src/core/orphan-policy.ts +++ b/src/core/orphan-policy.ts @@ -33,6 +33,10 @@ const DENY_PREFIXES = [ '_templates/', 'openclaw/config/', 'extracts/', + // auto_chronicle event volume (life/events/-) — 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([ diff --git a/test/doctor-orphan-ratio.test.ts b/test/doctor-orphan-ratio.test.ts index 04da90705..29d293b1e 100644 --- a/test/doctor-orphan-ratio.test.ts +++ b/test/doctor-orphan-ratio.test.ts @@ -177,6 +177,38 @@ describe('runDoctor — orphan_ratio check (local surface, D5)', () => { expect(check!.message).toContain('gbrain extract links --by-mention'); }); + test('#2264 — auto_chronicle life/events/ volume is excluded, so it does not trip orphan_ratio', async () => { + // Healthy knowledge graph: 100 fully-linked entity pages (no real decay). + for (let i = 0; i < 100; i++) { + await engine.putPage(`people/person-${i}`, { + type: 'person', title: `Person ${i}`, compiled_truth: 'b', timeline: '', frontmatter: {}, + }); + } + await engine.putPage('writing/index', { + type: 'note', title: 'Index', compiled_truth: 'index', timeline: '', frontmatter: {}, + }); + const links = []; + for (let i = 0; i < 100; i++) { + links.push({ + from_slug: 'writing/index', + to_slug: `people/person-${i}`, + link_type: 'mentions', link_source: 'markdown', context: '', + }); + } + await engine.addLinksBatch(links); + // Machine chronicle volume: 500 life/events/ pages, no inbound links by + // design. Without the exclusion these swamp the denominator (~83% orphan + // → FAIL); excluded, orphan_ratio reflects the healthy knowledge graph. + for (let i = 0; i < 500; i++) { + await engine.putPage(`life/events/2026-08-${i}-evt`, { + type: 'event', title: `Event ${i}`, compiled_truth: 'e', timeline: '', frontmatter: {}, + }); + } + const report = await runDoctorJson(); + const check = findCheck(report, 'orphan_ratio'); + expect(check!.status).toBe('ok'); + }); + test('zero entity pages → vacuous status ok', async () => { const report = await runDoctorJson(); const check = findCheck(report, 'orphan_ratio'); diff --git a/test/orphans-pure-fn.test.ts b/test/orphans-pure-fn.test.ts index 7dfd7a80e..c03b5c769 100644 --- a/test/orphans-pure-fn.test.ts +++ b/test/orphans-pure-fn.test.ts @@ -216,6 +216,8 @@ describe('shouldExclude — orphan filter regression (preserve curation)', () => expect(shouldExclude('dashboards/_index')).toBe(true); expect(shouldExclude('scripts/build')).toBe(true); expect(shouldExclude('output/foo')).toBe(true); + // #2264 — auto_chronicle event volume (life/events/…) is a machine leaf. + expect(shouldExclude('life/events/2026-08-01-abc123')).toBe(true); }); test('first-segment exclusions fire', () => { @@ -245,6 +247,12 @@ describe('shouldExclude — orphan filter regression (preserve curation)', () => expect(shouldExclude('companies/acme')).toBe(false); expect(shouldExclude('writing/post-1')).toBe(false); expect(shouldExclude('agents/arya/qa-reports/launch-review')).toBe(false); + // #2264 — knowledge classes must STAY in the denominator so real graph decay still trips. + expect(shouldExclude('concepts/information-architecture')).toBe(false); + expect(shouldExclude('notes/some-note')).toBe(false); + expect(shouldExclude('projects/proj-x')).toBe(false); + // #2264 — only life/events/ is excluded; human-authored life/diary/ stays counted. + expect(shouldExclude('life/diary/2026-08-01-xyz')).toBe(false); }); });