substrata-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../core/src/errors.ts","../../core/src/paths.ts","../../core/src/ids.ts","../../core/src/markdown.ts","../../core/src/config.ts","../../core/src/redaction.ts","../../core/src/footprint.ts","../../core/src/memory.ts","../../core/src/supersede.ts","../../core/src/init.ts","../../core/src/setup/gitignore.ts","../../core/src/setup/symlink.ts","../../core/src/setup/shellrc.ts","../../core/src/setup/agents-md.ts","../../core/src/setup/hook.ts","../../core/src/setup/plan.ts","../../search/src/indexer.ts","../../search/src/schema.ts","../../search/src/sqlite.ts","../../search/src/freshness.ts","../../search/src/query.ts","../../search/src/ranking.ts"],"sourcesContent":["import type { SecretFinding } from './types';\n\n/** Base error for all Substrata failures. */\nexport class SubstrataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SubstrataError';\n }\n}\n\n/** Configuration is missing, malformed, or has an unsupported schema_version. */\nexport class ConfigError extends SubstrataError {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\n/** A footprint/memory file could not be parsed or is missing required metadata. */\nexport class ParseError extends SubstrataError {\n readonly filePath?: string;\n\n constructor(message: string, filePath?: string) {\n super(filePath ? `${message} (${filePath})` : message);\n this.name = 'ParseError';\n this.filePath = filePath;\n }\n}\n\n/** One or more secrets were detected and the write was refused. */\nexport class SecretDetectedError extends SubstrataError {\n readonly findings: SecretFinding[];\n\n constructor(findings: SecretFinding[]) {\n const detail = findings.map((f) => `${f.name} at line ${f.line}`).join(', ');\n super(`Refusing to write: ${findings.length} potential secret(s) detected: ${detail}`);\n this.name = 'SecretDetectedError';\n this.findings = findings;\n }\n}\n\n/** A requested resource (e.g. a footprint id) could not be found. */\nexport class NotFoundError extends SubstrataError {\n constructor(message: string) {\n super(message);\n this.name = 'NotFoundError';\n }\n}\n","import path from 'node:path';\n\n/**\n * Path helpers for the .substrata directory.\n *\n * Repo-relative paths are always emitted with forward slashes so footprint\n * `file_path` values and config storage paths are stable across platforms.\n * Absolute on-disk paths use the platform separator (via node:path).\n */\n\nexport const SUBSTRATA_DIRNAME = '.substrata';\n\n/** Convert any path to forward-slash form. */\nexport function toPosix(p: string): string {\n return p.split(path.sep).join('/');\n}\n\n/** Absolute path to the .substrata directory. */\nexport function substrataDir(cwd: string): string {\n return path.join(cwd, SUBSTRATA_DIRNAME);\n}\n\n/** Absolute path to config.yml. */\nexport function configPath(cwd: string): string {\n return path.join(substrataDir(cwd), 'config.yml');\n}\n\n/** Absolute path to the footprints directory. */\nexport function footprintsDir(cwd: string): string {\n return path.join(substrataDir(cwd), 'footprints');\n}\n\n/** Absolute path to the memory directory. */\nexport function memoryDir(cwd: string): string {\n return path.join(substrataDir(cwd), 'memory');\n}\n\n/** Absolute path to the templates directory. */\nexport function templatesDir(cwd: string): string {\n return path.join(substrataDir(cwd), 'templates');\n}\n\n/** Absolute path to the generated SQLite index. */\nexport function indexPath(cwd: string): string {\n return path.join(substrataDir(cwd), 'index', 'footprint.sqlite');\n}\n\n/**\n * Repo-relative footprint path of the form\n * `YYYY/MM/YYYY-MM-DD-<slug>-<suffix>.md` (forward slashes).\n */\nexport function footprintRelativePath(date: Date, slug: string, suffix: string): string {\n const yyyy = String(date.getUTCFullYear()).padStart(4, '0');\n const mm = String(date.getUTCMonth() + 1).padStart(2, '0');\n const dd = String(date.getUTCDate()).padStart(2, '0');\n const filename = `${yyyy}-${mm}-${dd}-${slug}-${suffix}.md`;\n return `${yyyy}/${mm}/${filename}`;\n}\n\n/** Absolute on-disk path for a footprint, given its repo-relative path. */\nexport function footprintFilePath(cwd: string, relativePath: string): string {\n return path.join(footprintsDir(cwd), ...relativePath.split('/'));\n}\n\n/** Repo-relative (forward-slash) path from an absolute path. */\nexport function relativeToCwd(cwd: string, absolutePath: string): string {\n return toPosix(path.relative(cwd, absolutePath));\n}\n","import { randomBytes } from 'node:crypto';\n\n/**\n * ID and filename generation for footprints. See plan §5.\n *\n * id : fp_<YYYYMMDD>_<slug_underscored>_<suffix>\n * filename : YYYY/MM/YYYY-MM-DD-<slug>-<suffix>.md\n */\n\n/** Crockford-ish base32 alphabet without confusing chars (no i/l/o/u). */\nconst SUFFIX_ALPHABET = 'abcdefghjkmnpqrstvwxyz0123456789';\nconst SUFFIX_LENGTH = 6;\n\n/**\n * Slugify a title into a lowercase, hyphenated slug suitable for filenames.\n * Non-alphanumeric runs become single hyphens; leading/trailing hyphens trimmed.\n */\nexport function slugify(title: string): string {\n const slug = title\n .normalize('NFKD')\n // strip combining diacritical marks\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n return slug || 'untitled';\n}\n\n/** Generate a random lowercase base32 suffix (default 6 chars). */\nexport function randomSuffix(length: number = SUFFIX_LENGTH): string {\n const bytes = randomBytes(length);\n let out = '';\n for (let i = 0; i < length; i++) {\n out += SUFFIX_ALPHABET[bytes[i]! % SUFFIX_ALPHABET.length];\n }\n return out;\n}\n\n/**\n * Build a footprint id from a date and slug.\n * The slug is underscored (hyphens → underscores) per the id format.\n */\nexport function generateFootprintId(date: Date, slug: string, suffix?: string): string {\n const yyyy = String(date.getUTCFullYear()).padStart(4, '0');\n const mm = String(date.getUTCMonth() + 1).padStart(2, '0');\n const dd = String(date.getUTCDate()).padStart(2, '0');\n const underscored = slug.replace(/-/g, '_');\n const sfx = suffix ?? randomSuffix();\n return `fp_${yyyy}${mm}${dd}_${underscored}_${sfx}`;\n}\n\n/** Build the repo-relative footprint filename `YYYY/MM/YYYY-MM-DD-<slug>-<suffix>.md`. */\nexport function buildFootprintFilename(date: Date, slug: string, suffix: string): string {\n const yyyy = String(date.getUTCFullYear()).padStart(4, '0');\n const mm = String(date.getUTCMonth() + 1).padStart(2, '0');\n const dd = String(date.getUTCDate()).padStart(2, '0');\n return `${yyyy}/${mm}/${yyyy}-${mm}-${dd}-${slug}-${suffix}.md`;\n}\n","import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport type { FootprintSections, RejectedOption } from './types';\n\n/**\n * Frontmatter + section rendering/parsing for footprint markdown bodies.\n *\n * Frontmatter uses the `yaml` package. Body sections follow the layout in\n * plan §5 (Purpose, Decisions, Rejected options, Implementation notes,\n * Commands run, Memory learned, Future agent guidance).\n */\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\n\nexport type ParsedFrontmatter = {\n /** Parsed YAML frontmatter object (empty object when none present). */\n frontmatter: Record<string, unknown>;\n /** Body text after the closing `---`. */\n body: string;\n};\n\n/** Split a raw markdown document into frontmatter + body. */\nexport function parseFrontmatter(raw: string): ParsedFrontmatter {\n const match = FRONTMATTER_RE.exec(raw);\n if (!match) {\n return { frontmatter: {}, body: raw };\n }\n const parsed = parseYaml(match[1]!) as unknown;\n const frontmatter =\n parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {};\n return { frontmatter, body: match[2] ?? '' };\n}\n\n/** Serialize a frontmatter object and body back into a markdown document. */\nexport function serializeFrontmatter(frontmatter: Record<string, unknown>, body: string): string {\n const yaml = stringifyYaml(frontmatter, { lineWidth: 0 }).trimEnd();\n const trimmedBody = body.replace(/^\\r?\\n+/, '');\n return `---\\n${yaml}\\n---\\n\\n${trimmedBody.length > 0 ? `${trimmedBody.replace(/\\s+$/, '')}\\n` : ''}`;\n}\n\n/** Title is the first level-1 heading; returns '' if absent. */\nexport function extractTitle(body: string): string {\n const match = /^#\\s+(.+?)\\s*$/m.exec(body);\n return match ? match[1]!.trim() : '';\n}\n\n// --- Section rendering -------------------------------------------------------\n\nfunction renderListSection(heading: string, items: string[] | undefined): string | null {\n if (!items || items.length === 0) return null;\n const lines = items.map((item) => `- ${item}`);\n return `## ${heading}\\n\\n${lines.join('\\n')}`;\n}\n\nfunction renderProseSection(heading: string, text: string | undefined): string | null {\n if (!text || text.trim().length === 0) return null;\n return `## ${heading}\\n\\n${text.trim()}`;\n}\n\nfunction renderRejectedOptions(options: RejectedOption[] | undefined): string | null {\n if (!options || options.length === 0) return null;\n const blocks = options.map((o) => `### ${o.option}\\n\\n${o.reason.trim()}`);\n return `## Rejected options\\n\\n${blocks.join('\\n\\n')}`;\n}\n\nfunction renderCommands(commands: string[] | undefined): string | null {\n if (!commands || commands.length === 0) return null;\n return `## Commands run\\n\\n\\`\\`\\`bash\\n${commands.join('\\n')}\\n\\`\\`\\``;\n}\n\n/**\n * Render the full footprint body from a title and structured sections.\n * Only non-empty sections are emitted, in canonical order.\n */\nexport function renderFootprintBody(title: string, sections: FootprintSections): string {\n const parts: Array<string | null> = [\n `# ${title}`,\n renderProseSection('Purpose', sections.purpose),\n renderListSection('Decisions', sections.decisions),\n renderRejectedOptions(sections.rejectedOptions),\n renderProseSection('Implementation notes', sections.implementationNotes),\n renderCommands(sections.commandsRun),\n renderListSection('Memory learned', sections.memoryLearned),\n renderProseSection('Future agent guidance', sections.futureAgentGuidance),\n ];\n return `${parts.filter((p): p is string => p !== null).join('\\n\\n')}\\n`;\n}\n\n// --- Section parsing ---------------------------------------------------------\n\ntype RawSection = { heading: string; content: string };\n\n/** Split a body into level-2 (`##`) sections, ignoring the title and preamble. */\nfunction splitSections(body: string): RawSection[] {\n const lines = body.split(/\\r?\\n/);\n const sections: RawSection[] = [];\n let current: RawSection | null = null;\n let inFence = false;\n\n for (const line of lines) {\n if (/^```/.test(line.trim())) {\n inFence = !inFence;\n if (current) current.content += `${line}\\n`;\n continue;\n }\n const headingMatch = !inFence ? /^##\\s+(.+?)\\s*$/.exec(line) : null;\n if (headingMatch) {\n if (current) sections.push(current);\n current = { heading: headingMatch[1]!.trim(), content: '' };\n continue;\n }\n if (current) current.content += `${line}\\n`;\n }\n if (current) sections.push(current);\n return sections;\n}\n\nfunction parseListItems(content: string): string[] {\n return content\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .filter((l) => l.startsWith('- '))\n .map((l) => l.slice(2).trim())\n .filter((l) => l.length > 0);\n}\n\nfunction parseProse(content: string): string {\n return content.trim();\n}\n\nfunction parseCommands(content: string): string[] {\n const fenceMatch = /```(?:bash|sh)?\\r?\\n([\\s\\S]*?)```/.exec(content);\n const inner = fenceMatch ? fenceMatch[1]! : content;\n return inner\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n}\n\nfunction parseRejectedOptions(content: string): RejectedOption[] {\n const options: RejectedOption[] = [];\n const parts = content.split(/^###\\s+/m);\n for (const part of parts) {\n const trimmed = part.trim();\n if (!trimmed) continue;\n const newlineIdx = trimmed.indexOf('\\n');\n if (newlineIdx === -1) {\n options.push({ option: trimmed, reason: '' });\n continue;\n }\n const option = trimmed.slice(0, newlineIdx).trim();\n const reason = trimmed.slice(newlineIdx + 1).trim();\n options.push({ option, reason });\n }\n return options;\n}\n\n/** Parse a footprint body into structured sections. Headings are case-insensitive. */\nexport function parseFootprintBody(body: string): FootprintSections {\n const sections: FootprintSections = {};\n for (const raw of splitSections(body)) {\n const key = raw.heading.toLowerCase();\n switch (key) {\n case 'purpose':\n sections.purpose = parseProse(raw.content);\n break;\n case 'decisions':\n sections.decisions = parseListItems(raw.content);\n break;\n case 'rejected options':\n sections.rejectedOptions = parseRejectedOptions(raw.content);\n break;\n case 'implementation notes':\n sections.implementationNotes = parseProse(raw.content);\n break;\n case 'commands run':\n sections.commandsRun = parseCommands(raw.content);\n break;\n case 'memory learned':\n sections.memoryLearned = parseListItems(raw.content);\n break;\n case 'future agent guidance':\n sections.futureAgentGuidance = parseProse(raw.content);\n break;\n default:\n break;\n }\n }\n return sections;\n}\n","import { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport { ConfigError } from './errors';\nimport { configPath } from './paths';\nimport type { SubstrataConfig } from './types';\n\n/**\n * Default redaction keys (plan §12). The redactor also matches kebab/snake\n * variants case-insensitively, so only the canonical forms are listed.\n */\nexport const DEFAULT_REDACTION_KEYS: string[] = [\n 'token',\n 'apiKey',\n 'api_key',\n 'authorization',\n 'password',\n 'secret',\n 'cookie',\n 'set-cookie',\n 'privateKey',\n 'accessToken',\n 'refreshToken',\n];\n\n/** Default config shape (plan §13). projectName is filled by loadConfig/init. */\nexport const defaultConfig: SubstrataConfig = {\n schema_version: 1,\n project: {\n name: 'substrata-demo',\n },\n storage: {\n footprints_dir: '.substrata/footprints',\n memory_dir: '.substrata/memory',\n index_path: '.substrata/index/footprint.sqlite',\n },\n search: {\n default_limit: 8,\n max_context_tokens: 1600,\n },\n security: {\n redact: true,\n scan_content: true,\n entropy_scan: false,\n entropy_min_length: 32,\n block_on_secret: true,\n redaction_keys: [\n 'token',\n 'apiKey',\n 'api_key',\n 'authorization',\n 'password',\n 'secret',\n 'cookie',\n 'privateKey',\n 'accessToken',\n 'refreshToken',\n ],\n },\n agent: {\n default_actor: 'unknown-agent',\n require_footprint_after_non_trivial_work: true,\n },\n};\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Deep-merge `override` onto `base`. Arrays and scalars from override win wholesale. */\nfunction deepMerge<T>(base: T, override: unknown): T {\n if (!isObject(base) || !isObject(override)) {\n return override === undefined ? base : (override as T);\n }\n const result: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(override)) {\n if (value === undefined) continue;\n const baseValue = (base as Record<string, unknown>)[key];\n if (isObject(baseValue) && isObject(value)) {\n result[key] = deepMerge(baseValue, value);\n } else {\n result[key] = value;\n }\n }\n return result as T;\n}\n\n/**\n * Load `.substrata/config.yml`, deep-merging user values over defaults.\n * Throws ConfigError if the file is missing/malformed or schema_version !== 1.\n * The default project name falls back to the cwd basename.\n */\nexport async function loadConfig(cwd: string): Promise<SubstrataConfig> {\n const file = configPath(cwd);\n let raw: string;\n try {\n raw = await readFile(file, 'utf8');\n } catch {\n throw new ConfigError(`Config not found at ${file}. Run \\`substrata init\\`.`);\n }\n\n let parsed: unknown;\n try {\n parsed = parseYaml(raw);\n } catch (err) {\n throw new ConfigError(`Failed to parse ${file}: ${(err as Error).message}`);\n }\n\n if (!isObject(parsed)) {\n throw new ConfigError(`Config at ${file} must be a YAML mapping.`);\n }\n\n if (parsed.schema_version !== 1) {\n throw new ConfigError(\n `Unsupported config schema_version: ${String(parsed.schema_version)} (expected 1).`,\n );\n }\n\n const base: SubstrataConfig = {\n ...defaultConfig,\n project: { name: path.basename(cwd) },\n };\n return deepMerge(base, parsed);\n}\n\n/** Render a config.yml file for `init`, overriding the project name. */\nexport function renderConfig(projectName: string): string {\n const config: SubstrataConfig = {\n ...defaultConfig,\n project: { name: projectName },\n };\n return stringifyYaml(config, { lineWidth: 0 });\n}\n","import type { RedactionOptions, SecretFinding } from './types';\n\n/**\n * Redaction (key-based) + content/pattern secret scanning. See plan §12.\n */\n\nexport const DEFAULT_REDACTION_KEYS: string[] = [\n 'token',\n 'apiKey',\n 'api_key',\n 'authorization',\n 'password',\n 'secret',\n 'cookie',\n 'set-cookie',\n 'privateKey',\n 'accessToken',\n 'refreshToken',\n];\n\nconst REDACTED = '[REDACTED]';\n\n/** Normalize a key for matching: lowercase, strip `-`/`_` separators. */\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[-_]/g, '');\n}\n\n/**\n * Recursively replace values whose key matches a redaction key (case-insensitive,\n * ignoring kebab/snake separators) with `[REDACTED]`. Returns a new structure;\n * the input is not mutated.\n */\nexport function redactDeep(value: unknown, options: RedactionOptions = {}): unknown {\n const keys = options.keys ?? DEFAULT_REDACTION_KEYS;\n const replacement = options.replacement ?? REDACTED;\n const normalizedKeys = new Set(keys.map(normalizeKey));\n\n const walk = (node: unknown): unknown => {\n if (Array.isArray(node)) {\n return node.map(walk);\n }\n if (node && typeof node === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n if (normalizedKeys.has(normalizeKey(k))) {\n out[k] = replacement;\n } else {\n out[k] = walk(v);\n }\n }\n return out;\n }\n return node;\n };\n\n return walk(value);\n}\n\n/** Content secret patterns (plan §12). */\nexport const SECRET_PATTERNS: Array<{ name: string; re: RegExp }> = [\n { name: 'aws_access_key_id', re: /\\bAKIA[0-9A-Z]{16}\\b/ },\n { name: 'github_pat', re: /\\bghp_[A-Za-z0-9]{36}\\b/ },\n { name: 'github_fine_grained', re: /\\bgithub_pat_[A-Za-z0-9_]{60,}\\b/ },\n { name: 'gitlab_pat', re: /\\bglpat-[A-Za-z0-9_-]{20}\\b/ },\n { name: 'slack_token', re: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/ },\n { name: 'google_api_key', re: /\\bAIza[0-9A-Za-z_-]{35}\\b/ },\n { name: 'openai_key', re: /\\bsk-[A-Za-z0-9]{20,}\\b/ },\n { name: 'anthropic_key', re: /\\bsk-ant-[A-Za-z0-9_-]{20,}\\b/ },\n {\n name: 'jwt',\n re: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/,\n },\n {\n name: 'private_key_block',\n re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,\n },\n { name: 'bearer_header', re: /\\bBearer\\s+[A-Za-z0-9._-]{20,}\\b/ },\n { name: 'url_basic_auth', re: /\\b[a-z][a-z0-9+.-]*:\\/\\/[^/\\s:@]+:[^/\\s:@]+@/i },\n];\n\n/**\n * Scan text for secrets, returning findings with 1-based line numbers.\n * Each pattern may match multiple times across lines.\n */\nexport function scanForSecrets(text: string): SecretFinding[] {\n const findings: SecretFinding[] = [];\n const lines = text.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n for (const { name, re } of SECRET_PATTERNS) {\n // Use a fresh non-global regex test per line to avoid lastIndex state.\n if (re.test(line)) {\n findings.push({ name, line: i + 1 });\n }\n }\n }\n return findings;\n}\n\n/**\n * Replace secret-pattern matches in text with `[REDACTED:<name>]`.\n * Returns the redacted text; does not detect entropy-based secrets.\n */\nexport function redactText(text: string): string {\n let out = text;\n for (const { name, re } of SECRET_PATTERNS) {\n const globalRe = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);\n out = out.replace(globalRe, `[REDACTED:${name}]`);\n }\n return out;\n}\n\n/**\n * Optional high-entropy heuristic, off by default. Flags long standalone tokens\n * whose Shannon entropy exceeds a threshold. Gated by `options.enabled`.\n */\nexport function scanForHighEntropy(\n text: string,\n options: { enabled?: boolean; minLength?: number; minEntropy?: number } = {},\n): SecretFinding[] {\n if (!options.enabled) return [];\n const minLength = options.minLength ?? 32;\n const minEntropy = options.minEntropy ?? 4.0;\n const findings: SecretFinding[] = [];\n const lines = text.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const tokens = lines[i]!.split(/[\\s\"'`(),;]+/);\n for (const token of tokens) {\n if (token.length >= minLength && shannonEntropy(token) >= minEntropy) {\n findings.push({ name: 'high_entropy', line: i + 1 });\n break;\n }\n }\n }\n return findings;\n}\n\nfunction shannonEntropy(s: string): number {\n const counts = new Map<string, number>();\n for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);\n let entropy = 0;\n for (const count of counts.values()) {\n const p = count / s.length;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n","import type { Dirent } from 'node:fs';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { loadConfig } from './config';\nimport { ParseError, SecretDetectedError } from './errors';\nimport { buildFootprintFilename, generateFootprintId, randomSuffix, slugify } from './ids';\nimport {\n extractTitle,\n parseFootprintBody,\n parseFrontmatter,\n renderFootprintBody,\n serializeFrontmatter,\n} from './markdown';\nimport { footprintsDir, relativeToCwd } from './paths';\nimport { redactDeep, scanForSecrets } from './redaction';\nimport type {\n Footprint,\n FootprintFrontmatter,\n FootprintRelated,\n FootprintStatus,\n SecretFinding,\n WorkType,\n WriteFootprintInput,\n} from './types';\n\nconst REQUIRED_KEYS = [\n 'schema_version',\n 'id',\n 'created_at',\n 'actor',\n 'work_type',\n 'status',\n] as const;\n\nconst VALID_WORK_TYPES: ReadonlySet<string> = new Set<WorkType>([\n 'implementation',\n 'implementation_decision',\n 'bug_fix',\n 'refactor',\n 'investigation',\n 'architecture_decision',\n 'test_update',\n 'documentation',\n]);\n\nconst VALID_STATUSES: ReadonlySet<string> = new Set<FootprintStatus>([\n 'draft',\n 'completed',\n 'superseded',\n 'deprecated',\n]);\n\nfunction validateFrontmatter(fm: Record<string, unknown>, filePath?: string): FootprintFrontmatter {\n for (const key of REQUIRED_KEYS) {\n if (fm[key] === undefined || fm[key] === null) {\n throw new ParseError(`Footprint missing required frontmatter field \"${key}\"`, filePath);\n }\n }\n if (fm.schema_version !== 1) {\n throw new ParseError(\n `Footprint has unsupported schema_version: ${String(fm.schema_version)} (expected 1)`,\n filePath,\n );\n }\n if (typeof fm.id !== 'string' || fm.id.length === 0) {\n throw new ParseError('Footprint \"id\" must be a non-empty string', filePath);\n }\n if (typeof fm.actor !== 'string' || fm.actor.length === 0) {\n throw new ParseError('Footprint \"actor\" must be a non-empty string', filePath);\n }\n if (typeof fm.created_at !== 'string' || fm.created_at.length === 0) {\n throw new ParseError('Footprint \"created_at\" must be a non-empty string', filePath);\n }\n if (typeof fm.work_type !== 'string' || !VALID_WORK_TYPES.has(fm.work_type)) {\n throw new ParseError(`Footprint has invalid work_type: ${String(fm.work_type)}`, filePath);\n }\n if (typeof fm.status !== 'string' || !VALID_STATUSES.has(fm.status)) {\n throw new ParseError(`Footprint has invalid status: ${String(fm.status)}`, filePath);\n }\n return fm as FootprintFrontmatter;\n}\n\n/** Parse raw markdown into a Footprint, validating required frontmatter. */\nexport function parseFootprint(raw: string, filePath: string): Footprint {\n const { frontmatter, body } = parseFrontmatter(raw);\n const fm = validateFrontmatter(frontmatter, filePath);\n const title = extractTitle(body);\n const sections = parseFootprintBody(body);\n return { frontmatter: fm, title, body, sections, filePath, raw };\n}\n\n/** Read and parse a footprint file from disk. */\nexport async function parseFootprintFile(filePath: string): Promise<Footprint> {\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf8');\n } catch {\n throw new ParseError('Footprint file could not be read', filePath);\n }\n return parseFootprint(raw, filePath);\n}\n\nfunction mergeSupersedes(\n related: FootprintRelated | undefined,\n supersedes: string[] | undefined,\n): FootprintRelated | undefined {\n if (!related && (!supersedes || supersedes.length === 0)) return undefined;\n const merged: FootprintRelated = { ...(related ?? {}) };\n if (supersedes && supersedes.length > 0) {\n const existing = merged.supersedes ?? [];\n merged.supersedes = Array.from(new Set([...existing, ...supersedes]));\n }\n return merged;\n}\n\n/** Drop undefined/empty fields so YAML frontmatter stays clean. */\nfunction cleanFrontmatter(fm: FootprintFrontmatter): Record<string, unknown> {\n const out: Record<string, unknown> = {\n schema_version: fm.schema_version,\n id: fm.id,\n created_at: fm.created_at,\n };\n if (fm.updated_at) out.updated_at = fm.updated_at;\n out.actor = fm.actor;\n if (fm.requester) out.requester = fm.requester;\n if (fm.agent_model) out.agent_model = fm.agent_model;\n out.work_type = fm.work_type;\n out.status = fm.status;\n if (fm.repo && (fm.repo.name || fm.repo.branch)) out.repo = fm.repo;\n if (fm.related && Object.keys(fm.related).length > 0) out.related = fm.related;\n if (fm.files_touched && fm.files_touched.length > 0) out.files_touched = fm.files_touched;\n if (fm.tags && fm.tags.length > 0) out.tags = fm.tags;\n return out;\n}\n\n/**\n * `files_touched` entries land in committed frontmatter and are matched against\n * repo paths by search — only plain repo-relative paths are meaningful, and\n * absolute or `..`-traversing entries are likely mistakes (or mischief).\n */\nfunction validateFilesTouched(files: string[] | undefined): string[] | undefined {\n if (!files || files.length === 0) return undefined;\n return files.map((entry) => {\n // Normalize separators explicitly (not via toPosix): entries are plain\n // strings that may carry Windows separators regardless of host platform.\n const f = entry.trim().replace(/\\\\/g, '/');\n if (f === '') {\n throw new ParseError('files_touched entries must be non-empty repo-relative paths');\n }\n if (f.startsWith('/') || /^[A-Za-z]:\\//.test(f)) {\n throw new ParseError(`files_touched must be repo-relative, got absolute path: ${f}`);\n }\n if (f.split('/').includes('..')) {\n throw new ParseError(`files_touched must not contain '..' segments: ${f}`);\n }\n return f;\n });\n}\n\nfunction validateRelatedUrls(related: FootprintRelated | undefined): void {\n for (const u of related?.urls ?? []) {\n let parsed: URL;\n try {\n parsed = new URL(u);\n } catch {\n throw new ParseError(`related.urls entry is not a valid URL: ${u}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new ParseError(`related.urls only allows http(s) URLs, got: ${u}`);\n }\n }\n}\n\n/**\n * Write a new footprint: resolve config, generate id/filename, render markdown,\n * run redaction + secret scan, write the file, and return the parsed Footprint.\n * Throws SecretDetectedError when secrets remain and block_on_secret is set\n * (unless `allowSecret` is true).\n */\nexport async function writeFootprint(\n input: WriteFootprintInput & { cwd: string },\n): Promise<Footprint> {\n const { cwd } = input;\n const config = await loadConfig(cwd);\n\n const createdAt = input.createdAt ?? new Date().toISOString();\n const date = new Date(createdAt);\n const slug = slugify(input.title);\n const suffix = randomSuffix();\n const id = generateFootprintId(date, slug, suffix);\n const relPath = buildFootprintFilename(date, slug, suffix);\n const absPath = path.join(footprintsDir(cwd), ...relPath.split('/'));\n\n const workType: WorkType = input.workType ?? 'implementation';\n const status: FootprintStatus = input.status ?? 'completed';\n\n const filesTouched = validateFilesTouched(input.filesTouched);\n\n // Key-based redaction (plan §12) applies to structured frontmatter values\n // (e.g. nested objects under repo/related). It does not touch prose.\n const related = mergeSupersedes(input.related, input.supersedes);\n validateRelatedUrls(related);\n const frontmatter: FootprintFrontmatter = {\n schema_version: 1,\n id,\n created_at: createdAt,\n actor: input.actor,\n requester: input.requester,\n agent_model: input.agentModel,\n work_type: workType,\n status,\n repo: input.repo\n ? (redactDeep(input.repo, { keys: config.security.redaction_keys }) as typeof input.repo)\n : undefined,\n related: related\n ? (redactDeep(related, { keys: config.security.redaction_keys }) as typeof related)\n : undefined,\n files_touched: filesTouched,\n tags: input.tags,\n };\n\n // Render the body from structured sections.\n const body = renderFootprintBody(input.title, {\n purpose: input.purpose,\n decisions: input.decisions,\n rejectedOptions: input.rejectedOptions,\n implementationNotes: input.implementationNotes,\n commandsRun: input.commandsRun,\n memoryLearned: input.memoryLearned,\n futureAgentGuidance: input.futureAgentGuidance,\n });\n\n // Content (pattern) scan over the rendered body. Key-based redaction does not\n // mask prose secrets, so any remaining match blocks the write (plan §12).\n if (config.security.scan_content) {\n const findings: SecretFinding[] = scanForSecrets(body);\n if (findings.length > 0 && config.security.block_on_secret && !input.allowSecret) {\n throw new SecretDetectedError(findings);\n }\n }\n\n const raw = serializeFrontmatter(cleanFrontmatter(frontmatter), body);\n\n await mkdir(path.dirname(absPath), { recursive: true });\n await writeFile(absPath, raw, 'utf8');\n\n return parseFootprint(raw, absPath);\n}\n\nasync function walkMarkdown(dir: string): Promise<string[]> {\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await walkMarkdown(full)));\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n files.push(full);\n }\n }\n return files;\n}\n\n/** List all footprints under the footprints dir, sorted by created_at desc. */\nexport async function listFootprints(cwd: string): Promise<Footprint[]> {\n const dir = footprintsDir(cwd);\n const files = await walkMarkdown(dir);\n const footprints = await Promise.all(files.map((f) => parseFootprintFile(f)));\n footprints.sort((a, b) => {\n const at = a.frontmatter.created_at;\n const bt = b.frontmatter.created_at;\n return at < bt ? 1 : at > bt ? -1 : 0;\n });\n return footprints;\n}\n\n/** Find a footprint by id; returns null if not present. */\nexport async function findFootprintById(cwd: string, id: string): Promise<Footprint | null> {\n const files = await walkMarkdown(footprintsDir(cwd));\n for (const file of files) {\n const fp = await parseFootprintFile(file);\n if (fp.frontmatter.id === id) return fp;\n }\n return null;\n}\n\n/** Compute the repo-relative (forward-slash) path of a footprint. */\nexport function footprintRepoPath(cwd: string, fp: Footprint): string {\n return relativeToCwd(cwd, fp.filePath);\n}\n","import type { Dirent } from 'node:fs';\nimport { readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { ParseError } from './errors';\nimport { extractTitle, parseFrontmatter } from './markdown';\nimport { memoryDir } from './paths';\nimport type { MemoryDocument, MemoryFrontmatter } from './types';\n\n/**\n * Curated memory parsing + append-friendly entry insertion. See plan §6.\n *\n * Entries live between stable markers:\n * <!-- substrata:entries:start -->\n * ... entries ...\n * <!-- substrata:entries:end -->\n *\n * Each entry is wrapped in:\n * <!-- substrata:entry id=<sourceId> -->\n * <lines>\n * <!-- /substrata:entry -->\n */\n\nconst ENTRIES_START = '<!-- substrata:entries:start -->';\nconst ENTRIES_END = '<!-- substrata:entries:end -->';\n\nfunction entryOpen(id: string): string {\n return `<!-- substrata:entry id=${id} -->`;\n}\nconst ENTRY_CLOSE = '<!-- /substrata:entry -->';\n\nfunction validateMemoryFrontmatter(\n fm: Record<string, unknown>,\n filePath: string,\n): MemoryFrontmatter {\n if (fm.schema_version !== 1) {\n throw new ParseError(\n `Memory file has unsupported schema_version: ${String(fm.schema_version)} (expected 1)`,\n filePath,\n );\n }\n if (typeof fm.id !== 'string' || fm.id.length === 0) {\n throw new ParseError('Memory \"id\" must be a non-empty string', filePath);\n }\n return fm as MemoryFrontmatter;\n}\n\n/** Parse raw markdown into a MemoryDocument. */\nexport function parseMemory(raw: string, filePath: string): MemoryDocument {\n const { frontmatter, body } = parseFrontmatter(raw);\n const fm = validateMemoryFrontmatter(frontmatter, filePath);\n return { frontmatter: fm, title: extractTitle(body), body, filePath, raw };\n}\n\n/** Read and parse a memory file from disk. */\nexport async function parseMemoryFile(filePath: string): Promise<MemoryDocument> {\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf8');\n } catch {\n throw new ParseError('Memory file could not be read', filePath);\n }\n return parseMemory(raw, filePath);\n}\n\nasync function walkMarkdown(dir: string): Promise<string[]> {\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await walkMarkdown(full)));\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n files.push(full);\n }\n }\n return files;\n}\n\n/** List all memory documents under the memory dir, sorted by file path. */\nexport async function listMemoryDocuments(cwd: string): Promise<MemoryDocument[]> {\n const files = await walkMarkdown(memoryDir(cwd));\n files.sort();\n return Promise.all(files.map((f) => parseMemoryFile(f)));\n}\n\nexport type MemoryEntry = {\n sourceId: string;\n lines: string[];\n};\n\n/** Extract the set of sourceIds already present in a memory file body. */\nexport function existingEntryIds(content: string): Set<string> {\n const ids = new Set<string>();\n const re = /<!--\\s*substrata:entry\\s+id=([^\\s]+)\\s*-->/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(content)) !== null) {\n ids.add(match[1]!);\n }\n return ids;\n}\n\nfunction renderEntry(entry: MemoryEntry): string {\n return [entryOpen(entry.sourceId), ...entry.lines, ENTRY_CLOSE].join('\\n');\n}\n\n/**\n * Append entries to a memory file before the entries:end marker, creating the\n * marker block if absent. Existing content is preserved byte-for-byte; entries\n * whose sourceId already appears in the file are skipped (idempotent).\n * Returns true if the file was changed.\n */\nexport async function appendMemoryEntries(\n filePath: string,\n entries: MemoryEntry[],\n): Promise<boolean> {\n const original = await readFile(filePath, 'utf8');\n const existing = existingEntryIds(original);\n const toAdd = entries.filter((e) => !existing.has(e.sourceId));\n if (toAdd.length === 0) return false;\n\n const rendered = toAdd.map(renderEntry).join('\\n');\n\n let next: string;\n const endIdx = original.indexOf(ENTRIES_END);\n if (endIdx !== -1) {\n // Insert before the existing end marker, preserving everything around it.\n const before = original.slice(0, endIdx);\n const after = original.slice(endIdx);\n const sep = before.endsWith('\\n') ? '' : '\\n';\n next = `${before}${sep}${rendered}\\n${after}`;\n } else {\n // No marker block: create one at the end of the file.\n const base = original.endsWith('\\n') ? original : `${original}\\n`;\n next = `${base}\\n${ENTRIES_START}\\n${rendered}\\n${ENTRIES_END}\\n`;\n }\n\n await writeFile(filePath, next, 'utf8');\n return true;\n}\n","import { readFile, writeFile } from 'node:fs/promises';\n\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport { NotFoundError } from './errors';\nimport { findFootprintById } from './footprint';\nimport type { FootprintRelated } from './types';\n\n/**\n * Supersede relationship editing. See plan §5/§8.9.\n *\n * Edits ONLY the YAML frontmatter of both files (old + new); the markdown body\n * is preserved byte-for-byte. Old footprint gets status=superseded and\n * superseded_by += newId; new footprint gets supersedes += oldId.\n */\n\nconst FRONTMATTER_RE = /^(---\\r?\\n)([\\s\\S]*?)(\\r?\\n---\\r?\\n?)([\\s\\S]*)$/;\n\nfunction addUnique(list: string[] | undefined, value: string): string[] {\n const arr = list ? [...list] : [];\n if (!arr.includes(value)) arr.push(value);\n return arr;\n}\n\n/**\n * Rewrite a footprint file's frontmatter via `mutate`, preserving the exact\n * body bytes and the original frontmatter fences/newlines.\n */\nasync function editFrontmatter(\n filePath: string,\n mutate: (fm: Record<string, unknown>) => void,\n): Promise<void> {\n const raw = await readFile(filePath, 'utf8');\n const match = FRONTMATTER_RE.exec(raw);\n if (!match) {\n throw new NotFoundError(`Footprint at ${filePath} has no parseable frontmatter`);\n }\n const [, open, yamlText, close, body] = match;\n const parsed = parseYaml(yamlText!) as Record<string, unknown>;\n mutate(parsed);\n const newYaml = stringifyYaml(parsed, { lineWidth: 0 }).replace(/\\n$/, '');\n const next = `${open}${newYaml}${close}${body}`;\n await writeFile(filePath, next, 'utf8');\n}\n\n/**\n * Mark `oldId` as superseded by `newId`. Edits frontmatter only on both files.\n * Throws NotFoundError if either id cannot be located.\n */\nexport async function supersedeFootprint(cwd: string, oldId: string, newId: string): Promise<void> {\n const oldFp = await findFootprintById(cwd, oldId);\n if (!oldFp) throw new NotFoundError(`Footprint not found: ${oldId}`);\n const newFp = await findFootprintById(cwd, newId);\n if (!newFp) throw new NotFoundError(`Footprint not found: ${newId}`);\n\n await editFrontmatter(oldFp.filePath, (fm) => {\n fm.status = 'superseded';\n const related = (fm.related as FootprintRelated | undefined) ?? {};\n related.superseded_by = addUnique(related.superseded_by, newId);\n fm.related = related;\n });\n\n await editFrontmatter(newFp.filePath, (fm) => {\n const related = (fm.related as FootprintRelated | undefined) ?? {};\n related.supersedes = addUnique(related.supersedes, oldId);\n fm.related = related;\n });\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { renderConfig } from './config';\nimport { configPath, footprintsDir, memoryDir, substrataDir, templatesDir } from './paths';\nimport type { ChangeResult, InitOptions } from './types';\n\n/**\n * Project scaffolding for `substrata init`. Idempotent: existing files are never\n * overwritten. See plan §8.1 (\"Created on apply\") and §14.\n */\n\nconst README_CONTENTS = `# Substrata\n\nThis directory holds shared agent memory for this repository.\n\n- \\`footprints/\\` — committed source-of-truth records of agent-assisted work.\n- \\`memory/\\` — curated, durable repo knowledge agents should read often.\n- \\`templates/\\` — templates used when creating footprints and memory files.\n- \\`config.yml\\` — Substrata configuration (see the docs).\n- \\`index/\\` and \\`cache/\\` — generated, gitignored; safe to delete and rebuild.\n\nFootprints and memory files are committed. Do not store secrets, credentials,\nprivate keys, tokens, or sensitive user data here.\n`;\n\nconst FOOTPRINT_TEMPLATE = `---\nschema_version: 1\nid: fp_YYYYMMDD_slug_suffix\ncreated_at: 2026-01-01T00:00:00Z\nactor: unknown-agent\nwork_type: implementation\nstatus: completed\n---\n\n# Title\n\n## Purpose\n\nWhy this work was done.\n\n## Decisions\n\n- Decision one.\n\n## Rejected options\n\n### Option name\n\nWhy it was rejected.\n\n## Implementation notes\n\nWhat was changed.\n\n## Commands run\n\n\\`\\`\\`bash\npnpm test\n\\`\\`\\`\n\n## Memory learned\n\n- Something durable about this repo.\n\n## Future agent guidance\n\nWhat a future agent should check before changing this area.\n`;\n\nconst MEMORY_TEMPLATE = `---\nschema_version: 1\nid: mem_example\ntype: repo_conventions\ntags:\n - conventions\n---\n\n# Memory title\n\n## Section\n\n- Durable note one.\n\n<!-- substrata:entries:start -->\n<!-- substrata:entries:end -->\n`;\n\ntype FileSpec = { absPath: string; contents: string };\ntype DirSpec = string;\n\nasync function exists(p: string): Promise<boolean> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create the .substrata scaffold. Never overwrites existing files/dirs.\n * Returns the list of changes applied (create/skip per path).\n */\nexport async function initProject(cwd: string, options: InitOptions = {}): Promise<ChangeResult[]> {\n const projectName = options.projectName ?? path.basename(path.resolve(cwd));\n\n const dirs: DirSpec[] = [\n substrataDir(cwd),\n footprintsDir(cwd),\n memoryDir(cwd),\n templatesDir(cwd),\n ];\n\n const files: FileSpec[] = [\n { absPath: configPath(cwd), contents: renderConfig(projectName) },\n { absPath: path.join(substrataDir(cwd), 'README.md'), contents: README_CONTENTS },\n { absPath: path.join(templatesDir(cwd), 'footprint.md'), contents: FOOTPRINT_TEMPLATE },\n { absPath: path.join(templatesDir(cwd), 'memory.md'), contents: MEMORY_TEMPLATE },\n ];\n\n const changes: ChangeResult[] = [];\n\n for (const dir of dirs) {\n await mkdir(dir, { recursive: true });\n }\n\n for (const file of files) {\n if (await exists(file.absPath)) {\n changes.push({\n path: file.absPath,\n action: 'skip',\n description: 'already exists',\n });\n continue;\n }\n await writeFile(file.absPath, file.contents, 'utf8');\n changes.push({\n path: file.absPath,\n action: 'create',\n description: 'created',\n contents: file.contents,\n });\n }\n\n return changes;\n}\n","import { readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\n\nimport type { ChangeResult } from '../types';\nimport { isSymlink } from './symlink';\n\n/**\n * Ensure `.gitignore` ignores Substrata's generated directories without\n * duplicating lines. Pure + dry-runnable.\n */\n\nexport const GITIGNORE_LINES = ['.substrata/index/', '.substrata/cache/', '.substrata/tmp/'];\n\nfunction readIfExists(filePath: string): string | null {\n try {\n return readFileSync(filePath, 'utf8');\n } catch {\n return null;\n }\n}\n\nexport function ensureGitignore(cwd: string, dry: boolean = false): ChangeResult {\n const filePath = path.join(cwd, '.gitignore');\n if (isSymlink(filePath)) {\n return { path: filePath, action: 'skip', description: 'refused: .gitignore is a symlink' };\n }\n const existing = readIfExists(filePath);\n const present = existing ?? '';\n const presentLines = new Set(present.split(/\\r?\\n/).map((l) => l.trim()));\n\n const missing = GITIGNORE_LINES.filter((line) => !presentLines.has(line));\n\n if (missing.length === 0) {\n return {\n path: filePath,\n action: 'skip',\n description: existing === null ? 'no gitignore needed' : 'gitignore already covers Substrata',\n };\n }\n\n const header = '# Substrata generated files';\n const block = presentLines.has(header) ? missing.join('\\n') : `${header}\\n${missing.join('\\n')}`;\n\n let next: string;\n if (existing === null) {\n next = `${block}\\n`;\n } else {\n const sep = existing.endsWith('\\n') ? '' : '\\n';\n next = `${existing}${sep}${block}\\n`;\n }\n\n if (!dry) writeFileSync(filePath, next, 'utf8');\n\n return {\n path: filePath,\n action: existing === null ? 'create' : 'update',\n description: `add ${missing.length} gitignore line(s)`,\n contents: next,\n };\n}\n","import { lstatSync } from 'node:fs';\n\n/**\n * Setup writers refuse to write through symlinks: a repo could ship e.g. an\n * `AGENTS.md -> ~/.ssh/config` link and turn a marker-block upsert into a\n * clobber of a file outside the repo. Shell-rc writes are exempt by design —\n * dotfiles are commonly symlinked and the user explicitly opts into that write.\n */\nexport function isSymlink(filePath: string): boolean {\n try {\n return lstatSync(filePath).isSymbolicLink();\n } catch {\n return false;\n }\n}\n","import { readFileSync, writeFileSync } from 'node:fs';\n\nimport type { AttributionEnv, ChangeResult } from '../types';\n\n/**\n * Write a Substrata env block to a shell rc file, delimited by markers so a\n * re-run replaces it in place rather than appending. Pure + dry-runnable.\n */\n\nconst BEGIN = '# >>> substrata >>>';\nconst END = '# <<< substrata <<<';\n\nfunction readIfExists(filePath: string): string | null {\n try {\n return readFileSync(filePath, 'utf8');\n } catch {\n return null;\n }\n}\n\nfunction renderBlock(vars: AttributionEnv): string {\n const lines: string[] = [BEGIN, '# Substrata agent attribution (managed; safe to edit values)'];\n if (vars.actor) lines.push(`export SUBSTRATA_ACTOR=\"${vars.actor}\"`);\n if (vars.model) lines.push(`export SUBSTRATA_MODEL=\"${vars.model}\"`);\n if (vars.requester) lines.push(`export SUBSTRATA_REQUESTER=\"${vars.requester}\"`);\n lines.push(END);\n return lines.join('\\n');\n}\n\n/**\n * Insert or replace the Substrata env block in `rcPath`.\n * The block is delimited by stable markers; reruns replace in place.\n */\nexport function writeShellEnv(\n rcPath: string,\n vars: AttributionEnv,\n dry: boolean = false,\n): ChangeResult {\n const block = renderBlock(vars);\n const existing = readIfExists(rcPath);\n\n const markerRe = new RegExp(`${escapeRegExp(BEGIN)}[\\\\s\\\\S]*?${escapeRegExp(END)}`, 'm');\n\n let next: string;\n let action: ChangeResult['action'];\n\n if (existing === null) {\n next = `${block}\\n`;\n action = 'create';\n } else if (markerRe.test(existing)) {\n const replaced = existing.replace(markerRe, block);\n if (replaced === existing) {\n return { path: rcPath, action: 'skip', description: 'env block already current' };\n }\n next = replaced;\n action = 'update';\n } else {\n const sep = existing.endsWith('\\n') ? '' : '\\n';\n next = `${existing}${sep}\\n${block}\\n`;\n action = 'update';\n }\n\n if (!dry) writeFileSync(rcPath, next, 'utf8');\n\n return {\n path: rcPath,\n action,\n description: 'write Substrata env block',\n contents: next,\n };\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\n\nimport type { ChangeResult } from '../types';\nimport { isSymlink } from './symlink';\n\n/**\n * Insert/replace the Substrata section in AGENTS.md between begin/end markers.\n * Replace-in-place on rerun; never duplicates. Pure + dry-runnable. See plan §10.\n */\n\nconst BEGIN = '<!-- substrata:start -->';\nconst END = '<!-- substrata:end -->';\n\nexport const AGENTS_MD_SECTION = `${BEGIN}\n## Substrata Rules\n\nThis repository uses Substrata for shared agent memory.\n\nSet these once per agent session so footprints are attributed correctly:\n- \\`SUBSTRATA_ACTOR\\` (e.g. \"claude-code\")\n- \\`SUBSTRATA_MODEL\\` (e.g. \"claude-opus-4\")\n- \\`SUBSTRATA_REQUESTER\\` (falls back to git user.email)\n\nBefore making non-trivial changes:\n\n1. Run \\`substrata context \"<task description>\"\\`.\n2. Search for relevant files using \\`substrata search\\` or the MCP tool \\`substrata_context\\`.\n3. Respect prior architectural decisions unless the user explicitly asks to override them.\n\nAfter making non-trivial changes:\n\n1. Add a footprint with \\`substrata add\\` or MCP tool \\`substrata_add\\`.\n2. Include: purpose, requester, actor, files changed, decisions made, rejected\n alternatives, implementation notes, commands run, memory learned, future agent guidance.\n3. If the work changes durable repo conventions, update \\`.substrata/memory/\\`.\n4. If the work reverses a prior decision, use \\`substrata supersede\\`.\n\nDo not store secrets, credentials, private keys, tokens, or sensitive user data in Substrata files.\n${END}`;\n\nfunction readIfExists(filePath: string): string | null {\n try {\n return readFileSync(filePath, 'utf8');\n } catch {\n return null;\n }\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** Upsert the Substrata section into `<cwd>/AGENTS.md`. */\nexport function upsertAgentsMd(cwd: string, dry: boolean = false): ChangeResult {\n const filePath = path.join(cwd, 'AGENTS.md');\n if (isSymlink(filePath)) {\n return { path: filePath, action: 'skip', description: 'refused: AGENTS.md is a symlink' };\n }\n const existing = readIfExists(filePath);\n const markerRe = new RegExp(`${escapeRegExp(BEGIN)}[\\\\s\\\\S]*?${escapeRegExp(END)}`, 'm');\n\n let next: string;\n let action: ChangeResult['action'];\n\n if (existing === null) {\n next = `${AGENTS_MD_SECTION}\\n`;\n action = 'create';\n } else if (markerRe.test(existing)) {\n const replaced = existing.replace(markerRe, AGENTS_MD_SECTION);\n if (replaced === existing) {\n return { path: filePath, action: 'skip', description: 'AGENTS.md section already current' };\n }\n next = replaced;\n action = 'update';\n } else {\n const sep = existing.endsWith('\\n') ? '' : '\\n';\n next = `${existing}${sep}\\n${AGENTS_MD_SECTION}\\n`;\n action = 'update';\n }\n\n if (!dry) writeFileSync(filePath, next, 'utf8');\n\n return {\n path: filePath,\n action,\n description: 'write Substrata AGENTS.md section',\n contents: next,\n };\n}\n","import { chmodSync, readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\n\nimport type { ChangeResult } from '../types';\nimport { isSymlink } from './symlink';\n\n/**\n * Install a pre-commit hook that runs the Substrata staged-file secret scan as a\n * second line of defense. If a hook already exists, append a guarded block\n * rather than clobbering it. Pure-ish + dry-runnable. See plan §8.11/§12.\n */\n\nconst GUARD_BEGIN = '# >>> substrata pre-commit >>>';\nconst GUARD_END = '# <<< substrata pre-commit <<<';\n\nconst GUARDED_BLOCK = `${GUARD_BEGIN}\n# Substrata secret scan over staged .substrata files (second line of defense).\nnpx --no-install substrata internal-scan-staged || exit 1\n${GUARD_END}`;\n\nconst FULL_HOOK = `#!/bin/sh\n${GUARDED_BLOCK}\n`;\n\nfunction readIfExists(filePath: string): string | null {\n try {\n return readFileSync(filePath, 'utf8');\n } catch {\n return null;\n }\n}\n\n/** Install/refresh the pre-commit hook at `<cwd>/.git/hooks/pre-commit`. */\nexport function installSecretHook(cwd: string, dry: boolean = false): ChangeResult {\n const filePath = path.join(cwd, '.git', 'hooks', 'pre-commit');\n if (isSymlink(filePath)) {\n return { path: filePath, action: 'skip', description: 'refused: pre-commit hook is a symlink' };\n }\n const existing = readIfExists(filePath);\n\n let next: string;\n let action: ChangeResult['action'];\n\n if (existing === null) {\n next = FULL_HOOK;\n action = 'create';\n } else if (existing.includes(GUARD_BEGIN)) {\n return { path: filePath, action: 'skip', description: 'pre-commit hook already installed' };\n } else {\n const sep = existing.endsWith('\\n') ? '' : '\\n';\n next = `${existing}${sep}\\n${GUARDED_BLOCK}\\n`;\n action = 'update';\n }\n\n if (!dry) {\n writeFileSync(filePath, next, 'utf8');\n chmodSync(filePath, 0o755);\n }\n\n return {\n path: filePath,\n action,\n description: 'install Substrata pre-commit secret hook',\n contents: next,\n };\n}\n","import type { ChangeResult } from '../types';\n\n/**\n * Aggregate ChangeResults into a printable setup plan. Pure (no I/O).\n */\n\nconst ACTION_LABEL: Record<ChangeResult['action'], string> = {\n create: 'CREATE',\n update: 'UPDATE',\n skip: 'SKIP ',\n};\n\n/** Render a list of changes as aligned, human-readable plan lines. */\nexport function renderPlan(changes: ChangeResult[]): string {\n if (changes.length === 0) return 'No changes.';\n return changes\n .map((c) => `${ACTION_LABEL[c.action]} ${c.path}${c.description ? ` — ${c.description}` : ''}`)\n .join('\\n');\n}\n\n/** True if any change actually writes (create/update). */\nexport function hasEffectiveChanges(changes: ChangeResult[]): boolean {\n return changes.some((c) => c.action !== 'skip');\n}\n\n/** Summary counts by action, e.g. for a plan footer. */\nexport function summarizePlan(changes: ChangeResult[]): {\n create: number;\n update: number;\n skip: number;\n} {\n const summary = { create: 0, update: 0, skip: 0 };\n for (const c of changes) summary[c.action] += 1;\n return summary;\n}\n","import { stat } from 'node:fs/promises';\n\nimport {\n listFootprints,\n listMemoryDocuments,\n relativeToCwd,\n type Footprint,\n type MemoryDocument,\n} from '@substrata/core';\nimport type Database from 'better-sqlite3';\n\nimport { applySchema, dropSchema, SCHEMA_VERSION } from './schema';\nimport { closeDb, openIndexDb } from './sqlite';\n\nexport type BuildIndexOptions = {\n /**\n * Reserved for API symmetry with the plan signature. The MVP performs a full\n * rebuild on every call (drop & recreate rows), so this flag is a no-op today\n * but kept so callers can pass it without breaking.\n */\n rebuild?: boolean;\n};\n\n/** Row shape inserted into the `documents` table. */\ntype DocumentRow = {\n id: string;\n type: 'footprint' | 'memory';\n title: string;\n filePath: string;\n status: string | null;\n createdAt: string | null;\n updatedAt: string | null;\n tags: string[];\n files: string[];\n /** Full searchable content (joined sections / body). */\n content: string;\n workType: string | null;\n};\n\nfunction joinNonEmpty(parts: Array<string | undefined | null>): string {\n return parts\n .map((p) => (p ?? '').trim())\n .filter((p) => p.length > 0)\n .join('\\n\\n');\n}\n\n/** Flatten a footprint into a single searchable document row. */\nfunction footprintToRow(cwd: string, fp: Footprint): DocumentRow {\n const fm = fp.frontmatter;\n const s = fp.sections;\n\n const rejected = (s.rejectedOptions ?? []).map((r) => `${r.option}: ${r.reason}`).join('\\n');\n\n const content = joinNonEmpty([\n fp.title,\n s.purpose,\n (s.decisions ?? []).join('\\n'),\n rejected,\n s.implementationNotes,\n (s.memoryLearned ?? []).join('\\n'),\n s.futureAgentGuidance,\n (fm.tags ?? []).join(' '),\n (fm.files_touched ?? []).join(' '),\n ]);\n\n return {\n id: fm.id,\n type: 'footprint',\n title: fp.title,\n filePath: relativeToCwd(cwd, fp.filePath),\n status: fm.status,\n createdAt: fm.created_at,\n updatedAt: fm.updated_at ?? null,\n tags: fm.tags ?? [],\n files: fm.files_touched ?? [],\n content,\n workType: fm.work_type,\n };\n}\n\n/** Flatten a memory document into a single searchable document row. */\nfunction memoryToRow(cwd: string, doc: MemoryDocument): DocumentRow {\n const fm = doc.frontmatter;\n const tags = Array.isArray(fm.tags) ? fm.tags : [];\n\n const content = joinNonEmpty([doc.title, doc.body, tags.join(' ')]);\n\n return {\n id: fm.id,\n type: 'memory',\n title: doc.title,\n filePath: relativeToCwd(cwd, doc.filePath),\n status: null,\n createdAt: null,\n updatedAt: typeof fm.updated_at === 'string' ? fm.updated_at : null,\n tags,\n files: [],\n content,\n workType: null,\n };\n}\n\nasync function maxMtimeMs(filePaths: string[]): Promise<number> {\n let max = 0;\n for (const filePath of filePaths) {\n try {\n const st = await stat(filePath);\n if (st.mtimeMs > max) max = st.mtimeMs;\n } catch {\n // file vanished between listing and stat; ignore.\n }\n }\n return max;\n}\n\nfunction writeRows(db: Database.Database, rows: DocumentRow[]): void {\n const insertDoc = db.prepare(\n `INSERT INTO documents\n (id, type, title, file_path, status, created_at, updated_at, tags_json, files_json, raw_text, work_type)\n VALUES\n (@id, @type, @title, @filePath, @status, @createdAt, @updatedAt, @tagsJson, @filesJson, @content, @workType)`,\n );\n const insertFts = db.prepare(\n `INSERT INTO documents_fts (id, title, tags, files, content)\n VALUES (@id, @title, @tags, @files, @content)`,\n );\n\n for (const row of rows) {\n insertDoc.run({\n id: row.id,\n type: row.type,\n title: row.title,\n filePath: row.filePath,\n status: row.status,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n tagsJson: JSON.stringify(row.tags),\n filesJson: JSON.stringify(row.files),\n content: row.content,\n workType: row.workType,\n });\n insertFts.run({\n id: row.id,\n title: row.title,\n tags: row.tags.join(' '),\n files: row.files.join(' '),\n content: row.content,\n });\n }\n}\n\nfunction writeMeta(\n db: Database.Database,\n meta: { sourceMaxMtime: number; sourceFileCount: number },\n): void {\n const upsert = db.prepare(\n `INSERT INTO index_meta (key, value) VALUES (@key, @value)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value`,\n );\n upsert.run({ key: 'schema_version', value: String(SCHEMA_VERSION) });\n upsert.run({ key: 'built_at', value: new Date().toISOString() });\n upsert.run({ key: 'source_max_mtime', value: String(meta.sourceMaxMtime) });\n upsert.run({ key: 'source_file_count', value: String(meta.sourceFileCount) });\n}\n\n/**\n * Build (or fully rebuild) the search index for `cwd`. Loads footprints and\n * memory documents via `@substrata/core`, flattens each into one searchable\n * `documents` + `documents_fts` row, and records freshness metadata. The MVP\n * always does a full rebuild (drop & recreate rows) inside a transaction.\n */\nexport async function buildIndex(cwd: string, _options: BuildIndexOptions = {}): Promise<void> {\n const [footprints, memory] = await Promise.all([listFootprints(cwd), listMemoryDocuments(cwd)]);\n\n const rows: DocumentRow[] = [\n ...footprints.map((fp) => footprintToRow(cwd, fp)),\n ...memory.map((doc) => memoryToRow(cwd, doc)),\n ];\n\n const sourceFiles = [\n ...footprints.map((fp) => fp.filePath),\n ...memory.map((doc) => doc.filePath),\n ];\n const sourceMaxMtime = await maxMtimeMs(sourceFiles);\n\n const db = openIndexDb(cwd);\n try {\n const rebuild = db.transaction(() => {\n // Full rebuild: drop & recreate so a schema bump or removed source files\n // never leave stale rows behind.\n dropSchema(db);\n applySchema(db);\n writeRows(db, rows);\n writeMeta(db, { sourceMaxMtime, sourceFileCount: sourceFiles.length });\n });\n rebuild();\n } finally {\n closeDb(db);\n }\n}\n","import type Database from 'better-sqlite3';\n\n/**\n * SQLite index schema. See plan §11 (\"SQLite schema\").\n *\n * `documents` holds the structured row (for filtering + ranking inputs);\n * `documents_fts` is the FTS5 virtual table queried via MATCH; `index_meta`\n * carries freshness metadata (schema_version, built_at, source_max_mtime,\n * source_file_count) so the index can be detected as missing/stale/fresh\n * without re-parsing source files.\n */\n\n/** Bump when the schema or indexing semantics change. */\nexport const SCHEMA_VERSION = 1;\n\nconst CREATE_DOCUMENTS = `\nCREATE TABLE IF NOT EXISTS documents (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n file_path TEXT NOT NULL,\n status TEXT,\n created_at TEXT,\n updated_at TEXT,\n tags_json TEXT NOT NULL,\n files_json TEXT NOT NULL,\n raw_text TEXT NOT NULL,\n work_type TEXT\n);\n`;\n\nconst CREATE_DOCUMENTS_FTS = `\nCREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(\n id UNINDEXED,\n title,\n tags,\n files,\n content,\n tokenize = 'porter unicode61'\n);\n`;\n\nconst CREATE_INDEX_META = `\nCREATE TABLE IF NOT EXISTS index_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n`;\n\n/** Create all tables if they do not already exist. Idempotent. */\nexport function applySchema(db: Database.Database): void {\n db.exec(CREATE_DOCUMENTS);\n db.exec(CREATE_DOCUMENTS_FTS);\n db.exec(CREATE_INDEX_META);\n}\n\n/** Drop all index tables (used by a full rebuild). */\nexport function dropSchema(db: Database.Database): void {\n db.exec('DROP TABLE IF EXISTS documents;');\n db.exec('DROP TABLE IF EXISTS documents_fts;');\n db.exec('DROP TABLE IF EXISTS index_meta;');\n}\n","import { existsSync, mkdirSync } from 'node:fs';\nimport path from 'node:path';\n\nimport { indexPath } from '@substrata/core';\nimport Database from 'better-sqlite3';\n\nimport { applySchema } from './schema';\n\nexport type OpenIndexDbOptions = {\n /** Open the existing DB read-only; never create or migrate. */\n readonly?: boolean;\n};\n\n/**\n * Open (or create) the SQLite index database at `@substrata/core`'s\n * `indexPath(cwd)`. The parent `index/` directory is created if missing.\n *\n * In read-only mode the DB must already exist (better-sqlite3 will throw\n * otherwise); the schema is left untouched. In read-write mode the schema is\n * applied idempotently so a freshly created file is immediately usable.\n */\nexport function openIndexDb(cwd: string, options: OpenIndexDbOptions = {}): Database.Database {\n const dbPath = indexPath(cwd);\n\n if (!options.readonly) {\n mkdirSync(path.dirname(dbPath), { recursive: true });\n }\n\n const db = new Database(dbPath, { readonly: options.readonly ?? false });\n\n if (!options.readonly) {\n db.pragma('journal_mode = DELETE');\n applySchema(db);\n }\n\n return db;\n}\n\n/** True when an index database file exists on disk for `cwd`. */\nexport function indexDbExists(cwd: string): boolean {\n return existsSync(indexPath(cwd));\n}\n\n/** Close a database handle, ignoring errors from an already-closed handle. */\nexport function closeDb(db: Database.Database): void {\n try {\n db.close();\n } catch {\n // already closed; nothing to do.\n }\n}\n","import type { Dirent } from 'node:fs';\nimport { readdir, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { footprintsDir, memoryDir, type IndexStatus } from '@substrata/core';\n\nimport { SCHEMA_VERSION } from './schema';\nimport { closeDb, indexDbExists, openIndexDb } from './sqlite';\n\n/**\n * Cheap stat walk of the footprint/memory dirs: collect the max mtime (ms) and\n * the count of `.md` files. No file contents are read or parsed — this mirrors\n * what `buildIndex` records so freshness can be checked without re-indexing.\n */\nasync function walkStats(dir: string): Promise<{ maxMtime: number; count: number }> {\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return { maxMtime: 0, count: 0 };\n }\n\n let maxMtime = 0;\n let count = 0;\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n const sub = await walkStats(full);\n if (sub.maxMtime > maxMtime) maxMtime = sub.maxMtime;\n count += sub.count;\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n count += 1;\n try {\n const st = await stat(full);\n if (st.mtimeMs > maxMtime) maxMtime = st.mtimeMs;\n } catch {\n // ignore unreadable file\n }\n }\n }\n return { maxMtime, count };\n}\n\nfunction readMeta(cwd: string): Map<string, string> {\n const db = openIndexDb(cwd, { readonly: true });\n try {\n const rows = db.prepare('SELECT key, value FROM index_meta').all() as Array<{\n key: string;\n value: string;\n }>;\n return new Map(rows.map((r) => [r.key, r.value]));\n } finally {\n closeDb(db);\n }\n}\n\n/**\n * Report whether the on-disk index is missing, stale, or fresh for `cwd`.\n *\n * - missing: no index DB file on disk.\n * - stale/schema: recorded schema_version differs from the current one.\n * - stale/count: the source `.md` file count changed.\n * - stale/mtime: a source file is newer than the recorded build mtime.\n * - fresh: otherwise.\n *\n * Comparison is against a cheap stat walk only (plan §11) — no parsing.\n */\nexport async function getIndexStatus(cwd: string): Promise<IndexStatus> {\n if (!indexDbExists(cwd)) {\n return { state: 'missing' };\n }\n\n let meta: Map<string, string>;\n try {\n meta = readMeta(cwd);\n } catch {\n // Corrupt or unreadable DB — treat as missing so callers rebuild.\n return { state: 'missing' };\n }\n\n const recordedSchema = Number(meta.get('schema_version'));\n if (!Number.isFinite(recordedSchema) || recordedSchema !== SCHEMA_VERSION) {\n return { state: 'stale', reason: 'schema' };\n }\n\n const [fp, mem] = await Promise.all([walkStats(footprintsDir(cwd)), walkStats(memoryDir(cwd))]);\n const sourceCount = fp.count + mem.count;\n const sourceMaxMtime = Math.max(fp.maxMtime, mem.maxMtime);\n\n const recordedCount = Number(meta.get('source_file_count'));\n if (!Number.isFinite(recordedCount) || recordedCount !== sourceCount) {\n return { state: 'stale', reason: 'count' };\n }\n\n const recordedMtime = Number(meta.get('source_max_mtime'));\n // Allow a 1ms tolerance for filesystem mtime granularity differences.\n if (!Number.isFinite(recordedMtime) || sourceMaxMtime > recordedMtime + 1) {\n return { state: 'stale', reason: 'mtime' };\n }\n\n return { state: 'fresh' };\n}\n","import path from 'node:path';\n\nimport { toPosix, type FootprintStatus, type SearchResult, type WorkType } from '@substrata/core';\nimport type Database from 'better-sqlite3';\n\nimport { score } from './ranking';\nimport { closeDb, openIndexDb } from './sqlite';\n\nexport type SearchOptions = {\n cwd: string;\n limit?: number;\n /** Hard filter: only docs whose files_touched include one of these. */\n files?: string[];\n /** Hard filter: only docs carrying one of these tags. */\n tags?: string[];\n /** Drop superseded AND deprecated docs entirely. */\n excludeSuperseded?: boolean;\n};\n\nconst DEFAULT_LIMIT = 8;\n\n/**\n * Baseline relevance assigned to a direct file-hit in `getRelatedToFile` (these\n * rows do not come from an FTS MATCH, so they have no bm25). Chosen negative\n * (bm25 convention: lower is better) so a strong file match ranks comparably to\n * a good text match before the ×1.5 file-overlap boost is applied.\n */\nconst FILE_HIT_BM25 = -10;\n\n/** Row joined from documents + the FTS match. */\ntype JoinedRow = {\n id: string;\n type: string;\n title: string;\n file_path: string;\n status: string | null;\n created_at: string | null;\n updated_at: string | null;\n tags_json: string;\n files_json: string;\n work_type: string | null;\n bm25: number;\n snippet: string;\n};\n\nfunction parseJsonArray(value: string): string[] {\n try {\n const parsed = JSON.parse(value);\n return Array.isArray(parsed) ? (parsed as string[]) : [];\n } catch {\n return [];\n }\n}\n\n/**\n * Build a safe FTS5 MATCH expression from free-form user text. Each token is\n * wrapped in double quotes (with internal quotes doubled) so user punctuation\n * (e.g. `?!`, `:`, `-`) can never be interpreted as FTS5 query syntax. Tokens\n * are OR-ed so natural-language queries (e.g. \"why did we avoid Redis?!\") still\n * recall the relevant docs; ranking then orders by relevance. Returns null when\n * the query has no usable tokens.\n */\nexport function buildMatchQuery(query: string): string | null {\n const tokens = query\n .split(/[^A-Za-z0-9_]+/u)\n .map((t) => t.trim())\n .filter((t) => t.length > 0);\n if (tokens.length === 0) return null;\n return tokens.map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`).join(' OR ');\n}\n\nfunction rowToResult(row: JoinedRow, queryFiles: string[]): SearchResult {\n const tags = parseJsonArray(row.tags_json);\n const files = parseJsonArray(row.files_json);\n const status = (row.status ?? 'completed') as FootprintStatus;\n\n const ranked = score(\n {\n bm25: row.bm25,\n status: row.status as FootprintStatus | null,\n workType: row.work_type as WorkType | null,\n updatedAt: row.updated_at,\n createdAt: row.created_at,\n docFiles: files,\n queryFiles,\n },\n Date.now(),\n );\n\n return {\n id: row.id,\n title: row.title,\n filePath: row.file_path,\n score: ranked,\n snippet: row.snippet,\n tags,\n createdAt: row.created_at ?? undefined,\n filesTouched: files,\n status,\n };\n}\n\ntype ExecuteOptions = {\n excludeSuperseded?: boolean;\n files?: string[];\n tags?: string[];\n limit: number;\n};\n\n/**\n * Run a MATCH query against an open DB, apply hard tag/file/status filters in\n * SQL, then rank + sort in JS. Shared by `search` and `getRelatedToFile`.\n */\nfunction executeMatch(\n db: Database.Database,\n match: string,\n queryFiles: string[],\n options: ExecuteOptions,\n): SearchResult[] {\n const where: string[] = [];\n const params: Record<string, unknown> = { match };\n\n if (options.excludeSuperseded) {\n // Memory docs have NULL status and are always kept; only footprint statuses\n // superseded/deprecated are dropped.\n where.push(\"(d.status IS NULL OR d.status NOT IN ('superseded', 'deprecated'))\");\n }\n\n if (options.tags && options.tags.length > 0) {\n const clauses = options.tags.map((tag, i) => {\n params[`tag${i}`] = tag;\n return `EXISTS (SELECT 1 FROM json_each(d.tags_json) WHERE json_each.value = @tag${i})`;\n });\n where.push(`(${clauses.join(' OR ')})`);\n }\n\n if (options.files && options.files.length > 0) {\n const clauses = options.files.map((file, i) => {\n params[`file${i}`] = toPosix(file);\n return `EXISTS (SELECT 1 FROM json_each(d.files_json) WHERE json_each.value = @file${i})`;\n });\n where.push(`(${clauses.join(' OR ')})`);\n }\n\n const whereSql = where.length > 0 ? ` AND ${where.join(' AND ')}` : '';\n\n const sql = `\n SELECT\n d.id, d.type, d.title, d.file_path, d.status,\n d.created_at, d.updated_at, d.tags_json, d.files_json, d.work_type,\n bm25(documents_fts) AS bm25,\n snippet(documents_fts, 4, '[', ']', ' … ', 12) AS snippet\n FROM documents_fts\n JOIN documents d ON d.id = documents_fts.id\n WHERE documents_fts MATCH @match${whereSql}\n `;\n\n const rows = db.prepare(sql).all(params) as JoinedRow[];\n const results = rows.map((row) => rowToResult(row, queryFiles));\n results.sort((a, b) => b.score - a.score);\n return results.slice(0, options.limit);\n}\n\n/**\n * Full-text search over footprints + memory. Builds a safe MATCH expression,\n * applies hard tag/file filters and optional superseded exclusion, ranks per\n * plan §11, and returns up to `limit` results.\n */\nexport async function search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const limit = options.limit ?? DEFAULT_LIMIT;\n const match = buildMatchQuery(query);\n if (!match) return [];\n\n const queryFiles = (options.files ?? []).map((f) => toPosix(f));\n\n const db = openIndexDb(options.cwd, { readonly: true });\n try {\n return executeMatch(db, match, queryFiles, {\n excludeSuperseded: options.excludeSuperseded,\n files: options.files,\n tags: options.tags,\n limit,\n });\n } finally {\n closeDb(db);\n }\n}\n\n/**\n * Find footprints/memory related to a specific file. Primary signal is a doc\n * whose `files_json` contains the posix-normalized path; an FTS fallback on the\n * filename stem broadens recall. Results are merged (dedup by id, file-hit\n * preferred) and ranked by the same rules.\n */\nexport async function getRelatedToFile(\n filePath: string,\n options: SearchOptions,\n): Promise<SearchResult[]> {\n const limit = options.limit ?? DEFAULT_LIMIT;\n const posixPath = toPosix(filePath);\n const stem = path.basename(posixPath).replace(/\\.[^.]+$/, '');\n\n const db = openIndexDb(options.cwd, { readonly: true });\n try {\n // Hard file matches: docs whose files_touched contain the exact path.\n const fileRows = db\n .prepare(\n `SELECT\n d.id, d.type, d.title, d.file_path, d.status,\n d.created_at, d.updated_at, d.tags_json, d.files_json, d.work_type\n FROM documents d\n WHERE EXISTS (\n SELECT 1 FROM json_each(d.files_json) WHERE json_each.value = @path\n )`,\n )\n .all({ path: posixPath }) as Omit<JoinedRow, 'bm25' | 'snippet'>[];\n\n const byId = new Map<string, SearchResult>();\n for (const row of fileRows) {\n const joined: JoinedRow = { ...row, bm25: FILE_HIT_BM25, snippet: '' };\n // The file hit counts as overlap automatically: queryFiles includes\n // posixPath, so rowToResult applies the ×1.5 boost via the ranker.\n const result = rowToResult(joined, [posixPath]);\n byId.set(result.id, result);\n }\n\n // FTS fallback on the filename stem to broaden recall.\n const match = buildMatchQuery(stem);\n if (match) {\n const ftsResults = executeMatch(db, match, [posixPath], {\n excludeSuperseded: options.excludeSuperseded,\n tags: options.tags,\n limit: limit * 4,\n });\n for (const r of ftsResults) {\n if (!byId.has(r.id)) byId.set(r.id, r);\n }\n }\n\n let merged = Array.from(byId.values());\n if (options.excludeSuperseded) {\n merged = merged.filter((r) => r.status !== 'superseded' && r.status !== 'deprecated');\n }\n merged.sort((a, b) => b.score - a.score);\n return merged.slice(0, limit);\n } finally {\n closeDb(db);\n }\n}\n","import type { FootprintStatus, WorkType } from '@substrata/core';\n\n/**\n * Ranking helpers. See plan §11 (\"Ranking (MVP)\").\n *\n * All helpers are pure so they can be unit-tested in isolation. `score` applies\n * the full pipeline: BM25 → normalize → multiplicative boosts → status penalty.\n */\n\n/** Days over which the recency boost decays linearly from 1 to 0. */\nexport const RECENCY_DECAY_DAYS = 180;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/** Maximum fractional recency boost (plan: × (1 + 0.15 * decay)). */\nexport const RECENCY_BOOST_WEIGHT = 0.15;\n\n/** Multiplier when the doc's files overlap the queried files. */\nexport const FILES_OVERLAP_BOOST = 1.5;\n\n/** Multiplicative status penalties (plan §11 step 4). */\nexport const STATUS_PENALTIES: Record<FootprintStatus, number> = {\n draft: 0.5,\n completed: 1,\n superseded: 0.15,\n deprecated: 0.1,\n};\n\n/**\n * better-sqlite3's fts5 `bm25()` returns a score where *more negative is more\n * relevant*. Normalize to a positive relevance value so boosts/penalties read\n * naturally (higher = better).\n */\nexport function normalizeBm25(bm25: number): number {\n return -bm25;\n}\n\n/**\n * Linear recency decay in [0, 1]: 1 for \"now\", decaying to 0 at\n * RECENCY_DECAY_DAYS old and clamped at 0 beyond that. A missing/invalid date\n * yields 0 (no recency contribution).\n */\nexport function recencyDecay(timestamp: string | undefined, now: number = Date.now()): number {\n if (!timestamp) return 0;\n const t = Date.parse(timestamp);\n if (Number.isNaN(t)) return 0;\n const ageDays = (now - t) / MS_PER_DAY;\n if (ageDays <= 0) return 1;\n if (ageDays >= RECENCY_DECAY_DAYS) return 0;\n return 1 - ageDays / RECENCY_DECAY_DAYS;\n}\n\n/**\n * Recency boost multiplier: × (1 + weight * decay). For\n * `architecture_decision`, the boost contribution is halved so durable\n * decisions are not demoted merely for being old.\n */\nexport function recencyBoost(\n timestamp: string | undefined,\n workType: WorkType | null | undefined,\n now: number = Date.now(),\n): number {\n let contribution = RECENCY_BOOST_WEIGHT * recencyDecay(timestamp, now);\n if (workType === 'architecture_decision') {\n contribution /= 2;\n }\n return 1 + contribution;\n}\n\n/** True when any of the doc's files matches any queried file (case-insensitive). */\nexport function filesOverlap(docFiles: string[], queryFiles: string[]): boolean {\n if (queryFiles.length === 0 || docFiles.length === 0) return false;\n const want = new Set(queryFiles.map((f) => f.toLowerCase()));\n return docFiles.some((f) => want.has(f.toLowerCase()));\n}\n\n/** Multiplicative status penalty; unknown/undefined status is treated as neutral. */\nexport function statusPenalty(status: FootprintStatus | null | undefined): number {\n if (!status) return 1;\n return STATUS_PENALTIES[status] ?? 1;\n}\n\nexport type RankInput = {\n bm25: number;\n status?: FootprintStatus | null;\n workType?: WorkType | null;\n /** updated_at ?? created_at, used for recency. */\n updatedAt?: string | null;\n createdAt?: string | null;\n docFiles: string[];\n queryFiles: string[];\n};\n\n/**\n * Full ranking score: normalized BM25 with multiplicative file-overlap,\n * recency, and status modifiers applied per plan §11.\n */\nexport function score(input: RankInput, now: number = Date.now()): number {\n let s = normalizeBm25(input.bm25);\n\n if (filesOverlap(input.docFiles, input.queryFiles)) {\n s *= FILES_OVERLAP_BOOST;\n }\n\n const recencyTimestamp = input.updatedAt ?? input.createdAt ?? undefined;\n s *= recencyBoost(recencyTimestamp, input.workType ?? null, now);\n\n s *= statusPenalty(input.status ?? null);\n\n return s;\n}\n"],"mappings":"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","names":["path","parseYaml","stringifyYaml","readFile","readdir","writeFile","mkdir","readFileSync","writeFileSync","parseYaml","path","stringifyYaml","DEFAULT_REDACTION_KEYS","readFile","path","readFile","walkMarkdown","readdir","path","writeFile","FRONTMATTER_RE","parseYaml","stringifyYaml","stat","mkdir","readIfExists","readFileSync","writeFileSync","BEGIN","END","escapeRegExp","path","readdir","stat","path","path","path","readdir","stat"]}