diff --git a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts index 86546bf4c3..9ec95aeda7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts @@ -25,7 +25,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, existsSync } from "fs"; import { join } from "path"; -import { readTelosFreshness, sectionSlug, type SectionFreshness } from "./TelosFreshness"; +import { readTelosFreshness, sectionSlug, splitFilePathForSlug, parseFrontmatter, type SectionFreshness } from "./TelosFreshness"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -274,7 +274,17 @@ function parseTelosSections(content: string): Map { function extractSectionBody(target: RegistryTarget, sections: Map): string | null { if (!target.path.startsWith(TELOS_SECTION_PREFIX)) return null; - return sections.get(sectionSlug(target.name)) ?? null; + const slug = sectionSlug(target.name); + // Split-file mode wins: TELOS.md is a thin index, so the per-concept file is + // authoritative when it has content — matching GenerateTelosSummary and TELOS.md's + // own "split file wins" rule. Strip frontmatter so scoring matches how TELOS.md H2 + // bodies are measured (and a prior last_updated bump can't inflate the body). + const splitPath = splitFilePathForSlug(slug, TELOS_PATH); + if (splitPath) { + const body = parseFrontmatter(readFileSync(splitPath, "utf-8")).rest; + if (body != null && body.trim().length > 0) return body; + } + return sections.get(slug) ?? null; } function freshnessForSection(target: RegistryTarget): SectionFreshness | null { diff --git a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts index 7623a85c7c..63ee839520 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts @@ -32,7 +32,7 @@ for (const __k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { import { readFileSync, writeFileSync, existsSync } from "fs"; import { getDAName } from "../../hooks/lib/identity" -import { basename, join } from "path"; +import { basename, dirname, join } from "path"; // Normalize env path vars that Claude Code injects without shell expansion (LifeOS#1404) for (const k of ["LIFEOS_DIR", "LIFEOS_CONFIG_DIR", "PROJECTS_DIR"]) { @@ -115,6 +115,50 @@ export const STALENESS_THRESHOLDS: Record = { const DEFAULT_THRESHOLD_DAYS = 180; +// ─── Split-file mode support ──────────────────────────────────────────────── +// +// LifeOS schema "one concept → one file": TELOS.md can be a thin index while +// each section's content lives in its own ALLCAPS sibling file (MISSION.md, +// GOALS.md, …). This map is the section-slug → split-filename source of truth, +// consumed by readTelosFreshness (below) and InterviewScan.extractSectionBody +// so both read split content when the TELOS.md H2 is absent. Irregular pairs +// (food→FOOD_PREFERENCES, learning_interests→LEARNING) are explicit; consistent +// with GenerateTelosSummary's LEGACY_FILE_TO_SECTION inverse. +export const SPLIT_FILE_BY_SLUG: Record = { + mission: "MISSION.md", goals: "GOALS.md", problems: "PROBLEMS.md", + strategies: "STRATEGIES.md", challenges: "CHALLENGES.md", narratives: "NARRATIVES.md", + beliefs: "BELIEFS.md", models: "MODELS.md", frames: "FRAMES.md", traumas: "TRAUMAS.md", + wrong: "WRONG.md", wisdom: "WISDOM.md", predictions: "PREDICTIONS.md", ideas: "IDEAS.md", + sparks: "SPARKS.md", books: "BOOKS.md", authors: "AUTHORS.md", bands: "BANDS.md", + movies: "MOVIES.md", restaurants: "RESTAURANTS.md", food: "FOOD_PREFERENCES.md", + meetups: "MEETUPS.md", civic: "CIVIC.md", learning_interests: "LEARNING.md", team: "TEAM.md", +}; + +/** Absolute path to a section's split file if it exists as a TELOS-dir sibling, else null. */ +export function splitFilePathForSlug(slug: string, telosPath: string = TELOS_PATH): string | null { + const filename = SPLIT_FILE_BY_SLUG[slug]; + if (!filename) return null; + const p = join(dirname(telosPath), filename); + return existsSync(p) ? p : null; +} + +function prettifySlug(slug: string): string { + return slug.split("_").map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(" "); +} + +// First substantive body line, trimmed of markup and capped, for a section preview. +// Shared by the H2-section scan and the split-file scan. skipHeadings drops '#' +// lines (a split file body can carry its own headings; an H2 section body cannot). +function extractPreview(lines: string[], skipHeadings = false): string { + for (const line of lines) { + const t = line.trim(); + if (!t || t.startsWith("