Files
sabia/.opencode/plugin/openwolf/anatomy.ts
T
Lucas e989413018 Esqueleto inicial: sabia-core + sabia-cli + UI local, OpenWolf
- sabia-core: descoberta de interfaces, controle de modo monitor/canal
  via iw (ponte até ter nl80211 nativo), captura crua 802.11 via
  AF_PACKET (sem libpcap), parsing de radiotap e quadros de
  gerenciamento (beacon/probe/EAPOL), agregação em tabela de APs/
  clientes, escrita de .pcap, trilha de auditoria encadeada por hash,
  auto-atualização via GitHub releases (curl, sem dependências de
  rede extras)
- sabia-cli: `sabia iface`, `sabia scan`, `sabia update`,
  `sabia audit-verify`, `sabia ui` (servidor local + navegador,
  protegido por token, mesma filosofia do Guará)
- OpenWolf inicializado para memória de projeto entre agentes
2026-10-23 08:22:04 -03:00

425 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as fs from "node:fs"
import * as path from "node:path"
import type { AnatomyEntry } from "./types.js"
// ── Markdown format (canonical — mirrors src/hooks/anatomy-store.ts) ────────
// The auto-generated header block: never captured as preamble, or every
// import→render cycle would duplicate it.
const AUTO_HEADER = /^(?:# anatomy\.md\s*$|> Auto-maintained by OpenWolf\.|> Files:\s*\d+\s*tracked)/
// Legacy symbol sub-bullets (2.0.0–2.0.3 renders): mechanical output, dropped
// rather than preserved. Any OTHER indented bullet is hand-written and kept.
const SYMBOL_SUBBULLET = /^ {2}- (?:fn|class|method|section) `[^`]+` L\d+-\d+ \(~\d+ tok\)$/
/** Strip leading/trailing blank lines so preserved blocks don't grow by two lines per write. */
function trimBlankEdges(lines: string[]): string[] {
let start = 0
let end = lines.length
while (start < end && lines[start].trim() === "") start++
while (end > start && lines[end - 1].trim() === "") end--
return lines.slice(start, end)
}
export function parseAnatomyWithRaw(content: string): {
sections: Map<string, AnatomyEntry[]>
rawLines: Map<string, string[]>
preamble: string[]
} {
const sections = new Map<string, AnatomyEntry[]>()
const rawLines = new Map<string, string[]>()
const preamble: string[] = []
let currentSection = ""
for (const raw of content.split("\n")) {
const line = raw.replace(/\r$/, "")
const sm = line.match(/^## (.+)/)
if (sm) {
currentSection = sm[1].trim()
if (!sections.has(currentSection)) sections.set(currentSection, [])
continue
}
if (!currentSection) {
// Above the first section heading: preserve everything hand-written
// (issue #61), skipping only our own generated header block.
if (!AUTO_HEADER.test(line)) preamble.push(line)
continue
}
const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/)
if (em) {
sections.get(currentSection)!.push({
file: em[1],
description: em[2] || "",
tokens: parseInt(em[3], 10),
})
continue
}
if (line.trim() === "") continue
if (SYMBOL_SUBBULLET.test(line)) continue
if (!rawLines.has(currentSection)) rawLines.set(currentSection, [])
rawLines.get(currentSection)!.push(line)
}
return { sections, rawLines, preamble: trimBlankEdges(preamble) }
}
export function parseAnatomy(content: string): Map<string, AnatomyEntry[]> {
return parseAnatomyWithRaw(content).sections
}
// serializeAnatomy was removed: it could not carry rawLines or preamble, and
// every writer must go through renderStore so preserved content survives
// (issue #61).
export function extractDescription(filePath: string): string {
const MAX_DESC = 150
const basename = path.basename(filePath)
const ext = path.extname(basename).toLowerCase()
const known: Record<string, string> = {
"package.json": "Node.js package manifest",
"tsconfig.json": "TypeScript configuration",
".gitignore": "Git ignore rules",
"README.md": "Project documentation",
}
if (known[basename]) return known[basename]
let content: string
try {
const fd = fs.openSync(filePath, "r")
const buf = Buffer.alloc(12288)
const n = fs.readSync(fd, buf, 0, 12288, 0)
fs.closeSync(fd)
content = buf.subarray(0, n).toString("utf-8")
} catch {
return ""
}
if (!content.trim()) return ""
const cap = (s: string) => s.length <= MAX_DESC ? s : s.slice(0, MAX_DESC - 3) + "..."
if (ext === ".md" || ext === ".mdx") {
const m = content.match(/^#{1,2}\s+(.+)$/m)
if (m) return cap(m[1].trim())
}
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
if (basename === "page.tsx" || basename === "page.js") return "Next.js page component"
if (basename === "layout.tsx" || basename === "layout.js") return "Next.js layout"
const exports = (content.match(/export\s+(?:async\s+)?(?:function|class|const|interface|type|enum)\s+(\w+)/g) || [])
.map(e => e.match(/(\w+)$/)?.[1]).filter(Boolean) as string[]
if (exports.length > 0 && exports.length <= 5) return `Exports ${exports.join(", ")}`
if (exports.length > 5) return cap(`Exports ${exports.slice(0, 4).join(", ")} + ${exports.length - 4} more`)
}
const declM = content.match(/(?:function|class|const|interface|type|enum)\s+(\w+)/)
if (declM) return `Declares ${declM[1]}`
return ""
}
// ── Durable store + lock (mirrors src/hooks/anatomy-store.ts, F2b) ──────────
import * as crypto from "node:crypto"
import * as os from "node:os"
export const STORE_FILE = "anatomy-index.json"
const LOCK_STALE_MS = 10_000
export const LOCK_BUDGET_MS = 2_000
export function sha256(text: string): string {
return crypto.createHash("sha256").update(text).digest("hex")
}
export interface SymbolEntry {
name: string
kind: "fn" | "class" | "method" | "section"
startLine: number
endLine: number
tokens: number
}
export interface StoreFileEntry {
description: string
tokens: number
/** sha256 (first 16 hex chars) of file content when last indexed. */
hash?: string
size?: number
mtimeMs?: number
updatedAt: string
source: "hook" | "scan" | "md-import"
symbols?: SymbolEntry[]
}
export interface AnatomyStoreData {
version: 1
meta: {
lastScanned: string
fileCount: number
hits: number
misses: number
/** sha256 of the markdown this store last rendered — skew detection key. */
renderedHash: string
storeUpdatedAt: string
}
/** Keyed by full normalized relative path, e.g. "src/hooks/shared.ts". */
files: Record<string, StoreFileEntry>
/** Non-conforming anatomy.md lines preserved verbatim, keyed by section. */
rawLines?: Record<string, string[]>
/**
* Hand-written content above the first `## ` heading (issue #61). A typed
* field rather than a reserved rawLines key: old compiled hooks still
* installed in projects ignore an unknown field and round-trip it safely,
* whereas a magic "" section key would render as a stray `## ` heading.
*/
preamble?: string[]
}
export function newStore(): AnatomyStoreData {
const now = new Date().toISOString()
return { version: 1, meta: { lastScanned: now, fileCount: 0, hits: 0, misses: 0, renderedHash: "", storeUpdatedAt: now }, files: {} }
}
export function loadStore(wolfDir: string): AnatomyStoreData | null {
try {
const parsed = JSON.parse(fs.readFileSync(path.join(wolfDir, STORE_FILE), "utf-8"))
if (parsed && parsed.version === 1 && parsed.files && parsed.meta) return parsed as AnatomyStoreData
return null
} catch {
return null
}
}
export function saveStore(wolfDir: string, store: AnatomyStoreData): void {
store.meta.fileCount = Object.keys(store.files).length
store.meta.storeUpdatedAt = new Date().toISOString()
const filePath = path.join(wolfDir, STORE_FILE)
const tmp = filePath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"
const body = JSON.stringify(store, null, 2)
try {
fs.writeFileSync(tmp, body, "utf-8")
fs.renameSync(tmp, filePath)
} catch {
try { fs.writeFileSync(filePath, body, "utf-8") } catch {}
try { fs.unlinkSync(tmp) } catch {}
}
}
/** Section key for a relpath: "src/hooks/" or "./" for root files. */
export function sectionKeyOf(relPath: string): string {
const dir = path.dirname(relPath).split(path.sep).join("/")
return dir === "." ? "./" : dir + "/"
}
/**
* Render the store to markdown, byte-identical to the legacy format. Symbols
* deliberately do NOT render: they live in anatomy-index.json and reach the
* model through the per-file pre-read hint. Rendering them roughly 2.4x'd
* anatomy.md, and agents instructed to consult anatomy.md paid that bill
* wholesale every session.
*/
export function renderStore(store: AnatomyStoreData): string {
const bySection = new Map<string, Array<{ file: string; entry: StoreFileEntry }>>()
for (const [relPath, entry] of Object.entries(store.files)) {
const key = sectionKeyOf(relPath)
if (!bySection.has(key)) bySection.set(key, [])
bySection.get(key)!.push({ file: relPath.slice(relPath.lastIndexOf("/") + 1), entry })
}
const lines: string[] = [
"# anatomy.md",
"",
`> Auto-maintained by OpenWolf. Last scanned: ${store.meta.lastScanned}`,
`> Files: ${Object.keys(store.files).length} tracked | Anatomy hits: ${store.meta.hits} | Misses: ${store.meta.misses}`,
"",
]
const preamble = trimBlankEdges(store.preamble ?? [])
if (preamble.length > 0) {
lines.push(...preamble, "")
}
const rawBySection = store.rawLines ?? {}
const keys = [...new Set([...bySection.keys(), ...Object.keys(rawBySection)])].sort()
for (const key of keys) {
lines.push(`## ${key}`)
lines.push("")
const raws = rawBySection[key] ?? []
for (const raw of raws) {
lines.push(raw)
}
const entries = (bySection.get(key) ?? []).sort((a, b) => a.file.localeCompare(b.file))
if (raws.length > 0 && entries.length > 0) lines.push("")
for (const { file, entry } of entries) {
const desc = entry.description ? ` — ${entry.description}` : ""
lines.push(`- \`${file}\`${desc} (~${entry.tokens} tok)`)
}
lines.push("")
}
return lines.join("\n")
}
/** Write the rendered markdown atomically and pin its hash in the store. */
export function renderToFile(wolfDir: string, store: AnatomyStoreData): void {
const content = renderStore(store)
store.meta.renderedHash = sha256(content)
const anatomyPath = path.join(wolfDir, "anatomy.md")
const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"
try {
fs.writeFileSync(tmp, content, "utf-8")
fs.renameSync(tmp, anatomyPath)
} catch {
try { fs.writeFileSync(anatomyPath, content, "utf-8") } catch {}
try { fs.unlinkSync(tmp) } catch {}
}
}
/**
* Absorb out-of-band edits to anatomy.md (old compiled hooks, agent/human
* hand-edits) into the store. ADDITIVE-ONLY:
* - md entry differs → md wins description/tokens (newer intent)
* - md entry absent from store → added (source "md-import")
* - store entry absent from md → KEPT unless the file is gone from disk
* (deletions are exclusively the full scanner's job)
* Symbols always survive (they only flow store → render).
*/
export function importFromMarkdown(store: AnatomyStoreData, mdContent: string, projectRoot: string): void {
// Corruption guard: an empty/unreadable anatomy.md must never wipe the
// preserved hand-written content in the store.
if (!mdContent.trim()) return
const { sections, rawLines, preamble } = parseAnatomyWithRaw(mdContent)
if (rawLines.size > 0) {
store.rawLines = Object.fromEntries(rawLines)
} else {
delete store.rawLines
}
if (preamble.length > 0) {
store.preamble = preamble
} else {
delete store.preamble
}
const seen = new Set<string>()
for (const [sectionKey, entries] of sections) {
const dir = sectionKey === "./" ? "" : sectionKey
for (const e of entries) {
const relPath = (dir + e.file).split("\\").join("/")
seen.add(relPath)
const existing = store.files[relPath]
if (!existing) {
store.files[relPath] = { description: e.description, tokens: e.tokens, updatedAt: new Date().toISOString(), source: "md-import" }
} else if (existing.description !== e.description || existing.tokens !== e.tokens) {
existing.description = e.description
existing.tokens = e.tokens
existing.updatedAt = new Date().toISOString()
existing.source = "md-import"
}
}
}
// Entries the md no longer lists: keep unless the file is really gone.
for (const relPath of Object.keys(store.files)) {
if (seen.has(relPath)) continue
if (!fs.existsSync(path.join(projectRoot, relPath))) delete store.files[relPath]
}
}
/**
* Read-side lookup: resolve a file to its anatomy entry. Store-first with an
* O(1) relpath key; falls back to a suffix scan (paths outside the root) and
* finally to parsing anatomy.md for projects that predate the store.
* `normalizedFile` and `projectDir` use forward slashes.
*/
export function lookupEntry(
wolfDir: string,
projectDir: string,
normalizedFile: string
): { file: string; description: string; tokens: number; symbols?: SymbolEntry[]; size?: number; mtimeMs?: number } | null {
const rel = normalizedFile.startsWith(projectDir + "/")
? normalizedFile.slice(projectDir.length + 1)
: normalizedFile.startsWith("/") ? null : normalizedFile
const store = loadStore(wolfDir)
if (store) {
const toResult = (rp: string, e: StoreFileEntry) => ({
file: rp.slice(rp.lastIndexOf("/") + 1),
description: e.description,
tokens: e.tokens,
symbols: e.symbols,
size: e.size,
mtimeMs: e.mtimeMs,
})
const hit = rel ? store.files[rel] : undefined
if (hit) return toResult(rel!, hit)
for (const [rp, e] of Object.entries(store.files)) {
if (normalizedFile === rp || normalizedFile.endsWith("/" + rp)) {
return toResult(rp, e)
}
}
return null
}
// Pre-store project: legacy markdown scan.
let md: string
try {
md = fs.readFileSync(path.join(wolfDir, "anatomy.md"), "utf-8")
} catch {
return null
}
for (const [sectionKey, entries] of parseAnatomy(md)) {
const dir = sectionKey === "./" ? "" : sectionKey
for (const entry of entries) {
const entryRelPath = (dir + entry.file).split("\\").join("/")
if (normalizedFile === entryRelPath || normalizedFile.endsWith("/" + entryRelPath)) {
return entry
}
}
}
return null
}
/**
* Standard writer entry point: load the store (bootstrapping from anatomy.md
* on first contact), and absorb any md-side divergence before the caller
* mutates. Call ONLY while holding the anatomy lock.
*/
export function loadStoreReconciled(wolfDir: string, projectRoot: string): AnatomyStoreData {
let store = loadStore(wolfDir)
let md: string | null = null
try { md = fs.readFileSync(path.join(wolfDir, "anatomy.md"), "utf-8") } catch {}
if (!store) {
store = newStore()
if (md) importFromMarkdown(store, md, projectRoot)
return store
}
if (md !== null && sha256(md) !== store.meta.renderedHash) importFromMarkdown(store, md, projectRoot)
return store
}
function lockSleep(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
}
export function withAnatomyLock<T>(wolfDir: string, budgetMs: number, fn: () => T): T | null {
const lockPath = path.join(wolfDir, "anatomy-index.lock")
const deadline = Date.now() + budgetMs
while (true) {
try {
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, hostname: os.hostname(), acquiredAt: Date.now() }), { flag: "wx" })
break
} catch {}
let stale = false
try {
const body = JSON.parse(fs.readFileSync(lockPath, "utf-8"))
stale = typeof body.acquiredAt !== "number" || Date.now() - body.acquiredAt > LOCK_STALE_MS
if (!stale && body.hostname === os.hostname() && typeof body.pid === "number") {
try { process.kill(body.pid, 0) } catch (err) { stale = (err as NodeJS.ErrnoException).code === "ESRCH" }
}
} catch {
try { stale = Date.now() - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS } catch {}
}
if (stale) {
const graveyard = lockPath + "." + crypto.randomBytes(4).toString("hex") + ".stale"
try { fs.renameSync(lockPath, graveyard); try { fs.unlinkSync(graveyard) } catch {} } catch {}
}
if (Date.now() >= deadline) return null
lockSleep(25 + Math.floor(Math.random() * 25))
}
try {
return fn()
} finally {
try { fs.unlinkSync(lockPath) } catch {}
}
}