diff --git a/src/core/cycle/synthesize.ts b/src/core/cycle/synthesize.ts index 975485665..3aaf3eb9e 100644 --- a/src/core/cycle/synthesize.ts +++ b/src/core/cycle/synthesize.ts @@ -40,7 +40,7 @@ import { MinionQueue } from '../minions/queue.ts'; import { waitForCompletion, TimeoutError } from '../minions/wait-for-completion.ts'; import { makeSubagentHandler } from '../minions/handlers/subagent.ts'; import type { MinionJobInput, MinionJobContext, MinionHandler, SubagentHandlerData } from '../minions/types.ts'; -import { discoverTranscripts, type DiscoveredTranscript } from './transcript-discovery.ts'; +import { discoverTranscripts, DEFAULT_EXCLUDE_PATTERNS, type DiscoveredTranscript } from './transcript-discovery.ts'; import { serializeMarkdown, serializePageToMarkdown } from '../markdown.ts'; import type { Page, PageType } from '../types.ts'; import { validateSourceId } from '../utils.ts'; @@ -862,7 +862,7 @@ async function loadSynthConfig(engine: BrainEngine): Promise { DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS, ); - let excludePatterns: string[] = ['medical', 'therapy']; + let excludePatterns: string[] = [...DEFAULT_EXCLUDE_PATTERNS]; if (excludeStr) { try { const parsed = JSON.parse(excludeStr); diff --git a/src/core/cycle/transcript-discovery.ts b/src/core/cycle/transcript-discovery.ts index d7dc46200..cf8313d06 100644 --- a/src/core/cycle/transcript-discovery.ts +++ b/src/core/cycle/transcript-discovery.ts @@ -119,6 +119,14 @@ export function isDreamOutput(content: string, bypass = false): boolean { return DREAM_OUTPUT_MARKER_RE.test(content); } +/** + * Sensitivity categories dream excludes from transcript ingestion by default. + * The single source for this vocabulary: `dream.synthesize.exclude_patterns` + * overrides it at runtime, and test/frontmatter-inference.test.ts asserts no + * shipped DIRECTORY_RULE binds a path to one of these. + */ +export const DEFAULT_EXCLUDE_PATTERNS: readonly string[] = ['medical', 'therapy']; + /** * Auto-wrap bare-word patterns in `\b\b`. Power users can pass full * regex (e.g. `^therapy:`) which we honor verbatim. Heuristic: any input diff --git a/src/core/frontmatter-inference.ts b/src/core/frontmatter-inference.ts index 5055e8ee0..2350730d5 100644 --- a/src/core/frontmatter-inference.ts +++ b/src/core/frontmatter-inference.ts @@ -112,14 +112,6 @@ export const DIRECTORY_RULES: DirectoryRule[] = [ datePattern: 'filename', titleStrategy: 'filename', }, - { - pathPrefix: 'apple notes/jan bowman notes/', - type: 'apple-note', - source: 'apple-notes', - tags: ['therapy', 'jan-bowman'], - datePattern: 'filename', - titleStrategy: 'filename', - }, // Catch-all for Apple Notes not in a subfolder { pathPrefix: 'apple notes/', diff --git a/test/frontmatter-inference.test.ts b/test/frontmatter-inference.test.ts index 743894baf..bc84f3a83 100644 --- a/test/frontmatter-inference.test.ts +++ b/test/frontmatter-inference.test.ts @@ -6,6 +6,10 @@ */ import { describe, test, expect } from 'bun:test'; +import { + compileExcludePatterns, + DEFAULT_EXCLUDE_PATTERNS, +} from '../src/core/cycle/transcript-discovery.ts'; import { inferFrontmatter, extractDateFromFilename, @@ -336,6 +340,28 @@ describe('DIRECTORY_RULES', () => { return sameFields && [...(r.tags ?? [])].sort().join(',') === derived; }; + // Transcripts matching gbrain's sensitivity vocabulary are dropped before any + // LLM call. A shipped DIRECTORY_RULE that maps a specific person's folder + // onto one of those categories encodes a private fact about a real individual + // into every install, which the Privacy rule in CLAUDE.md forbids for + // checked-in code. Personal mappings belong in the operator's own notes. + // + // Matched with the production compiler, not a local `includes`: bare words + // compile to \b\b, so `therapy-notes` and `medical records` are caught + // the same way discoverTranscripts catches them. An exact-equality check + // would let those spellings recreate the association with CI green. + test('no shipped rule binds a path to a sensitive category', () => { + const res = compileExcludePatterns([...DEFAULT_EXCLUDE_PATTERNS]); + expect(res.length).toBe(DEFAULT_EXCLUDE_PATTERNS.length); // no silent drop + const offenders = DIRECTORY_RULES.filter(r => + (r.tags ?? []).some(t => res.some(re => re.test(t))), + ); + // Report indices, not prefixes: an offending prefix is by definition the + // kind of string this test exists to keep out of public artifacts, and a + // failing assertion is printed into CI logs. + expect(offenders.map(r => DIRECTORY_RULES.indexOf(r))).toEqual([]); + }); + test('Apple Notes rules are more specific than the catch-all', () => { const appleRules = DIRECTORY_RULES.filter(isAppleRule); expect(appleRules.length).toBeGreaterThan(1); // subfolder rules + catch-all