import { posix } from "node:path"; import type { RuleSetId } from "./detect-project.js"; import { ECC_COMMIT, ECC_REPOSITORY_URL, type RuleDocument } from "./rules.js"; export const MANAGED_BLOCK_START = ""; export const MANAGED_BLOCK_END = ""; const FRONTMATTER_PATTERN = /^---\r?\n[\s\S]*?\r?\n---\r?\n/; const HEADING_PATTERN = /^(\s{0,3})(#{1,6})(?=\s)/gm; const RELATIVE_MARKDOWN_LINK_PATTERN = /\]\(((?:\.\.\/|\.\/)[^)\s]+\.md)(#[^)]+)?\)/g; function countOccurrences(value: string, search: string): number { let count = 0; let offset = 0; while (true) { const index = value.indexOf(search, offset); if (index < 0) return count; count += 1; offset = index + search.length; } } function rewriteRelativeLinks(content: string, relativePath: string): string { const sourceDirectory = posix.dirname(`rules/${relativePath}`); return content.replace( RELATIVE_MARKDOWN_LINK_PATTERN, (_match, destination: string, fragment: string | undefined) => { const resolvedPath = posix.normalize(posix.join(sourceDirectory, destination)); if (!resolvedPath.startsWith("rules/")) return _match; return `](${ECC_REPOSITORY_URL}/blob/${ECC_COMMIT}/${resolvedPath}${fragment ?? ""})`; }, ); } export function transformRuleDocument(document: RuleDocument): string { const withoutFrontmatter = document.content.replace(FRONTMATTER_PATTERN, ""); const withPinnedLinks = rewriteRelativeLinks(withoutFrontmatter, document.relativePath); return withPinnedLinks .replace(HEADING_PATTERN, (_match, indentation: string, hashes: string) => { const depth = Math.min(6, hashes.length + 3); return `${indentation}${"#".repeat(depth)}`; }) .trim(); } export function renderManagedRules( ruleSets: readonly RuleSetId[], documents: readonly RuleDocument[], ): string { const adaptedRulePaths = documents .filter((document) => document.source === "pi") .map((document) => `\`${document.relativePath}\``); const sections = documents.map((document) => [ `### \`${document.relativePath}\``, "", transformRuleDocument(document), ].join("\n")); return [ MANAGED_BLOCK_START, "## ECC-derived Rules for Pi (managed)", "", "> Generated by `rinco-pi-rule` via `/rules:init`. Edit content outside the managed markers; rerunning `/rules:init` replaces only this block.", `> Upstream source: [affaan-m/ECC](${ECC_REPOSITORY_URL}/tree/${ECC_COMMIT}/rules) (MIT). Pi-adapted overrides are maintained by rinco-pi-rule.`, "", `- Detected rule sets: ${ruleSets.map((ruleSet) => `\`${ruleSet}\``).join(", ")}`, `- Source commit: \`${ECC_COMMIT.slice(0, 7)}\``, `- Pi-adapted rules: ${adaptedRulePaths.join(", ") || "none"}`, "- Rule priority: later, more specific layers override earlier layers.", "", ...sections.flatMap((section, index) => index === 0 ? [section] : ["---", "", section]), MANAGED_BLOCK_END, ].join("\n"); } export function mergeManagedRules(existingContent: string | undefined, managedBlock: string): string { if (existingContent === undefined || existingContent.length === 0) { return `${managedBlock}\n`; } const startCount = countOccurrences(existingContent, MANAGED_BLOCK_START); const endCount = countOccurrences(existingContent, MANAGED_BLOCK_END); if (startCount === 0 && endCount === 0) { return `${existingContent.trimEnd()}\n\n${managedBlock}\n`; } const startIndex = existingContent.indexOf(MANAGED_BLOCK_START); const endIndex = existingContent.indexOf(MANAGED_BLOCK_END); if (startCount !== 1 || endCount !== 1 || startIndex > endIndex) { throw new Error("AGENTS.md contains malformed or duplicate rinco-pi-rule managed markers."); } const suffixIndex = endIndex + MANAGED_BLOCK_END.length; return `${existingContent.slice(0, startIndex)}${managedBlock}${existingContent.slice(suffixIndex)}`; }