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); }); });